3. QEMU入门与安装
设备树(Device Tree)这东西,说白了就是一块「硬件描述板」。你告诉内核:CPU 有几个核、内存多大、外设挂在哪根总线上。内核不再去猜,直接读设备树就行。我刚开始接触 RISC-V 时,总觉得设备树可有可无,直到有一次在 QEMU 上启动 Linux,内核死活找不到串口——嗯,从那以后我再也不敢跳过设备树了。
3.1 DTS 文件结构
一个典型的 DTS 文件,长这样:
/dts-v1/;
/ {
model = "QEMU RISC-V Virt Machine";
compatible = "riscv-virtio";
chosen {
bootargs = "console=ttyS0";
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "riscv";
reg = <0>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x10000000>;
};
};
结构其实很简单:
- 根节点:用
/表示,整棵树的起点 - 子节点:如
cpus、memory,描述具体硬件模块 - 属性:键值对,描述节点的特征
我个人习惯把设备树想象成一棵倒挂的树。根在上,枝叶在下。每个节点就是一个硬件模块,属性就是它的参数表。
/dts-v1/; 开头,表示使用设备树 v1 格式。没有这行,编译器会报错。
3.2 节点(node)与属性(property)
节点用 node-name@unit-address 命名。比如 cpu@0,cpu 是名字,0 是地址。地址可以省略,但如果有 reg 属性,建议加上,方便区分多个同类设备。
属性就是节点的「身份证」。常见属性有:
| 属性名 | 含义 | 示例 |
|---|---|---|
compatible |
设备兼容性标识 | "riscv-virtio" |
reg |
地址/大小描述 | <0x0 0x80000000 0x0 0x10000000> |
#address-cells |
地址字段的 cell 数 | <1> |
#size-cells |
大小字段的 cell 数 | <1> |
device_type |
设备类型(内存/CPU) | "memory" |
你想想看,如果没有这些属性,内核怎么知道内存从哪开始、有多大?全靠 reg 告诉它。
3.3 #address-cells 与 #size-cells
这两个属性,我当年学的时候绕了好大一圈。简单说:
#address-cells:表示reg中地址部分用几个 32 位整数表示#size-cells:表示reg中大小部分用几个 32 位整数表示
举个例子:
memory@80000000 {
reg = <0x0 0x80000000 0x0 0x10000000>;
};
如果父节点定义了 #address-cells = <2> 和 #size-cells = <2>,那么 reg 的前两个数(0x0 0x80000000)是地址,后两个数(0x0 0x10000000)是大小。地址是 0x80000000,大小是 0x10000000(256MB)。
#address-cells 设为 2,结果内存地址被截断,内核直接 panic。记住:64 位地址一定要用 2 个 cell!
3.4 reg 属性
reg 是设备树里最重要的属性之一。它描述设备的地址空间。格式是:
reg = <address1 size1 address2 size2 ...>;
每个地址-大小对,描述一段连续的地址空间。比如一个 UART 控制器,可能占用 0x10000000 开始的 0x100 字节:
uart@10000000 {
compatible = "ns16550";
reg = <0x0 0x10000000 0x0 0x100>;
};
这里 #address-cells = <2>,所以地址是 0x0 0x10000000(即 0x10000000),大小是 0x0 0x100(即 256 字节)。
reg 的 cell 数量必须与父节点的 #address-cells 和 #size-cells 一致。多一个或少一个,设备树编译器都会报错。
3.5 compatible 属性
compatible 是设备树的「身份证号」。内核通过它找到对应的驱动程序。格式是:
compatible = "厂商,设备型号";
比如:
compatible = "riscv-virtio", "virtio";
内核会先尝试匹配 riscv-virtio,如果找不到,再尝试 virtio。这种「回退机制」在 QEMU 上特别有用——同一个设备,不同版本可能用不同的 compatible 字符串。
我个人习惯把 compatible 写成「厂商名+设备名」的格式,比如 qemu,riscv-virtio。这样既清晰,又避免和其他平台的设备冲突。
3.6 知识结构图
下面这张图,帮你理清设备树的核心概念:
3.7 实战:编写一个简单的 DTS
假设我们要在 QEMU RISC-V 上描述一个带 UART 和 GPIO 的系统:
/dts-v1/;
/ {
model = "My RISC-V Board";
compatible = "my,riscv-board";
#address-cells = <2>;
#size-cells = <2>;
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x10000000>;
};
uart@10000000 {
compatible = "ns16550";
reg = <0x0 0x10000000 0x0 0x100>;
interrupt-parent = <&plic>;
interrupts = <10>;
};
gpio@10001000 {
compatible = "my,gpio";
reg = <0x0 0x10001000 0x0 0x100>;
gpio-controller;
#gpio-cells = <2>;
};
};
这个例子涵盖了:
- 根节点定义
- 内存节点(
device_type+reg) - UART 节点(
compatible+reg+ 中断) - GPIO 节点(
gpio-controller+#gpio-cells)
3.8 常见错误与避坑
| 错误 | 原因 | 解决方法 |
|---|---|---|
| 内核找不到内存 | reg 的 cell 数不对 |
检查 #address-cells 和 #size-cells |
| 驱动 probe 失败 | compatible 不匹配 |
确认驱动支持的 compatible 字符串 |
| 设备树编译报错 | 语法错误或缺少 /dts-v1/ |
用 dtc -I dts -O dtb 检查 |
| 中断不生效 | 缺少 interrupt-parent |
显式指定中断控制器 |
我曾经在调试一个 RISC-V 板子时,UART 死活不工作。折腾了两天,最后发现是 reg 的地址写错了——多了一个 0。嗯,从那以后我每次写 reg 都要数三遍 cell 数。
reg 时一定要确认字节序,否则地址会完全错位。