1.sbi介绍
参考:
基于Opensbi服务完成控制台输出 | TimerのBlog (yanglianoo.github.io)
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,规定了函数调用:
在监管者和SEE
之间,使用ECALL
作为控制传输指令,监管者就是S
模式的软件程序
a7
编码SBI
扩展ID(EID)
a6
编码SBI
函数ID(FID)
,对于任何在a7
中编码的SBI
扩展,其定义在SBI v0.2
之后。
在SBI
调用期间,除了a0
和a1
寄存器外,所有寄存器都必须由被调用方保留。
SBI
函数必须在a0
和a1
中返回一对值,其中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
文件夹下面新建 Makefile
、entry.S
、main.c
、os.ld
、 sbi.c
、 sbi.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 #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 , SBI_EXT_0_1_SEND_IPI = 0x4 , 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 , SBI_EXT_0_1_SHUTDOWN = 0x8 , SBI_EXT_BASE = 0x10 , SBI_EXT_TIME = 0x54494D45 , SBI_EXT_IPI = 0x735049 , SBI_EXT_RFENCE = 0x52464E43 , SBI_EXT_HSM = 0x48534D , SBI_EXT_SRST = 0x53525354 , SBI_EXT_PMU = 0x504D55 , }; 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 #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 ; 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; } 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 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 SRCS_C = \ sbi.c \ main.c \ 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!