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就是一个节点。@后面的数字是它的基地址,方便人眼识别。节点名可以随便起,但最好和设备类型对应。

我的习惯:节点名尽量用芯片手册里的外设名,比如lcdc、i2c0、uart1。这样回头查手册时,一眼就能对上号。

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()时,内核会自动帮你映射这段地址。

我曾经踩过的坑:有一次我写显示驱动,reg里的长度写小了。结果驱动访问寄存器时,内核报“ioremap失败”。查了半天才发现是长度不对。记住:长度一定要大于等于芯片手册里描述的寄存器范围。

如果设备有多个地址段,比如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上。你得告诉内核,你的设备归哪个中断控制器管。

关键点:中断号不是随便写的。你得查芯片手册里的中断映射表。比如AM335x的LCD控制器中断号是36,这是硬件设计决定的,不能改。

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引用具体的管脚配置节点。内核在设备初始化时,会自动把管脚配置成指定模式。

我的经验:管脚配置值怎么算?你得看芯片手册的“Control Module”章节。每个管脚有一个偏移地址和一个配置寄存器。配置寄存器的低几位控制复用功能,高几位控制上下拉、驱动能力等。我一般先在芯片手册里查好,再写到设备树里。

为什么pinctrl这么重要?因为管脚配错了,设备根本没法工作。我曾经遇到过LCD显示花屏,查了半天,结果是LCD_DATA0和LCD_DATA1的管脚复用模式配反了。从那以后,我每次写pinctrl都会对着芯片手册逐行核对。

3.8 知识体系总览

下面这张图总结了设备树基础语法的核心知识点。你可以把它当作“速查表”。

设备树基础语法知识体系 设备树节点 (Node) compatible reg interrupts status pinctrl 驱动匹配钥匙 厂商,设备型号 地址空间描述 <基址 长度> 中断资源 <中断号 触发方式> 设备开关 okay / disabled 管脚复用 偏移+配置值 核心原则:设备树描述硬件,驱动读取设备树 实践:修改.dts → 编译dtb → 内核解析 → 驱动匹配

3.9 实战建议

学完这些语法,你可能会问:我该怎么开始写设备树?我的建议是:

  1. 先看芯片厂商提供的.dtsi文件。比如TI、NXP、Rockchip都会提供SoC级的设备树头文件。里面已经定义好了所有外设节点,你只需要在板级.dts里引用和修改。
  2. 对着芯片手册核对reg和interrupts。不要相信网上的“通用配置”,每个板子都可能不一样。
  3. pinctrl一定要逐管脚确认。我见过太多因为管脚复用配错导致的“灵异问题”——明明驱动加载成功了,设备就是不工作。
  4. 用status = "disabled"管理未使用的设备。这样既能保留.dtsi的完整性,又能避免驱动误加载。
避坑指南:我曾经在移植显示驱动时,LCD一直黑屏。查了三天,最后发现是pinctrl里少配了一个管脚——LCD的背光使能脚。那个脚默认是GPIO输入模式,LCD背光不亮,当然黑屏。从那以后,我写pinctrl都会把设备所有相关管脚列出来,一个不漏。

设备树语法不难,难的是细心。每个属性、每个值都对应着硬件手册里的一个数字。你写对了,驱动就工作;写错了,调试起来很痛苦。嗯,慢慢来,多练几次就熟了。