第2章 设备树基础:从DTS语法到实战编译
设备树(Device Tree)这东西,刚接触时觉得挺玄乎。说白了,它就是一套描述硬件信息的“说明书”。Linux内核通过它才知道你板子上接了哪些外设、内存多大、中断怎么连。我在Tina Linux项目里调过不下二十块板子,每次第一件事就是翻设备树——硬件怎么接的,全写在这里了。
核心认知:设备树 = 硬件拓扑的文本化描述。内核不直接读原理图,它只认.dts文件。
2.1 DTS语法入门——先看懂再说
DTS(Device Tree Source)的语法其实很简单。它用树形结构描述硬件,根节点是“/”,下面挂各种外设节点。每个节点可以包含属性和子节点。
来看一个最简模板:
/dts-v1/;
/ {
compatible = "allwinner,sun8i";
#address-cells = <1>;
#size-cells = <1>;
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x0>;
};
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x20000000>;
};
};
注意几个要点:
- /dts-v1/; 是版本声明,必须写在第一行
- 每个节点用
节点名@地址格式,地址部分可以省略 - 属性值用
=赋值,字符串加引号,数值用尖括号 - 分号结尾,花括号表示层级——跟C语言很像
我的习惯:写设备树时,我会先把根节点下的 #address-cells 和 #size-cells 写好。这两个决定了reg属性的地址宽度,忘写了后面编译会报一堆错。
2.2 节点与属性——设备树的“零件”
每个设备树节点代表一个硬件设备或总线。节点里最重要的就是 compatible 属性——内核靠它匹配驱动。
| 属性名 | 作用 | 示例 |
|---|---|---|
compatible |
驱动匹配字符串 | "allwinner,i2c0" |
reg |
寄存器地址/长度 | <0x01c2b000 0x400> |
interrupts |
中断号/触发方式 | <0 5 4> |
status |
设备使能状态 | "okay" 或 "disabled" |
clocks |
时钟引用 | <&ccu CLK_I2C0> |
我遇到过最坑的事:某次I2C驱动死活不工作,查了两天才发现 status = "disabled" 没改成 "okay"。嗯,这种低级错误,犯过一次就记住了。
2.3 reg属性详解——地址怎么描述
reg 属性描述设备的地址空间。格式是 <地址1 长度1 地址2 长度2 ...>。每个地址和长度占几个cell,由父节点的 #address-cells 和 #size-cells 决定。
// 父节点定义地址宽度
soc@01c00000 {
compatible = "simple-bus";
#address-cells = <1>; // 地址占1个cell(32位)
#size-cells = <1>; // 长度占1个cell(32位)
uart0@01c28000 {
compatible = "allwinner,sun8i-uart";
reg = <0x01c28000 0x400>; // 基址0x01c28000,长度0x400
interrupts = <0 1 4>;
};
};
如果 #address-cells = <2>,那地址就要写两个数,比如 <0x0 0x40000000 0x0 0x20000000>。64位地址空间常见这种写法。
避坑指南:我曾经在移植全志V3s的SDK时,把 #address-cells 写成了2,但reg里只给了一个地址值。编译不报错,但内核解析出来的地址完全不对,外设根本访问不到。记住:address-cells和reg里的地址数量必须匹配。
2.4 interrupts属性详解——中断怎么配
中断属性稍微复杂点。它通常包含三个值:中断类型、中断号、触发方式。具体含义依赖 interrupt-parent 指定的中断控制器。
// 中断控制器定义
gic: interrupt-controller@01c81000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>; // 每个中断占3个cell
interrupt-controller;
reg = <0x01c81000 0x1000>;
};
// 外设引用中断
uart0@01c28000 {
compatible = "allwinner,sun8i-uart";
reg = <0x01c28000 0x400>;
interrupt-parent = <&gic>; // 指定中断控制器
interrupts = <0 1 4>; // 类型0(SPI) 中断号1 触发方式4(高电平)
};
三个cell的含义(以GIC为例):
- 第一个cell:中断类型。0=SPI(共享外设中断),1=PPI(私有外设中断)
- 第二个cell:中断号。SPI从32开始编号,PPI从16开始
- 第三个cell:触发方式。1=上升沿,2=下降沿,4=高电平,8=低电平
你想想看,如果触发方式配错了会怎样?中断要么不触发,要么疯狂触发。我在调试GPIO按键中断时就遇到过——配成了电平触发,结果按键一直按着,中断一直进,系统直接卡死。
2.5 设备树编译与反编译——实战工具链
.dts文件不能直接给内核用,得编译成二进制.dtb。反过来,拿到别人的.dtb也可以反编译成.dts看看人家怎么配的。
常用命令就三个:
# 编译:dts → dtb
dtc -I dts -O dtb -o sun8i-v3s.dtb sun8i-v3s.dts
# 反编译:dtb → dts
dtc -I dtb -O dts -o sun8i-v3s.dts sun8i-v3s.dtb
# 带调试信息的编译(推荐)
dtc -I dts -O dtb -o output.dtb -@ input.dts
-@ 参数会保留符号信息,方便后续动态加载overlay。我在调试Tina Linux的LCD驱动时,经常用反编译看别人板子的时序参数——比自己对着手册算快多了。
实用技巧:编译报错时,先检查括号是否匹配、分号是否漏了。DTC的报错信息有时候不太友好,但90%的错误都是语法问题。我习惯用 dtc -O dts -o /dev/null input.dts 只做语法检查,不生成文件。
2.6 知识结构总览
下面这张图把设备树的核心流程串起来了。从.dts源文件开始,经过编译生成.dtb,内核启动时解析dtb,最终驱动通过 compatible 匹配到设备节点。
2.7 实战要点总结
最后列几个我反复踩过的坑,你写设备树时多留个心眼:
- 地址对齐:reg里的基地址必须是4字节对齐,有些外设还要求页对齐(4KB)。不对齐的话,内核ioremap会失败
- 中断号偏移:全志平台的中断号在设备树里通常要减32(因为SPI从32开始编号)。我见过新手直接抄数据手册的原始号,结果中断永远不触发
- 引用标签:用
&标签引用节点时,确保标签在dtsi头文件里定义过。编译时不会报错,但链接阶段会生成空节点 - 头文件包含:.dts文件可以
#include.dtsi文件。我习惯把SoC公共部分放.dtsi,板级差异放.dts,这样复用性最好
血的教训:有一次我改完设备树,只编译了.dtb没拷到boot分区,重启后还是旧配置。折腾了半天才发现是忘了更新boot分区里的dtb文件。记住:编译完一定要确认dtb被正确部署到了启动分区。