第2章:QEMU RISC-V 平台基础
这一章,我们来聊聊 QEMU 的 RISC-V 平台到底长什么样。说白了,就是搞清楚 QEMU 模拟出来的那块「虚拟开发板」——它叫 virt。我刚开始接触 QEMU 时,也纳闷过:为什么非得用 virt?它跟真实硬件有啥区别?别急,我们一步步拆开看。
2.1 QEMU 机器模型:virt
QEMU 为 RISC-V 提供了多种机器模型,但最常用、最通用的就是 virt。你可以把它理解成一块「参考设计板」——它不绑定任何具体厂商的 SoC,而是把 RISC-V 标准规定的外设都挂上去。
核心特点:
- 纯虚拟平台,不模拟真实芯片的引脚时序
- 外设通过 MMIO(内存映射 I/O)访问
- 支持 OpenSBI + U-Boot 或直接加载 Linux 内核
- 设备树自动生成,无需手动编写 DTS
我在项目中遇到过一个问题:用 virt 跑 Linux 内核,结果串口死活没输出。后来发现是 QEMU 版本升级后,默认的串口基地址变了。嗯,这就是虚拟平台的「坑」——它虽然方便,但版本之间可能有细微差异。
| 机器模型 | 适用场景 | 外设丰富度 | 启动方式 |
|---|---|---|---|
| virt | 通用开发、驱动验证 | 高(UART、GPIO、RTC、PCIe等) | OpenSBI + 内核 / 直接加载 |
| sifive_u | SiFive Unleashed 板模拟 | 中 | FSBL + U-Boot |
| spike | 裸机测试、指令集验证 | 低(仅 HTIF) | 直接加载 ELF |
我的建议:做驱动开发,优先选 virt。它外设齐全,调试方便。等驱动在 virt 上跑通了,再移植到真实板卡上。
2.2 设备树基础:DTS 与 DTB
设备树(Device Tree)是 RISC-V 平台上描述硬件信息的标准方式。你想想看,Linux 内核怎么知道板子上有哪些外设?它们的地址是多少?中断号是多少?这些信息就写在设备树里。
设备树的处理流程很简单:
- 编写
.dts源文件(人类可读) - 用
dtc编译成.dtb二进制文件 - QEMU 启动时加载
.dtb,传递给内核
QEMU 的 virt 平台有个好处:它可以在启动时自动生成设备树。你不需要自己写 DTS,除非你想定制外设。我个人习惯先用 -M virt,dumpdtb= 参数把生成的 DTB 导出来,再用 dtc 反编译看看里面到底有什么。
# 导出 QEMU 自动生成的设备树
qemu-system-riscv64 -M virt,dumpdtb=qemu-virt.dtb -m 2G -nographic
# 反编译为 DTS 文件
dtc -I dtb -O dts -o qemu-virt.dts qemu-virt.dtb
打开生成的 qemu-virt.dts,你会看到类似这样的结构:
/dts-v1/;
/ {
#address-cells = <2>;
#size-cells = <2>;
compatible = "riscv-virtio";
model = "riscv-virtio,qemu";
chosen {
bootargs = "console=ttyS0";
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x80000000>;
};
soc {
#address-cells = <2>;
#size-cells = <2>;
ranges;
uart@10000000 {
compatible = "ns16550a";
reg = <0x0 0x10000000 0x0 0x100>;
interrupts = <10>;
interrupt-parent = <&plic>;
clock-frequency = <3686400>;
};
gpio@10100000 {
compatible = "virtio,mmio";
reg = <0x0 0x10100000 0x0 0x200>;
interrupts = <1>;
interrupt-parent = <&plic>;
};
};
plic@c000000 {
compatible = "riscv,plic0";
reg = <0x0 0xc000000 0x0 0x4000000>;
interrupts-extended = <&cpu0 11 &cpu0 9>;
riscv,ndev = <127>;
};
};
我曾经踩过的坑:设备树里的 reg 属性,地址和大小都是 2 个 32 位值组成的 64 位数。写驱动时如果只取低 32 位,高地址外设就会访问失败。记得用 of_iomap() 或 platform_get_resource() 来获取完整地址。
2.3 启动流程分析
QEMU RISC-V virt 平台的启动流程,我把它分成三个阶段。你想想看,从按下「电源」到 Linux 内核跑起来,中间发生了什么?
启动时,QEMU 会按以下顺序加载:
- OpenSBI:运行在机器模式(M-mode),负责初始化硬件、提供 SBI 服务
- 设备树:QEMU 自动生成,放在内存中,通过 a1 寄存器传递给内核
- Linux 内核:运行在监管模式(S-mode),解析设备树,初始化驱动
调试小技巧:启动时加 -d in_asm -D trace.log 可以记录每条指令的执行。我曾经用这个方法定位过一个驱动初始化顺序的问题——内核在访问外设时,PLIC 还没配置好,导致中断丢失。
2.4 实战:启动一个最小系统
光说不练假把式。我们来实际跑一下 QEMU RISC-V virt 平台。你需要准备:
- QEMU 7.0+(支持 RISC-V 64位)
- OpenSBI 预编译固件(fw_jump.bin)
- RISC-V 64位 Linux 内核(Image)
- 根文件系统(rootfs.ext4)
# 启动命令
qemu-system-riscv64 \
-M virt \
-m 2G \
-smp 2 \
-bios fw_jump.bin \
-kernel Image \
-append "root=/dev/vda rw console=ttyS0" \
-drive file=rootfs.ext4,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-nographic
启动后,你会看到 OpenSBI 的打印信息,然后是 Linux 内核的启动日志。如果一切顺利,最后会出现一个 shell 提示符。
注意:如果串口没有输出,检查两点:一是 -nographic 参数是否加上了,二是内核配置里是否开启了 CONFIG_SERIAL_8250 和 CONFIG_SERIAL_OF_PLATFORM。我遇到过好几次,内核编译时忘了加串口驱动,结果只能干瞪眼。
2.5 本章小结
这一章我们覆盖了三个核心知识点:
- virt 机器模型:QEMU 的通用 RISC-V 虚拟平台,外设丰富,适合驱动开发
- 设备树:描述硬件信息的标准方式,QEMU 自动生成,但你需要读懂它
- 启动流程:OpenSBI → 设备树 → Linux 内核,三个阶段环环相扣