4. 设备树基础语法:DTS/DTC/DTB 关系、节点与属性定义、reg/interrupts 属性详解
设备树,说白了就是一套描述硬件信息的“说明书”。Linux 内核通过它来知道你的板子上接了哪些外设、内存多大、中断怎么连。我刚开始接触时也觉得这东西有点绕,但摸清 DTS、DTC、DTB 这三者的关系后,就豁然开朗了。
4.1 DTS / DTC / DTB:三者的“前世今生”
这三个缩写,你几乎每天都会碰到。它们的关系其实很简单:
- DTS(Device Tree Source):文本文件,人类可读。你用编辑器写的就是它。
- DTC(Device Tree Compiler):编译器,把 DTS 转成 DTB。
- DTB(Device Tree Blob):二进制文件,内核启动时解析的就是它。
流程就是:你写 .dts → DTC 编译 → 生成 .dtb → Bootloader 加载 → 内核解析。
核心流程: DTS(源文件) → DTC(编译器) → DTB(二进制) → 内核解析
我个人习惯在 Makefile 里加一句 dtbs 目标,一键编译所有设备树。省得每次手动敲命令。
小技巧: 用 dtc -I dtb -O dts -o output.dts input.dtb 可以把 DTB 反编译回 DTS。调试时很有用,我经常这么干。
4.2 节点与属性定义:设备树的“骨架”
设备树的结构,说白了就是一棵树。根节点是 /,下面挂各种子节点。每个节点代表一个硬件设备或总线。
4.2.1 节点定义
基本语法很简单:
/dts-v1/;
/ {
compatible = "acme,coyote";
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x0>;
};
serial@101f0000 {
compatible = "ns16550";
reg = <0x101f0000 0x1000>;
interrupts = <1 0>;
};
};
节点名格式:节点名@地址。地址部分通常写的是寄存器基址,用来区分同名节点。
注意: 节点名里的 @地址 只是标识,真正决定地址的是 reg 属性。两者不一致时,以 reg 为准。我曾经见过一个同事把地址写错,排查了半天。
4.2.2 属性定义
属性就是键值对。常见的有:
| 属性名 | 值格式 | 说明 |
|---|---|---|
compatible |
字符串 | 驱动匹配的关键字,格式 "厂商,型号" |
reg |
32位整数数组 | 描述地址空间:基址 + 长度 |
interrupts |
32位整数数组 | 中断号、触发类型等 |
status |
字符串 | "okay" 或 "disabled" |
#address-cells |
整数 | reg 中地址字段占几个 cell(32位) |
#size-cells |
整数 | reg 中长度字段占几个 cell |
你想想看,#address-cells 和 #size-cells 这两个属性,决定了子节点 reg 的解析方式。如果父节点写 #address-cells = <2>,那子节点的 reg 里地址部分就要写两个 32 位数。
4.3 reg 属性详解:地址空间的“身份证”
reg 属性描述设备占用的地址范围。格式是:
reg = <address1 length1 address2 length2 ...>;
每个 address 和 length 占几个 32 位 cell,由父节点的 #address-cells 和 #size-cells 决定。
举个例子:
/ {
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x40000000>; // 基址 0x80000000,长度 1GB
};
gpio@101e4000 {
compatible = "arm,pl061";
reg = <0x101e4000 0x1000>; // 基址 0x101e4000,长度 4KB
};
};
如果地址是 64 位的,#address-cells 就要设为 2:
/ {
#address-cells = <2>;
#size-cells = <2>;
memory@800000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>;
// 高32位 低32位 高32位 低32位
// 地址 = 0x80000000,长度 = 0x40000000
};
};
关键点: reg 的 cell 数量 = #address-cells + #size-cells。多段地址就重复这个组合。
4.4 interrupts 属性详解:中断的“接线图”
中断属性描述设备的中断请求线。它依赖 interrupt-parent 和 #interrupt-cells 来解析。
4.4.1 基本用法
/ {
interrupt-parent = <&gic>; // 指定中断控制器
gic: interrupt-controller@fff01000 {
compatible = "arm,gic-400";
reg = <0xfff01000 0x1000>;
interrupt-controller;
#interrupt-cells = <3>; // GIC 每个中断用 3 个 cell
};
serial@101f0000 {
compatible = "ns16550";
reg = <0x101f0000 0x1000>;
interrupts = <0 29 4>; // 类型 0(SPI),中断号 29,触发方式 4(高电平)
};
};
对于 ARM GIC,#interrupt-cells = <3> 的三个 cell 含义:
| Cell 位置 | 含义 | 取值 |
|---|---|---|
| 第 1 个 | 中断类型 | 0 = SPI(共享外设中断),1 = PPI(私有外设中断) |
| 第 2 个 | 中断号 | SPI 从 32 开始,PPI 从 16 开始 |
| 第 3 个 | 触发方式 | 1 = 上升沿,2 = 下降沿,4 = 高电平,8 = 低电平 |
经验之谈: 中断号在设备树里是硬件中断号,不是 Linux IRQ 号。内核会帮你映射。我曾经在移植时直接写了 Linux IRQ 号,结果中断死活不触发。嗯,这个坑我替你们踩过了。
4.4.2 中断级联
如果板子上有多个中断控制器,可以用 interrupt-map 做级联:
/ {
interrupt-parent = <&gic>;
gpio@101e4000 {
interrupt-controller;
#interrupt-cells = <2>;
interrupt-map = <
0 0 &gic 0 42 4
1 0 &gic 0 43 4
>;
};
};
这里 GPIO 控制器把它的中断 0 映射到 GIC 的 SPI 42,中断 1 映射到 SPI 43。
4.5 避坑指南:我踩过的几个坑
- reg 地址写错: 我遇到过把
#address-cells写成 2,但reg里只写了一个 32 位数的情况。内核解析出来的地址完全不对。 - 中断号偏移: 有些 SoC 的手册写的是 Linux IRQ 号,不是硬件中断号。设备树里必须用硬件号。我建议你对照 TRM 确认。
- 忘记加
interrupt-controller;: 如果一个节点要作为中断控制器,必须显式声明这个属性。否则内核不认。 - DTC 编译报错: 最常见的是括号不匹配或分号漏了。用
dtc -I dts -O dtb -o /dev/null test.dts只做语法检查,不生成文件。
重要提醒: 设备树语法错误不会导致内核编译失败,但会导致启动时解析失败,板子直接挂掉。每次修改后务必用 DTC 检查一遍。
好了,设备树的基础语法就这些。DTS 是源文件,DTC 是编译器,DTB 是内核吃的东西。节点和属性是骨架,reg 管地址,interrupts 管中断。把这些搞明白,后面写驱动移植时就顺手多了。