3、设备树基础语法:节点、属性、reg、interrupts、compatible、status、pinctrl
设备树(Device Tree)是什么?说白了,它就是一块“硬件描述板”。你写驱动时,不用再在代码里硬编码各种地址、中断号了。设备树把这些信息抽出来,用一套统一的语法描述硬件。内核启动时解析它,驱动就能知道自己该管哪些资源。
我个人习惯,把设备树看作“硬件说明书”。你想想看,一个嵌入式板子上有多少外设?UART、I2C、SPI、GPIO、LCD……每个外设的地址、中断、时钟都不一样。如果没有设备树,驱动里就得写死一堆宏定义,换个板子就得改代码。设备树解决了这个问题——硬件变了,改.dts文件就行,驱动代码不用动。
3.1 节点:设备树的基本单元
设备树里的一切都是节点(node)。每个节点代表一个设备或一段总线。节点从根(/)开始,层层嵌套。
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
lcdc: lcdc@4830e000 {
compatible = "ti,am335x-lcdc";
reg = <0x4830e000 0x1000>;
interrupts = <36>;
status = "okay";
};
};
看到没?lcdc@4830e000就是一个节点。@后面的数字是它的基地址,方便人眼识别。节点名可以随便起,但最好和设备类型对应。
3.2 属性:描述节点的“形容词”
节点里可以放各种属性(property)。属性就是键值对,用来描述节点的特征。值可以是字符串、整数、数组、二进制数据等。
| 属性类型 | 写法示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "ti,am335x-lcdc"; |
用于匹配驱动 |
| 32位整数 | reg = <0x4830e000 0x1000>; |
地址和长度 |
| 整数数组 | interrupts = <36 0>; |
中断号和触发方式 |
| 布尔值 | status = "okay"; |
设备是否启用 |
| 二进制 | mac-address = [00 11 22 33 44 55]; |
MAC地址等 |
嗯,这里要注意:属性名不能乱写。内核有标准属性名,比如compatible、reg、interrupts。自定义属性也可以,但驱动里得用of_property_read_*系列函数去读。
3.3 reg:描述地址空间
reg属性是设备树里最常用的属性之一。它描述设备占用的地址范围。格式是:reg = <地址1 长度1 地址2 长度2 ...>;
lcdc: lcdc@4830e000 {
reg = <0x4830e000 0x1000>;
};
这表示LCD控制器从0x4830e000开始,占用0x1000字节(4KB)的地址空间。驱动里调用devm_ioremap_resource()时,内核会自动帮你映射这段地址。
如果设备有多个地址段,比如FIFO和配置寄存器分开,可以这样写:
dma: dma@4a000000 {
reg = <0x4a000000 0x1000>, /* 配置寄存器 */
<0x4a001000 0x2000>; /* FIFO区域 */
};
3.4 interrupts:描述中断资源
中断描述也很简单。interrupts属性告诉内核:这个设备用哪个中断号、什么触发方式。
lcdc: lcdc@4830e000 {
interrupts = <36 0>;
interrupt-parent = <&intc>;
};
这里36是中断号,0是触发标志(0表示上升沿触发,1表示低电平触发,具体看芯片)。interrupt-parent指向中断控制器节点,通常是GIC(通用中断控制器)。
为什么要有interrupt-parent?因为一个SoC里可能有多个中断控制器。比如有些外设挂在GPIO中断上,有些挂在GIC上。你得告诉内核,你的设备归哪个中断控制器管。
3.5 compatible:驱动匹配的“钥匙”
compatible是设备树里最重要的属性。它决定了内核用哪个驱动来管理这个设备。格式是:compatible = "厂商,设备型号";
compatible = "ti,am335x-lcdc", "ti,da830-lcdc";
内核驱动里会有一个of_match_table,里面列出了这个驱动支持的compatible字符串。设备树里的compatible和驱动里的匹配上,驱动才会被加载。
我个人习惯,compatible字符串按“从具体到通用”的顺序写。内核匹配时,会从左到右依次尝试。第一个匹配上的驱动就被使用。这样,如果某个板子有特殊需求,可以写一个更具体的compatible,优先匹配自定义驱动。
| compatible字符串 | 匹配优先级 | 说明 |
|---|---|---|
ti,am335x-lcdc |
最高 | AM335x专用驱动 |
ti,da830-lcdc |
次高 | 兼容DA830的通用驱动 |
3.6 status:设备的“开关”
status属性控制设备是否启用。常用值就两个:
"okay":设备启用"disabled":设备禁用
为什么需要这个属性?因为同一个.dtsi文件可能被多个板子引用。比如某个SoC有3个I2C控制器,但你的板子只用了2个。你可以在板级.dts里把没用的那个设成disabled。
&i2c2 {
status = "disabled";
};
嗯,这里要注意:status默认值是"okay"还是"disabled"?这取决于.dtsi里的定义。如果.dtsi里没写,默认就是"okay"。所以,如果你想让某个设备默认禁用,记得在.dtsi里显式写上status = "disabled";。
3.7 pinctrl:管脚复用配置
pinctrl是设备树里最让人头疼的部分,但也是最重要的。它负责配置芯片管脚的复用功能——同一个管脚,可能做GPIO,也可能做UART TX,或者I2C SCL。你得告诉内核,这个设备用哪些管脚、什么功能。
lcd_pins: lcd_pins {
pinctrl-single,pins = <
0x20 0x08 /* LCD_DATA0, PIN_OUTPUT, MUX_MODE0 */
0x24 0x08 /* LCD_DATA1, PIN_OUTPUT, MUX_MODE0 */
...
>;
};
lcdc: lcdc@4830e000 {
pinctrl-names = "default";
pinctrl-0 = <&lcd_pins>;
};
这里pinctrl-names定义状态名,pinctrl-0引用具体的管脚配置节点。内核在设备初始化时,会自动把管脚配置成指定模式。
为什么pinctrl这么重要?因为管脚配错了,设备根本没法工作。我曾经遇到过LCD显示花屏,查了半天,结果是LCD_DATA0和LCD_DATA1的管脚复用模式配反了。从那以后,我每次写pinctrl都会对着芯片手册逐行核对。
3.8 知识体系总览
下面这张图总结了设备树基础语法的核心知识点。你可以把它当作“速查表”。
3.9 实战建议
学完这些语法,你可能会问:我该怎么开始写设备树?我的建议是:
- 先看芯片厂商提供的.dtsi文件。比如TI、NXP、Rockchip都会提供SoC级的设备树头文件。里面已经定义好了所有外设节点,你只需要在板级.dts里引用和修改。
- 对着芯片手册核对reg和interrupts。不要相信网上的“通用配置”,每个板子都可能不一样。
- pinctrl一定要逐管脚确认。我见过太多因为管脚复用配错导致的“灵异问题”——明明驱动加载成功了,设备就是不工作。
- 用
status = "disabled"管理未使用的设备。这样既能保留.dtsi的完整性,又能避免驱动误加载。
设备树语法不难,难的是细心。每个属性、每个值都对应着硬件手册里的一个数字。你写对了,驱动就工作;写错了,调试起来很痛苦。嗯,慢慢来,多练几次就熟了。