最近要在RV64的平台上把Linux给bringup起来,由于当下的工作主要集中在底层硬件接口驱动、CPU的操作及RTOS应用等,虽然之前搞过Arm Linux的开发工作,但是比较基础的玩的比较少,所以真正要搞把系统bringup起来,我之前的知识体系还是欠缺的,经过一段时间恶补学习,终于给是搞起来了。这里把最近学习opensbi相关内容整理分享下。
另外,鉴于很多想入手RISCV的同学手头并没有对应开发板,而opensbi又是一个纯软件的项目,现在该项目已经比较完善,很多设计公司的内核在opensbi上都有适配,所以也是这里也是基于QEMU的opensbi来入手。
另外个人结合学习内容也开发了一个demo实践,整篇博客也是基于该demo介绍,建议结合该demo学习理解,比干巴巴的干撸代码或者看文档更事半功倍。
git clone git@github.com:liuxiaohan-813/opensbi_baremetal_test.git
1 简介
OpenSBI(Open Supervisor Binary Interface)是专为 RISC-V 架构设计的开源引导加载程序(Bootloader)和运行时环境,运行在 RISC-V 的最高特权模式——机器模式(Machine Mode, M-mode) 下。它充当硬件与操作系统之间的桥梁,为操作系统(如 Linux)提供标准化的服务接口,确保跨平台兼容性和硬件抽象。
所以在RISCV上玩Linux,了解下OpenSBI是很有必要的。
1.1 核心功能
- 硬件初始化:在RISCV启动时初始化 CPU、内存、外设等基础硬件
- 运行时服务:通过标准 API 提供关键服务,包括:
- 中断管理:处理硬件中断,向上层传递中断事件。
- 定时器服务:为操作系统调度提供时间管理支持。
- 内存管理:管理物理内存分配及权限(如通过 PMP 硬件单元划分安全域)
- 安全隔离:控制硬件级权限,防止不同安全域的操作系统互相干扰
1.2 架构设计
1、OpenSBI一般运行在 RISC-V 的S模式(Supervisor Mode),负责硬件初始化从存储介质加载自身并执行;
2、提供标准化 SBI 接口,供操作系统通过 ecall 指令调用 M-mode 服务;
3、提供适配层,来支持不同RISCV平台(Sifive、Nuclei等)。
1.3 启动流程
1.3.1 典型RV Linux启动
我们先来看下一个RISCV内核Linux操作系统的典型启动流程:
1、系统上电或者复位重启后,从ROM启动,将spl(一级LOADER)加载到片上SRAM
2、引导spl运行:1、初始化ddr,2、加载opensbi(RUNTIME)和uboot(BOOTLOADER)到DDR
3、引导opensbi运行:1、基础硬件初始化 2、系统安全配置等
4、引导u-boot启动: 1、文件系统、网络、存储等配置 2、从存储(EMMC、DRAM等)中加载Linux(OS)到DDR
5、最后运行在RISCV core上的只有opensbi和Linux,而Linux可以通过sbi接口来调用opensbi
1.3.2 opensbi启动流程
opensbi提供了三种启动模式:
1、FW_PAYLOAD 2、FW_JUMP 3、FW_DYNAMIC
-
1、FW_PAYLOAD
opensbi和下一级启动固件绑定,作为整体;
支持设备树,一般和uboot绑定,成熟的芯片方案都会选择该方案(让用户专注于上层应用开发);
但也有缺点:1、整体编译,任何改动都需要opensbi和bootloader一起重编 2、无法从前一bootloader传递参数到FW_PAYLOAD。 -
2、FW_JUMP
跳转到一个固定地址,来执行下一级固件;
一般在学习opensbi时,十分高效,缺点是前一bootloader必须将下一boot镜像加载到固定地址,无法传递地址参数。 -
3、FW_DYNAMIC
opensbi可以动态传递下一级启动的地址;
前一级启动加载后续启动bootloader,然后通过a2寄存器将struct fw_dynamic_info结构体传递给下一级bootloader
确定就是会将fw_dynamic_info信息暴露出去。
前面图中名词:
ZSBL (Zero Stage Boot Loader)
- 定义:固化在芯片 ROM 中的最底层引导代码(硬件厂商提供,不可修改)
FSBL (First Stage Boot Loader)
- 定义:由芯片厂商或开发板厂商提供的二级引导程序(通常开源可定制)
2 基于opensbi裸机固件实现
我们先基于QEMU
来把opensbi
跑起来,然后简单实现一个逻辑固件,再简单调用下SBI;所以通过FW_JUMP
的模式来进行。
我已经做了demo,下载工程代码:
git clone git@github.com:liuxiaohan-813/opensbi_baremetal_test.git
工程里我把opensbi作为了子模块来管理,所以就不用下载opensbi了。直接在工程目录下,运行:
git submodule update --init --recursive
按照工程下README.md
有说明。
2.1 QEMU和工具链环境搭建
我在前面博客有介绍过RISCV QEMU的环境的搭建,具体的搭建流程就不再赘述了。可以参考:
RISC-V汇编学习(四)—— RISCV QEMU平台搭建(基于芯来平台)
不同是这里用的工具链是:riscv64-unknown-linux-gnu-
,芯来科技官网也可以下载到。
2.2 opensbi和裸机固件编译
我已经把opensbi和裸机固件的编译放到了Makefile里。
分别执行:
make opensbi
和
make firmware
或者make all
一起来编译。
编译sbi的实际命令如下:
make PLATFORM=generic CROSS_COMPILE=riscv64-unknown-linux-gnu- FW_TEXT_START=0x80000000 PLATFORM_RISCV_XLEN=64 all
PLATFORM就是芯片或者SOC平台,值得注意的是我修改了FW_TEXT_START=0x80000000
,也就是说opensbi从0x80000000启动。
裸机固件,我这里就简单调用了opensbi的sbi实现了打印,并调用了第三方开源库tinyprint实现字符串打印,编译可以参考具体代码,关于裸机固件细节后面章节具体说明。
2.3 QEMU运行opensbi及裸机固件
执行:
make run_qemu
可以直接运行opensbi和裸机固件,实际执行命令:
qemu-system-riscv64 -M virt -m 256M -nographic -bios opensbi/build/platform/generic/firmware/fw_jump.elf -kernel firmware/helloworld/helloworld.elf
通过指定bios和kernel的可执行文件,来实现。
效果已在README.me
中展示,在opensbi运行完后,跳转到下一级固件执行,打印内容。
3 opensbi代码分析
3.1 opensbi整体架构
3.1.1 固件组成
1、通用的SBI库,不依赖芯片或者SOC平台
2、特定平台库(包含特定的平台驱动),方便不同芯片或者SOC平台移植适配
3、特定固件启动方式:1、FW_PAYLOAD 2、FW_JUMP 3、FW_DYNAMIC
3.1.2 代码结构
├── CONTRIBUTORS.md
├── COPYING.BSD
├── docs #帮助文档
├── firmware #各种启动方式的启动汇编和链接脚本
│ ├── external_deps.mk
│ ├── fw_base.ldS
│ ├── fw_base.S
│ ├── fw_dynamic.elf.ldS
│ ├── fw_dynamic.S
│ ├── fw_jump.elf.ldS
│ ├── fw_jump.S
│ ├── fw_payload.elf.ldS
│ ├── fw_payload.S
│ ├── Kconfig
│ ├── objects.mk
│ └── payloads
│ ├── objects.mk
│ ├── test.elf.ldS
│ ├── test_head.S
│ └── test_main.c
├── include #头文件
│ ├── sbi
│ └── sbi_utils
├── Kconfig
├── lib
│ ├── sbi #输出平台无关的libsbi.a库文件
│ └── utils #参与输出平台相关的libplatsbi.a文件
├── Makefile
├── platform #各芯片平台和SOC的代码
│ ├── fpga
│ ├── generic
│ ├── kendryte
│ ├── nuclei
│ └── template
├── README.md
├── scripts
└── ThirdPartyNotices.md
3.1.3 链接脚本和启动汇编
从链接脚本里可以清晰看出固件的地址规划和排布。
fw_jump.elf.ldS:
OUTPUT_ARCH(riscv)
ENTRY(_start)SECTIONS
{#include "fw_base.ldS"PROVIDE(_fw_reloc_end = .);
}
fw_base.ldS
Makefile通过FW_TEXT_START
来指定opensbi固件的启动地址。具体内容就不深入分析了,感兴趣可以看看。
. = FW_TEXT_START;
/* Don't add any section between FW_TEXT_START and _fw_start */
PROVIDE(_fw_start = .);. = ALIGN(0x1000); /* Need this to create proper sections *//* Beginning of the code section */.text :{PROVIDE(_text_start = .);*(.entry)*(.text)*(.text.*). = ALIGN(8);PROVIDE(_text_end = .);
}/* End of the code sections */. = ALIGN(0x1000); /* Ensure next section is page aligned *//* Beginning of the read-only data sections */PROVIDE(_rodata_start = .);.rodata :
{*(.rodata .rodata.*). = ALIGN(8);
}.dynsym :
{*(.dynsym)
}. = ALIGN(0x1000); /* Ensure next section is page aligned */.rela.dyn : {PROVIDE(__rel_dyn_start = .);*(.rela*)PROVIDE(__rel_dyn_end = .);
}PROVIDE(_rodata_end = .);/* End of the read-only data sections *//** PMP regions must be to be power-of-2. RX/RW will have separate* regions, so ensure that the split is power-of-2.*/
. = ALIGN(1 << LOG2CEIL((SIZEOF(.rodata) + SIZEOF(.text)+ SIZEOF(.dynsym) + SIZEOF(.rela.dyn))));PROVIDE(_fw_rw_start = .);/* Beginning of the read-write data sections */.data :
{PROVIDE(_data_start = .);*(.sdata)*(.sdata.*)*(.data)*(.data.*)*(.readmostly.data)*(*.data). = ALIGN(8);PROVIDE(_data_end = .);
}. = ALIGN(0x1000); /* Ensure next section is page aligned */.bss :
{PROVIDE(_bss_start = .);*(.sbss)*(.sbss.*)*(.bss)*(.bss.*). = ALIGN(8);PROVIDE(_bss_end = .);
}/* End of the read-write data sections */. = ALIGN(0x1000); /* Need this to create proper sections */PROVIDE(_fw_end = .);
启动汇编,也可以叫做是汇编loader,用来完成基础硬件初始化和控制启动流程。具体内容可以也不再展示,也不在做具体介绍,demo里opensbi仓里有fw_base.S
(也通过下面链接查看)。
https://github.com/riscv-software-src/opensbi/blob/13abda516928a8823e6b7b65b0a29bb338484227/firmware/fw_base.S
重点只看一点:FW_JUMP_ADDR
,用来指定opensbi启动完成后下一级固件的地址,FW_JUMP默认情况下:RV64的下级固件地址为:0x80200000
。这一点很重要,后面给裸机固件做链接脚本时会用到。
3.2 opensbi初始化
了解了opensbi整体结构后,下面来看下opensbi的初始化流程,看下opensbi做了哪些事,从而对初始化细节有所了解,也对RISCV启动有点认识。
链接脚本fw_jump.elf.ldS
入口为_start
。我们就从_start
开始。
- 1、_start
作用是选择用来boot的hart,如果未指定,它将选择一个hart来执行,操作一系列的初始化;后面进来的hart则会进入_wait_for_boot_hart,等待初始化完成。
_start:/* 保存关键寄存器(a0-a2)到临时寄存器(s0-s2) */MOV_3R s0, a0, s1, a1, s2, a2 // s0=a0, s1=a1, s2=a2/* 获取引导HART ID */call fw_boot_hart // 调用函数获取引导HART IDadd a6, a0, zero // a6 = 返回值 (引导HART ID)/* 恢复原始寄存器值 */MOV_3R a0, s0, a1, s1, a2, s2 // a0=s0, a1=s1, a2=s2/* 检查是否指定了引导HART */li a7, -1 // a7 = -1 (无效HART标志)beq a6, a7, _try_lottery // 如果引导HART=-1,跳转到彩票机制/* 当前HART不是引导HART则等待 */bne a0, a6, _wait_for_boot_hart // 如果a0(当前HART)≠a6(引导HART),跳转等待_try_lottery:/* 使用原子操作竞争引导权限 */lla a6, _boot_lottery // a6 = 彩票变量地址(通常为0)li a7, BOOT_LOTTERY_ACQUIRED // a7 = 彩票获取值(通常=1)
#ifdef __riscv_atomicamoswap.w a6, a7, (a6)bnez a6, _wait_for_boot_hart
#elif __riscv_zalrsc
_sc_fail:lr.w t0, (a6)sc.w t1, a7, (a6)bnez t1, _sc_failbnez t0, _wait_for_boot_hart
#else
#error "need a or zalrsc"
#endif
-
2、_relocate
判断_load_start与_start是否一致,若不一致,则需要将代码重定位,来保证程序正常运行。 -
3、_relocate_done
功能:- 1、通过
_reset_regs
清除寄存器值,这里设备树地址的a1、a2不会清除 - 2、通过
_bss_zero
清除bss段(用来存放未初始化的全局变量,默认为0) - 3、设置栈顶sp(栈是向上增长的,地址为
_fw_end+0x2000
)
- 1、通过
-
4、fw_platform_init
之后调用fw_platform_init
C函数接口,根据解析到的设备树信息,进行平台相关的初始化;
入参是对应a0~a4
寄存器,根据sbi汇编规则:
a0: hart_id、a1: 设备树地址、a2: 设备树大小、a3: 0(目前实际用到a0和a1)。
另外这里用到了Linux和uboot下设备树,将设备信息和驱动进行了解耦,方便编译。
-
5、_scratch_init
在这里设置了每个hart的scratch空间
(可以理解为临时工作区),该空间用于保存处理器的运行时信息。(下图是从其他博主那copy过来,简单展示下,侵删)
-
6、fdt重定位
核心功能是根据编译时的配置参数,计算并返回FDT的地址,供下一阶段(如 U-Boot 或 Linux 内核)使用。 -
7、sbi_init
- 初始化当前HART的OpenSBI库:接收
struct sbi_scratch
参数(通过汇编阶段初始化并存储在CSR_MSCRATCH
),完成基本环境设置(如栈指针、中断禁用)。 - 启动模式判断:根据
next_mode
字段(M/S/U模式)验证当前HART是否支持目标特权模式。 - 冷启动(
Coldboot
)与热启动(Warmboot
)选择:- 随机选择一个满足条件的HART执行完全初始化(Coldboot)。
- 其余HART执行部分初始化(Warmboot),跳过重复配置。
- 平台相关初始化:调用平台回调函数(如
sbi_platform_early_init
),完成硬件特定配置(时钟、中断、串口等)。 - 关键组件初始化:
- 中断代理(SSIP/STIP/SEIP、异常代理)。
- 控制台(
sbi_console_init
)。 - PMU、TLB、定时器(sbi_pmu_init/sbi_tlb_init/sbi_timer_init)等。
- 跳转至下一阶段:根据
next_addr
和next_mode
,将控制权移交下一引导阶段(如U-Boot/Linux)。
- 初始化当前HART的OpenSBI库:接收
3.3 sbi服务接口相关
- 1、 异常handler注册:
我在RISC-V特权模式及切换 ,有介绍过
软件可以通过执行ecall指令主动请求进入高特权级别(系统调用),以及简单的流程说明。异常handler地址的注册,本质上就是把异常处理地址写入CSR_MTVEC
寄存器。下面具体来看下opensbi是如何做的:
fw_bsse.S:
/* Setup trap handler */
lla a4, _trap_handler # 将 _trap_handler 的地址加载到 a4
csrr a5, CSR_MISA # 读取 MISA CSR(机器 ISA 信息寄存器)
srli a5, a5, ('H' - 'A') # 右移 ('H' - 'A') 位,检查是否支持 Hypervisor 扩展
andi a5, a5, 0x1 # 取最低位
beq a5, zero, _skip_trap_handler_hyp # 如果不支持 Hypervisor,跳过
lla a4, _trap_handler_hyp # 否则,加载 _trap_handler_hyp(Hypervisor 模式的处理程序)
_skip_trap_handler_hyp:
csrw CSR_MTVEC, a4 # 将 a4 的值写入 MTVEC(机器陷阱向量基址寄存器).section .entry, "ax", %progbits # 定义 .entry 段,可分配、可执行
.align 3 # 8 字节对齐
.globl _trap_handler # 声明 _trap_handler 为全局符号
_trap_handler:TRAP_SAVE_AND_SETUP_SP_T0 # 宏:保存上下文并设置栈指针(SP)TRAP_SAVE_MEPC_MSTATUS 0 # 宏:保存 MEPC 和 MSTATUS 寄存器TRAP_SAVE_GENERAL_REGS_EXCEPT_SP_T0 # 宏:保存通用寄存器(除 SP 和 T0)TRAP_SAVE_INFO 0 0 # 宏:保存额外信息TRAP_CALL_C_ROUTINE # 宏:调用 C 处理函数TRAP_RESTORE_GENERAL_REGS_EXCEPT_A0_T0TRAP_RESTORE_MEPC_MSTATUS 0TRAP_RESTORE_A0_T0mret # 从陷阱返回.macro TRAP_CALL_C_ROUTINEadd a0, sp, zero # 将栈指针 SP 的值赋给 a0(作为第一个参数)call sbi_trap_handler # 调用 C 函数 sbi_trap_handler
.endm
- 2、异常handler定义:
当异常(中断也是一种异常)发生时,cpu根据当前异常类型进行不同的处理,下面来分析下opensbi是如何处理异常场景的。
struct sbi_trap_context *sbi_trap_handler(struct sbi_trap_context *tcntx)
{// 初始化处理状态和错误信息int rc = SBI_ENOTSUPP; // 默认返回"不支持"const char *msg = "trap handler failed"; // 默认错误信息struct sbi_scratch *scratch = sbi_scratch_thishart_ptr(); // 获取当前Hart的私有数据区const struct sbi_trap_info *trap = &tcntx->trap; // 陷阱信息struct sbi_trap_regs *regs = &tcntx->regs; // 寄存器状态ulong mcause = tcntx->trap.cause; // 从陷阱上下文获取mcause值/* Update trap context pointer */tcntx->prev_context = sbi_trap_get_context(scratch); // 保存之前的陷阱上下文sbi_trap_set_context(scratch, tcntx); // 设置当前陷阱上下文// 中断处理(异步事件)if (mcause & MCAUSE_IRQ_MASK) { // 检查是否是中断if (sbi_hart_has_extension(sbi_scratch_thishart_ptr(),SBI_HART_EXT_SMAIA)) // 检查是否支持AIA扩展rc = sbi_trap_aia_irq(); // 使用AIA中断处理elserc = sbi_trap_nonaia_irq(mcause & ~MCAUSE_IRQ_MASK); // 使用传统中断处理msg = "unhandled local interrupt"; // 更新错误信息goto trap_done; // 跳转到清理阶段}// 异常处理(同步事件)switch (mcause) { // 根据异常类型分发处理case CAUSE_ILLEGAL_INSTRUCTION: // 非法指令异常rc = sbi_illegal_insn_handler(tcntx);msg = "illegal instruction handler failed";break;case CAUSE_MISALIGNED_LOAD: // 加载地址不对齐sbi_pmu_ctr_incr_fw(SBI_PMU_FW_MISALIGNED_LOAD); // 更新PMU计数器rc = sbi_misaligned_load_handler(tcntx);msg = "misaligned load handler failed";break;case CAUSE_MISALIGNED_STORE: // 存储地址不对齐sbi_pmu_ctr_incr_fw(SBI_PMU_FW_MISALIGNED_STORE);rc = sbi_misaligned_store_handler(tcntx);msg = "misaligned store handler failed";break;case CAUSE_SUPERVISOR_ECALL: // 监管模式环境调用case CAUSE_MACHINE_ECALL: // 机器模式环境调用rc = sbi_ecall_handler(tcntx); // SBI服务入口点msg = "ecall handler failed";break;case CAUSE_LOAD_ACCESS: // 加载访问错误sbi_pmu_ctr_incr_fw(SBI_PMU_FW_ACCESS_LOAD);rc = sbi_load_access_handler(tcntx);msg = "load fault handler failed";break;case CAUSE_STORE_ACCESS: // 存储访问错误sbi_pmu_ctr_incr_fw(SBI_PMU_FW_ACCESS_STORE);rc = sbi_store_access_handler(tcntx);msg = "store fault handler failed";break;case CAUSE_DOUBLE_TRAP: // 双重陷阱(严重错误)rc = sbi_double_trap_handler(tcntx);msg = "double trap handler failed";break;default: // 其他未定义异常/* If the trap came from S or U mode, redirect it there */msg = "trap redirect failed";rc = sbi_trap_redirect(regs, trap); // 重定向到低权限模式处理break;}
trap_done:// 错误处理if (rc) // 如果有错误发生sbi_trap_error(msg, rc, tcntx); // 报告错误信息// 处理挂起的监管者软件事件(SSE)if (sbi_mstatus_prev_mode(regs->mstatus) != PRV_M) // 如果来自非M模式sbi_sse_process_pending_events(regs); // 处理SSE事件// 恢复之前的陷阱上下文sbi_trap_set_context(scratch, tcntx->prev_context);return tcntx; // 返回更新后的陷阱上下文
}
int sbi_ecall_handler(struct sbi_trap_context *tcntx)
{int ret = 0; // 处理结果状态码struct sbi_trap_regs *regs = &tcntx->regs; // 访问寄存器状态struct sbi_ecall_extension *ext; // SBI扩展模块指针unsigned long extension_id = regs->a7; // 从a7获取扩展IDunsigned long func_id = regs->a6; // 从a6获取功能IDstruct sbi_ecall_return out = {0}; // 调用返回结构bool is_0_1_spec = 0; // 标记是否0.1规范扩展// 步骤1: 根据扩展id查找对应的SBI扩展处理模块ext = sbi_ecall_find_extension(extension_id);// 步骤2: 调用扩展处理函数if (ext && ext->handle) { // 找到有效扩展ret = ext->handle(extension_id, func_id, regs, &out); // 执行扩展处理// 标记0.1规范扩展 (范围检查)if (extension_id >= SBI_EXT_0_1_SET_TIMER &&extension_id <= SBI_EXT_0_1_SHUTDOWN)is_0_1_spec = 1;} else { // 未找到扩展ret = SBI_ENOTSUPP; // 返回"不支持"}// 步骤3: 更新寄存器状态(除非标记跳过)if (!out.skip_regs_update) {// 错误检查:确保返回值在合法范围内if (ret < SBI_LAST_ERR || // 小于最小错误码(extension_id != SBI_EXT_0_1_CONSOLE_GETCHAR && // 排除特定扩展SBI_SUCCESS < ret)) { // 大于成功码sbi_printf("%s: Invalid error %d for ext=0x%lx func=0x%lx\n", __func__, ret, extension_id, func_id);ret = SBI_ERR_FAILED; // 强制设为通用错误}/* 更新返回寄存器 */regs->mepc += 4; // 跳过ecall指令(4字节)regs->a0 = ret; // a0 = 状态码// 0.2+规范:额外返回值放在a1if (!is_0_1_spec)regs->a1 = out.value; // a1 = 扩展返回值}return 0; // 固定返回0(即使处理失败)
}
- 3、异常handler注册流程
异常handler是提前在初始化阶段就已经注册好的回调函数,当异常发生时,进行相应的处理,下面以打印接口为例,看下异常handler初始化注册流程:
注册所有支持的 SBI 扩展
int sbi_ecall_init(void)
{int ret;struct sbi_ecall_extension *ext;// 遍历扩展数组并注册每个扩展for (int i = 0; sbi_ecall_exts[i]; i++) {ext = sbi_ecall_exts[i];if (ext->register_extensions) {ret = ext->register_extensions();if (ret) return ret; // 注册失败则退出}}return 0;
}
SBI 扩展定义数组
struct sbi_ecall_extension *const sbi_ecall_exts[] = {&ecall_time, // 时间管理&ecall_rfence, // 远程屏障&ecall_ipi, // 核间中断&ecall_base, // 基础功能&ecall_legacy, // 传统 SBI 0.1 功能 ← 包含打印功能// ... 其他扩展 ...NULL // 结束标记
};
传统 SBI 0.1 扩展注册
static int sbi_ecall_legacy_register_extensions(void)
{// 注册传统扩展return sbi_ecall_register_extension(&ecall_legacy);
}
传统 SBI 0.1 扩展定义
struct sbi_ecall_extension ecall_legacy = {.name = "legacy",.extid_start = SBI_EXT_0_1_SET_TIMER, // 起始扩展ID.extid_end = SBI_EXT_0_1_SHUTDOWN, // 结束扩展ID.register_extensions = sbi_ecall_legacy_register_extensions,.handle = sbi_ecall_legacy_handler, // 核心处理函数
};
传统 SBI 0.1 功能处理
static int sbi_ecall_legacy_handler(unsigned long extid, unsigned long funcid,struct sbi_trap_regs *regs,struct sbi_ecall_return *out)
{int ret = 0;// 根据扩展ID和功能ID分发处理switch (extid) {case SBI_EXT_0_1_SET_TIMER: // 设置定时器sbi_timer_event_start(regs->a0);break;case SBI_EXT_0_1_CONSOLE_PUTCHAR: // 控制台输出 ← 打印功能sbi_putc(regs->a0); // 实际打印字符break;case SBI_EXT_0_1_CONSOLE_GETCHAR: // 控制台输入ret = sbi_getc();break;case SBI_EXT_0_1_CLEAR_IPI: // 清除IPIsbi_ipi_clear_smode();break;case SBI_EXT_0_1_SEND_IPI: // 发送IPI// 处理核间中断发送break;case SBI_EXT_0_1_SHUTDOWN: // 系统关机sbi_system_reset(SBI_SRST_RESET_TYPE_SHUTDOWN, SBI_SRST_RESET_REASON_NONE);break;// 其他传统功能处理...default:ret = SBI_ENOTSUPP; // 不支持的功能}return ret;
}
- 4、异常处理流程
通过前面流程,可以总结下,当异常发生时,opensbi整体的处理流程,如下:
4 裸机固件
4.1 sbi接口封装
- 1、可以参考官方文档来了解下 https://github.com/riscv/riscv-sbi-doc/blob/master/riscv-sbi.adoc
- 2、Linux下提供了整套的实现方案
4.1.1 实现机制
ECALL 系统调用触发方式:
- 在 RISC-V 架构中,S 模式(Supervisor Mode)通过 ECALL 指令请求 M 模式(Machine Mode)提供服务。
- 寄存器用途:
- a6:调用具体功能编号(FID)
- a7:调用分类编号(EID)
- a0-a5:传递调用参数。
- 返回值通过 a0 返回。
中断管理特性:
- S 模式不直接管理定时器中断(Timer)或软件中断(Software IPI),需通过 ECALL 请求 M 模式设置。
- OpenSBI 作为 M 模式固件,提供 SBI(Supervisor Binary Interface)接口实现中断代理和资源管理。
目前版本的服务接口已经很丰富了,下面仅列举些比较common的接口,感兴趣的参考官方spec。
Function Name | FID | EID | Replacement EID |
---|---|---|---|
sbi_set_timer | 0 | 0x00 | 0x54494D45 |
sbi_console_putchar | 0 | 0x01 | N/A |
sbi_console_getchar | 0 | 0x02 | N/A |
sbi_clear_ipi | 0 | 0x03 | N/A |
sbi_send_ipi | 0 | 0x04 | 0x735049 |
sbi_remote_fence_i | 0 | 0x05 | 0x52464E43 |
sbi_remote_sfence_vma | 0 | 0x06 | 0x52464E43 |
sbi_remote_sfence_vma_asid | 0 | 0x07 | 0x52464E43 |
sbi_shutdown | 0 | 0x08 | 0x53525354 |
RESERVED | 0x09-0x0F |
下面也主要在了解官方spec后参考Linux的实现方案来时先sbi接口的封装调用;通过ecall系统调用opensbi,来实现下打印效果。
4.2 链接脚本和启动汇编
这里不在粘贴代码,可以参考我开源的库firmware仓下link.ld和entry.S。
注意启动地址在:0x80200000
。
4.3 代码实现
4.3.1 调用sbi打印服务
根据调用规则给通用寄存器传参:
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);
}
sbi_ecall 通过 ecall 指令触发系统调用,实现从 Supervisor 模式(S 模式)到 Machine 模式(M 模式)的切换
,以请求OpenSBI执行特定服务(比如打印)。
解析:
- 指令:ecall
触发系统调用异常,切换到 M 模式执行 SBI 处理程序。 - 约束:
输出 (:+r):a0 和 a1 是读写操作数(+ 表示输入输出),调用后存储结果。
输入 (:r):a2-a7 是只读输入操作数,传递参数。 - 破坏列表 (:“memory”):
“memory”:告知编译器内存可能被修改(防止编译器错误优化)。
隐含破坏:ecall 会修改寄存器 a0/a1(已在输出约束声明),其他寄存器由 SBI 规范保证不被破坏(如 s0-s11 被调用者保存)。
实际打印函数就通过ecall传递了两个参数:1、串口打印EID 2、打印字符
关于RISCV内联汇编具体语法可以参考:RISC-V汇编学习(五)—— 汇编实战、GCC内联汇编(基于芯来平台)
链接是我之前关于RISCV内联汇编有过介绍,感兴趣可以自行了解学习。
4.3.2 printf实现
sbi_console_putchar单个字符输出打印,我们想要实现字符串或者格式化等打印,实际深入了解过printf接口,会发现这个常用的接口并不是那么好实现;这里也不是我们的侧重点,感兴趣的话,可以随便找个C库学习下。
第三方开源tinyprintf库实现了该功能,仓里有demo可供参考,我这里也是直接借用了,如下:
#include "sbi.h"
#include "tinyprintf/tinyprintf.h"
static void stdout_putc(void *unused,char ch)
{sbi_console_putchar(ch);
}
int main()
{init_printf(0, stdout_putc);sbi_console_putchar('\n');sbi_console_putchar('R');sbi_console_putchar('I');sbi_console_putchar('S');sbi_console_putchar('C');sbi_console_putchar('V');sbi_console_putchar('\n');tfp_printf("hello world\n");while(1) {}
}
只需要封装好stdout_putc,通过init_printf初始化,就可以调用tfp_printf完成打印了。
效果如下:
Boot HART ID : 0
Boot HART Domain : root
Boot HART Priv Version : v1.12
Boot HART Base ISA : rv64imafdc
Boot HART ISA Extensions : zicntr,zihpm,smcntrpmf,zicboz,zicbom,sdtrig,svadu
Boot HART PMP Count : 16
Boot HART PMP Granularity : 2 bits
Boot HART PMP Address Bits : 54
Boot HART MHPM Info : 29 (0xfffffff8)
Boot HART Debug Triggers : 2 triggers
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109RISCV
hello world
5 最后
源码、子模块、makefile、链接脚本等都已经放在了开源仓:git@github.com:liuxiaohan-813/opensbi_baremetal_test.git,具体环境搭建、使用步骤和使用方法可以参考README.md和各级Makefile,想要学习的同学赶快下载入手吧。
另,不足之处请大佬们批评指正。
参考
opensbi下的riscv64裸机系列编程1(串口输出)
riscv-sbi.adoc
RISC-V64 opensbi启动过程
OpenSBI启动流程分析一
OpenSBI 固件代码分析(三): sbi_init.c
OpenSBI 固件代码分析(四):coldboot