1.sbi介绍

参考:

  1. 基于Opensbi服务完成控制台输出 | TimerのBlog (yanglianoo.github.io)
  2. RISC-V体系结构的U-Boot引导过程_riscv uboot-CSDN博客
  • SBI:即是supervisor binary interface,允许在所有的riscv运行。
  • 简单来说就是RISCV官方定义了一个规范接口,运行在S模式或VS模式(启动虚拟化)的软件如os可以使用这些标准接口使得能够在不同的硬件平台上具有良好的移植性而不用去适配。
  • 有两种架构的SBI,一种在CPU未启动虚拟化拓展

未启动虚拟化

启动虚拟化:

启动虚拟化

SBI扩展ID(EID)和SBI函数ID(FID)被编码为有符号的32位整数。 sbi-v0.2,规定了函数调用:

  1. 在监管者和SEE之间,使用ECALL作为控制传输指令,监管者就是S模式的软件程序
  2. a7编码SBI扩展ID(EID)
  3. a6编码SBI函数ID(FID),对于任何在a7中编码的SBI扩展,其定义在SBI v0.2之后。
  4. SBI调用期间,除了a0a1寄存器外,所有寄存器都必须由被调用方保留。
  5. SBI函数必须在a0a1中返回一对值,其中a0返回错误代码。类似于返回C结构体。
1
2
3
4
struct sbiret {
long error;
long value;
};

sbi的错误类型以及返回值如下:

错误类型
SBI_SUCCESS 成功 0
SBI_ERR_FAILED 失败 -1
SBI_ERR_NOT_SUPPORTED 不支持操作 -2
SBI_ERR_INVALID_PARAM 非法参数 -3
SBI_ERR_DENIED 拒绝 -4
SBI_ERR_INVALID_ADDRESS 非法地址 -5
SBI_ERR_ALREADY_AVAILABLE (资源)已可用 -6
SBI_ERR_ALREADY_STARTED (操作)已启动 -7
SBI_ERR_ALREADY_STOPPED (操作)已停止 -8

SBI-v0.1版本函数:

函数名 SBI 版本 FID EID 替代 EID 函数用途
sbi_set_timer 0.1 0 0x00 0x54494D45 设置时钟
sbi_console_putchar 0.1 0 0x01 N/A 控制台字符输出
sbi_console_getchar 0.1 0 0x02 N/A 控制台字符输入
sbi_clear_ipi 0.1 0 0x03 N/A 清除IPI
sbi_send_ipi 0.1 0 0x04 0x735049 发送IPI
sbi_remote_fence_i 0.1 0 0x05 0x52464E43 远程FENCE.I
sbi_remote_sfence_vma 0.1 0 0x06 0x52464E43 远程SFENCE.VMA
sbi_remote_sfence_vma_asid 0.1 0 0x07 0x52464E43 远程SFENCE.VMA(指定地址空间标识符)
sbi_shutdown 0.1 0 0x08 0x53525354 系统关闭
保留 0x09-0x0F

1.1sbi与 opensbi

SBI,Supervisor Binary Interface,是一个定义了超级管理程序(hypervisor)或引导程序(bootloader)与操作系统之间接口的规范。SBI 提供了一组标准化的接口,使得操作系统可以调用底层的硬件资源和服务,而不需要知道具体的硬件实现细节。

SBI 的主要功能

  • 控制台输入/输出:提供输出字符和读取字符的功能。
  • 定时器:设置和管理定时器。
  • 中断:发送和清除中断。
  • 内存屏障:远程内存屏障指令。
  • 电源管理:如关机和重启等功能。

OpenSBI 是 SBI 规范的一个开源实现。它提供了一组库和工具,使得 RISC-V 平台可以快速、方便地支持 SBI 接口。OpenSBI 通常作为固件运行,在操作系统(如 Linux)启动之前初始化系统,并提供 SBI 接口供操作系统调用。

OpenSBI 的主要功能

  • 实现 SBI 接口:OpenSBI 实现了所有标准化的 SBI 接口,使得操作系统可以调用这些接口进行各种低级别操作。
  • 平台初始化:在操作系统启动之前,OpenSBI 负责初始化平台硬件,如 CPU、内存和设备等。
  • 提供扩展功能:除了标准的 SBI 接口,OpenSBI 还可以提供额外的扩展功能,帮助优化和定制特定平台的行为。

二者之间的联系:

规范与实现的关系

  • SBI 是一个规范,它定义了操作系统与底层固件或引导程序之间的接口。
  • OpenSBI 是这个规范的具体实现,它提供了一个符合 SBI 规范的实现,使得 RISC-V 平台可以实际使用这些接口。

调用关系

  • 操作系统调用 SBI 接口来完成一些低级别的操作,这些接口由 OpenSBI 提供和实现。
  • OpenSBI 运行在特权级别最高的机器模式(M-mode),在操作系统之前加载,并在操作系统启动后提供这些服务。

软件栈中的位置

  • SBI 作为接口,位于操作系统和硬件之间
  • OpenSBI 作为固件,运行在硬件之上,为操作系统提供 SBI 接口。

示例代码

上面的 sbi.c 代码展示了如何通过 SBI 接口向控制台输出字符。具体实现如下:

1
2
3
4
void sbi_console_putchar(int ch)
{
sbi_ecall(SBI_EXT_0_1_CONSOLE_PUTCHAR, 0, ch, 0, 0, 0, 0, 0);
}

这段代码调用了 SBI 接口 SBI_EXT_0_1_CONSOLE_PUTCHAR,而这个接口是由 OpenSBI 实现并提供的

2.基于opensbi的控制台字符输出

本节目标:在S模式下使用ecall 调用sbi_console_putchar函数向控制台打印字符

移植uboot 和 Linux 系统:基于qemu-riscv从0开始构建嵌入式linux系统ch8. U-Boot — 主页 (quard-star-tutorial.readthedocs.io)

在上章中(riscv-6)定义了untrust_domain,其运行在S模式下,而opensbi是运行在EMM(M模式)与 OS之间的(S模式)

我们在udomain中定义了两个地址参数:一个是下级程序的参数,一个是下级程序的起始地址

1
2
next-arg1 = <0x0 0x82200000>;
next-addr = <0x0 0x82000000>;

下级的程序为我们编写的OS,设定地址为0x80200000

注意需要在设备树文件中修改:dts/…

1
2
next-arg1 = <0x0 0x82000000>;
next-addr = <0x0 0x80200000>;

2.1新建OS

新建OS文件夹,在OS文件夹下面新建 Makefileentry.Smain.cos.ldsbi.csbi.h

OS/entry.S

定义了64kb的栈空间,将栈指针sp指向栈顶,然后调用os_main函数

两个部分,分别是.text.entry 和 .bss.stack

1
2
3
4
5
6
7
8
9
10
11
12
     .section .text.entry      		# 定义了一个代码段,名为entry
.globl _start # 声明 _start 标签为全局符号,使其在链接过程中可见。
_start:
la sp, boot_stack_top # 加载 bst地址到栈指针sp
call os_main # 调用 os_main函数

.section .bss.stack # 定义了一个未初始化数据段.bss,用作栈空间
.globl boot_stack_lower_bound #
boot_stack_lower_bound:
.space 4096 * 16 # 分别 4096 * 16 即 64kb空间作于栈空间
.globl boot_stack_top # 声明 boot_stack_top 标签为全局符号,使其在链接过程中可见。
boot_stack_top: # 定义 boot_stack_top 标签,表示栈空间的结束地址。

OS/sbi.h

定义了EID枚举变量和SBI的返回结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*sbi.h*/
#ifndef __SBI_H__
#define __SBI_H__

enum sbi_ext_id {
SBI_EXT_0_1_SET_TIMER = 0x0, // 设置定时器
SBI_EXT_0_1_CONSOLE_PUTCHAR = 0x1, // 输出一个字符
SBI_EXT_0_1_CONSOLE_GETCHAR = 0x2, // 获取一个字符
SBI_EXT_0_1_CLEAR_IPI = 0x3, // 清除中断处理器间中断IPI
SBI_EXT_0_1_SEND_IPI = 0x4, // 发送IPI
SBI_EXT_0_1_REMOTE_FENCE_I = 0x5, // 远程指令缓存刷新
SBI_EXT_0_1_REMOTE_SFENCE_VMA = 0x6, // 远程地址空间刷新
SBI_EXT_0_1_REMOTE_SFENCE_VMA_ASID = 0x7, // 远程地址刷新,基于ASID 地址空间标识符
SBI_EXT_0_1_SHUTDOWN = 0x8, // 关闭系统
SBI_EXT_BASE = 0x10, // 基本拓展
SBI_EXT_TIME = 0x54494D45, // 时间管理扩展
SBI_EXT_IPI = 0x735049, // ipi拓展
SBI_EXT_RFENCE = 0x52464E43, // 远程内存屏障扩展
SBI_EXT_HSM = 0x48534D, // 硬件状态机扩展
SBI_EXT_SRST = 0x53525354, // 系统复位扩展
SBI_EXT_PMU = 0x504D55, // 性能监控单元扩展
};

/* sbi 返回结构体*/
struct sbiret {
long error;
long value;
};

#endif

OS/sbi.c

定义了sbi_ecall函数调用opensbi提供的服务,最后定义了sbi_console_putchar函数传入想要输出的字符,然后传入EID和FID,去查上面的表EID=0x01,FID=0。

struct sbiret sbi_ecall(...) 函数用于进行 SBI 调用。它使用了 GCC 的扩展语法将函数参数传递给 RISC-V 的寄存器,并执行 ecall 指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*sbi.c*/
#include "sbi.h"
#include "stdint.h"
struct sbiret sbi_ecall(int ext, int fid, unsigned long arg0,
unsigned long arg1, unsigned long arg2,
unsigned long arg3, unsigned long arg4,
unsigned long arg5)
{
struct sbiret ret;

//使用GCC的扩展语法,用于将一个值存储到RISC-V架构中的寄存器a0中。
register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0);
register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1);
register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2);
register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3);
register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4);
register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5);
register uintptr_t a6 asm ("a6") = (uintptr_t)(fid);
register uintptr_t a7 asm ("a7") = (uintptr_t)(ext);
asm volatile ("ecall"
: "+r" (a0), "+r" (a1)
: "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7)
: "memory");
ret.error = a0;
ret.value = a1;

return ret;
}


/**
* sbi_console_putchar() - Writes given character to the console device.
* @ch: The data to be written to the console.
*
* Return: None
*/
void sbi_console_putchar(int ch)
{
sbi_ecall(SBI_EXT_0_1_CONSOLE_PUTCHAR, 0, ch, 0, 0, 0, 0, 0);
}

OS/main.c

定义了os_main函数

1
2
3
4
5
6
7
8
9
10
11
extern sbi_console_putchar(int ch);

void os_main()
{
sbi_console_putchar('h');
sbi_console_putchar('e');
sbi_console_putchar('l');
sbi_console_putchar('l');
sbi_console_putchar('o');
sbi_console_putchar('!');
}

OS/os.ld

定义内存起始地址0x80200000 以及大小 128M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
OUTPUT_ARCH(riscv)
ENTRY(_start)

MEMORY
{
ram (rxai!w) : ORIGIN = 0x80200000, LENGTH = 128M # r:可读 x:可执行 a:可分配 i:初始化 !w:不可写
}
SECTIONS
{
.text : {
*(.text .text.*) # *(.text .text.*) 表示将所有 .text 段和以 .text. 开头的段放到这里
} >ram

.rodata : {
*(.rodata .rodata.*)
} >ram

.data : {
. = ALIGN(4096); # 对齐 4096
*(.sdata .sdata.*) # 表示将所有 .sdata 段和以 .sdata. 开头的段放到这里。
*(.data .data.*) # 表示将所有 .data 段和以 .data. 开头的段放到这里。
PROVIDE(_data_end = .); # 定义一个符号 _data_end,其值为当前地址。
} >ram

.bss :{
*(.sbss .sbss.*)
*(.bss .bss.*)
*(COMMON) # 表示将所有公共段放到这里。
} >ram

}

OS/Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

CROSS_COMPILE = riscv64-unknown-elf-
CFLAGS = -nostdlib -fno-builtin # -nostdlib 表示不使用标准库,-fno-builtin 表示禁用内置函数。

# riscv64-unknown-elf-gcc 工具链可以同时编译汇编和 C 代码
CC = ${CROSS_COMPILE}gcc -L/opt/riscv/riscv64-unknown-elf/lib -L/opt/riscv/lib
OBJCOPY = ${CROSS_COMPILE}objcopy
OBJDUMP = ${CROSS_COMPILE}objdump

# 汇编文件列表
SRCS_ASM = \
entry.S
# c文件列表
SRCS_C = \
sbi.c \
main.c \

# 将源文件替换为 .o 文件
OBJS = $(SRCS_ASM:.S=.o)
OBJS += $(SRCS_C:.c=.o)


os.elf: ${OBJS}
${CC} ${CFLAGS} -T os.ld -o os.elf $^
${OBJCOPY} -O binary os.elf os.bin

%.o : %.c
${CC} ${CFLAGS} -c -o $@ $<

%.o : %.S
${CC} ${CFLAGS} -c -o $@ $<


.PHONY : clean
clean:
rm -rf *.o *.bin *.elf

2.2测试

build.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
# 编译os
if [ ! -d "$SHELL_FOLDER/output/os" ]; then
mkdir $SHELL_FOLDER/output/os
fi
cd $SHELL_FOLDER/OS
make
cp $SHELL_FOLDER/os/os.bin $SHELL_FOLDER/output/os/os.bin
make clean
...

# 写入 os.bin,地址偏移为 1k * 8k = 0x800000
dd of=fw.bin bs=1k conv=notrunc seek=8k if=$SHELL_FOLDER/output/os/os.bin

boot/start.s

将os.bin 从flash的0x20800000 加载到0x80200000

1
2
3
4
5
6
7
8
9
//load os.bin
//[0x20800000:0x20C00000] --> [0x80200000:0x80600000]
li a0, 0x208
slli a0, a0, 20 //a0 = 0x20800000
li a1, 0x802
slli a1, a1, 20 //a1 = 0x80200000
li a2, 0x806
slli a2, a2, 20 //a2 = 0x80600000
load_data a0,a1,a2

显示了hello!

image-20240610220040962