3. 设备树基础(DTS):语法、节点与关键属性详解
设备树,说白了就是一块“硬件描述板”。
你写驱动,总得知道CPU接了什么外设、中断挂在哪条线上、寄存器地址是多少。以前这些信息散落在C代码里,改个引脚就要重新编译内核。后来社区搞出了设备树,把硬件描述和驱动代码彻底解耦。我个人觉得,这是嵌入式Linux发展史上最聪明的设计之一。
核心思想:设备树是一个树形数据结构,用文本文件(.dts)描述硬件,编译成二进制(.dtb)后由Bootloader传递给内核。驱动通过解析设备树来获取硬件资源,不再硬编码。
3.1 设备树语法——其实没那么玄乎
先看一个最简单的例子:
/dts-v1/;
/ {
model = "Rockchip RK3566 EVB";
compatible = "rockchip,rk3566-evb", "rockchip,rk3566";
chosen {
stdout-path = &uart2;
};
uart2: serial@fe660000 {
compatible = "rockchip,rk3566-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe660000 0x0 0x100>;
interrupts = <0 116 4>;
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
clock-names = "baudclk", "apb_pclk";
status = "disabled";
};
};
嗯,结构很清晰:
- /dts-v1/:版本声明,必须写在第一行。
- /:根节点,整棵树的起点。
- uart2: serial@fe660000:一个子节点,
uart2是标签(label),serial@fe660000是节点名+地址。 - 属性:
compatible、reg、interrupts这些就是节点的“身份证”。
我的习惯:写节点名时,尽量用功能名@基地址的格式,比如i2c@ff3f0000。这样一看就知道是什么外设、地址在哪,调试时省不少事。
3.2 节点与属性——树是怎么长出来的
设备树的节点可以嵌套,就像文件夹套文件夹。每个节点可以有多个属性,属性值可以是字符串、整数、数组、二进制数据等。
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "rockchip,rk3566-uart"; |
驱动匹配的关键 |
| 32位整数 | reg = <0xfe660000 0x100>; |
地址+长度 |
| 64位整数 | reg = <0x0 0xfe660000 0x0 0x100>; |
高32位+低32位 |
| 布尔值 | status = "disabled"; |
存在即true,不存在即false |
| 数组 | interrupts = <0 116 4>; |
中断类型+编号+触发方式 |
| 引用 | &uart2 |
通过标签引用其他节点 |
这里有个容易踩的坑——地址位数。我在项目中遇到过,有人把64位地址写成了32位格式,结果驱动死活读不到正确的寄存器基址。你想想看,reg = <0xfe660000 0x100>在32位系统里没问题,但在64位系统里,内核会认为第一个0xfe660000是地址,第二个0x100是长度。如果地址超过32位,必须写成<0x0 0xfe660000 0x0 0x100>,两个cell一组。
3.3 关键属性详解——compatible、reg、interrupts
3.3.1 compatible:驱动匹配的“钥匙”
这是最重要的属性,没有之一。内核通过compatible字符串找到对应的驱动。写法有讲究:
compatible = "rockchip,rk3566-uart", "snps,dw-apb-uart";
第一个字符串是最具体的,第二个是通用的。内核匹配时,先找第一个,找不到再找第二个。这样做的好处是:如果Rockchip的UART有特殊寄存器,可以用第一个字符串绑定专用驱动;如果没有,就退回到通用的DesignWare驱动。
我曾经见过有人把顺序写反了,结果内核总是匹配到通用驱动,专用驱动的优化功能没生效。排查了半天才发现是compatible顺序的问题。记住:越具体越靠前。
3.3.2 reg:告诉驱动“我在哪”
reg描述设备的地址空间。格式是reg = <地址1 长度1 地址2 长度2 ...>。每个地址+长度称为一个reg区域(reg region)。
reg = <0x0 0xfe660000 0x0 0x100>; // 64位系统:地址0xfe660000,长度0x100
驱动里通过platform_get_resource(pdev, IORESOURCE_MEM, 0)拿到第一个reg区域。如果有多个区域,索引依次递增。
我个人的习惯是:每个外设只用一个reg区域,除非硬件设计上确实有多个不连续的地址块。这样驱动代码更简洁,不容易出错。
3.3.3 interrupts:中断怎么配
中断属性的格式取决于中断控制器。在Rockchip平台上,GIC(通用中断控制器)的格式通常是:
interrupts = <中断类型 中断号 触发方式>;
- 中断类型:0=SPI(共享外设中断),1=PPI(私有外设中断)。
- 中断号:从32开始算。比如GIC里的中断号116,实际硬件中断线是116+16=132(因为前16个是CPU内部中断)。
- 触发方式:4=高电平触发,1=上升沿触发,2=下降沿触发,8=低电平触发。
举个例子:
interrupts = <0 116 4>; // SPI中断,硬件中断号132,高电平触发
避坑指南:不同厂商的中断号定义可能不一样。Rockchip的TRM里写的中断号通常是硬件中断号(从0开始),而设备树里用的是GIC中断号(从32开始)。我曾经因为没搞清楚这个偏移,配出来的中断一直不触发。后来对着TRM和内核代码逐行比对才找到问题。
3.4 知识体系总览
下面这张图概括了设备树的核心结构,我建议你把它存下来,写设备树时对照着看:
3.5 实战建议——从看懂到会写
刚开始接触设备树,别急着自己从头写。我的做法是:
- 找一块开发板的完整dts,比如Rockchip官方SDK里的
rk3566-evb.dts。 - 对照原理图,一个一个节点看过去。看它的compatible是什么、reg地址对不对、中断号是否匹配。
- 改一个节点试试,比如把UART的status从"disabled"改成"okay",重新编译dtb,看内核能不能识别。
- 用
dtc工具反编译:dtc -I dtb -O dts -o output.dts input.dtb,看看实际生效的设备树长什么样。
一个小技巧:在/sys/firmware/devicetree/base/目录下,你可以直接看到内核解析后的设备树结构。每个属性都是一个文件,用cat就能读出原始二进制数据。调试时特别有用。