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 文件以 /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)。

避坑指南:我曾经在 64 位 RISC-V 平台上写设备树,忘了把 #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 知识结构图

下面这张图,帮你理清设备树的核心概念:

设备树核心概念结构图 根节点 / cpus 节点 memory 节点 uart 节点 #address-cells #size-cells 定义 reg 的格式 reg 属性 地址 + 大小 描述硬件地址空间 compatible 属性 厂商,设备型号 匹配驱动程序 设备树 = 节点 + 属性 节点描述硬件,属性描述参数

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)
个人经验:我建议初学者先从最简单的 DTS 开始,只包含 CPU 和内存。然后在 QEMU 上测试,确认内核能启动。再逐步添加 UART、GPIO 等外设。这样出了问题,容易定位。

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 数。

警告:设备树是大小端敏感的。RISC-V 默认小端,但有些外设可能是大端。写 reg 时一定要确认字节序,否则地址会完全错位。