4. 设备树节点与属性详解:核心属性
设备树里那些属性,说白了就是硬件和驱动之间的「沟通语言」。我刚开始学的时候,看着一堆属性名也头疼。后来发现,其实核心的就那么几个,搞懂了它们,大部分设备树你都能看懂。
今天咱们就一个一个来拆解。我会结合我实际项目中踩过的坑,帮你把这些属性吃透。
4.1 compatible 属性:你是谁?
compatible 属性,是设备树里最重要的属性,没有之一。它的作用就是告诉内核:「我是哪个设备,用哪个驱动来管我」。
格式很简单,就是一个字符串列表。比如:
compatible = "vendor,device", "generic-device";
这里有个讲究:越具体的名字放前面,越通用的放后面。内核匹配驱动时,会从左到右依次尝试。
核心规则:compatible 的值通常写成 "厂商名,设备名" 的形式。比如 "ti,am335x-uart"、"fsl,imx6q-uart"。
我在项目中遇到过一个问题:某款 WiFi 芯片,设备树里只写了 "broadcom,bcm4330",结果内核找不到驱动。后来发现,驱动里匹配的是 "brcm,bcm4330"。厂商缩写不一致,折腾了我半天。所以,写 compatible 时一定要和驱动代码里的一模一样,大小写、标点都不能错。
我的习惯:写设备树前,先去内核源码里搜一下对应驱动的 of_match_table,看看它到底匹配什么字符串。这样最保险。
4.2 reg 属性:你在哪?
reg 属性描述设备的地址空间信息。说白了,就是告诉驱动:「你的寄存器在哪个地址,占多大地方」。
它的格式是:
reg = <address1 length1 address2 length2 ...>;
每个地址和长度组成一个「地址段」。一个设备可能有多个地址段,比如控制寄存器和数据缓冲区分开的情况。
来看个实际例子:
uart@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupts = <72>;
};
这个 UART 设备的寄存器基地址是 0x44e09000,长度是 0x2000(8KB)。
我曾经踩过的坑:有一次我写 reg 属性时,地址写对了,但长度写小了。结果驱动访问寄存器时,只映射了部分地址空间,读写某些寄存器直接导致系统崩溃。嗯,这个错误排查起来特别痛苦。
关于 reg 的地址位数,由父节点的 #address-cells 和 #size-cells 决定。比如:
soc {
#address-cells = <1>; // 地址用 1 个 32 位整数表示
#size-cells = <1>; // 长度用 1 个 32 位整数表示
uart@44e09000 {
reg = <0x44e09000 0x2000>; // 地址 0x44e09000,长度 0x2000
};
};
如果是 64 位地址,#address-cells 就是 2:
bus {
#address-cells = <2>;
#size-cells = <2>;
device@100000000 {
reg = <0x1 0x00000000 0x0 0x1000>; // 地址 0x100000000,长度 0x1000
};
};
4.3 interrupts 属性:怎么通知你?
设备需要和 CPU 通信,最常见的方式就是中断。interrupts 属性就是描述设备用哪个中断号、什么触发方式。
格式:
interrupts = <中断号 触发类型>;
触发类型常用的有:
| 值 | 含义 |
|---|---|
| 0 | 上升沿触发 |
| 1 | 下降沿触发 |
| 2 | 高电平触发 |
| 3 | 低电平触发 |
| 4 | 上升沿和下降沿都触发 |
实际例子:
gpio-keys {
compatible = "gpio-keys";
button@1 {
label = "Power";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
};
};
这里中断号是 3,触发方式是下降沿触发。
我建议:中断触发类型尽量和硬件实际信号一致。比如按键按下是低电平,就用下降沿或低电平触发。选错了,可能会出现按一次触发两次,或者松手才触发的问题。
另外,如果设备有多个中断,可以这样写:
interrupts = <10 IRQ_TYPE_EDGE_RISING>, <11 IRQ_TYPE_LEVEL_HIGH>;
驱动里通过 platform_get_irq() 按索引获取。
4.4 status 属性:设备能用吗?
status 属性控制设备的启用状态。说白了就是告诉内核:「这个设备现在能不能用」。
常用的值:
| 值 | 含义 |
|---|---|
| okay | 设备可用,正常启用 |
| disabled | 设备禁用,内核不会去初始化它 |
| reserved | 保留,通常用于特殊用途 |
| fail | 设备检测失败 |
这个属性在板级设备树中特别有用。比如 SoC 默认把所有外设都打开,但具体板子上可能只用一部分:
// 在 SoC 的 dtsi 文件中
uart1: uart@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
status = "disabled"; // 默认禁用
};
// 在板级 dts 文件中
&uart1 {
status = "okay"; // 这个板子用 UART1,启用它
};
重要:status 属性是可选的。如果不写,默认就是 "okay"(设备可用)。但为了代码清晰,我建议显式写出来。
我记得有一次调试,某个 I2C 设备死活不工作。查了半天,发现是 status 被写成了 "ok"(少了个 a)。内核解析时没识别出来,直接当 disabled 处理了。嗯,这种低级错误,犯过一次就记住了。
4.5 其他常用属性
除了上面四个核心属性,还有几个也经常见到:
- label:给设备起个名字,方便人识别。比如 "label = "Power Button"。
- clock-frequency:指定设备的工作频率。比如 I2C 控制器常用。
- pinctrl-0、pinctrl-names:引脚复用配置,后面章节会详细讲。
- dmas、dma-names:DMA 通道配置。
4.6 知识体系图
下面这张图,帮你理清这几个核心属性的关系:
4.7 实战小技巧
最后,分享几个我平时写设备树的习惯:
- 先看 datasheet:写 reg 和 interrupts 前,一定先看芯片手册,确认寄存器基地址和中断号。
- 参考现成的 dts:内核源码里有很多现成的设备树文件,找同款芯片的参考一下,比自己从头写快得多。
- 编译检查:写完设备树,用
dtc -I dts -O dtb编译一下,语法错误能提前发现。 - 查看 /proc/device-tree:系统启动后,去这个目录看看,设备树解析成什么样一目了然。
特别注意:设备树里不要写驱动逻辑。它只描述硬件,不描述行为。比如「按键按下时做什么」是驱动的活,设备树只管「这个按键接在哪个 GPIO 上」。
好了,核心属性就这些。你想想看,搞懂了 compatible、reg、interrupts、status,大部分设备树你都能读懂了。下次看到一段设备树,试着按这个思路去分析,很快就能上手。