最近要在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
  • 4、fw_platform_init
    之后调用fw_platform_init C函数接口,根据解析到的设备树信息,进行平台相关的初始化;

入参是对应a0~a4寄存器,根据sbi汇编规则:

a0: hart_ida1: 设备树地址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_addrnext_mode,将控制权移交下一引导阶段(如U-Boot/Linux)。

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_ecall_init
遍历sbi_ecall_exts
注册ecall_time
注册ecall_rfence
注册...
注册ecall_legacy
sbi_ecall_register_extension
添加到全局扩展表

注册所有支持的 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整体的处理流程,如下:
陷阱处理流程
SBI 调用处理
中断
异常
ECALL
ecall_legacy
CONSOLE_PUTCHAR
硬件自动跳转 MTVEC
CPU 触发 trap
执行 _trap_handler 汇编
保存寄存器上下文
调用 sbi_trap_handler C 函数
判断 trap 类型
中断处理
调用 sbi_ecall_handler
异常分发
查找扩展处理函数
调用 sbi_ecall_legacy_handler
功能分发
调用 sbi_putc
UART 驱动输出字符
操作系统执行 ecall 指令
恢复寄存器
mret 返回
继续执行 ecall 后指令

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 NameFIDEIDReplacement EID
sbi_set_timer00x000x54494D45
sbi_console_putchar00x01N/A
sbi_console_getchar00x02N/A
sbi_clear_ipi00x03N/A
sbi_send_ipi00x040x735049
sbi_remote_fence_i00x050x52464E43
sbi_remote_sfence_vma00x060x52464E43
sbi_remote_sfence_vma_asid00x070x52464E43
sbi_shutdown00x080x53525354
RESERVED0x09-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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/86324.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/86324.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/86324.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python打卡:Day36

复习日 浙大疏锦行

开发过程中的时空权衡:如何优雅地平衡时间与空间效率

文章目录 恒的开发者困境一、理解时间与空间的基本概念1. 时间复杂度2. 空间复杂度 二、时空权衡的基本原则1. 硬件环境决定优先级2. 应用场景决定策略3. 数据规模的影响 三、实际开发中的权衡策略1. 缓存为王&#xff1a;用空间换时间2. 压缩数据&#xff1a;用时间换空间3. 预…

RAG 应用实战指南:从商业目标到系统落地与运营 E2E 实践

专栏入口 前言 在当今信息爆炸的时代&#xff0c;如何高效地从海量数据中提取有用信息并提供智能问答服务&#xff0c;成为众多企业关注的焦点。检索增强生成&#xff08;Retrieval-Augmented Generation, RAG&#xff09;技术以其结合了检索模型的精准性和生成模型的灵活性&a…

关于晨脉的概念解释

晨脉&#xff08;Resting Morning Pulse&#xff09;是指​​人体在清晨清醒后、未进行任何活动前​​&#xff0c;于卧床状态下测量的每分钟脉搏或心率次数。它反映了人体在无运动消耗、无神经干扰时的基础代谢状态&#xff0c;是评估心脏功能、身体恢复情况及运动适应性的重要…

自然语言处理入门

一、概念 自然语言处理&#xff08;Natural Language Processing, 简称NLP&#xff09;是计算机科学与语言中关注于计算机与人类语言间转换的领域。 二、发展史 2012年&#xff1a;深度学习的崛起 Word2Vec的提出&#xff08;Mikolov等&#xff0c;2013年正式发表&#xff0c…

【算法 day12】LeetCode 226.翻转二叉树 |101. 对称二叉树 |104.二叉树的最大深度|111.二叉树的最小深度

226.翻转二叉树 &#xff08;前序&#xff0c;后序&#xff09; 题目链接 | 文档讲解 |视频讲解 : 链接 1.思路&#xff1a; 翻转的是指针&#xff0c;不是数值 前序遍历和后序遍历都可以 中序不行&#xff0c;中序遍历的顺序是左中右,反转左指针后,到根节点&#xff0c;…

Spring Boot 整合 Swagger3 如何生成接口文档?

前后端分离的项目&#xff0c;接口文档的存在十分重要。与手动编写接口文档不同&#xff0c;swagger是一个自动生成接口文档的工具&#xff0c;在需求不断变更的环境下&#xff0c;手动编写文档的效率实在太低。与新版的swagger3相比swagger2配置更少&#xff0c;使用更加方便。…

Rust 的智能指针

在 Rust 中&#xff0c;智能指针是一种特殊的数据结构&#xff0c;它不仅存储数据的地址&#xff0c;还提供了额外的功能&#xff0c;如自动内存管理、引用计数等。智能指针在 Rust 中非常重要&#xff0c;因为它们帮助开发者管理内存&#xff0c;同时保持代码的安全性和效率。…

Redis RDB 持久化:原理、触发方式与优缺点全解析

引言 作为 Redis 最经典的持久化机制之一&#xff0c;RDB&#xff08;Redis DataBase&#xff09;凭借高效的快照生成能力和快速的恢复速度&#xff0c;一直是开发者的心头好。但很多人对它的底层原理、触发时机和适用场景仍存在疑惑。今天咱们就对RDB进行全解析&#xff0c;帮…

设计模式精讲 Day 12:代理模式(Proxy Pattern)

【设计模式精讲 Day 12】代理模式&#xff08;Proxy Pattern&#xff09; 文章内容 在软件开发中&#xff0c;代理模式是一种常见的结构型设计模式&#xff0c;它通过引入一个代理对象来控制对真实对象的访问。这种模式不仅能够增强系统的安全性、灵活性和可扩展性&#xff0c…

企业级知识库私有化部署:腾讯混元+云容器服务TKE实战

1. 背景需求分析 在金融、医疗等数据敏感行业&#xff0c;企业需要构建完全自主可控的知识库系统。本文以某证券机构智能投研系统为原型&#xff0c;演示如何基于腾讯混元大模型与TKE容器服务实现&#xff1a; 千亿级参数模型的私有化部署金融领域垂直场景微调高并发低延迟推…

Qt事件系统详解

一、Qt事件系统概述 Qt事件系统是Qt框架中处理用户输入、窗口交互、定时器、异步操作等机制的核心。所有事件均继承自QEvent类&#xff0c;并通过事件循环&#xff08;Event Loop&#xff09;分发到目标对象。 事件系统基本概念 事件(Event)&#xff1a;描述应用程序内部或外…

CPU性能篇-系统中出现大量不可中断进程和僵尸进程怎么办? Day 05

在上下文切换的文章中&#xff0c;学习并分析了系统 CPU 使用率高的问题&#xff0c;剩下的等待 I/O 的 CPU 使用率&#xff08;以下简称为 iowait&#xff09;升高&#xff0c;也是最常见的一个服务器性能问题。今天就来看一个多进程 I/O 的案例&#xff0c;并分析这种情况。 …

ASP.NET Core + Jenkins 实现自动化发布

一、安装Jenkins 我这边服务器是Linux CentOS 7 &#xff0c;使用SSH 登录云服务器后&#xff0c;输入以下命令安装jenkins. sudo wget -O /etc/yum.repos.d/jenkins.repo \https://pkg.jenkins.io/redhat-stable/jenkins.repo sudo rpm --import https://pkg.jenkins.io/red…

Java项目RestfulAPI设计最佳实践

大家好&#xff0c;我是锋哥。今天分享关于【Java项目RestfulAPI设计最佳实践】面试题。希望对大家有帮助&#xff1b; Java项目RestfulAPI设计最佳实践 超硬核AI学习资料&#xff0c;现在永久免费了&#xff01; 设计一个高效、易维护的 Java 项目中的 RESTful API 涉及到一…

FANUC机器人教程:用户坐标系标定及其使用方法

目录 概述 工作站创建 任务描述 用户坐标系标定方法 用户坐标系标定操作 用户坐标系手动测试 用户坐标系在程序中的应用 用户坐标系选择指令介绍 机器人示教编程 仿真运行 仿真案例资源下载 概述 FANUC机器人的用户坐标系&#xff0c;是用户对每个作业空间定义的直…

动态库与静态库【Linux】

程序编译过程 源代码(.cpp) → 预处理(.i) → 编译(.s) → 汇编(.o) → 链接(可执行文件) g -o main.i -E main.cpp 参数说明&#xff1a; 参数功能输出文件类型-E仅预处理.i-S预处理 编译.s-c预处理 编译 汇编.o无完整流程&#xff08;预处理→编译→汇编→链接&…

MySQL MHA 故障转移-VIP

MHA故障转移-VIP #手工在主库添加VIP ifconfig ens33:1 192.168.80.200/24配置VIP脚本 vim /usr/local/bin/master_ip_failoverchmod x /usr/local/bin/#!/usr/bin/env perl use strict; use warnings FATAL > all;use Getopt::Long;my ( $command, $ssh_user, $orig_mast…

Elasticsearch索引字段的类型

在 Elasticsearch 中&#xff0c;索引字段的类型&#xff08;即 Mapping 中的字段类型&#xff09;对搜索和存储性能影响很大。下面是各种常用数据类型的用途及推荐使用场景总结&#xff1a; 1. keyword 类型&#xff08;精确匹配&#xff09; 适合数据&#xff1a; 不需要分词…

kubernetes证书续签-使用kubeadm更新证书(下)

#作者&#xff1a;任少近 文章目录 查看kubelet证书查看kubelet当前所使用的证书 更换 node上的kubelet证书生成node1所需要的kubelet.conf文件生成node2所需要的kubelet.conf文件查看csr 更新 ~/.kube/config 文件重启相关组件 查看kubelet证书 以上少了kubelet的证书&#…