3. 设备树基础语法(二):标准属性详解与实战

各位同学,咱们接着聊设备树。上一章我们把设备树的基本骨架搭起来了,这一章要往骨架上填肉——也就是那些标准属性。说白了,这些属性就是设备树世界的「通用语言」,不管你是做I2C控制器,还是写GPIO驱动,都得跟它们打交道。

我个人习惯把标准属性分成三类:身份标识类(你是谁)、资源描述类(你用啥)、状态控制类(你活没活)。咱们一个一个来啃。

3.1 compatible:你的设备叫什么名字

compatible 属性,我愿称之为设备树的「身份证」。内核通过它来匹配驱动。格式很简单:

compatible = "厂商,型号", "通用型号";

注意顺序:越具体的越靠前。内核匹配时从左往右找,找到第一个能驱动的就停。

实战经验:我在项目中遇到过一个问题——某款LCD屏的compatible写成了"ilitek,ili9341",但驱动里注册的是"ilitek,ili9341-v1"。结果屏死活点不亮。排查了半天,发现就是字符串不匹配。从那以后,我每次写compatible都会去驱动源码里确认一遍。

来看个完整例子:

// 一个SPI Flash节点的compatible写法
spi_flash: flash@0 {
    compatible = "winbond,w25q128", "jedec,spi-nor";
    reg = <0>;
    spi-max-frequency = <104000000>;
};

这里写了两个兼容型号。第一个是精确的Winbond芯片,第二个是通用的JEDEC SPI NOR标准。万一内核没有Winbond的驱动,还能用通用驱动兜底。你想想看,这设计是不是挺巧妙的?

3.2 reg:你的设备占哪些资源

reg 属性描述设备的地址空间。对于内存映射设备,它告诉内核:我的寄存器从哪个地址开始,占多大地方。

格式是:

reg = <地址1 长度1>, <地址2 长度2>, ...;

嗯,这里要注意:地址和长度的位数由父节点的 #address-cells 和 #size-cells 决定。默认都是1个32位单元。

#address-cells #size-cells 含义
1 1 32位地址,32位长度
2 1 64位地址,32位长度
1 0 只有地址,没有长度(常用于片选信号)

避坑指南:我曾经在调试一个FPGA桥接设备时,reg写成了 reg = <0x43C00000 0x10000>,但父节点声明的是 #address-cells = <2>。结果内核解析出来的地址完全不对,设备根本没法访问。后来查了半天才发现是cells数量不匹配。记住:子节点的reg格式必须跟父节点的cells声明一致!

多段地址空间的例子:

// 一个以太网控制器,有两段寄存器空间
ethernet@ff700000 {
    compatible = "allwinner,sun8i-emac";
    reg = <0xff700000 0x10000>,  // 主寄存器
          <0xff714000 0x1000>;   // DMA描述符区
    interrupts = <0 57 4>;
};

3.3 interrupts:你的设备怎么通知CPU

interrupts 属性描述设备的中断信息。它告诉内核:我这个设备用哪个中断号,触发方式是什么。

格式:

interrupts = <中断号 触发类型>;

触发类型的常见值:

  • 0:上升沿触发
  • 1:下降沿触发
  • 2:高电平触发
  • 3:低电平触发
  • 4:边沿触发(上升沿和下降沿都触发)

但要注意,中断号的编码方式取决于中断控制器。ARM GIC(通用中断控制器)通常用这种方式:

interrupts = <0 30 4>;  // 第一个0表示SPI中断,30是中断号,4是触发类型

这里的第一个数字:0=SPI(共享外设中断),1=PPI(私有外设中断)。

小技巧:我建议你在写interrupts之前,先找到中断控制器的设备树绑定文档。不同厂家的GIC实现可能有细微差别。比如有些芯片把中断号从0开始编号,有些从16开始。搞错了,中断就收不到。

来看一个带中断控制器的完整例子:

// 中断控制器
gic: interrupt-controller@1c81000 {
    compatible = "arm,gic-400";
    reg = <0x1c81000 0x1000>,
          <0x1c82000 0x2000>;
    interrupt-controller;
    #interrupt-cells = <3>;
};

// 使用中断的设备
uart0: serial@1c28000 {
    compatible = "snps,dw-apb-uart";
    reg = <0x1c28000 0x400>;
    interrupts = <0 1 4>;  // SPI中断,中断号1,高电平触发
    clocks = <&uart_clk>;
    status = "okay";
};

3.4 status:你的设备活没活

status 属性最简单,但也最容易忽略。它控制设备是否启用:

  • "okay":设备启用
  • "disabled":设备禁用
  • "reserved":保留(不常用)
  • "fail":设备检测失败

为什么需要这个属性?因为很多SoC的.dtsi文件里把所有外设都定义好了,但默认都是disabled。板级.dts文件里再根据需要逐个打开。这样设计的好处是:一个芯片的.dtsi可以被多个板子复用。

实战场景:我记得有一次做项目,客户说SPI接口用不了。我检查了所有配置,时钟、引脚、驱动都没问题。最后发现.dts里spi节点的status写成了"disable"——少了个d!内核解析时直接忽略了。改回"disabled"再改成"okay",问题解决。这种低级错误,排查起来最费时间。

典型用法:

// 芯片级.dtsi中定义,默认关闭
&uart0 {
    status = "disabled";
};

// 板级.dts中打开
&uart0 {
    status = "okay";
    pinctrl-0 = <&uart0_pins>;
    pinctrl-names = "default";
};

3.5 实战:组装一个完整的设备节点

好了,理论讲完了。咱们来动手组装一个真实的设备节点。假设我们要描述一个I2C总线上的温度传感器:

// I2C控制器节点
i2c0: i2c@1c2ac00 {
    compatible = "allwinner,sun8i-h3-i2c";
    reg = <0x1c2ac00 0x400>;
    interrupts = <0 6 4>;
    clocks = <&ccu CLK_BUS_I2C0>;
    resets = <&ccu RST_BUS_I2C0>;
    pinctrl-0 = <&i2c0_pins>;
    pinctrl-names = "default";
    status = "okay";
    #address-cells = <1>;
    #size-cells = <0>;

    // 温度传感器子节点
    temp_sensor: lm75@48 {
        compatible = "national,lm75";
        reg = <0x48>;  // I2C地址
        status = "okay";
    };
};

这个例子把今天讲的所有属性都用上了。你仔细看看:compatible标识设备类型,reg描述地址空间,interrupts定义中断,status控制启用状态。每个属性各司其职,缺一不可。

我的建议:刚开始写设备树时,别急着从头写。先找一个参考板子的.dts文件,对照着改。把别人的compatible、reg、interrupts抄过来,只改你需要的那部分。等熟练了再自己写。我当年就是这么学的。

3.6 知识体系总览

下面这张图把今天讲的内容串起来了。你可以看到每个属性在设备树中的角色和关系:

设备树标准属性知识体系 设备节点 compatible "厂商,型号" 驱动匹配 reg <地址 长度> 资源描述 interrupts <中断号 触发类型> 通知CPU status "okay"/"disabled" 启用控制 实战要点 • compatible 写具体型号在前,通用型号在后 • reg 的cells数量必须与父节点声明一致 • interrupts 先查中断控制器文档再写

这张图把四个属性的关系理清楚了。compatible负责「你是谁」,reg负责「你在哪」,interrupts负责「你怎么叫人」,status负责「你活没活」。四个属性配合起来,一个完整的设备描述就出来了。

好了,这一章的内容就到这。标准属性是设备树的基石,你写多了自然就熟练了。记住:多查文档,多对比参考板,少走弯路。

课后练习:找一个你手头开发板的.dts文件,找到任意一个外设节点,把它的compatible、reg、interrupts、status四个属性抄下来,对照芯片手册理解每个值的含义。这是最快的学习方法。

可可精品资料库