riscv-4-opensbi
1.riscv的多级启动引导流程介绍
之前的步骤都是板子的一些外设添加以及,上一节写了个简单的固件程序,目前已经实现了板子的cpu,MROM,SRAM,FLASH,DDR,UART和RTC
参考:
三种主要的操作模式,riscv 规范定义了三种主要的操作模式
- U模式(用户模式),运行用户程序的模式,权限级别最低。不能直接访问I/O或特权指令或内核内存或其他进程。
- S模式(管理模式),大多数Linux内核或其他O/S运行的模式。通过I/O remap函数访问最特权的指令和I/O控制。内存管理单元可能打开或关闭。
- M模式(机器模式),机器模式:裸机程序/第一阶段引导加载程序和FSBL(First Stage Bootloader )在此模式下运行。FSBL以固件的形式存在
固件:固件(Firmware)是一种嵌入在硬件设备中的软件,用于控制硬件的功能和操作。它介于硬件和高级软件(如操作系统和应用程序)之间,提供基本的硬件抽象和控制。 通常存在ROM或者EEPROM,例如BIOS 或者 UEFI等,
uboot 可以运行在M模式或者 S模式下, 取决于它是否在SBI的固件初始化之前运行。
SBISupervisor Binary Interface 是 S模式 和SEE之间的调用约定/接口。SEE`Supervisor Execution Environment S执行环境 ,其调用风格就像System call一样。OpenSBI是一个SBI实现,可以在不同的模式下与U-Boot一起使用。opensbiRISC-V Open Source Supervisor Binary Interface- 如下图,ABI 是 应用与 S模式之间的接口约定, SBI 是 S模式与 M模式之间的接口约定

多级启动流程

- 实线箭头代表加载的操作
- 虚线箭头代表跳转操作
流程:
- ROM上的代码负责对电源,时钟进行初始化设置,并且将loader的代码加载到SRAM上并跳转执行LOADER —- 对应到
riscv_setup_rom_reset_vec - LOADER 初始化DDR(其实也是一种ram),然后加载opensbi 固件到ddr,也可以直接跳转到bootloader执行
- 最后bootloader会加载os 并执行

- 每个步骤运行在哪个模式下,
- zsbl
Zero Stage Boot Loader,运行在M模式下,事实上我们在qemu中是通过drive 将固件直接加载到了flash的地方,所以rom上不需要执行加载的操作 - fsbl,运行在M模式下,这里需要加载 opensbi固件,加载设备树,然后跳转opensbi执行
- opensbi,执行在M模式下
- 跳转执行uboot执行在S模式下
- os 运行在 S模式下

内存布局
qemu模拟的riscv中,多核启动的流程都是先多个核竞争一个主核,由主核对共享资源进行初始化,然后其余从核进行自身的初始化
riscv_setup_rom_reset_vec用于设置 RISC-V 处理器的复位向量地址。复位向量是处理器在复位(重启)后执行的第一条指令的地址。该函数通常用于配置处理器的启动过程,确保处理器在复位后从正确的地址开始执行代码。fw_dynamic_info 结构体,这个结构体 包含了下一个阶段程序启动的地址,魔数,下一阶段CPU位于 S模式,初始化完毕后又调用
rom_add_blob_fixed_as函数将fw_dynamic_info拷贝到rom的reset_vec之后,用于下一阶段的启动
此时内存的布局是这样的:

上一章节,将启动固件加载在flash后的内存布局:


2.Opensbi介绍
SBI指的是 Supervisor Binary Interface,运行在 M模式下的程序,操作系统(S模式)通过SBI 来调用M模式的硬件资源 相当于上层系统运行时的系统调用 opensbi是一种开源sbi的实现
FW_PAYLOAD:下一引导阶段被作为 payload 打包进来,通常是 U-Boot 或 Linux。这是兼容 Linux 的 RISC-V 硬件所使用的默认 firmware 。FW_JUMP:不直接包含下一个阶段的代码,跳转到一个固定地址,该地址上需存有下一个加载器。QEMU 的早期版本曾经使用过它。FW_DYNAMIC:带有动态信息的固件,根据前一个阶段传入的信息加载下一个阶段。通常是 U-Boot SPL 使用它。现在 QEMU 默认使用 FW_DYNAMIC。
opensbi源码下载:Releases · riscv-software-src/opensbi (github.com)
下载1.2版本。
当前版本opensbi开发者倾向于不要让ic设计尚加入太多的板级支持代码,因此opensbi本身也需要加载一份设备树文件,opensbi通过解析设备树文件了解soc内部的硬件结构,进而使用标准的驱动代码对其进行配置使用。
因此我们目前可以确认:
- 首先系统从MROM 启动– BL0
- 然后跳转到 flash 的首地址 执行上一节编写的 lowlevelboot 程序 – BL1
- 然后将 flash 上的 opensbi程序和 所需要的设备树资源文件加载到ddr上并跳转执行 – BL2
设备树:设备树是从linux内核中广泛使用的一种设备描述文件,可以简化驱动代码的编写并提高驱动代码的复用率移植性,因此逐渐扩展到各个嵌入式平台级代码项目中
opensbi中fw_base.S汇编文件正是opensbi的启动所在
- .
entry段_start符号即为链接脚本中第一个代码段,上级loader程序加载完成后自然跳转到该地址指令执行。 - 首先启动代码进行判断非boot核心跳转
_wait_for_boot_hart等待,boot核心先进行一次代码_relocate,可以发现如果opensbi如果不在自己的链接地址内运行,则会实现自身代码的拷贝到目标ram上运行,因此可以以类似spl的方式从flash中启动。当然我们因为已经使用了自己编写的loader程序,这段_relocate不会执行, - 之后的流程是
.bss段的清零和SP指针的初始化。接下来就是调用fw_platform_init函数,注意此时传入参数a0——hart id,a1——fdt地址,a2,a3,a4均为上级loader程序的传入参数,这个函数由platform来实现如果不使用则该函数由弱定义空函数来代替,platform函数具体内容我们后面实现时再来看,此处暂时跳过。 - 接下来就是
_scratch_init函数,scratch你可以认为就是另一个sp指针的东西,定义了一片内存用来存放一些数据,同栈一样,先进后出。_scratch_init其实是按顺序写入了sbi下一级程序的地址参数等信息,由工程内的预定于宏指定,其实这里对我们作用不大,因为我们使用设备树文件提供给opensbi来解析得到下一级启动地址等信息。 - 在向下就是
_fdt_reloc,和代码reloc类似,对fdt进行,我们的设计不会执行到这个,最后来到了_start_warm,此时boot核心将标志释放,其余等待在_wait_for_boot_hart的核心也将要跳转到_start_warm。_start_warm针对每一个核心复位寄存器建立自己的栈空间,配置trap异常等完成后调用sbi_init离开汇编代码的世界。

3.Opensbi的移植
需要明确的:
- 采用
opensbi的固件是FW_JUMP,会被加载到DRAM0x80000000处执行 - 需要编写设备树编译将设备树的地址传递给
Opensbi,rom上的fw_dynamic_info用不到 - 需要编写在
flash上运行代码将opensbi的固件加载到DRAM处 然后跳转执行
使用tree -d -L 2 查看源码目录的层次

要为我们的quard_star 板卡 新增opensbi的支持,如下进行操作:
- 在 platform 文件下新建一个 名为
quard_star的文件夹 - 在quard_star文件夹下新增三个文件
Kconfig,objects.mk,platform.c - 在quard_satr文件夹下新增一个文件夹
configs,在configs目录下新建一个名为defconfig的文件
新增完的目录如下:

quard_star/Kconfig
1 | # SPDX-License-Identifier: BSD-2-Clause |
quard_star/objects.mk
配置固件为 jump 已经jump跳转地址
1 | # |
quard_star/configs/defconfig
指定配置需要哪些硬件
1 | CONFIG_PLATFORM_ALLWINNER_D1=y |
quard_star/platform.c
设备树的理解:【Linux内核|驱动模型】设备树的展开unflatten_device_tree - 知乎 (zhihu.com)
fw_platform_init函数,注意此时传入参数a0——hart id,a1——fdt地址,a2,a3,a4均为上级loader程序的传入参数。
函数逻辑:
- 首先,通过解析设备树来获取平台的模型名称(”model” 属性),并将其存储在
platform.name变量中。 - 接下来,在设备树的 “/
cpus” 路径下遍历处理器节点,获取每个处理器的hartid(处理器标识符)。 - 根据获取的
hartid,将其存储在quard_star_hart_index2id数组中,并增加hart_count变量的计数。 - 最后,设置
platform.hart_count变量为hart_count,表示平台上处理器的数量。 - 函数返回 arg1,即原始的设备树指针。
1 | unsigned long fw_platform_init(unsigned long arg0, unsigned long arg1, |
platform_ops 结构体 其结构体类型为
const struct sbi_platform_operations,用于指定平台的相关的操作函数每个成员对应一个平台相关的操作函数,用于在opensbi初始化过程中进行特定的操作和配置,每个函数在相应的阶段被调用,以完成平台相关的初始化,配置和资源管理等工作
拓展:
.early_init使用点号(.)在结构体初始化时指定成员变量的名称是一种称为“指定初始化器”(Designated Initializers)的语法。这种语法允许你在初始化结构体时明确地指出要初始化的成员变量,从而提高代码的可读性和可维护性。
1 | const struct sbi_platform_operations platform_ops = { |
每个函数的定义如下:
1 | static int quard_star_early_init(bool cold_boot) |
platform结构体
1 | struct sbi_platform platform = { |
