2、QEMU RISC-V 平台介绍:机器类型、内存映射与启动流程

好,咱们正式开始动手之前,得先把“战场”搞清楚。QEMU 模拟的 RISC-V 平台有好几种,它们各有各的脾气。我个人习惯是先把这些机器类型摸透,不然配设备树的时候很容易踩坑。

2.1 QEMU 支持的 RISC-V 机器类型

QEMU 目前主要支持三种 RISC-V 机器,我列个表,你一看就明白区别了。

机器类型 命令行参数 特点 我的评价
virt -machine virt 通用虚拟平台,设备树完整,支持 PCIe、VirtIO 最推荐,开发调试首选
sifive_u -machine sifive_u 模拟 SiFive Unleashed 开发板,外设固定 适合移植真实板级代码
spike -machine spike 最简平台,几乎无外设,仅用于 ISA 测试 我一般只用来跑裸机程序

核心建议: 咱们这门课全程用 virt 机器。它最灵活,设备树生成最规范,Linux 内核支持也最好。你想想看,如果连 virt 都搞不定,换到真实板卡只会更痛苦。

2.2 virt 机器的内存映射与设备布局

virt 机器的内存布局是固定的。说白了,QEMU 在启动时就把这些地址告诉 CPU 了。我刚开始学的时候,总觉得记地址很烦,后来发现其实只要记住几个关键区域就行。

下面这张图是我自己整理的 virt 机器内存布局,你保存好,后面配设备树会反复用到。

QEMU RISC-V virt 机器内存映射图 QEMU RISC-V virt 机器内存映射 主内存 (Main Memory) 0x80000000 - 0xFFFFFFFF (默认 2GB) Linux 内核通常加载到这里 高地址 MMIO 外设区域 0x00100000 - 0x10000000 UART0 (0x10000000) RTC (0x101000) PLIC (0x0C000000) VirtIO (0x10001000) ROM / CLINT / Debug 0x00000000 - 0x00100000 低地址 💡 内核入口固定为 0x80000000,这是 QEMU 的硬规定

我的经验: 你不需要背所有地址。记住三个关键点就够了:内核入口 0x80000000、UART 在 0x10000000、PLIC 中断控制器在 0x0C000000。其他的用到再查。

2.3 关键外设地址速查表

下面这几个地址,你在设备树里会反复写。我建议你把这个表截图存手机里。

外设 基地址 大小 用途
CLINT 0x02000000 0x00010000 定时器、软件中断
PLIC 0x0C000000 0x04000000 外部中断管理
UART0 0x10000000 0x00000100 串口控制台
VirtIO 0x10001000 0x00001000 块设备/网络
RTC 0x00101000 0x00001000 实时时钟
Flash 0x20000000 0x10000000 固件存储

注意: 不同 QEMU 版本的外设地址可能有细微差异。我建议你每次搭建环境后,先跑一下 qemu-system-riscv64 -machine virt -dumpdtb virt.dtb,然后用 dtc 反编译看看实际地址。我曾经因为版本差异,在 UART 地址上浪费了半天。

2.4 启动流程概览

QEMU 启动 RISC-V Linux 内核的流程,说白了就三步。我画了个流程图,你跟着走一遍就明白了。

QEMU RISC-V Linux 启动流程 QEMU RISC-V Linux 启动流程 Step 1: QEMU 初始化硬件 创建 CPU、内存、外设、生成 FDT Step 2: 加载固件/内核 将 -kernel 指定的文件加载到 0x80000000 Step 3: CPU 从 0x80000000 执行 OpenSBI 先运行,然后跳转到内核 Step 4: Linux 内核启动 解析设备树、初始化驱动、挂载根文件系统 💡 设备树 (FDT) 由 QEMU 自动生成,通过 a1 寄存器传递给内核 我们后面要做的,就是修改这个自动生成的设备树

嗯,这里有个关键点你要记住:设备树不是内核自己生成的,是 QEMU 在启动时动态创建并放在内存里的。内核启动时,从 a1 寄存器拿到设备树地址,然后解析它来发现硬件。

一句话总结: QEMU 负责“造硬件”和“写设备树”,内核负责“读设备树”和“驱动硬件”。我们做设备树配置,就是在 QEMU 和内核之间当“翻译官”。

2.5 我踩过的坑

最后分享一个我早期的教训。有一次我配设备树,怎么改内核都不认我的新外设。折腾了两天,最后发现是 设备树中的 reg 属性地址写错了。我把 UART 的地址写成了 0x10000100,而实际 QEMU 映射的是 0x10000000。差一个字节,内核直接忽略。

所以我的建议是:永远不要手写设备树地址。先用 -dumpdtb 导出 QEMU 生成的原始设备树,然后基于它修改。这是最稳妥的做法。