OpenSBI作为RISC-V Linux运行在M模式下的固件,其支持三种不同的平台固件类型,今天聊聊它们的区别。
OpenSBI固件类型
在介绍OpenSBI固件类型前,我们需要知道一点RISC-V Linux的启动过程:
启动流程:ZSBL-->FSBL-->OpenSBI-->u-boot-->Linux
ZSBL和FSBL通常固化在芯片内部,不在本文讨论范围,本文关注OpenSBI。
可以看到,在启动uboot之前,需要先启动OpenSBI,然后再执行uboot/linux。而OpenSBI提供了不同的固件类型来处理不同平台早期启动阶段的差异。
OpenSBI简介
OpenSBI为特定平台提供固件构建,支持不同类型的固件来处理不同平台早期启动阶段之间的差异。所有固件将根据平台特定代码以及OpenSBI通用库代码执行平台硬件的相同初始化过程。支持的固件类型在如何处理平台早期启动阶段传递的参数以及如何处理和执行固件之后的启动阶段方面会有所不同。
上一个引导阶段将通过RISC-V CPU的以下寄存器传递信息:
hart id
通过a0
寄存器传递- 通过
a1
寄存器在内存中存储设备树 blob 地址。地址必须与 8 个字节对齐。
OpenSBI 目前支持三种不同类型的固件:
fw_dynamic
固件:带有动态信息的固件fw_jump
固件:指定下一引导阶段的跳转地址,不直接包含下一阶段的二进制代码fw_payload
固件:包含下一引导阶段有效负载的二进制代码,通常这个有效负载是bootloader或者操作系统镜像
FW_DYNAMIC固件
FW_DYNAMIC固件在运行时从上一个启动阶段获取有关下一个启动阶段的信息,例如引导加载程序或操作系统内核。
- 上一个启动阶段(即LOADER)通过
a2
寄存器将struct fw_dynamic_info
的位置传递给FW_DYNAMIC - 之前的启动阶段(即LOADER)需要知道
struct fw_dynamic_info
struct fw_dynamic_info { /** Info magic */ unsigned long magic; /** Info version */ unsigned long version; /** Next booting stage address */ unsigned long next_addr; /** Next booting stage mode */ unsigned long next_mode; /** Options for OpenSBI library */ unsigned long options; unsigned long boot_hart; } __packed;
FW_JUMP固件
FW_JUMP固件假定下一个引导阶段的地址固定,但是不直接包含下一阶段的二进制代码,只是告诉OpenSBI,它运行完后需要到哪个地址执行。
例如,OpenSBI执行完后,需要加载kernel,而Kernel的加载地址为0x80200000
,那么我们指定0x80200000
为需要跳转的地址,OpenSBI执行完毕后,就会跳转到0x80200000
处去加载kernel。
具体例子:
OpenSBI执行完后,去执行uboot或者kernel,假设uboot或者kernel在内存中的地址为0x80200000
,则编译OpenSBI:
make PLATFORM=generic FW_JUMP_ADDR=0X80200000
在编译OpenSBI时,加入参数FW_JUMP_ADDR
,即可指定需要跳转的地址。
注意,由于OpenSBI运行在M模式下,本质上也是一个bootloader,启动时,OpenSBI就会进行一些硬件的初始化操作,加上fw_jump固件可以指定跳转地址,因此对于RISC-V Linux而言,OpenSBI执行完后,可以不执行uboot,直接启动kernel,将uboot去掉,不影响RISC-V Linux正常启动。
在某些内存优化的场景下,可以考虑将uboot去掉,利用fw_jump固件直接启动kernel,从而节省内存。
FW_PAYLOAD固件
FW_PAYLOAD固件直接包含下一引导阶段的二进制代码,下一引导阶段通常是bootloader或os镜像。
不同于FW_JUMP固件的指定地址跳转,FW_PAYLOAD固件是将bootloader或os镜像直接打包进来。
具体例子:
将uboot打包进来,则编译OpenSBI:
make PLATFORM=generic FW_PAYLOAD_PATH=uboot.bin
将Linux kernel打包进来,则编译OpenSBI:
make PLATFORM=generic FW_PAYLOAD_PATH=Image
在编译OpenSBI时,加入参数FW_PAYLOAD_PATH
,即可以将下一引导阶段的二进制代码打包进来。相当于将OpenSBI、uboot、kernel合并为一个文件。
实际中,我们用的更多的是FW_PAYLOAD固件,将uboot和OpenSBI一起编译。
配置和编译
OpenSBI的配置选项位于不同平台下的config.mk
文件,例如platform/generic/config.mk
:
# Blobs to build FW_TEXT_START=0x80000000 FW_DYNAMIC=y FW_JUMP=y FW_PAYLOAD=y ifeq ($(PLATFORM_RISCV_XLEN), 32) # This needs to be 4MB aligned for 32-bit system FW_JUMP_ADDR=$(shell printf "0x%X" $$(($(FW_TEXT_START) + 0x400000))) else # This needs to be 2MB aligned for 64-bit system FW_JUMP_ADDR=$(shell printf "0x%X" $$(($(FW_TEXT_START) + 0x200000))) endif FW_JUMP_FDT_ADDR=$(shell printf "0x%X" $$(($(FW_TEXT_START) + 0x2200000))) ifeq ($(PLATFORM_RISCV_XLEN), 32) # This needs to be 4MB aligned for 32-bit system FW_PAYLOAD_OFFSET=0x400000 else # This needs to be 2MB aligned for 64-bit system FW_PAYLOAD_OFFSET=0x200000 endif FW_PAYLOAD_FDT_ADDR=$(FW_JUMP_FDT_ADDR)
选择编译需要的固件类型:
三种固件,根据自己的需求,在对应的固件类型配置为y:
FW_DYNAMIC
=yFW_JUMP
=yFW_PAYLOAD
=y
默认三种固件类型都编译。
FW_TEXT_START
OpenSBI的运行地址,需要把OpenSBI下载到FW_TEXT_START指定的地址才能运行
FW_JUMP_ADDR
FW_JUMP固件的跳转地址,对于RV32需要4M对齐,对于RV64需要2M对齐
FW_PAYLOAD_OFFSET
FW_PAYLOAD固件的偏移地址,对于RV32需要4对齐,对于RV64需要2M对齐
FW_JUMP_FDT_ADDR
和FW_PAYLOAD_FDT_ADDR
设备树的地址
这里可能有个疑问:为什么跳转地址/偏移地址需要2M对齐或4M对齐?这个跟RISC-V Linux启动时建立的页表有关,需要深入分析才知道。
OpenSBI常用编译选项
选项 | 说明 |
FW_FDT_PATH | 指定设备树的路径,一起编译进来 |
FW_PAYLOAD_PATH | 指定有效负载的路径,一起编译进来,通常是uboot或者kernel |
FW_JUMP_ADDR | 指定下一引导阶段的跳转地址,用于FW_JUMP固件 |
FW_OPTIONS | 控制OpenSBI运行时行为,例如是否开启打印,0x0禁止打印,0x1开启打印 |
更多的信息可以参考OpenSBI官方文档,docs目录下进行了详细的介绍和说明:
不同平台的配置和编译:
https://github.com/riscv-software-src/opensbi/tree/v1.0/docs/platform
不同类型固件的说明:
https://github.com/riscv-software-src/opensbi/tree/v1.0/docs/firmware
总结
学习RISC-V Linux,OpenSBI是必须要知道的,它是RISC-V Linux运行在M模式下的固件,uboot和kernel都运行在S模式,应用程序运行在S模式。
如果钻研RISC-V Linux的内核,那么也会经常和OpenSBI打交道,这就涉及RISC-V Linux和OpenSBI之间的交互。因此OpenSBI有很多可挖掘的地方。
另外,OpenSBI其实是一个裸机代码,也是一个bootloader,相比于uboot,它的代码量比较少,很适合深入分析学习。