前言
在本系列此前的文章中,《开发环境搭建》解决了"写出第一个程序"的问题,《操作系统与软件生态》解决了"选哪个系统"的问题。而两者之间还有一段普遍被忽视的路径:芯片上电之后、内核打印第一行日志之前,发生了什么。
这段路径属于启动流程与 Bootloader。它是系统中最不透明的环节——串口无输出、根文件系统挂载失败等问题的根因大多藏在这里;同时启动时间又是嵌入式产品体验的第一道门槛。本文沿"复位向量 → OpenSBI → U-Boot → Linux 内核"的主线拆解完整链路,全部步骤可在 QEMU 上复现。
一、启动流程全景:四个阶段
RISC-V 架构规范并未规定统一的复位地址(与 x86 的 0xFFFFFFF0、经典 ARM 的 0x00000000 不同),复位向量由 SoC 实现方自行定义。上电后所有硬件线程(hart)从复位向量取指,处于最高特权级 M-mode。
特权级体系直接决定了启动链路的形态:Linux 内核运行于 S-mode,无法直接访问 M-mode 能力,一切时钟、处理器间中断、系统复位操作都必须通过 SBI 接口向 M-mode 固件请求。这使 RISC-V 启动链路天然多出一层,也使 OpenSBI 成为不可绕开的组件。完整链路可概括为四个阶段:
+----------+ +-----------+ +-----------+ +---------------+
| ROM / | | OpenSBI | | U-Boot | | Linux Kernel |
| SPL | --> | (M-mode) | --> | (S-mode) | --> | (S-mode) |
+----------+ +-----------+ +-----------+ +---------------+
- ROM / SPL:最小系统初始化,真实 SoC 上主要是 DDR 训练与初始化;
- OpenSBI:M-mode 固件完成平台初始化,随后切换到 S-mode 交给下一级,自身常驻内存提供运行时服务;
- U-Boot:从存储或网络加载内核与设备树,设置启动参数后移交控制权;
- 内核:初始化、挂载根文件系统、启动 init。
以 QEMU virt 平台为例的典型内存布局:0x1000 为模拟掩膜 ROM,0x80000000 加载 OpenSBI,0x80200000 加载 U-Boot 或内核。理解这一布局是排查"镜像加载到了哪里"类问题的关键。与 ARM 的 TF-A(BL1/BL2/BL31)相比,RISC-V 的启动模型更"组合式"——OpenSBI、U-Boot、内核来自不同社区,通过 SBI 规范与启动协议衔接,任何一级都可独立替换,代价是开发者需要理解各级的职责边界。
二、OpenSBI:M-mode 固件层
OpenSBI 是 RISC-V 事实标准的 M-mode 固件,职责有二:平台初始化,以及实现 SBI 规范定义的运行时服务。内核在启动早期重度依赖 SBI 调用(经 ecall 指令陷入):设置时钟事件、发送处理器间中断、远程启动其他 hart 等。缺少这一层,S-mode 的内核无法管理时间与多核。
OpenSBI 提供三种固件形态:
| 模式 | 下一级入口确定方式 | 适用场景 |
|---|---|---|
fw_jump | 编译时固定(FW_JUMP_ADDR) | 下一级地址固定的系统 |
fw_payload | 将下一级镜像嵌入固件 | 单一镜像交付、产品化 |
fw_dynamic | 运行时由加载器传递 | QEMU、SPL 动态加载,主流选择 |
实战:编译并在 QEMU 上启动验证(Ubuntu 22.04 + riscv64-unknown-linux-gnu- 工具链):
git clone https://github.com/riscv-software-src/opensbi.git
cd opensbi
export CROSS_COMPILE=riscv64-unknown-linux-gnu-
make PLATFORM=generic -j$(nproc)
qemu-system-riscv64 -M virt -m 1G -nographic \
-bios build/platform/generic/firmware/fw_dynamic.bin
正常输出(节选):
OpenSBI v1.5.1
Platform Name : riscv-virtio,qemu
Firmware Base : 0x80000000
Boot HART ID : 0
看到这组信息说明 M-mode 固件层就绪,OpenSBI 正在等待下一级镜像,停留于此是预期行为而非故障。真实 SoC 的适配集中在 platform/ 目录下的平台代码;玄铁处理器的 OpenSBI、U-Boot 与内核适配补丁及 SDK、启动镜像模板可从玄铁资源中心获取,这是真实硬件启动链路能否顺利点亮的第一道分水岭。
三、U-Boot:加载内核并移交控制权
U-Boot 承担"搬运工 + 指挥台"双重角色:从介质加载镜像,并提供串口交互环境允许启动前检查内存、修改设备树、调整参数。QEMU virt 这类内存开箱即用的平台可省略 SPL,由 OpenSBI 直接进入 U-Boot 主体。
实战:编译并以 fw_jump(默认跳转地址恰为 QEMU 的内核加载地址 0x80200000)串联启动:
git clone https://source.denx.de/u-boot/u-boot.git
cd u-boot
make qemu-riscv64_smode_defconfig
make -j$(nproc) CROSS_COMPILE=riscv64-unknown-linux-gnu-
qemu-system-riscv64 -M virt -m 1G -nographic \
-bios ../opensbi/build/platform/generic/firmware/fw_jump.bin \
-kernel u-boot.bin
串口依次出现 OpenSBI 横幅与 U-Boot 提示符 => 即成功。两个高频失败点值得提前知晓:其一,defconfig 的模式区分——qemu-riscv64_smode_defconfig 面向 S-mode(须由 OpenSBI 引导),选错模式是"编译成功但启动失败"的最常见原因;其二,链接地址不符——FW_JUMP_ADDR 与镜像实际加载位置不一致时表现为串口无输出,排查时优先核对这一组地址。
四、设备树与启动参数:贯通固件与内核
设备树是启动链路中的"硬件清单",由固件层一路传递至内核。RISC-V 的关键节点包括:/cpus 下每个 cpu 节点的 riscv,isa 属性(指令集扩展声明)、soc 下的 plic 与 clint(中断子系统初始化的数据来源)、/memory(物理内存范围)以及 /chosen 中的 bootargs 与 stdout-path。大量"内核无输出"问题最终都定位到设备树节点缺失或 compatible 与驱动不匹配。
U-Boot 的 fdt 命令组支持在启动前直接检视与修改设备树,无需重编:
=> fdt addr ${fdtcontroladdr}
=> fdt print /soc/serial@10000000
=> fdt set /chosen/bootargs "console=ttyS0 root=/dev/vda rw"
RISC-V 内核启动协议非常精简:跳转进入内核时,a0 携带 hart ID,a1 指向设备树,U-Boot 的 booti 命令即按此约定移交控制权。调试阶段最有价值的内核参数是 earlycon=uart8250,mmio,0x10000000——它使内核在串口驱动框架就绪之前即具备输出能力,是分辨"内核没跑起来"与"内核在跑但串口配置错误"的第一利器。
实战:用 Buildroot 生成根文件系统并跑通完整链路:
git clone https://gitlab.com/buildroot/buildroot.git
cd buildroot && make qemu_riscv64_virt_defconfig && make -j$(nproc)
# 方式一:QEMU 直接引导(最快路径)
qemu-system-riscv64 -M virt -m 1G -nographic \
-bios output/images/fw_dynamic.bin \
-kernel output/images/Image \
-append "root=/dev/vda rw console=ttyS0" \
-drive file=output/images/rootfs.ext2,format=raw,if=virtio
# 方式二:经 U-Boot 完整链路(贴近真实硬件)
=> setenv kernel_addr 0x80400000
=> dhcp && tftpboot ${kernel_addr} Image
=> setenv bootargs "root=/dev/vda rw console=ttyS0 earlycon=uart8250,mmio,0x10000000"
=> virtio scan
=> booti ${kernel_addr} - ${fdtcontroladdr}
两条路径都应最终停在登录提示符。遇到 VFS: Cannot open root device 类 panic 时,根因集中于三点:root= 参数与实际设备不符、initrd 未加载、内核缺少对应存储驱动。
五、启动优化与调试排错
优化前先测量,三层工具组合:grabserial 为串口逐行打时间戳(成本最低);内核参数 initcall_debug 配合 scripts/bootgraph.py 定位内核初始化耗时大户;U-Boot 的 bootstage report 输出固件层耗时。典型分布为"内核占大头(40%–50%)、固件与 Bootloader 合计占 30%–40%"。常用优化手段:
| 层级 | 手段 | 典型收益 |
|---|---|---|
| SPL | 裁剪介质扫描、固化 DDR 训练参数 | 0.3–0.8s |
| U-Boot | 禁用网络等待与多余总线扫描 | 1–2s |
| U-Boot | Falcon Mode(SPL 直启内核) | 1–3s,牺牲交互能力,仅产品化阶段用 |
| 内核 | 裁剪未用驱动与文件系统类型 | 1–2s |
以某基于玄铁 RISC-V 处理器的边缘计算板卡为例,冷启动至 shell 可用从约 8 秒优化至 2.5 秒:SPL 裁剪为仅保留 eMMC 并固化 DDR 训练参数(1.2s→0.35s);U-Boot 移除 DHCP 等待与 USB/PCI 扫描(2.8s→0.9s);内核依据 initcall 瀑布图裁剪未用协议栈与文件系统(3.5s→1.0s);非关键守护进程改为按需启动(0.5s→0.25s)。该平台的 SDK 与板级参考配置可在玄铁资源中心获取。
排查串口全程无输出时按"越早越好"的顺序:先确认波特率(通常 115200)与串口时钟;QEMU 检查是否遗漏 -nographic;真实硬件用逻辑分析仪确认引脚有无波形——有波形但乱码问题在时钟配置,无波形则问题在 ROM/SPL 阶段。最后一个黑盒手段是源码级单步:QEMU 以 -S -s 启动 GDB stub,用 riscv64-unknown-linux-gnu-gdb 连接后在 OpenSBI 入口(0x80000000)与 U-Boot 入口分别下断点,逐指令观察 mret 特权级切换与 a1 寄存器中的设备树指针传递。真实硬件上等价能力由 JTAG 调试器提供。
结语
RISC-V 启动链路的每一级都通过标准化接口衔接:理解了 SBI 规范、启动协议与设备树的边界,就掌握了拆解与重组整个流程的钥匙。建议的进阶路径:通读 OpenSBI 的 firmware 目录理解 M-mode 到 S-mode 的切换实现,将本文流程移植到一块真实开发板体验 SPL 与 DDR 初始化的完整复杂度,再结合本系列调试优化篇将 GDB 追踪与性能剖分固化为日常工程手段。掌握启动流程之后,从 Bootloader 到操作系统再到上层应用的整条路径,都将清晰可驾驭。
221



被折叠的 条评论
为什么被折叠?



