RISC-V启动流程详解:从Reset Vector到Bootloader与内核加载 如果你也跟我一样这几年从 ARM 或纯 MCU 开发转到 RISC-V第一件需要适应的往往不是指令集本身而是启动流程。无论你怎么折腾上电之后从 Reset Vector 到 Bootloader再到内核加载这一整条路径都带着“约定大于标准”的味道。RISC-V 规范本身没有规定芯片该怎么做 Bootloader没有像 x86 那样强制要求 BIOS/UEFI也没有像 ARM 那样有固件接口标准来约束顶层设计。它只给了一堆可裁剪的指令集和特权模式剩下的全交给芯片厂商和软件生态自己拼。这篇文章我想把这条链路上的内容完整拆开讲从 CPU 复位后的第一条指令到 Bootloader 把内核镜像搬进内存并跳转进去覆盖每个阶段的职责、原理和常见坑。适合三类人看一是刚接触 RISC-V 想做裸机或 RTOS 移植的开发者二是本来做 ARM Linux 或 U-Boot、想切到 RISC-V 平台的系统工程师三是正在学习嵌入式底层、想弄清楚“上电之后到底发生了什么”的学生。我尽量不堆空话结合代码路径和实际调试经验来讲。1. 从“上电”说起RISC-V 启动链路先要理清的几个基本概念1.1 RISC-V 没有统一启动规范但底层逻辑惊人一致不少刚接触 RISC-V 的朋友会先去找“标准启动流程”翻了一圈发现每个厂商差异很大容易懵。实际上 RISC-V 更接近一种“指令集 特权架构”的约定它只规定机器上电后进入 Machine Mode从某种 Reset Vector 开始执行却不说这个 Reset Vector 到底指向哪块存储、由谁来初始化 DDR、由谁来加载系统镜像。但这并不代表没有规律。仔细观察主流的 RISC-V SoC比如 SiFive、StarFive、Allwinner、平头哥等芯片的公开资料会发现它们本质上都沿着一条相似链路在走CPU 复位后从芯片内部 ROM 或固定地址取指执行 Mask ROM 代码。Mask ROM 初始化最基本的外设比如 SPI、SDIO 或 UART从外部介质加载一小段引导代码到 SRAM。这一小段引导代码负责初始化 DDR并把完整的 Bootloader比如 U-Boot搬到内存里。Bootloader 从存储介质读取内核镜像解析设备树准备好启动参数跳转进入内核。内核完成剩余初始化挂载根文件系统最终启动用户态程序。就算芯片不同这条链路里要解决的问题也高度一致芯片内部 SRAM 太小放不下完整引导程序DDR 需要软件初始化不能用一条普通的内存访问指令直接操作外部存储介质五花八门必须由一段可配置的代码来统一抽象。理解了这些你再看各家芯片的 Boot ROM 实现基本就能举一反三。1.2 Reset Vector、Hart 和 Machine Mode上电后的真实世界RISC-V 架构里有个容易混淆的概念CPU 上电后并不是运行在你熟悉的 Linux 或 RTOS 环境里而是运行在一个“什么都要自己管”的 Machine Mode。x86 上电后先跑固件固件再切到保护模式ARM 上电先执行 BootROM再切到 EL2/EL1。RISC-V 的做法类似只是名字叫 Machine Mode通常简写为 M Mode。上电瞬间 PC 被设置到某个硬件固定的地址这就是 Reset Vector。具体地址由芯片设计者决定有的芯片把它放在片内 SRAM 的起始地址有的放在 Flash 映射区。比较极端的例子是某些低成本 MCU 级 RISC-V 芯片Reset Vector 直接指向 Flash 地址 0x00000000而运行 Linux 的应用处理器通常会把 Reset Vector 放在芯片内部 ROM 区域比如 0x00001000 这类位置。还一个术语必须弄明白Hart。RISC-V 把每个独立的硬件执行线程称为 Hart类似 ARM 里的 Core。多核芯片上电后所有 Hart 可能同时从 Reset Vector 开始执行。这时候 Bootloader 里要有一个“选主”的过程通常约定 Hart 0 作为主核负责初始化内存、加载镜像其它 Hart 进入 WFI 等待或者轮询一个内存标志位等主核准备好了再被唤醒。很多刚上手的人在这里翻车所有核一起往下跑结果多个核同时初始化 DDR导致总线异常或数据竞争。在特权模式方面RISC-V 定义了 M Mode、S Mode 和 U Mode。Linux 内核运行在 S Mode用户态程序运行在 U Mode而 M Mode 运行着 OpenSBI 或厂商自定义固件。从 S Mode 切换到 M Mode靠的是ecall指令这其实是一种“陷入”操作例如内核想要关机或读取时钟就需要通过 SBI 调用进入 M Mode 让固件来处理。理解了这三层权限关系后面看 OpenSBI 和 U-Boot 的分工就顺理成章了。1.3 链接地址和加载地址不一致是常态别用 MCU 裸机思维硬套做裸机 STM32 开发时很多人习惯把程序链接到 0x08000000因为芯片就是从 Flash 起始地址开始执行的链接地址和加载地址天然一致。到了 RISC-V Linux 类平台这个习惯非常危险。芯片上电时Mask ROM 会从 Flash 或 SD 卡里读取一段引导代码到 SRAM这段代码可能是“位置无关”的不需要放到固定地址也能跑。但 Bootloader 主程序通常会被链接到 DDR 的高地址区域比如常见平台把 U-Boot 链接到 0x80200000 附近。问题在于DDR 此时可能还没有初始化代码必须先待在 SRAM 里运行一小段直到完成 DDR 控制器的初始化才能跳转到 DDR 里的 U-Boot 主程序。也就是说同一份代码可能有两个地址一个是“它实际存放在哪儿”另一个是“它编译时认为自己在哪儿”。如果编译时用的链接地址和实际运行地址不一致代码里所有绝对跳转、全局变量访问都会崩。很多刚入门 RISC-V 的朋友编译了一个最小 Bootloader下载到板子上发现完全没有输出排查半天才发现是链接脚本里指定的地址和芯片实际加载地址对不上。2. Bootloader 体系分级Boot ROM、FSBL、OpenSBI 到底各自在干嘛2.1 为什么不能把整个 U-Boot 直接烧进 Mask ROM既然最终要运行 U-Boot能不能把 U-Boot 直接放在芯片内部 ROM 里省去前面那堆跳转步骤理论上可以实际操作中有两个绕不开的问题。第一是芯片内部 ROM 容量极小。RISC-V 应用处理器的 Boot ROM 通常只有几十 KB更小的 MCU 可能只有几 KB。U-Boot 自带网络协议栈、文件系统、显示驱动等一堆功能编译出来动辄几百 KB根本塞不进去。第二是 ROM 是芯片出厂时固化的用户改不了。芯片厂商不可能替所有客户决定用什么 Bootloader、支持哪些启动介质。所以厂商只会在 ROM 里放一段极简代码初始化最基本的外设然后去外部存储里读下一级引导程序。这种做法和你在 STM32 上做 IAP 有点相似出厂 Bootloader 先跑起来检查有没有升级请求没有就跳转到用户 App。RISC-V 只是把这条链拉得更长了分出了更多层级。笼统地说芯片内部的 Mask ROM 相当于“一级引导”它从 Flash/SD/eMMC 里加载的下一段程序相当于“二级引导”之后的 U-Boot 主程序、内核、根文件系统依序接力。2.2 一张表看清 ZSBL、FSBL、SSBL、OpenSBI 的职责差异不同厂商文档里术语不一致有的叫 ZSBL、FSBL、SSBL有的直接叫 Bootloader、U-Boot SPL、U-Boot Proper。本质上还是同一套分层逻辑。我按照常见叫法整理了一个对应关系方便你对照。层级常见名称运行的存储介质主要职责典型实现第 0 级Mask ROM / BootROM芯片内部 ROM初始化时钟、最小外设接口从外部介质读 FSBL芯片厂商固化第 1 级ZSBL / FSBLSRAM 或内部 RAM初始化 DDR验证并加载下一级镜像厂商代码或 OpenSBI第 2 级SSBL / U-Boot ProperDDR加载内核、设备树、initramfs提供命令行交互U-Boot、UEFI第 3 级OS Loader 或内核自解压DDR解压/启动内核切换到虚拟内存管理Linux 内核、RTOS这里特别要提一下 OpenSBI。它本身不完全是 Bootloader更像是一个运行在 M Mode 的固件服务层。Linux 内核运行在 S Mode不能直接操作 M Mode 才允许访问的寄存器或硬件资源比如定时器、IPI、电源管理等。内核遇到这些需求时会通过 SBI 陷入 M Mode让 OpenSBI 帮忙完成。所以现代 RISC-V Linux 平台的典型启动组合是Boot ROM 加载 OpenSBIOpenSBI 初始化硬件后跳转到 S Mode 的 U-BootU-Boot 再加载内核。2.3 为什么 OpenSBI 成了 RISC-V 生态的事实标准刚开始接触 RISC-V 启动时我也觉得奇怪为什么非得插一个 OpenSBI 进来直接把 U-Boot 运行在 M Mode加载内核后切到 S Mode 不也行吗技术上确实有厂商这么干但这里有个深层问题权限边界的清晰化。Linux 内核假设自己运行在 S Mode如果某个版本的 U-Boot 把所有初始化做完了跳转时却忘记正确切模式内核一路跑下去就会出问题。OpenSBI 把这一层标准化了它负责接管 M Mode 的底层资源对外提供统一的 SBI 调用接口。无论你用的是 Linux、BSD 还是 RT-Thread只要遵循 SBI 约定就能在同一个 OpenSBI 之上运行。另一个考虑是可移植性。RISC-V 规范对中断控制器、定时器、串口这些硬件没有做统一规定芯片厂商可以自由发挥。OpenSBI 相当于在硬件差异和操作系统之间垫了一层适配层让内核开发者不必关心每个芯片的底层细节。CPU 厂商要适配新的 RISC-V 芯片通常是写 OpenSBI 的 platform 支持而不是去改内核核心代码。理解这一点你就明白那条启动链上每个角色为什么都不可替代。3. U-Boot 在 RISC-V 平台上的启动流程拆解3.1 为什么 RISC-V 平台普遍使用 U-Boot而不是自定义 Bootloader做 RISC-V Linux 移植绕不开 U-Boot。原因也很朴素U-Boot 在 ARM 生态里打磨了十几年已经具备设备树解析、网络引导、文件系统、内存读写、环境变量管理等一系列成熟功能。RISC-V 架构的代码在 U-Boot 里维护得越来越好与其重复造轮子不如直接在现有框架里添加 RISC-V 支持。不过和 ARM 平台相比RISC-V 上 U-Boot 的启动模式有一个显著特点非常依赖上一层固件。如果上一级是 OpenSBIU-Boot 会被引导到 S Mode 运行如果芯片没有 OpenSBIU-Boot 也可以主动运行在 M Mode自己完成所有底层初始化。这种灵活性当然是好事但也带来一个问题不同启动模式下U-Boot 能访问的资源范围不同遇到启动异常时排查难度也更高。我一般建议优先采用“OpenSBI U-Boot S Mode”的组合而不是让 U-Boot 直接跑在 M Mode。原因在于Linux 内核生态对 S Mode 下的引导路径测试更充分出问题时上游社区更容易定位。如果做产品化量产还可以把 OpenSBI 和 U-Boot 打包成一个固件镜像简化烧录流程。3.2 SPL 与 U-Boot Proper解决片上 SRAM 装不下的问题如果你熟悉 ARM 平台的 U-Boot应该听过 SPL 和 U-Boot Proper 的说法。RISC-V 也沿用这套机制原因和 ARM 一模一样DDR 还没有初始化之前U-Boot 主程序根本没地方放。CPU 只能先执行存放在片内 SRAM 或内部 ROM 里的一段小代码也就是 SPLSecondary Program Loader。SPL 是 U-Boot 源码树的同一个项目编译出来的一个精简版本只保留初始化 DDR、读取下一级镜像、跳转这些基础功能。它通常被链接到片内 SRAM 的某个固定地址由芯片 Boot ROM 或上一级 BSBL 加载并跳转进入。SPL 完成 DDR 初始化后会把 U-Boot Proper完整的 U-Boot从存储介质拷贝到 DDR 里的指定位置然后执行跳转。很多刚接触的开发者不理解为什么 U-Boot 编译一次会生成好几个二进制原因就在这里。常规的 U-Boot 编译流程会根据配置生成u-boot.bin、u-boot-spl.bin、u-boot.itb等不同产物。实际烧录时需要结合你所用芯片的启动要求选择正确的那一个。如果芯片手册明确说 Boot ROM 只会从 SD 卡固定偏移位置加载一段不超过几百 KB 的镜像那你需要烧录的是 SPL而不是体积庞大的 U-Boot Proper。3.3 U-Boot 启动路径里的关键节点从复位到命令行进入 U-Boot 源码后RISC-V 的启动入口一般在arch/riscv/cpu/start.S。这段代码要做的事情和你在 ARM 里看到的start.S很像但细节上有不少差异。CPU 复位后先进入 start.S它要做的第一件事往往是保存 a0/a1 寄存器中的内容。这里的原因比较特殊在 ARM 平台引导加载器向内核传递参数通常依靠特定内存地址或设备树但在 RISC-V 启动约定中上一级固件会通过寄存器向下一级传递信息。比如从 OpenSBI 跳转到 U-Boot 时a0 代表当前 Hart IDa1 代表设备树地址。如果 start.S 一上来就用掉了这些寄存器后续就拿不到设备树指针了所以必须先保存。start.S 还会初始化栈指针和全局指针。RISC-V 的 GP 寄存器通常被用来寻址小范围内的全局变量编译时由链接脚本决定这个指针放在哪里。启动阶段要特别注意是否启用了位置无关代码。如果没有启用位置无关起始代码必须运行在链接地址上否则全局变量访问会指向错误的内存区域。随后代码会经历板级初始化、堆栈准备、设备树解析、驱动模型初始化、存储设备初始化等阶段。等到串口驱动初始化完成你会在终端上看到 U-Boot 的启动日志接着进入命令行交互环境。在这个阶段U-Boot 会读取环境变量执行 bootcmd 里定义的启动命令把控制权转交给内核。3.4 你经常配置的几个参数TEXT_BASE、LOAD_ADDR 与设备树地址玩 U-Boot 的人都会天天和几个地址参数打交道。我最早移植的时候对这些参数理解不深导致反复踩坑。CONFIG_SYS_TEXT_BASE是 U-Boot 主程序的链接基地址也就是它认为自己会被加载到内存的哪个位置。如果这个值和你的启动介质加载地址不一致U-Boot 可能会在执行早期就崩溃甚至不打印任何信息。CONFIG_SYS_LOAD_ADDR则是 U-Boot 用来暂存下载数据比如 TFTP 加载内核的内存地址它必须指向一块 bootloader 不会覆盖的空闲内存区域。这两个地址往往和内核加载地址一起被安排在内存映射表中形成一套固定的布局。比如一个典型 64 位 RISC-V Linux 平台U-Boot 链接在 0x80200000内核镜像加载到 0x82000000设备树临时存放地址可能落在 0x88000000。如果定义重叠轻则内核启动参数被覆盖重则镜像被毁坏启动失败。设备树地址尤其容易被忽略。上一级固件通过寄存器或固定内存位置把设备树地址传给 U-Boot 后U-Boot 会把指针存在全局数据结构中。后面执行booti命令时如果环境变量里没有显式指定设备树地址U-Boot 就会使用这个内部指针。但问题是U-Boot 可能为了某种需要移动设备树的位置如果你直接从环境变量写了错误的地址内核解析设备树时会遇到各种诡异问题。4. 从 Bootloader 到内核加载最后一跳的细节决定成败4.1 Linux RISC-V 引导协议a0、a1 寄存器不是随便传的Bootloader 做了这么多事最终目的就是干净地跳进内核。在 RISC-V 平台上这一步不是简单的jalr跳转地址而是要完全遵守 Linux 内核文档里定义的引导协议。简单来说当 U-Boot 跳转到 Linux 内核入口时CPU 必须运行在 S Mode并且 a0 寄存器要保存当前 Hart IDa1 寄存器要保存设备树二进制文件DTB所在的物理地址。内核入口代码会读取这两个值用来确定当前运行在哪颗核上以及设备树在哪里。如果 a0 传错了内核在多核启动时可能无法正确识别主核如果 a1 传错了内核解析不到设备树连内存大小都不知道启动会直接卡在一个很早期的阶段。很多你看到的“卡死在 Starting kernel ...”现象根因往往不是跳转本身有问题而是跳转前的寄存器或模式状态不对。跳转前还要确认页表已经关闭或处于禁用状态。U-Boot 运行阶段可能打开过 MMU跳进内和前段分页功能必须没有开启或者处于内核可接受的状态。RISC-V 里这个过程通常涉及satp寄存器跳转代码必须确保它没有残留旧地址映射。如果这块没处理干净内核启动后会立刻遇到页表错误。4.2 内核镜像格式与解压Image、Image.gz 和自解压流程很多刚接触 ARM 的朋友知道内核镜像常见格式是 zImage 或 Image到了 RISC-V 平台名字会有点变化。RISC-V Linux 内核编译出来通常生成Image也就是没有压缩的 ELF 转成的裸二进制格式符合引导协议要求可以直接被跳转执行。为了减小体积还会生成Image.gz压缩格式。启动压缩镜像时内核入口处有一段自解压代码它会把自己解压到某个内存地址再跳转到真正内核入口。也就是说即便你加载的是压缩内核最终执行它时仍然要经历一次“搬运再跳转”。U-Boot 的booti命令支持直接加载 Image 格式而bootm命令通常配合 FIT 镜像或带头部的镜像使用。如果搞混了出现“Unsupported image format”的报错是常有的事。内核镜像要加载到哪个地址也很有讲究。以常见的 64 位平台为例Linux 内核通常要求 Image 被加载到 RAM 起始地址之后的某个对齐偏移处常见约定是 2MB 对齐例如 0x80200000。这个地址要和设备树里描述的内存区域一致否则内核解压时会把自己覆盖掉或者解压后找不到关键代码。4.3 设备树在这个阶段发挥什么作用设备树是 U-Boot 和内核之间最重要的交接物之一。Bootloader 运行阶段已经初始化了串口、MMC 控制器、网络接口等设备但这些信息怎么告诉内核答案就是设备树。U-Boot 会把当前板级对应的 DTB 放在约定内存位置并把地址通过 a1 寄存器传给内核。内核通过解析 DTB 来了解内存布局、外设地址、中断控制器类型、时钟频率等基础信息。这里面有一个细节值得注意U-Boot 修改设备树的情况很常见。比如你在 U-Boot 命令行里设置了bootargsU-Boot 在跳转前可能要把启动参数写入设备树的/chosen节点。如果设备树被放在一个只读区域或被先前代码占用U-Boot 会把它重新拷贝到一个可读写的内存地址再修改并传递下去。因此如果内核打印里显示内存节点错误或者启动参数没生效不妨怀疑一下设备树地址是否被正确传递。除此之外你可能还会遇到内核启动时提示 “No FDT found” 或者 “FDT address is invalid”这说明内核没有在预期的寄存器或内存位置找到合法的设备树。排查思路不外乎两点一是 Bootloader 跳转前有没有正确保存和传递 a1二是 DTB 本身是否被正确加载到内存且未被覆盖。绝大多数这类问题都出在这两个环节之间。5. 用 QEMU 完整复现一次 RISC-V 上电启动从 OpenSBI 到 U-Boot 再到内核5.1 为什么先用 QEMU 做实验最划算直接上手开发板做启动调试不是不行但成本很高你需要一款支持 RISC-V 的开发板需要确认 Boot ROM 逻辑需要准备烧录工具还会消耗大量时间在硬件问题上。相比之下QEMU 提供了一个非常适合学习的虚拟平台virt它实现了足够标准的 RISC-V 硬件模型能够运行 OpenSBI、U-Boot 和 Linux 内核非常适合先把启动流程完整跑通。用 QEMU 的好处很明显你可以随时修改固件代码、重新编译、重启模拟器整个过程可控可重复。而且 QEMU 会把 OpenSBI 打印的内容、U-Boot 日志以及内核启动日志按顺序输出到终端正好对应文章前面讲的三段接力过程。第一次在这上面完整跑通启动后你再看真实芯片的启动日志就非常有画面感了。5.2 编译 OpenSBI 和 U-Boot并让它们在 QEMU 上跑起来在开始之前你需要准备一个 Linux 环境并且安装 RISC-V 交叉编译工具链。以 Ubuntu 为例通常需要gcc-riscv64-linux-gnu、qemu-system-misc以及必要的构建工具。然后按照下面的步骤操作。先编译 OpenSBI。下载 OpenSBI 源码后在源码根目录执行make CROSS_COMPILEriscv64-linux-gnu- PLATFORMgeneric这会生成build/platform/generic/firmware/fw_payload.elf和fw_payload.bin。fw_payload的含义是把一个指定的下一级镜像直接打包进 OpenSBI方便我们一次性启动。接下来编译 U-Boot。进入 U-Boot 源码目录选择一个 RISC-V 64 位的 QEMU 配置make qemu-riscv64_smode_defconfig make CROSS_COMPILEriscv64-linux-gnu- -j$(nproc)编译完成后会生成u-boot.bin。为了让 OpenSBI 在完成初始化后自动把控制权交给 U-Boot可以在编译 OpenSBI 时指定 payload 路径make CROSS_COMPILEriscv64-linux-gnu- PLATFORMgeneric FW_PAYLOAD_PATH/path/to/u-boot.bin最后用 QEMU 启动qemu-system-riscv64 -M virt -m 2G -nographic -bios fw_payload.elf如果一切顺利你会先看到 OpenSBI 打印板级信息接着 U-Boot 启动日志出现最后停在 U-Boot 命令行提示符。这一步跑通后你已经走完了整条启动链路的前半段。5.3 在 QEMU 里从 U-Boot 加载内核镜像仅跑到 U-Boot 命令行还不够我们还要让 U-Boot 把内核加载起来。你需要准备一个 RISC-V 64 位 Linux 内核镜像并且把镜像放到 QEMU 能访问的介质上。比较快的做法是制作一个简单的 FAT 格式磁盘镜像把内核镜像放进去再用 QEMU 挂载为 virtio 设备。启动 QEMU 时增加磁盘参数qemu-system-riscv64 -M virt -m 2G -nographic \ -bios fw_payload.elf \ -drive filedisk.img,formatraw,ifvirtio进入 U-Boot 后执行virtio scan load virtio 0:1 0x80200000 /boot/Image setenv bootargs root/dev/vda rw consolettyS0 booti 0x80200000 - ${fdt_addr}这里的fdt_addr在 U-Boot 中通常已经由启动路径自动保存。如果环境变量里为空可以查看 U-Boot 的bdinfo命令输出找到设备树地址再手动传入。内核加载后你会在终端看到内核 startup 日志最终进入 rootfs 的 shell。第一次在 QEMU 里看到完整启动链路时建议你对照前面的内容把启动拆成三段来看OpenSBI 做了哪些打印U-Boot 做了哪些初始化内核又从哪里开始接管。一眼能看明白三个阶段的界限说明你对这条启动链路的整体理解已经到位了。6. 常见问题与排查技巧实录我踩过的那些坑6.1 板子完全没输出先查复位、串口和链接地址启动调试最让人头疼的问题是“完全没有输出”。你在终端上什么也看不到无法判断 CPU 到底跑没跑起来U-Boot 到哪一步挂的都不知道。我的排查习惯是先分三层看。第一层是硬件有没有上电复位尤其是 QEMU 或开发板场景有些人会忘记给核心板供电或者串口转接芯片驱动没装好。第二层是 Boot ROM 有没有成功加载固件这可以通过 JTAG 调试器读取 PC 寄存器值来判断。第三层是代码能不能执行到串口初始化如果代码从一开始就跑飞了连串口都来不及打开。在 RISC-V 平台上最常见的无输出原因有三个一是链接地址错误导致代码在绝对跳转时跳到非法地址二是时钟初始化不对导致串口波特率完全错乱三是启动介质选择错误Boot ROM 找不到你的引导镜像。曾经有一块板子Boot ROM 只支持从 SD 卡扇区偏移量读取 SPL而我每次烧录都放在了文件系统偏移位置导致它总是加载不到有效镜像。6.2 跳转到内核后死机问题往往出在寄存器、设备树和启动参数与 RISC-V 启动相关的另一类高频故障是“U-Boot 已经打印 Starting kernel但内核没有任何输出”。这不是内核没加载而是跳转后的环境不满足内核引导协议。我排查时会先检查 a0/a1 有没有被破坏。因为 U-Boot 这段链路上可能会多次跳转如果某级跳转代码覆盖了寄存器内核拿到错误数据早期初始化就会失败。其次是检查内核运行地址是不是符合内存布局。如果 U-Boot 把内核加载到某个地址但该地址和设备树里描述的内存区域冲突内存在初始化过程中可能把自己的代码覆盖掉。设备树地址错误的表现也很迷惑。内核可能已经打印出“Machine model: ...”但后续突然停在某个地方。这时候我会在 U-Boot 命令行里显式指定设备树地址再用fdt addr和fdt print /命令检查 DTB 内容是否完整可读。不要轻信环境变量里的默认值它很可能被上一次实验残留下来的内容污染了。6.3 打开 FPU 后的浮点异常一个容易忽略的 RISC-V 启动细节有些 RISC-V 平台带硬件 FPU但没有正确初始化浮点状态时在 S Mode 或 U Mode 执行浮点指令会触发非法指令异常。如果你在内核或 RTOS 启动早期启用 FPU 支持必须确认 M Mode 固件已经把mstatus.FS状态设置为可用否则 Linux 内核切换上下文时无法正确保存和恢复浮点寄存器。这个坑在 U-Boot 阶段偶尔也会遇到。U-Boot 本身可能不需要浮点但如果你打开某个驱动或工具链优化选项意外加入了浮点指令而 U-Boot 又没有正确初始化 FPU 环境就会在启动中段突然崩溃并且打印出的异常信息非常有限。这类问题最有效的排查方式是在代码里搜索是否有浮点运算被意外引入同时确认编译器没有启用超过目标架构能力的优化级别。6.4 如果目标不是 Linux而是 RT-Thread启动路径会短在哪里很多人会问我不想跑 Linux只想在 RISC-V 上跑 RT-Thread 这类 RTOS还需要折腾 OpenSBI 和 U-Boot 吗答案是不一定。RT-Thread 的启动初始化流程比 Linux 简单得多。它的入口通常也是_start完成栈指针、BSS 清零、MMU 关闭等基础设置后直接跳入 C 语言的entry接着调用rt_hw_board_init、rt_components_board_init、rt_system_scheduler_init等函数最后启动调度器开始多线程运行。整个过程更像一个“增强版裸机程序”不需要设备树也不需要复杂的内核引导协议。如果你的目标平台是 MCU 级别的 RISC-V完全没有 MMU 和 S Mode直接写一个最小的启动汇编文件加载 RT-Thread 固件即可OpenSBI 和 U-Boot 都不是必须的。但如果芯片规模够大、最终要同时跑 Linux 和 RTOS那还是建议把 OpenSBI 保留在系统里让 RTOS 也运行在 S Mode 上这样可以为后续扩展留好余地。6.5 我长期使用的调试三板斧总结这些年调试 RISC-V 启动的经验有三件工具是绕不开的。第一是 OpenSBI 自带的平台日志它会把 M Mode 下的初始化过程打印出来通过它你能判断固件有没有正常接管第二是 GDB 连接硬件调试器或 QEMU 的远程调试端口实时查看 PC 寄存器和内存内容第三是 U-Boot 命令行里的各种内存操作命令比如md、mw、fdt用来快速验证某个地址的内存到底存了什么。如果你用 QEMU 做实验有个小技巧非常实用在 QEMU 中指定-s参数然后在另一个终端启动 GDB并执行target remote :1234就能直接连接 QEMU 里的 CPU 状态。出问题时先打断点查看 PC 停在哪个地址往往比反复改代码重新编译高效得多。启动流程是理解 RISC-V 系统软件栈的一把钥匙。把一个最小系统的启动路径从第一行汇编跟踪到内核入口比自己盲目看 RISC-V 指令集文档要有效得多。这块内容最开始会显得比较杂是因为它牵扯到芯片设计、固件分层、操作系统引导协议各个层面但你只要亲手跑通一次完整启动再回来看各家芯片的资料基本就能保持思路清晰。