第三章 设备树绑定文档:从规范到实战

设备树绑定文档,说白了就是一份“说明书”。它告诉内核:某个外设该怎么配置、哪些属性是必须的、哪些是可选的。我刚开始接触设备树时,总觉得写绑定文档是件麻烦事——反正代码能跑就行呗。直到有一次,我接手一个同事离职留下的项目,设备树里一堆自定义属性,没有任何文档说明……嗯,从那以后,我再也不敢轻视绑定文档了。

3.1 为什么需要绑定文档?

设备树本身只是一棵描述硬件的数据树。没有绑定文档,内核驱动就不知道如何解析这棵树。绑定文档起到了三个关键作用:

  • 契约:驱动开发者与设备树编写者之间的约定
  • 规范:定义属性名、类型、值的合法范围
  • 文档:让后来者(包括三个月后的你自己)能看懂配置意图

核心原则:绑定文档不是写给编译器看的,是写给人看的。好的绑定文档,能让一个不熟悉该外设的工程师在10分钟内完成配置。

3.2 绑定文档的格式规范

Linux内核社区对设备树绑定文档有明确的格式要求。我个人习惯遵循以下结构:

章节 内容 是否必须
标题 设备名称与简述 是
描述 硬件功能概述、适用场景 推荐
属性 必选属性、可选属性列表 是
示例 完整的设备树节点示例 是
注意事项 兼容性、中断号、电源域等 推荐

绑定文档通常以YAML格式编写,存放在内核源码的 Documentation/devicetree/bindings/ 目录下。YAML格式的好处是:机器可解析、人类可阅读、易于校验。

小技巧:写绑定文档时,先用 make dt_binding_check 验证格式,再用 make dtbs_check 验证实际设备树。这两个命令能帮你省下不少调试时间。

3.3 常用外设绑定示例

接下来,我们逐个看几个最常用的外设绑定。这些都是我在项目中反复用到的,你迟早也会遇到。

3.3.1 GPIO 绑定

GPIO是最基础的外设。绑定文档需要描述:哪个控制器、哪个引脚、输入还是输出、是否需要上拉。

# gpio-led.yaml
description: 一个简单的GPIO LED控制器

properties:
  compatible:
    const: gpio-leds

  gpios:
    description: GPIO引脚描述,格式为<phandle gpio-specifier>
    maxItems: 1

  label:
    description: LED的名称,用于用户空间识别
    $ref: /schemas/types.yaml#/definitions/string

required:
  - compatible
  - gpios

examples:
  - |
    leds {
      compatible = "gpio-leds";
      led-0 {
        gpios = &gpio1 5 GPIO_ACTIVE_HIGH;
        label = "power-led";
      };
    };

我在项目中遇到过一个问题:某个GPIO LED在系统休眠后无法唤醒。查了半天,发现是绑定文档里漏写了 wakeup-source 属性。嗯,这种坑踩过一次就记住了。

3.3.2 I2C 绑定

I2C设备绑定需要特别注意地址和时钟频率。你想想看,I2C总线上挂多个设备时,地址冲突是常见问题。

# i2c-sensor.yaml
description: I2C温湿度传感器

properties:
  compatible:
    items:
      - enum:
          - ti,hdc1080
          - sensirion,sht30

  reg:
    description: I2C从设备地址
    maxItems: 1

  measurement-interval:
    description: 测量间隔,单位毫秒
    $ref: /schemas/types.yaml#/definitions/uint32
    default: 2000

required:
  - compatible
  - reg

examples:
  - |
    i2c {
      #address-cells = <1>;
      #size-cells = <0>;

      temp-sensor@40 {
        compatible = "ti,hdc1080";
        reg = <0x40>;
        measurement-interval = <5000>;
      };
    };

注意:I2C地址必须是7位或10位格式,且不能与总线上的其他设备冲突。我曾经见过一个案例,两个设备用了同一个地址,结果总线直接锁死——连SCL都被拉低了。

3.3.3 SPI 绑定

SPI设备的绑定比I2C复杂一些,因为涉及时钟极性(CPOL)和时钟相位(CPHA)。这两个参数配错了,数据根本读不对。

# spi-flash.yaml
description: SPI NOR Flash存储器

properties:
  compatible:
    items:
      - pattern: "^jedec,spi-nor$"

  reg:
    description: SPI片选编号
    maxItems: 1

  spi-max-frequency:
    description: SPI时钟最大频率,单位Hz
    $ref: /schemas/types.yaml#/definitions/uint32

  spi-cpol:
    description: 时钟极性,1表示空闲时为高电平
    type: boolean

  spi-cpha:
    description: 时钟相位,1表示第二个边沿采样
    type: boolean

required:
  - compatible
  - reg
  - spi-max-frequency

examples:
  |
    &spi1 {
      status = "okay";

      flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <50000000>;
        spi-cpol;
        spi-cpha;
      };
    };

我记得有一次调试SPI Flash,读出来的数据全是0xFF。折腾了两天,最后发现是SPI的MISO和MOSI接反了。硬件工程师和软件工程师互相甩锅的场景,至今记忆犹新。

3.3.4 UART 绑定

UART绑定相对简单,但流控和波特率是容易出问题的地方。

# uart-bluetooth.yaml
description: UART蓝牙模块

properties:
  compatible:
    items:
      - const: ti,wl1837-st

  device-wakeup-gpios:
    description: 设备唤醒GPIO
    maxItems: 1

  enable-gpios:
    description: 使能GPIO
    maxItems: 1

  max-speed:
    description: 最大波特率
    $ref: /schemas/types.yaml#/definitions/uint32
    default: 115200

required:
  - compatible
  - device-wakeup-gpios

examples:
  |
    &uart3 {
      status = "okay";
      pinctrl-names = "default";
      pinctrl-0 = <&uart3_pins>;

      bluetooth {
        compatible = "ti,wl1837-st";
        device-wakeup-gpios = <&gpio2 7 GPIO_ACTIVE_HIGH>;
        enable-gpios = <&gpio2 8 GPIO_ACTIVE_HIGH>;
        max-speed = <3000000>;
      };
    };

避坑指南:我曾经在UART绑定里忘了配pinctrl,结果蓝牙模块的TX/RX引脚被复用成了GPIO。系统启动后蓝牙能识别,但一收发数据就崩溃。所以,pinctrl一定要写,而且要跟原理图对清楚。

3.4 绑定文档的整体知识体系

下面这张图,是我自己整理的一个绑定文档知识框架。你可以把它当作写绑定文档时的检查清单。

设备树绑定文档知识体系 绑定文档 格式规范 属性定义 示例代码 校验验证 YAML语法 compatible reg/地址 中断/GPIO dt_binding_check GPIO I2C SPI UART 核心原则 绑定文档 = 格式规范 + 属性定义 + 示例代码 + 校验验证

3.5 写绑定文档的几个习惯

最后,分享几个我个人的习惯,希望能帮你少走弯路:

  1. 先写文档,再写代码:绑定文档写清楚了,驱动实现就清晰了。反过来,驱动写完了再补文档,很容易遗漏细节。
  2. 属性名要见名知意:别用 foo,bar 这种名字。我见过一个项目,属性名叫 vendor,magic,代码里注释都没写,后来谁也看不懂。
  3. 示例要完整可运行:示例不是摆设,是要能直接复制到设备树里用的。包含必要的父节点、地址、状态等。
  4. 用工具自动校验:dt-schema 工具可以自动检查绑定文档和设备树的一致性。我每次提交代码前都会跑一遍。

血的教训:我曾经在一个量产项目里,因为绑定文档里写错了GPIO的active电平——文档写的是 GPIO_ACTIVE_HIGH,实际硬件是低电平有效。结果产线上一批设备都点不亮LED。从那以后,我每次写完绑定文档,都会拿着原理图逐行核对。

绑定文档看似是“文档工作”,实际上是嵌入式开发中承上启下的关键环节。它连接了硬件设计、设备树配置和内核驱动。写好了,整个项目都顺畅;写不好,调试时处处是坑。