[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
RISC-V UEFI 架构支持详解,第 1 部分 - OpenSBI/U-Boot/UEFI 简介
Author: Jacob Wang jiangbo.jacob@outlook.com Date: 2022/03/19 Project: RISC-V Linux 内核剖析
前言
从 邮件列表 中找到了一笔 patchset:adds UEFI support for RISC-V。该 patchset 实现 RISC-V 如下引导启动支撑:
Qemu (both RV32 & RV64) for the following bootflow
OpenSBI->U-Boot->Linux
EDK2->Linux
HiFive unleashed using (RV64) for the following bootflow
OpenSBI->U-Boot->Linux
EDK2->Linux
本文尝试对 OpenSBI,U-Boot 和 UEFI 的相关概念以及代码进行基本的解析。
OpenSBI
OpenSBI 项目致力于为 RISC-V 平台特有固件提供 RISC-V SBI 规范的开源参考实现,这类固件运行在 RISC-V M 模式。
OpenSBI 加载过程涉及到的相关概念
RISC-V 架构下有 3 种特权级别,分别是 Machine、Supervisor 和 User,简称 M 模式、S 模式和 U 模式。
- M 模式权限最高,在这个级别下的程序可以访问一切硬件和执行所有特权指令;
- S 模式一般用于运行操作系统,可以设置 MMU 使用虚拟地址;
- U 模式一般是普通应用程序使用,权限最低。
OpenSBI 加载过程
OpenSBI 会经历底层初始化阶段,该阶段主要是准备 C 执行环境;然后在布置好的 C 环境执行设备初始化;最后实现二级引导的跳转。
以下以 OpenSBI v1.0 版本为例,开展源码分析如下。
- OpenSBI 底层初始化
其实现在 /firmware/fw_base.S
, 大体流程如下:
_start:
/* Find preferred boot HART id */ # 判断 HART id
MOV_3R s0, a0, s1, a1, s2, a2d
call fw_boot_hart
add a6, a0, zero
MOV_3R a0, s0, a1, s1, a2, s2
li a7, -1
beq a6, a7, _try_lottery
/* Jump to relocation wait loop if we are not boot hart */
bne a0, a6, _wait_relocate_copy_done
_try_lottery:
/* Reset all registers for boot HART */ # 清除寄存器值
li ra, 0
call _reset_regs
/* Zero-out BSS */ # 清除 BSS 段
lla s4, _bss_start
lla s5, _bss_end
_bss_zero:
REG_S zero, (s4)
add s4, s4, __SIZEOF_POINTER__
blt s4, s5, _bss_zero
/* Setup temporary stack */ # 设置 SP 栈指针
lla s4, _fw_end
li s5, (SBI_SCRATCH_SIZE * 2)
add sp, s4, s5
/*
* Initialize platform
* Note: The a0 to a4 registers passed to the
* firmware are parameters to this function.
*/
MOV_5R s0, a0, s1, a1, s2, a2, s3, a3, s4, a4
call fw_platform_init # 读取设备树中的设备信息
# FDT 重定位
/*
* Relocate Flatened Device Tree (FDT)
* source FDT address = previous arg1
* destination FDT address = next arg1
*
* Note: We will preserve a0 and a1 passed by
* previous booting stage.
*/
...
/* Initialize SBI runtime */
csrr a0, CSR_MSCRATCH
call sbi_init # 底层关键初始化结束,跳转到 sbi_init
...
进入 sbi_init 会首先判断是通过 S 模式还是 M 模式启动
- OpenSBI 设备初始化
进入 sbi_init
,执行 init_coldboot
, init_coldboot
实现在 /lib/sbi/sbi_init.c
,大体初始化工作如下:
static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid)
{
...
# 初始化动态加载的镜像的模块
/* Note: This has to be second thing in coldboot init sequence */
rc = sbi_domain_init(scratch, hartid);
...
# 平台的早期初始化
rc = sbi_platform_early_init(plat, TRUE);
# 控制台初始化,从这里开始,就可以使用串口输出
rc = sbi_console_init(scratch);
# 核间中断初始化
rc = sbi_ipi_init(scratch, TRUE);
# MMU 的 TLB 表的初始化
rc = sbi_tlb_init(scratch, TRUE);
# Timer 初始化
rc = sbi_timer_init(scratch, TRUE);
sbi_printf("%s: PMP configure failed (error %d)\n",
__func__, rc);
sbi_hart_hang();
}
# 准备下一级的 Bootloader
sbi_hsm_prepare_next_jump(scratch, hartid);
- OpenSBI 二级 Bootloader 的跳转
在 init_coldboot
的最后,会跳转到二级 Bootloader,并切换到 S 模式。
static void __noreturn init_coldboot(struct sbi_scratch *scratch, u32 hartid)
{
sbi_hart_switch_mode(hartid, scratch->next_arg1, scratch->next_addr,
scratch->next_mode, FALSE);
}
关于 sbi_hart_switch_mode()
的具体实现以及 next_arg1
, next_addr
等的设定,可能需要分析 lib/sbi/sbi_hsm.c
,待后续再结合 qemu 调试来详细分解。
U-Boot
U-Boot 是一种流行的嵌入式 Linux 系统引导加载程序。
U-Boot 加载过程涉及的相关概念
SRAM
掉电易失(失去电源供电后 SRAM 里存储的数据不存在了),可以随意 读写 数据。(容量小,程序运行速度快,价格高,一般在 SoC 里面)
SDRAM
掉电易失(失去电源供电后 SDRAM 里存储的数据不存在了),上电后没有初始化 DDR 控制器,无法进行数据读写。
SPL
U-Boot 分为 uboot-spl 和 uboot 两个组成部分。SPL 是 Secondary Program Loader 的简称,第二阶段程序加载器。
这里所谓的第二阶段是相对于 SOC 中的 SRAM 来说的,SOC 启动最先执行的是固化的程序。ROM 会通过检测启动方式来加载第二阶段 bootloader。
U-Boot 已经是一个 bootloader 了,那么为什么还多一个 U-Boot SPL 呢?
主要原因是对于一些SOC来说,它的内部 SRAM 可能会比较小,小到无法装载下一个完整的 U-Boot 镜像,那么就需要 SPL。它主要负责初始化外部 RAM 和环境,并加载真正的 U-Boot 镜像到外部 RAM 中来执行。所以由此来看,SPL 应该是一个非常小的 loader 程序,可以运行于 SOC 的内部 SRAM 中,它的主要功能就是加载真正的 U-Boot 并运行。
U-Boot 加载过程
嵌入式系统的 SOC 内部会有比较小的 SRAM,而外部的一般会有 DDR 或者 SDRAM,后面的 RAM 就是外部 RAM; SPL 会先被加载到 SRAM 中,然后初始化 DDR 或者 SDRAM,总之会初始化外部的 RAM,然后再把主 U-Boot 加载到 RAM;
如下图所示:
上图解释如下:
- 图中(1)是 SPL 在 U-Boot 第一阶段的装载程序,初始化最基本的硬件,比如关闭中断,内存初始化,设置堆栈等最基本的操作,设置重定位;
- 图中(2)是会装载主 U-Boot 程序,然后初始化其他板级硬件,比如网卡,NAND Flash 等待,设置 U-Boot 本身的命令和环境变量;
- 图中(3)是加载 Kernel 到 RAM,然后启动内核。
+--------+----------------+----------------+----------+
| Boot | Terminology #1 | Terminology #2 | Actual |
| stage | | | program |
| number | | | name |
+--------+----------------+----------------+----------+
| 1 | Primary | - | ROM code |
| | Program | | |
| | Loader | | |
| | | | |
| 2 | Secondary | 1st stage | U-Boot |
| | Program | bootloader | SPL |
| | Loader (SPL) | | |
| | | | |
| 3 | - | 2nd stage | U-Boot |
| | | bootloader | |
| | | | |
| 4 | - | - | kernel |
| | | | |
+--------+----------------+----------------+----------+
所以通常启动顺序是:ROM code –> SPL –> u-boot –> kernel。
U-Boot 代码分析
U-Boot 其启动过程主要可以分为两个部分,Stage1 和 Stage2 。其中 Stage1 是用汇编语言实现的,主要完成硬件资源的初始化。而 Stage2 则是用 C 语言实现。
以下以 U-boot v2022.04 版本为例,开展源码分析如下。
第一阶段路径位于 arch/riscv/cpu/start.S
文件。
第二阶段启动阶段中, 会重点涉及到 位于 common/board_r.c
的 board_init_f 函数和位于 common/board_r.c
文件的的 board_init_r 函数。board_init_f 会初始化必要的板卡和 global_data 结构体,然后调用 board_init_r 进行下一阶段的板卡初始化工作。board_init_r 运行结束之后, 会调用位于 common/main.c
的 main_loop() 函数进行。来重点看一下这三个函数。
- board_init_f 分析
static const init_fnc_t init_sequence_f[] = {
...
setup_mon_len,
initf_bootstage, /* uses its own timer, so does not need DM */
event_init,
...
setup_spl_handoff,
arch_cpu_init, /* basic arch cpu dependent setup */
mach_cpu_init, /* SoC/machine dependent CPU setup */
initf_dm,
...
env_init, /* initialize environment */
init_baud_rate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_options, /* say that we are here */
display_text_info, /* show debugging info if required */
checkcpu,
...
INIT_FUNC_WATCHDOG_RESET
/*
* Now that we have DRAM mapped and working, we can
* relocate the code and continue running from DRAM.
*
* Reserve memory at end of RAM for (top down in that order):
* - area that won't get touched by U-Boot and Linux (optional)
* - kernel log buffer
* - protected RAM
* - LCD framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr,
...
do_elf_reloc_fixups,
...
clear_bss,
};
void board_init_f(ulong boot_flags)
{
...
if (initcall_run_list(init_sequence_f))
hang();
...
}
- board_init_r 函数
static init_fnc_t init_sequence_r[] = {
initr_trace,
...
initr_caches, # 使能 cache 接口
initr_reloc_global_data, # 设置 monitor_flash_len
...
initr_barrier,
initr_malloc,
log_init,
initr_bootstage, /* Needs malloc() but has its own timer */
...
board_init, /* Setup chipselects */
...
pci_init,
...
cpu_init_r,
...
};
# board_init_r 主要执行 init_sequence_r 中一系列初始化函数
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
...
if (initcall_run_list(init_sequence_r))
hang();
...
}
- main_loop 函数
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
# 设置 U-boot 的版本号
if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
env_set("ver", version_string); /* set version variable */
# 命令初始化,初始化 shell 相关的变量
cli_init();
# 获取环境变量 perboot 的内容
if (IS_ENABLED(CONFIG_USE_PREBOOT))
run_preboot_environment_command();
...
# 读取环境变量 bootdelay 和 bootcmd 的内容
# 然后将 bootdelay 的值赋值给全局变量 stored_bootdelay
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
# 检查倒计时是否结束
autoboot_command(s);
# 命令处理函数,负责接收好处理输入的命令
cli_loop();
panic("No CLI available");
}
UEFI
大概 20 多年的发展和积累中,UEFI 的代码量已经很庞大,相关标准白皮书也有很多
1999 年:EFI 1.0 推出
2000 年:EFI 1.02 发布
2002 年:EFI 1.10 发布
2006 年:UEFI 2.0 发布
2007 年:UEFI 2.1
2008 年:UEFI 2.2
剥离开技术细节,引导软件基本做的就是初始化硬件和提供硬件的软件抽象,并完成引导启动。
启动阶段搞来搞去基本就三个步骤:
Rom 阶段
该阶段没有内存,没有 C 语言运行需要的栈空间,开始往往是汇编语言,直接在 ROM 空间上运行。在找到临时空间后( Cache As Ram, CAR),C 语言可以使用,然后开始用 C 语言初始化内存。
Ram 阶段
这个阶段有大内存可以使用。开始会初始化芯片组、CPU、主板模块等等核心过程。
引导 OS 阶段
枚举设备,发现启动设备,并把启动设备之前需要依赖的节点统统打通。移交工作,Windows 或者 Linux 的时代开始。
UEFI 与硬件及 OS 的关系
EFI 在过程中会提供运行时服务和 EFI 引导时服务; (引导时服务只在 boot time 可用,runtime service 在引导后面的 OS 后还是可以继续被使用的)
如上图所示,EFI 在过程中会提供运行时服务和 EFI 引导时服务,其中引导时服务只在 boot time 可用,runtime service 在引导后面的 OS 后还是可以继续被使用的。
下图是基本的引导过程,文章后面会更详细地展开这部分。
UEFI 的引导流程
UEFI 启动过程遵循 UEFI 平台初始化(Platform Initialization)标准,共经历了七个阶段。
7 个阶段分别为 SEC (Security)
,PEI (Pre-EFi Initalization)
,DXE (Driver Execution Environment)
,BDS (Boot Device Selection)
,TSL (Transient System Load)
,RT (Runtime)
,AL (After Life)
。前三个阶段是 UEFI 初始化阶段。 下面介绍一下每个阶段的主要任务;
SEC 阶段
处理系统上电或重启;创建临时内存;提供安全信任链的根;传送系统参数到下一阶段。
PEI 阶段
依次进行平台的初始化,如 CPU,内存控制器,I/O 控制器,以及各种平台接口。
DXE 阶段
该阶段执行系统初始化工作,为后续 UEFI 和操作系统提供了 UEFI 系统表、启动服务和运行时服务。
BDS 阶段
复现每个启动设备,并执行启动策略。如果 BDS 启动失败,系统会重新调用 DXE 派遣器,再次进入寻找启动设备的流程。
TSL 阶段
临时系统,是 UEFI 将系统控制权转移给操作系统前的一个中间状态。
RT 阶段
UEFI 各种系统资源被转移,启动服务不能再使用,仅保留运行时服务供操作系统使用。
AL 阶段
这个阶段的功能一般由厂商自定义。
EDK2
EDK2 是一个现代、功能丰富的跨平台固件开发环境,适用于 UEFI 相应编译和调试工作。在 Linux 平台上,可以结合 QEMU+GDB 进行相应调试。
引导程序生态系统
引导程序生态系统除了 UEFI,还有 Coreboot/Libreboot 等组件。各个组件的组合和流动很灵活。
Coreboot 引导阶段与 UEFI 对比
UEFI 的 PEI 和 DXE 阶段,分别对应 Coreboot 的 romstage 和 ramstage 阶段。
下图比较形象的对比了两者的区别:
参考文档
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- TinyBPT 和面向 buildroot 的二进制包管理服务(3):服务端说明
- TinyBPT 和面向 buildroot 的二进制包管理服务(2):客户端说明
- TinyBPT 和面向 buildroot 的二进制包管理服务(1):设计简介与框架
- RISC-V Linux 内核及周边技术动态第 118 期
- RISC-V Linux 内核及周边技术动态第 117 期