2. 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 机器内存布局,你保存好,后面配设备树会反复用到。
我的经验: 你不需要背所有地址。记住三个关键点就够了:内核入口 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 在启动时动态创建并放在内存里的。内核启动时,从 a1 寄存器拿到设备树地址,然后解析它来发现硬件。
一句话总结: QEMU 负责“造硬件”和“写设备树”,内核负责“读设备树”和“驱动硬件”。我们做设备树配置,就是在 QEMU 和内核之间当“翻译官”。
2.5 我踩过的坑
最后分享一个我早期的教训。有一次我配设备树,怎么改内核都不认我的新外设。折腾了两天,最后发现是 设备树中的 reg 属性地址写错了。我把 UART 的地址写成了 0x10000100,而实际 QEMU 映射的是 0x10000000。差一个字节,内核直接忽略。
所以我的建议是:永远不要手写设备树地址。先用 -dumpdtb 导出 QEMU 生成的原始设备树,然后基于它修改。这是最稳妥的做法。