第四节:设备树绑定文档——如何阅读内核 Documentation/devicetree/bindings 下的文档

说实话,我刚接触设备树那会儿,最头疼的就是看内核里那一堆 Documentation/devicetree/bindings/ 下的文档。密密麻麻的 YAML 和文本描述,看着就头大。但后来我发现,这些文档其实是设备树开发的「法律条文」——你写的每个节点、每个属性,都得按它说的来。

今天我就带你把这套文档读透。说白了,读懂了绑定文档,你写设备树就不会翻车。

4.1 绑定文档到底长什么样?

内核 5.x 以后,大部分绑定文档都改成了 YAML 格式,后缀是 .yaml。老一点的还是纯文本 .txt。我建议你优先看 YAML 格式的,因为它结构清晰,还能被工具自动校验。

随便打开一个,比如 Documentation/devicetree/bindings/gpio/gpio.txt(老格式)或者 .../gpio/gpio.yaml(新格式),你会发现它基本包含这几块:

  • 标题和描述:这个 IP 是干什么的,用在什么芯片上
  • 属性列表:必须有的属性(required)、可选的属性(optional)
  • 示例:一个完整的设备树节点写法
  • 兼容性字符串:比如 compatible = "ti,omap-gpio"

核心原则:绑定文档里写的「required」属性,一个都不能少。少了内核驱动就 probe 失败,或者直接 panic。

4.2 怎么快速定位你要找的文档?

内核的绑定文档是按子系统分类的。目录结构大概是:

Documentation/devicetree/bindings/
├── arm/           # ARM 平台相关
├── clock/         # 时钟控制器
├── gpio/          # GPIO 控制器
├── i2c/           # I2C 控制器
├── interrupt-controller/  # 中断控制器
├── memory/        # 内存控制器
├── mmc/           # SD/MMC 控制器
├── net/           # 网络设备
├── pinctrl/       # 引脚复用
├── regulator/     # 电源管理
├── serial/        # 串口
├── spi/           # SPI 控制器
├── timer/         # 定时器
└── vendor-prefixes.yaml  # 厂商前缀列表

我个人习惯是:先确定你要写的设备属于哪个子系统,然后直接进对应目录。比如你要写一个 I2C 触摸屏,就去 bindings/input/touchscreen/ 下找。

小技巧:在源码根目录下用 grep -r "compatible" Documentation/devicetree/bindings/ 可以快速搜到你要的兼容字符串。我在移植 U-Boot 时经常这么干。

4.3 读懂 YAML 格式的绑定文档

YAML 格式的文档,其实比纯文本好读多了。咱们拿一个实际的例子来分析:

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

title: GPIO Controller

maintainers:
  - Linus Walleij <linus.walleij@linaro.org>

properties:
  compatible:
    description: 兼容性字符串
    items:
      - enum:
          - ti,omap-gpio
          - nxp,lpc-gpio

  reg:
    description: 寄存器地址和长度
    maxItems: 1

  gpio-controller: true
  "#gpio-cells":
    const: 2

required:
  - compatible
  - reg
  - gpio-controller
  - "#gpio-cells"

examples:
  - |
    gpio0: gpio@44e07000 {
        compatible = "ti,omap-gpio";
        reg = <0x44e07000 0x1000>;
        gpio-controller;
        #gpio-cells = <2>;
    };

你看,结构非常清晰:

  • properties 下面列出了所有属性及其约束
  • required 告诉你哪些是必须写的
  • examples 直接给你一个完整示例,照着抄就行

嗯,这里要注意:#gpio-cells 这个属性名在 YAML 里必须用引号括起来,因为 # 是 YAML 的注释符号。我刚开始写的时候没注意,结果 dtc 编译报错,找了半天才发现是这个问题。

4.4 纯文本格式的绑定文档怎么读?

虽然 YAML 是趋势,但很多老驱动还是纯文本格式。比如 Documentation/devicetree/bindings/i2c/i2c-imx.txt

* Freescale i.MX I2C Controller

Required properties:
- compatible: Should be "fsl,imx1-i2c", "fsl,imx21-i2c" or "fsl,imx6q-i2c"
- reg: Physical address and size of the I2C controller registers
- interrupts: Should contain the I2C interrupt

Optional properties:
- clock-frequency: Desired I2C bus clock frequency in Hz (default 100000)
- fsl,use-gpio: Use GPIO for SDA/SCL lines

Example:
i2c@43f80000 {
    compatible = "fsl,imx6q-i2c";
    reg = <0x43f80000 0x4000>;
    interrupts = <0 67 IRQ_TYPE_LEVEL_HIGH>;
    clock-frequency = <100000>;
};

读这种文档,我有个习惯:先看 Required properties,再看 Example。把 Example 里的节点和 Required 属性一一对应,确认没漏掉。Optional 的属性,按需添加就行。

避坑指南:我曾经在移植一个 SPI 驱动时,漏掉了 spi-max-frequency 这个可选属性。结果 SPI 总线跑在默认频率上,导致通信不稳定。后来加上这个属性,指定了合适的频率,问题就解决了。所以可选属性也别随便忽略,尤其是跟时序相关的。

4.5 绑定文档里的「兼容性」到底怎么选?

这是新手最容易懵的地方。绑定文档里经常看到这样的写法:

compatible:
  items:
    - enum:
        - ti,am335x-uart
        - ti,omap3-uart
    - const: ti,omap-uart

这表示兼容字符串可以是 "ti,am335x-uart","ti,omap-uart" 或者 "ti,omap3-uart","ti,omap-uart"。第一个是具体型号,第二个是通用型号。驱动匹配时,会先找具体型号,找不到再找通用型号。

我建议你:优先使用最具体的兼容字符串。比如你的芯片是 AM335x,就用 "ti,am335x-uart",而不是偷懒只用 "ti,omap-uart"。为什么?因为内核后续版本可能会为具体型号添加特殊处理,你用通用字符串就享受不到这些优化。

4.6 如何验证你的设备树是否符合绑定文档?

光读文档还不够,你得会验证。内核提供了一个工具叫 dt-bindings-check,或者直接用 make dt_binding_check

# 检查所有绑定文档的语法
make dt_binding_check

# 检查某个具体的绑定文档
make dt_binding_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/gpio/gpio.yaml

# 检查你的设备树文件是否符合绑定
make dtbs_check

我在项目中经常这么干:写完设备树后,先跑 make dtbs_check,看看有没有报错。如果有,就对照绑定文档逐条排查。这比手动检查靠谱多了。

个人经验dtbs_check 的输出有时候比较隐晦,比如它可能只告诉你「属性类型不匹配」,但不告诉你具体哪个属性。这时候我会用 dtc -I dtb -O dts 把编译好的 dtb 反编译成 dts,然后对照绑定文档手动检查。虽然麻烦,但能发现一些自动化工具漏掉的问题。

4.7 绑定文档的常见陷阱

读多了绑定文档,你会发现一些容易踩坑的地方:

  • 属性名大小写:设备树属性名都是小写+连字符,比如 clock-frequency,不是 clockFrequency。我见过有人写成驼峰式,结果驱动匹配不上。
  • 中断类型interrupts 属性的第三个值(中断触发类型)在不同架构下含义不同。ARM 用 IRQ_TYPE_LEVEL_HIGH,RISC-V 可能用别的。一定要看绑定文档里的说明。
  • 地址和大小reg 属性的地址和大小,单位是字节。但有些文档里写的是 0x1000,实际可能是 4KB。别想当然,以文档为准。

我曾经踩过的坑:在写一个 MMC 控制器的设备树时,绑定文档里说 bus-width 属性可选,默认值是 1。我没写这个属性,结果 SD 卡只能以 1-bit 模式工作,速度慢得离谱。后来加上 bus-width = <4>,速度就正常了。所以可选属性里跟性能相关的,一定要根据硬件实际能力写上去。

4.8 总结一下我的阅读方法

读绑定文档,我一般按这个步骤来:

  1. 先看标题和描述:确认这个文档是不是你要找的
  2. 再看 Required properties:列出所有必须写的属性,一个都不能少
  3. 然后看 Optional properties:根据你的硬件需求,决定哪些要加
  4. 最后看 Example:照着示例写,但别忘了把地址、中断号改成你自己的
  5. 跑一遍 dtbs_check:让工具帮你验证

你想想看,设备树说白了就是硬件的「身份证」。绑定文档就是身份证的「填写规范」。按规范填,驱动就能认出来;不按规范填,驱动就不认识你。就这么简单。

下一节,我会讲怎么从零开始写一个设备树文件,到时候咱们会频繁用到今天讲的这些知识。记得把绑定文档的阅读方法练熟,后面会省很多事。