4. 设备树绑定(Binding):从文档到验证

设备树绑定,说白了就是一份“说明书”。它告诉内核:某个设备节点里,哪些属性是必须的,哪些是可选的,每个属性是什么类型、什么含义。没有绑定,设备树就是一盘散沙。

我刚接触设备树那会儿,总觉得写个节点、配几个属性就完事了。结果呢?驱动加载失败,内核报错说找不到资源。查了半天,原来是少了个 interrupt-parent。从那以后,我养成了一个习惯——先看绑定文档,再写设备树。

4.1 绑定文档长什么样

绑定文档通常放在内核源码的 Documentation/devicetree/bindings/ 目录下。每个子系统都有自己的子目录,比如 i2c/、gpio/、pinctrl/。

一个典型的绑定文档包含以下几部分:

  • 标题和描述:说明这个设备是什么,用在什么场景。
  • 兼容性字符串:列出所有支持的 compatible 值。
  • 属性列表:分“必需属性”和“可选属性”两类。
  • 示例节点:一个完整的设备树节点示例。

举个例子,这是 I2C 控制器的一个简化绑定:

# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/i2c/i2c-imx.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: Freescale i.MX I2C Controller

maintainers:
  - Oleksij Rempel <o.rempel@pengutronix.de>

properties:
  compatible:
    enum:
      - fsl,imx1-i2c
      - fsl,imx21-i2c
      - fsl,imx6q-i2c

  reg:
    maxItems: 1

  interrupts:
    maxItems: 1

required:
  - compatible
  - reg
  - interrupts

additionalProperties: false

examples:
  - |
    i2c@53f8000 {
        compatible = "fsl,imx6q-i2c";
        reg = <0x53f8000 0x4000>;
        interrupts = <0 56 4>;
    };

嗯,这里要注意:现在的绑定文档已经全面转向 YAML 格式了。以前那种纯文本的 .txt 格式基本被淘汰了。YAML 的好处是结构清晰,还能自动校验。

4.2 自定义绑定:给自家芯片写说明书

如果你用的是非标设备,或者自己设计的 FPGA 外设,那就得写自定义绑定了。我个人习惯是:先写绑定文档,再写驱动,最后配设备树。顺序别搞反了,否则容易漏属性。

自定义绑定的步骤大致如下:

  1. 确定 compatible 字符串:格式是 厂商,型号,比如 acme,my-uart。
  2. 列出所有属性:包括寄存器地址、中断号、时钟、GPIO 等。
  3. 写 YAML 绑定文件:放在对应子系统的目录下。
  4. 用 dt_binding_check 验证:确保语法正确。

来看一个我实际做过的例子。当时给一个 FPGA 上的 SPI 控制器写绑定:

# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
%YAML 1.2
---
$id: http://devicetree.org/schemas/spi/acme,spi-fpga.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: ACME FPGA SPI Controller

maintainers:
  - Zhang San <zhangsan@acme.com>

properties:
  compatible:
    const: acme,spi-fpga

  reg:
    description: SPI controller registers
    maxItems: 1

  interrupts:
    maxItems: 1

  clocks:
    description: SPI module clock
    maxItems: 1

  cs-gpios:
    description: Chip select GPIOs
    minItems: 1
    maxItems: 4

required:
  - compatible
  - reg
  - interrupts
  - clocks

additionalProperties: false

examples:
  - |
    spi@40020000 {
        compatible = "acme,spi-fpga";
        reg = <0x40020000 0x1000>;
        interrupts = <0 42 4>;
        clocks = <&clk 100>;
        cs-gpios = <&gpio1 10 0>,
                   <&gpio1 11 0>;
        #address-cells = <1>;
        #size-cells = <0>;

        flash@0 {
            compatible = "jedec,spi-nor";
            reg = <0>;
            spi-max-frequency = <50000000>;
        };
    };
小技巧:写绑定的时候,additionalProperties: false 这个字段很重要。它告诉校验工具:除了列出的属性,不允许出现其他属性。这能帮你尽早发现拼写错误。

4.3 绑定验证:别让内核吃哑巴亏

绑定文档写完了,怎么知道对不对?内核提供了一套验证工具。

最常用的是 dt_binding_check 和 dtbs_check。前者检查绑定文档本身的语法,后者用绑定文档去校验实际的设备树。

运行方式很简单:

# 只检查绑定文档
make dt_binding_check DT_SCHEMA_FILES=acme,spi-fpga.yaml

# 检查设备树
make dtbs_check DT_SCHEMA_FILES=acme,spi-fpga.yaml

我曾经在一个项目里吃过亏。写了个自定义绑定,自认为没问题,结果 dtbs_check 报了一堆错。仔细一看,原来是属性名写错了——把 clocks 写成了 clock。少个 s,内核就不认。

常见的验证错误类型有:

错误类型 典型原因 解决办法
属性缺失 required 里列了,但节点里没写 补上对应属性
类型不匹配 属性值类型与绑定定义不一致 检查类型:uint32、string、phandle 等
多余属性 节点里写了绑定未定义的属性 要么删掉,要么加到绑定里
compatible 不匹配 节点 compatible 不在绑定枚举中 修正字符串或更新绑定
注意:验证工具只能检查格式和类型,不能检查逻辑。比如你给一个 UART 设备配了 I2C 的地址,工具不会报错,但驱动肯定跑不起来。所以,写绑定的时候,脑子里的“逻辑图”一定要清晰。

4.4 绑定与驱动的“契约”

绑定文档其实是驱动和设备树之间的“契约”。驱动通过 of_match_table 匹配 compatible 字符串,然后通过 of_property_read_* 系列函数读取属性。

举个例子,驱动里这样读属性:

static int my_spi_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct resource *res;
    int irq;
    u32 max_speed;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    irq = platform_get_irq(pdev, 0);

    /* 读取自定义属性 */
    of_property_read_u32(dev->of_node, "spi-max-frequency", &max_speed);

    /* 读取 GPIO 属性 */
    cs_gpio = of_get_named_gpio(dev->of_node, "cs-gpios", 0);

    /* ... 初始化硬件 ... */
    return 0;
}

static const struct of_device_id my_spi_of_match[] = {
    { .compatible = "acme,spi-fpga", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);

static struct platform_driver my_spi_driver = {
    .probe = my_spi_probe,
    .driver = {
        .name = "my-spi",
        .of_match_table = my_spi_of_match,
    },
};

你想想看,如果绑定文档里写了 cs-gpios 是必需的,但驱动里没读它,或者读的方式不对,那设备树配得再漂亮也没用。所以,绑定和驱动必须同步更新。

4.5 绑定文档的维护

绑定文档不是写一次就完事了。随着硬件版本迭代,属性可能会增减。我建议:

  • 每次修改驱动,同步更新绑定文档。
  • 提交补丁时,绑定文档和驱动代码放在同一个 patch 系列里。
  • 使用版本管理,绑定文档的变更历史要清晰。

内核社区对绑定文档的审查很严格。我记得有一次提交,reviewer 要求我把一个属性名从 foo-gpios 改成 foo-gpio,理由是“内核惯例是单数”。虽然只是改个名字,但体现了社区对规范性的重视。

核心要点:绑定文档是设备树的“语法书”。写设备树之前,先看绑定;写驱动之前,先写绑定。验证工具是你的好帮手,但逻辑正确性还得靠你自己。

设备树绑定工作流 绑定文档 (YAML) dt_binding_check / dtbs_check 设备树源文件 (.dts) 驱动代码 (of_match_table) 设备正常工作 关键点 • 绑定定义属性规则 • 验证确保一致性 • 驱动按绑定读取 常见错误 • 属性名拼写错误 • 类型不匹配 • 遗漏必需属性

这张图展示了绑定文档在整个设备树开发流程中的位置。从绑定文档出发,经过验证,再到设备树和驱动,最终让设备正常工作。每一步都环环相扣,缺一不可。