4. 设备树基础
设备树这东西,说白了就是一块「硬件描述板」。你想想看,操作系统要跑起来,总得知道CPU有几个核、内存多大、外接了什么设备吧?传统做法是在内核里写死一堆板级代码,换一块板子就得重新编译。RISC-V这边更灵活——我们用设备树来描述硬件,内核启动时解析它,动态适配。
我在做RISC-V移植时遇到过这么个坑:设备树里一个中断号写错了,系统启动到一半就挂掉。查了两天才发现是设备树的问题。从那以后,我对设备树的每个字段都格外小心。
设备树的概念与作用
设备树(Device Tree)是一种描述硬件资源的数据结构。它把CPU、内存、总线、外设、中断控制器等信息,组织成一棵树形结构。内核在启动阶段读取这棵树,就知道该怎么初始化硬件了。
它的核心作用有三个:
- 硬件描述标准化:同一份内核镜像,换不同的设备树就能适配不同板卡
- 减少板级代码:不用在内核源码里塞一堆arch/arm/mach-xxx的文件夹
- 动态加载支持:U-Boot或OpenSBI可以在启动时修改设备树,比如调整内存大小
关键理解:设备树不是驱动,它是驱动的「配置文件」。驱动代码根据设备树里的信息,决定怎么操作硬件寄存器。
DTS与DTB文件格式
设备树有两种形态:源码和二进制。
| 格式 | 全称 | 特点 | 使用场景 |
|---|---|---|---|
| DTS | Device Tree Source | 文本文件,人类可读写 | 开发阶段编辑、维护 |
| DTB | Device Tree Blob | 二进制文件,机器可解析 | 启动时传递给内核 |
| DTSI | Device Tree Source Include | 公共片段,被DTS引用 | SoC级通用描述 |
DTS到DTB的转换,靠的是dtc工具。我个人习惯在Makefile里加一条规则:
# 编译设备树
dtc -I dts -O dtb -o board.dtb board.dts
# 反编译(调试时很有用)
dtc -I dtb -O dts -o board.dts board.dtb
小技巧:反编译DTB时加-s参数可以显示排序后的节点,方便对比差异。我曾经用这个功能抓到一个内存地址写错的bug。
设备树节点与属性
设备树的每个节点代表一个硬件设备或总线。节点里用属性来描述具体参数。
一个典型的节点长这样:
/ {
#address-cells = <2>;
#size-cells = <2>;
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>;
};
soc {
#address-cells = <2>;
#size-cells = <2>;
compatible = "simple-bus";
ranges;
uart@10000000 {
compatible = "ns16550a";
reg = <0x0 0x10000000 0x0 0x1000>;
interrupts = <10>;
interrupt-parent = <&plic>;
clock-frequency = <3686400>;
};
};
};
这里有几个关键属性:
- compatible:驱动匹配的关键字,内核靠它找到对应的驱动
- reg:地址和长度,格式由#address-cells和#size-cells决定
- interrupts:中断号,具体含义依赖中断控制器
- interrupt-parent:指向中断控制器的phandle
注意:reg里的地址是总线地址,不是物理地址。如果节点挂在simple-bus上,地址直接映射到物理地址空间。但如果有IOMMU或地址转换,情况就复杂了。
RISC-V设备树特有属性
RISC-V的设备树有一些自己的规矩。我刚开始做RISC-V移植时,就被这些特有属性坑过几次。
CPU节点描述
RISC-V的CPU节点必须包含riscv,isa属性,用来描述指令集扩展:
cpu@0 {
device_type = "cpu";
reg = <0>;
status = "okay";
compatible = "riscv";
riscv,isa = "rv64imafdc";
mmu-type = "riscv,sv48";
clock-frequency = <1000000000>;
interrupt-controller {
#interrupt-cells = <1>;
interrupt-controller;
compatible = "riscv,cpu-intc";
};
};
riscv,isa里的字符串含义:
| 字符 | 含义 | 说明 |
|---|---|---|
| I | 整数指令 | 基础指令集,必须包含 |
| M | 乘除法 | 硬件乘除法支持 |
| A | 原子操作 | LR/SC指令 |
| F | 单精度浮点 | 32位浮点运算 |
| D | 双精度浮点 | 64位浮点运算 |
| C | 压缩指令 | 16位指令编码 |
中断控制器(PLIC与CLINT)
RISC-V平台通常有两个中断控制器:
- CLINT:处理定时器和软件中断,挂在CPU的核间中断线上
- PLIC:处理外设中断,支持多级优先级
设备树里这样描述:
clint@2000000 {
compatible = "riscv,clint0";
reg = <0x0 0x2000000 0x0 0x10000>;
interrupts-extended = <&cpu0 3 &cpu1 3>;
};
plic@c000000 {
compatible = "riscv,plic0";
reg = <0x0 0xc000000 0x0 0x4000000>;
interrupts-extended = <&cpu0 11 &cpu1 11>;
interrupt-controller;
#interrupt-cells = <2>;
riscv,ndev = <127>;
};
riscv,ndev这个属性很关键,它告诉PLIC最大支持多少个外设中断源。我见过有人把这个值设小了,结果高编号的中断根本收不到。
内存与地址映射
RISC-V设备树里,内存节点用device_type = "memory"标识。但要注意,如果有多段不连续的内存,需要多个memory节点:
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>;
};
memory@1000000000 {
device_type = "memory";
reg = <0x0 0x1000000000 0x0 0x40000000>;
};
经验之谈:QEMU的virt平台默认把内存放在0x80000000开始的位置。如果你自己写设备树,记得跟QEMU的启动参数-m保持一致,否则内核可能认不全内存。
知识结构总览
下面这张图把设备树的核心脉络串起来了:
从这张图能看出来,设备树就是一棵从根节点向下展开的树。每个节点挂载不同的硬件模块,节点里用属性描述具体参数。RISC-V特有的就是CPU的riscv,isa、PLIC的riscv,ndev这些属性,写的时候一个都不能少。
避坑提醒:我曾经在设备树里漏写了interrupt-controller这个空属性,结果PLIC节点没有被识别为中断控制器,所有外设中断都失效了。这个属性虽然不携带数据,但它的存在本身就是一种声明。