4、设备树基础概念:DTS/DTC/DTB的关系、节点与属性语法、reg与interrupts属性
设备树(Device Tree)这个概念,我刚开始接触时也觉得挺玄乎。说白了,它就是一套描述硬件信息的“说明书”。内核通过这份说明书,才知道你板子上接了哪些外设、内存多大、中断怎么连。
在没有设备树的年代,这些信息都硬编码在 arch/arm/mach-xxx 的板级文件里。每次换一颗芯片、改一个引脚,都得重新编译内核。你想想看,这多麻烦。设备树把硬件描述和内核代码解耦了,一套内核镜像,换不同的 .dtb 就能适配不同板子。
4.1 DTS / DTC / DTB 三者的关系
这三个缩写,我经常被新人问起。其实它们的关系很简单,就是“源文件 → 编译器 → 二进制产物”的链条。
| 缩写 | 全称 | 作用 | 类比 |
|---|---|---|---|
| DTS | Device Tree Source | 文本格式的硬件描述文件 | .c 源文件 |
| DTC | Device Tree Compiler | 将 DTS 编译为 DTB 的工具 | gcc 编译器 |
| DTB | Device Tree Blob | 二进制格式,Bootloader 传递给内核 | .o / .elf 可执行文件 |
流程是这样的:你写一个 .dts 文件,然后用 dtc 工具把它编译成 .dtb。Bootloader(比如 U-Boot)在启动内核时,把 .dtb 的物理地址通过寄存器 r2 传给内核。内核解析这个二进制块,构建出设备树的内存结构。
我个人的习惯:调试阶段用 dtc -I dts -O dtb 手动编译,配合 fdtdump 反编译查看二进制内容。这样能快速定位是 dts 写错了,还是内核解析出了问题。
4.2 节点与属性语法
设备树的语法,其实比 C 语言简单多了。它就是一个树形结构,根节点是 /,下面挂各种子节点。每个节点可以有自己的属性和子节点。
我给大家看一个典型的例子:
/dts-v1/;
/ {
compatible = "acme,coyote", "acme,board";
#address-cells = <1>;
#size-cells = <1>;
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges;
serial@101f0000 {
compatible = "ns16550";
reg = <0x101f0000 0x1000>;
interrupts = <1 0>;
clock-frequency = <0x1e8480>;
};
gpio@101f3000 {
compatible = "brcm,bcm2835-gpio";
reg = <0x101f3000 0x20>;
interrupts = <2 0>;
gpio-controller;
#gpio-cells = <2>;
};
};
};
这里有几个关键点,我挨个说一下:
- 节点命名:格式是 node-name@unit-address。@ 后面跟的是设备在总线上的地址,比如 memory@80000000 表示内存起始地址是 0x80000000。
- 属性值类型:字符串用双引号,32 位整数用 <> 括起来,64 位整数用 <0x0 0x12345678> 这种形式,字节数组用 []。
- compatible 属性:这是最重要的属性。内核通过它来匹配驱动。格式是 "厂商,型号"。我建议第一个写具体的型号,第二个写兼容的通用型号。
- #address-cells 和 #size-cells:这两个属性定义了 reg 属性中地址和长度占几个 32 位单元。1 表示 32 位,2 表示 64 位。
避坑指南:我曾经在一个项目里,把 #address-cells 写成了 2,但 reg 里只给了 32 位地址。结果内核解析出来的地址完全不对,设备死活不工作。查了两天才发现是这里的问题。记住:父子节点的 address-cells 和 size-cells 是独立的,子节点按父节点的定义来解析。
4.3 reg 属性详解
reg 属性描述设备在总线上的地址空间。它的格式是:reg = <地址1 长度1 地址2 长度2 ...>。每个地址和长度占几个 cell,由父节点的 #address-cells 和 #size-cells 决定。
举个例子:
// 32 位地址空间
#address-cells = <1>;
#size-cells = <1>;
reg = <0x101f0000 0x1000>; // 基地址 0x101f0000,长度 4KB
// 64 位地址空间
#address-cells = <2>;
#size-cells = <2>;
reg = <0x0 0x80000000 0x0 0x20000000>; // 基地址 0x80000000,长度 512MB
在驱动里,我们通过 platform_get_resource() 来获取 reg 信息。内核会自动处理地址转换,你拿到的是 CPU 视角的物理地址。
我遇到过的情况:有些 SoC 的片上外设挂在不同的总线桥上,比如 AHB 和 APB。每个总线桥有自己的地址映射。这时候 ranges 属性就派上用场了,它定义了子总线地址到父总线地址的映射关系。没有 ranges,内核就不知道如何转换地址。
4.4 interrupts 属性详解
中断属性描述设备使用的中断资源。它的格式和中断控制器有关。最常见的是 ARM GIC(Generic Interrupt Controller)的情况。
先看一个例子:
// 中断控制器节点
gic: interrupt-controller@f0001000 {
compatible = "arm,cortex-a9-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0xf0001000 0x1000>,
<0xf0000100 0x100>;
};
// 使用中断的设备
serial@101f0000 {
compatible = "ns16550";
reg = <0x101f0000 0x1000>;
interrupts = <0 33 4>;
interrupt-parent = <&gic>;
};
对于 ARM GIC,每个中断用 3 个 cell 描述:
| Cell | 含义 | 取值 |
|---|---|---|
| 第 1 个 | 中断类型 | 0 = SPI(共享外设中断),1 = PPI(私有外设中断) |
| 第 2 个 | 中断号 | SPI 从 32 开始编号,PPI 从 16 开始 |
| 第 3 个 | 触发方式 | 1 = 上升沿,2 = 下降沿,4 = 高电平,8 = 低电平 |
嗯,这里要注意:SPI 的中断号在设备树里是从 0 开始编号的,但 GIC 内部 SPI 从 ID32 开始。所以设备树里写 33,实际对应 GIC 的 ID65。这个偏移量是硬件决定的,驱动里不用管,GIC 驱动会处理好。
我曾经踩过的坑:在一个项目中,我把中断触发方式写成了 1(上升沿触发),但实际硬件是电平触发。结果中断只触发了一次,后续的中断全部丢失。用 cat /proc/interrupts 一看,中断计数停在 1 不动了。改成 4(高电平触发)后一切正常。所以,一定要确认硬件的中断触发方式,不要想当然。
4.5 中断级联与 interrupt-map
复杂系统中,可能会有多个中断控制器级联。比如一个 SoC 有 GIC,外接的 PCIe 桥有自己的中断控制器。这时候就需要 interrupt-map 来做中断路由。
pcie@10000000 {
compatible = "pci-host-ecam-generic";
reg = <0x10000000 0x100000>;
#address-cells = <3>;
#size-cells = <2>;
#interrupt-cells = <1>;
interrupt-map-mask = <0xf800 0 0 7>;
interrupt-map = <
0x0000 0 0 1 &gic 0 123 4
0x0000 0 0 2 &gic 0 124 4
0x0000 0 0 3 &gic 0 125 4
0x0000 0 0 4 &gic 0 126 4
>;
};
interrupt-map 的每一行格式是:子中断描述 + &父中断控制器 + 父中断描述。内核在解析时,会先根据 interrupt-map-mask 做掩码匹配,然后找到对应的父中断。
说实话,interrupt-map 的语法有点绕。我个人的建议是:除非你在做 PCIe 或者复杂总线桥的驱动,否则一般用不到。大部分嵌入式设备,一个 GIC 就搞定了。
4.6 常用节点与属性速查
最后,我整理了一个常用属性和节点的速查表,方便大家写 dts 时参考:
| 属性/节点 | 用途 | 示例 |
|---|---|---|
| compatible | 驱动匹配 | "ti,am335x-uart", "ns16550" |
| reg | 地址空间 | <0x48022000 0x2000> |
| interrupts | 中断资源 | <0 33 4> |
| interrupt-parent | 指定中断控制器 | <&gic> |
| clocks | 时钟源 | <&clk 0x123> |
| status | 设备状态 | "okay" / "disabled" |
| pinctrl-0 | 引脚复用配置 | <&uart0_pins> |
| memory 节点 | 描述内存 | memory@80000000 |
| chosen 节点 | 启动参数 | bootargs = "console=ttyO0,115200" |
设备树的内容其实就这些核心概念。你只要理解了 DTS/DTC/DTB 的关系,掌握了节点和属性的基本语法,再搞清楚 reg 和 interrupts 这两个最常用的属性,大部分设备的描述都能写出来。剩下的就是多写多练,遇到问题用 dtc 反编译看看,或者加 printk 打印 of 函数的返回值,慢慢就熟练了。