RISC-V SoC跑Linux:从OpenSBI到U-Boot的嵌入式开发实战
2026/8/27 17:24:14 网站建设 项目流程

我最早接触 RISC-V 时,周围人第一反应都是:这东西做教学可以,真拿来跑 Linux 是不是想多了?那些单周期处理器实验和五级流水线实验,连个内存管理单元都没有,最多在 FPGA 上点个灯、跑个贪吃蛇。直到 SiFive 拿出能跑 Linux 的 RISC-V SoC,整个讨论层次完全变了:RISC-V 不再只是指令集课程里的作业,它开始直接挑战 ARM 在嵌入式 Linux 市场里的地位。

如果你和我一样,从 STM32、全志、飞思卡尔这些平台切过来,想搞清楚“SiFive 凭什么说自己能跑 Linux”,或者正打算用 RISC-V 做嵌入式 Linux 项目,这篇文章应该能帮你省下不少时间。我会按自己的理解,从 SoC 架构、启动链路、QEMU 验证、驱动调试到选型,一条线聊下来,中间穿插一些我实际踩过的坑。

1. 能跑 Linux 的 RISC-V SoC:从“教学玩具”到“可用平台”的关键一跃

1.1 能跑 Linux 为什么是分水岭

很多刚从单片机转过来的朋友会低估“能跑 Linux”这句话的重量。你在课堂上写一个 RISC-V 单周期 CPU,能跑通汇编指令已经算不错了;再往上走,要跑 Linux,光靠 ALU、寄存器堆、简单的总线是远远不够的。

Linux 对硬件有个最低要求:CPU 要有 MMU(内存管理单元)或者至少能配合软件完成页表切换和地址翻译。RISC-V 的 MMU 方案叫 Sv39/Sv48,也就是 39 位或 48 位虚拟地址空间,配合特权级才能实现进程隔离、用户态和内核态分离。除此之外,你还需要可靠的中断控制器、定时器、内存控制器、串口或显示设备,以及一套能初始化这些硬件的启动固件。换句话说,从裸机 RTOS 跳到 Linux,不是简单“加个操作系统”,而是整个硬件平台的可管理性、软件生态和调试手段都要上一个台阶。

所以 SiFive 发布 Linux-capable RISC-V SoC 这个动作,真正打破的不是某个性能指标,而是生态断点。以前你要做一个嵌入式 Linux 项目,几乎没有正经的 RISC-V SoC 可选,只能回到 ARM;现在有了官方支持 Linux 的 RISC-V 平台,U-Boot、OpenSBI、Linux 内核、Buildroot 这些基础设施全部串起来了,门槛才真正降下来。

1.2 FU540 的硬件布局:四个 U54 加一个 E51 的“不对称”

SiFive 早期最典型的 Linux-capable SoC 是 FU540(HiFive Unleashed 开发板上那颗)。它的 CPU 配置很有意思:四个 U54 核心加一个 E51 管理核心,合计五颗 RISC-V 核心。U54 是 RV64GC 架构,带 MMU,可以跑 Linux,是真正干活的“应用核心”;E51 则是 RV64IMAC,不带 MMU,不适合跑 Linux,但它在 SoC 里的角色很关键——上电后先由它完成初始化和固件加载,再唤醒其他 U54 核心。

这种“管理核 + 应用核”的非对称设计,在 ARM 世界其实也有类似思路,比如 Cortex-A 系列里的安全核心,但 RISC-V 这边把 M 态(机器态)固件职责交给独立的小核心,软件上就更容易做隔离。E51 不需要和 Linux 抢缓存、抢中断,它专心跑 OpenSBI 之类的 Machine 态固件;Linux 则在四个 U54 上正常调度。实际开发时,你甚至可以不关心 E51,但要知道它的存在,否则看到四个核心之外还有一个 CPU 节点时容易蒙。

FU540 的外设集成度在当年也算很全的:DDR 控制器、千兆以太网、SD 卡控制器、PCIe、多路 UART/I2C/SPI/GPIO 都有。这个集成度对跑 Linux 非常重要,因为 Linux 启动后首先要挂根文件系统,如果板卡连个像样的存储控制器都没有,后面的所有事情都无从谈起。

1.3 集成外设与可定制能力:SiFive 的商业模式

SiFive 和其他芯片设计公司不太一样,它不只是卖一颗现成的 SoC,更核心的业务是授权 RISC-V 核心 IP 和 SoC 集成方案。官方发布 Linux-capable SoC,等于告诉下游客户:你不必从零设计 CPU,也不用自己验证 MMU、总线、中断控制器这些最难啃的环节,可以直接拿一个已经跑通 Linux 的 SoC 模板去改、去集成。

对中小团队来说,这个模式很友好。你可以基于 SiFive 提供的核心配置,加上自己的加速器、自定义指令、专用外设,做成一颗面向边缘计算、AI 推理、存储控制或者工业自动化的定制芯片。这也解释了为什么热词里会同时出现“risc-v 单周期 cpu 实验”和“soc芯片启动”——教学层面大家还在理解指令流水线,产业层面已经开始把它当作一个真正可交付的产品平台了。

2. 启动一套 RISC-V Linux 的完整链路:从固化ROM里的第一条指令到 Shell 提示符

2.1 先把“M态、S态、U态”分清楚

RISC-V 和 ARM 一样有特权级设计:M 态(Machine Mode)是最高特权级,S 态(Supervisor Mode)给操作系统内核用,U 态(User Mode)给应用程序用。ARM 那边常见的是 EL3、EL2、EL1、EL0,概念差不多,但 RISC-V 的生态里,M 态固件的位置尤其突出。

Linux 内核本身运行在 S 态,那 M 态谁管?在 ARM 平台上通常是 ATF(Arm Trusted Firmware),而 RISC-V 这边事实标准是 OpenSBI。OpenSBI 是一个跑在 M 态的轻量级固件,提供 SBI 服务:定时器、IPI(核间中断)、远程 fence、系统复位等。Linux 内核和 U-Boot 需要这些能力时,通过ecall指令陷入 M 态,由 OpenSBI 完成,然后再返回 S 态。这个机制比直接访问硬件更安全,也让底层平台差异被隔离在 OpenSBI 之下。

所以启动链路的整体脉络就是:芯片上电后,固化在 ROM 里的 ZSBL 先初始化 DDR 和基础时钟,然后把下一级固件加载到内存;OpenSBI 接管 M 态,再把控制权交给 U-Boot;U-Boot 运行在 S 态,负责从 SD 卡、网络或闪存加载 Linux 内核和设备树;最后 Linux 内核在 S 态启动,应用跑在 U 态。任何一个环节断了,你看到的不是“启动失败”,而是一堆串口乱码或者直接黑屏。

2.2 OpenSBI 提供的是什么服务

OpenSBI 最常用的镜像有两种:fw_jumpfw_dynamicfw_jump编译时需要指定一个跳转地址,比如FW_JUMP_ADDR=0x80200000,U-Boot 固定放在这个地址;fw_dynamic则是运行时从上一级启动器接收信息,更灵活,QEMU 和开发板都支持。

你只需要把 OpenSBI 看作“RISC-V 界的 BIOS 碎片化补丁”。不同厂商的 SoC 在定时器、中断、复位这些底层细节上多少有点差异,Linux 内核不可能为每颗芯片都写一堆 M 态汇编,于是统一通过 SBI 调用。这样内核代码更干净,板卡厂商只需要把自己的私有逻辑放到 OpenSBI 平台实现里。实际编译时,用PLATFORM=sifive/fu540或者PLATFORM=generic,前者针对 SiFive 真板,后者适合 QEMU virt。别选错平台,否则固件可能连串口初始化都做不对。

2.3 U-Boot 与设备树:真正容易卡壳的位置

U-Boot 本身也是一个 S 态的程序,它干的事和 ARM 平台上基本一样:初始化板级设备、读取设备树、加载内核镜像。为了在 RISC-V 上跑 U-Boot,你需要用sifive_unleashed_defconfig这样的配置,并确认CONFIG_TEXT_BASE和 OpenSBI 的跳转地址一致。如果 OpenSBI 跳转到 0x80200000,U-Boot 的链接地址也得是 0x80200000,否则一启动就 PC 乱飞。

设备树是另一个容易心态爆炸的地方。RISC-V 的设备树写法和 ARM 有些差异,核心 CPU 节点里必须有mmu-type = "riscv,sv39"riscv,isa = "rv64imafdc"这样的属性,Linux 早期启动时依赖它们来决定内存管理策略。中断控制器节点也有讲究,PLIC(平台级中断控制器)的interrupts-extended要指明每个 CPU 的 S 态外部中断号。RISC-V 规定 S 态外部中断号是 9,M 态外部中断号是 11。我见过有人把这两个数字写反,结果内核启动到一半就没了中断源,调度器完全卡死。

一个简化的设备树片段大概是这样的:

cpus { #address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { compatible = "sifive,rocket0", "riscv"; reg = <0>; riscv,isa = "rv64imafdc"; mmu-type = "riscv,sv39"; cpu0_intc: interrupt-controller { #interrupt-cells = <1>; compatible = "riscv,cpu-intc"; interrupt-controller; }; }; }; soc { plic: interrupt-controller@c000000 { compatible = "sifive,plic-1.0.0", "riscv,plic0"; reg = <0x0 0xc000000 0x0 0x4000000>; interrupt-controller; #interrupt-cells = <1>; interrupts-extended = <&cpu0_intc 9>; }; uart0: serial@10010000 { compatible = "sifive,uart0"; reg = <0x0 0x10010000 0x0 0x1000>; interrupt-parent = <&plic>; interrupts = <3>; }; };

这里interrupts-extended = <&cpu0_intc 9>的意思是 PLIC 的每个中断源会通过 CPU 的中断控制器,以 S 态外部中断的方式通知 CPU。如果这里写的是 11,OpenSBI 可能拿到,但 Linux 看不到,表现出来就是串口能输出但无法响应按键。

2.4 工具链、内核配置和根文件系统:软件栈别拖后腿

内核编译本身不复杂,关键是工具链要对。Debian/Ubuntu 上可以直接装gcc-riscv64-linux-gnu,或者用 Bootlin 提供的交叉编译工具链。编译内核时设置环境变量:

export ARCH=riscv export CROSS_COMPILE=riscv64-linux-gnu- make defconfig make -j$(nproc) Image

新版内核的默认defconfig基本能覆盖 SiFive 平台。但为了稳妥,我建议再检查几个配置项:CONFIG_SERIAL_SIFIVE=y(串口驱动)、CONFIG_SIFIVE_PLIC=y(中断控制器)、CONFIG_SIFIVE_CLINT=y(定时器)、CONFIG_SIFIVE_UART=y。这些不打开,板子启动到一半就静默了。

根文件系统最容易用 Buildroot 生成。它会把 toolchain、BusyBox、内核和 rootfs 一起搞定。如果你只想快速验证,也可以直接下载一个 riscv64 的 Debian rootfs 压缩包,解压到 SD 卡第二个分区。关键是别在“文件系统”这个环节恋战,先把 Shell 跑起来,后面的事情都好说。

3. 不买开发板先用 QEMU 跑通 Linux:OpenSBI、U-Boot、rootfs 的联调记录

3.1 为什么先用 QEMU 验证

很多人的第一反应是“直接买开发板,拿来就烧”,但我的经验是:先花一小时在 QEMU 上跑通,再上板子,能省出好几天的调板时间。原因很简单,RISC-V Linux 软件栈的边界问题——OpenSBI 和 U-Boot 配置是否匹配、内核 defconfig 缺没缺驱动、设备树地址对不对——用 QEMU 就能暴露一大半,而且 QEMU 可以随时重启、快照、看寄存器,比串口拉日志高效得多。

QEMU 对 RISC-V 有很好的模拟支持,-M virt是通用虚拟平台,-M sifive_u是模拟 SiFive FU540 的平台。我建议直接用sifive_u,和真板子的启动路径更接近,后面切换到 HiFive Unleashed 或类似板卡时,差异会小很多。

3.2 编译 OpenSBI 和 U-Boot

先编译 OpenSBI。针对 SiFive 平台,用:

git clone https://github.com/riscv-software-src/opensbi cd opensbi make CROSS_COMPILE=riscv64-linux-gnu- PLATFORM=sifive/fu540

编译完会在build/platform/sifive/fu540/firmware/下生成fw_dynamic.elf。这个固件是后面所有环节的底座,别拿错目录。

接着编译 U-Boot:

git clone https://gitlab.denx.de/u-boot/u-boot.git cd u-boot export ARCH=riscv export CROSS_COMPILE=riscv64-linux-gnu- make sifive_unleashed_defconfig make -j$(nproc)

把 OpenSBI 和 U-Boot 串起来,用 QEMU 启动最基本的检查:

qemu-system-riscv64 -M sifive_u -smp 4 -m 2G \ -bios opensbi/build/platform/sifive/fu540/firmware/fw_dynamic.elf \ -kernel u-boot/u-boot.bin \ -nographic

如果一切正常,先能看到 OpenSBI 的 logo 和版本信息,然后出现 U-Boot 的提示符。在这一步卡住,九成是地址默认值对不上。检查 U-Boot 的CONFIG_TEXT_BASE是否和 OpenSBI 的跳转地址一致,别急着怀疑工具链。

3.3 制作 SD 卡镜像:fdisk、dd、losetup 这些命令又回来了

QEMU 里跑通 U-Boot 之后,下一步是把内核和 rootfs 做成一块虚拟 SD 卡。你会发现当年那些 Linux 常用命令在嵌入式调试里反而成了主角。

dd if=/dev/zero of=sd.img bs=1M count=2048 fdisk sd.img

分区建议:第一个分区 512MB,格式化成 FAT32,放内核镜像Image和设备树;第二个分区用剩余空间,格式化成 ext4,放 rootfs。创建好分区后,用losetup -fP sd.img把镜像挂成 loop 设备,然后mkfs.vfat /dev/loop0p1mkfs.ext4 /dev/loop0p2

注意,U-Boot 默认读卡命令和分区表有很大关系。U-Boot 里习惯用mmc 0:1表示第一块 SD 卡的第一个分区,如果你在 SD 卡镜像制作时把类型标错了,U-Boot 会报invalid partition。这种小问题不会让你怀疑“Linux 不能跑”,只会让你怀疑人生。

接下来在 U-Boot 里手动引导一次:

setenv bootargs "root=/dev/mmcblk0p2 rw console=ttySIF0,115200" load mmc 0:1 0x80200000 /Image load mmc 0:1 0x84000000 /hifive-unleashed-a00.dtb booti 0x80200000 - 0x84000000

booti是 RISC-V 64 位上引导 Image 格式内核的命令,注意后面中间短横线表示没有 initramfs。如果看到 Linux 内核刷屏后停在VFS: Cannot open root device,那不是内核坏了,是 rootfs 没挂上,回去检查分区号和 bootargs。

3.4 串口工具和日志里的线索

QEMU 里用-nographic直接把串口映射到终端,开发板上则常用minicom -D /dev/ttyUSB0 -b 115200。连接真实板卡时,第一件事不是敲命令,而是确认串口电平、地线、波特率。RISC-V 开发板的调试串口通常是 115200 8N1,但也有用 57600 的板子,一上来花五分钟查原理图,比在乱码里猜半天强。

启动阶段如果earlycon参数配得好,能提前看到很多信息。RISC-V 上可以用earlycon=sbi,它会走 OpenSBI 的串口输出;如果你的平台串口是标准 8250 兼容,也可以用earlycon=uart8250,mmio,0x10010000。我的习惯是同时加上earlyconconsole=ttySIF0,115200,这样从第一行开始就能追踪,定位问题是固件层还是内核层会快很多。

4. 跑通内核只是开始:串口、中断、外设驱动里的真实障碍

4.1 串口驱动与时钟频率:最简单的设备也分分钟让你自闭

很多人以为串口是最简单的设备,结果往往是它在 Linux 启动早期给你第一记耳光。SiFive 的 UART 外设看起来简单,但 Linux 端要正确识别它,设备树里的compatible必须和驱动匹配,中断号必须正确,还有一个容易被忽略的点:串口时钟频率。

如果 DTS 里clocks写错,内核可能在启动阶段收到一堆乱码,然后完全静默。遇到过一种情况:U-Boot 能正确输出,但 Linux 一接管串口就乱掉。原因是 U-Boot 和 Linux 对 UART 时钟源的选择不同,Linux 驱动按设备树里的clock-frequency算波特率,而实际时钟频率和 U-Boot 初始化时不一致。此时不要急着改驱动,先在设备树里核对 PRCI(时钟控制器)节点的频率和分频关系。

4.2 PLIC 和 CLINT:RISC-V 的中断不是 GIC

从 ARM 平台转过来的同学,最不适应的可能就是这个:RISC-V 没有 GIC。ARM 用 GIC 统一管理 SPI、PPI、SGI,而 RISC-V 这边用的是 PLIC(平台级中断控制器)和 CLINT(核心本地中断控制器)。

PLIC 负责收集外设中断,并根据优先级把中断路由到目标 CPU;CLINT 则负责软件中断和定时器中断。Linux 里对应的 irqchip 驱动是irq-sifive-plicclint,它们分别处理两条完全不同的中断路径。调试时最容易踩的坑是:你配置了外设中断,但忘记在 PLIC 里设置priority,结果产生的中断因为优先级为 0 而被直接忽略。还有一次,我在设备树里把 CPU 的 S 态外部中断号写成 11,Linux 驱动明明注册了 handler,却永远不触发。改成 9 之后立刻恢复正常。

定时器中断也一样,CLINT 提供的时间戳和比较寄存器,是 Linux 调度器的心跳。如果设备树里没有正确的clint节点,内核起来后会一直卡在Calibrating delay loop,没有任何日志报错,只能靠earlycon输出和dmesg一点点排查。

4.3 外设驱动缺失时的嵌入式开发日常

Linux-capable SoC 只代表“核心平台能启动”,不代表所有外设驱动都齐了。很多厂商会把 SiFive 核心和自家 FPGA 逻辑或者专用 IP 结合,这时候就得自己写 Linux 驱动。

最省力的方式是写一个平台设备驱动,用compatible和设备树节点匹配:

static const struct of_device_id mydevice_of_match[] = { { .compatible = "myvendor,mydevice", }, { } }; MODULE_DEVICE_TABLE(of, mydevice_of_match); static struct platform_driver mydevice_driver = { .probe = mydevice_probe, .remove = mydevice_remove, .driver = { .name = "mydevice", .of_match_table = mydevice_of_match, }, }; module_platform_driver(mydevice_driver);

驱动编译好之后,insmod mydevice.ko,再用dmesg | tail看 probe 有没有被执行。如果内核提示Failed to match device,九成是 DTS 里的compatible字符串和设备树节点对不上。这种工作没什么玄学,就是查设备树、查驱动、查ls /sys/bus/platform/devices/

4.4 性能验证不能只看“能跑”

跑通 Shell 之后,我建议立刻用几个标准工具验证性能:time测内核编译速度、dd测存储读写带宽、iperf测网络吞吐。RISC-V 的性能调优和 ARM 有相似之处,但要注意工具链的优化等级。很多第三方工具链默认没有开-march=rv64gc,导致生成的二进制指令集保守,跑起来和没开优化一样。给内核和应用统一使用匹配的-march-mabi,性能可能直接翻倍。

另外,Linux 的容器生态也开始支持 RISC-V 了,理论上可以在 QEMU 或开发板上跑 Docker。不过当前镜像仓库里的 RISC-V 容器镜像还比较少,基础镜像需要自己基于 rootfs 构建。想快速体验,可以先在 QEMU 里跑一个最小 BusyBox 容器,再逐步补齐运行时依赖。

5. 真拿 SiFive SoC 做产品,选型和迁移该怎么看

5.1 不同核心 IP 的差异:E 系列、U 系列、P 系列怎么选

SiFive 的核心 IP 产品线拉得很开,我不建议一上来就挑“最强”的,先分清楚需求:

核心系列架构/特性MMU应用场景
E 系列(E31/E76)RV32IMAC/RV32GC实时控制、IoT、裸机/RTOS
S 系列(S76)RV64GC实时应用,适合对安全隔离要求高的场景
U 系列(U54/U74)RV64GC,带 MMU跑 Linux,适合边缘计算、工控
P 系列(P550/P650/P670)RV64GC,性能更强中高端 Linux 应用处理器

U 系列和 P 系列都是 Linux-capable 的核心。如果你的产品要跑完整 Linux、接千兆网、跑 Python/Node.js 这类运行时,选带 MMU 的 U 或 P 系列更合适。如果产品只需要轻量级实时控制,那就没必要上 Linux,E 系列加 RTOS 反而省电、省成本、更容易过认证。

5.2 软件成本与授权模式:别只盯着 ISA 开源

RISC-V 的指令集是开源的,但“开源”不等于“免费”。SiFive 的核心 IP、SoC 集成工具和技术支持都是商业化授权模式,具体费用取决于核心数量、性能档位和支持等级。相比 ARM,RISC-V 授权门槛确实更低,但产品化过程中的软件成本容易被人忽视。

嵌入式 Linux 项目的真实成本大头不在 CPU,而在驱动适配、BSP 维护、安全启动、OTA 升级和长期内核版本维护。ARM 这边有成熟的板级支持包,RISC-V 的很多代码虽然已经进主线,但外设厂商的支持力度参差不齐。如果选一个比较偏门的 SoC,Linux 内核版本升级时没人帮你测试,你就要自己承担验证工作。所以选型时一定要确认:这颗 SoC 的 BSP 到底由谁维护,主线内核支持度如何,OpenSBI 和 U-Boot 的更新是否活跃。

5.3 从 ARM 迁移过来,我的三条建议

第一条:先用 QEMU 和 Buildroot 建立一套完整的构建脚本,把工具链、OpenSBI、U-Boot、内核、rootfs 全部 CI 化。这样可以提前暴露“环境依赖”问题,避免团队里每个人各自编译导致各种环境差异。RISC-V 生态发展很快,但坑也多,自动化是最可靠的护身符。

第二条:别把“Linux 能启动”当成项目完成。真正要评估的是你的业务应用在 RISC-V 上的表现,比如算法库是否支持 RISC-V 向量扩展,第三方库有没有 riscv64 的预编译包,数据库、消息中间件能不能跑。很多基础软件在 ARM 上一键安装,在 RISC-V 上可能需要手动交叉编译。

第三条:如果是量产项目,优先选已经有成熟 SiFive 核心的 SoC 模组,而不是自己从 IP 集成开始。自己做 SoC 意味着要额外面对物理设计、封装、测试、认证、量产良率等一系列芯片级问题,这已经不是嵌入式软件团队能轻易搞定的了。用成品 SoC 模组,软件栈依然可控,风险却小很多。

我自己实际把 OpenSBI、U-Boot、Linux 内核跑到 Shell 提示符之后,最大的感受是:RISC-V 的短板早就不在 CPU 指令集了,而在“你有没有把它当产品级平台来对待”。开发时多用 QEMU 做验证,多读设备树,多给上游社区提 bug,这个生态会比你想象中更快成熟起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询