4. 设备树绑定(binding):YAML绑定文件的作用、绑定文件结构、属性定义与约束

好,咱们接着聊设备树。上一章我们把设备树源文件(.dts)的结构讲清楚了,你大概能看懂一个设备节点长什么样了。但有个问题你肯定想过:Zephyr 怎么知道某个节点里那些属性是什么意思?

比如你写了个 reg = <0x40002000 0x1000>;,系统怎么知道第一个数字是基地址,第二个是长度?又比如你写了个 clock-frequency = <1000000>;,它怎么知道单位是 Hz?

答案就在绑定文件里。说白了,绑定文件就是设备节点的“说明书”。我刚开始接触 Zephyr 时,也纳闷为什么非要搞这么个东西。后来踩了几个坑才明白——没有绑定文件,代码根本没法跟设备树数据对上号。

4.1 绑定文件到底在解决什么问题?

你想想看,设备树本身只是一堆键值对。比如:

uart0: uart@40002000 {
    compatible = "nordic,nrf-uarte";
    reg = <0x40002000 0x1000>;
    interrupts = <39 1>;
    status = "okay";
};

这段数据里,reg 有两个数字,interrupts 也有两个数字。但它们的含义完全不同:reg 是“地址+长度”,interrupts 是“中断号+触发方式”。

绑定文件就是告诉 Zephyr 的代码生成工具:“嘿,看到 nordic,nrf-uarte 这个 compatible 字符串时,它的 reg 属性应该解析成地址和长度,它的 interrupts 属性应该解析成中断号和标志位。”

我在一个项目里就吃过亏。当时自己写了个简单的传感器驱动,没写绑定文件,结果编译出来的代码死活读不到寄存器地址。查了半天才发现——系统根本不知道我的节点里那些数字是什么意思。嗯,从那以后我再也不敢偷懒了。

4.2 绑定文件长什么样?

绑定文件是 YAML 格式的,后缀是 .yaml。它们通常放在每个驱动目录下的 bindings/ 文件夹里,或者放在 dts/bindings/ 全局目录下。

我拿一个典型的 UART 绑定文件来拆解:

# uart-nrf-uarte.yaml
description: Nordic nRF UARTE (UART with EasyDMA)

compatible: "nordic,nrf-uarte"

include: [uart-controller.yaml, pinctrl-device.yaml]

properties:
  reg:
    required: true
    type: array
    description: |
      Address and length of the UARTE register map.
      First cell: base address
      Second cell: size

  interrupts:
    required: true
    type: array
    description: |
      Interrupt specifier for the UARTE.
      First cell: IRQ number
      Second cell: flags

  current-speed:
    required: false
    type: int
    default: 115200
    description: Initial baud rate in Hz

  hw-flow-control:
    type: boolean
    required: false
    description: Enable hardware flow control (RTS/CTS)

你看,这个文件结构其实很清晰。咱们逐块来看。

4.3 绑定文件的核心结构

一个绑定文件通常由这几部分组成:

4.3.1 描述和兼容性

文件开头是 descriptioncompatible。前者就是个给人看的说明,后者才是关键——它必须跟设备树节点里的 compatible 字符串完全一致

我见过有人把 compatible 写成了 "nordic,nrf-uarte",但设备树里写的是 "nordic,nrf-uarte-v2"。结果绑定文件根本没被匹配上,驱动加载失败。这种低级错误,排查起来特别费时间。

4.3.2 include 机制

你注意到上面那个 include: [uart-controller.yaml, pinctrl-device.yaml] 了吗?这是 Zephyr 绑定文件的一个很实用的设计。你可以把通用的属性定义抽出来,放到一个基础绑定文件里,然后让具体的设备绑定文件去引用。

比如 uart-controller.yaml 里可能已经定义了 reginterruptsclocks 这些通用属性。你的 UARTE 绑定文件只需要定义自己特有的属性就行。这样既减少了重复代码,也保证了统一性。

我个人习惯是:先看看有没有现成的 include 可以用。Zephyr 在 dts/bindings/ 下提供了很多基础绑定文件,比如 base.yamlgpio-controller.yamlspi-device.yaml 等等。能复用的就别自己写。

4.3.3 属性定义

这是绑定文件的核心部分。properties 下面列出了这个设备支持的所有属性。每个属性都有几个关键字段:

字段 含义 说明
required 是否必须 true 表示设备树里必须写这个属性,否则编译报错
type 数据类型 常见的有 int、array、string、boolean、phandle 等
default 默认值 如果设备树里没写,就用这个默认值
description 说明文字 给人看的,解释这个属性是干什么的
enum 枚举值 限定属性只能取某些特定值
const 固定值 属性必须等于这个值,不能改

举个例子,current-speed 这个属性:

  • required: false 表示它不是必须的,设备树里可以不写
  • type: int 表示它是一个整数
  • default: 115200 表示如果没写,默认就是 115200
  • description 说明了单位是 Hz

这样,驱动代码里就可以直接用 DT_PROP(node_id, current_speed) 来获取这个值,而且保证单位是 Hz。是不是很省心?

4.4 属性约束:别让用户乱写

绑定文件还有一个很重要的功能——约束。你可以通过 enumconst 来限制属性的取值范围。

比如一个 SPI 设备的绑定文件里,可能有这样的定义:

properties:
  spi-max-frequency:
    type: int
    required: true
    enum:
      - 1000000
      - 2000000
      - 4000000
      - 8000000
    description: Maximum SPI clock frequency in Hz

这就意味着,用户只能在 1MHz、2MHz、4MHz、8MHz 里选一个。如果写了 3MHz,编译就会报错。我在一个项目里就用这个特性来防止硬件工程师配错时钟频率——把约束写在绑定文件里,比写在代码里更早发现问题

const 就更严格了,它要求属性必须等于某个固定值。比如:

properties:
  "#address-cells":
    const: 1

这就意味着这个节点的 #address-cells 只能是 1,写 2 就报错。这种约束通常用在那些硬件设计上就固定死了的参数上。

4.5 绑定文件是怎么被用起来的?

你可能好奇,绑定文件跟代码之间到底是怎么连接的?

Zephyr 的构建系统会在编译时做这么几件事:

  1. 读取所有 .dts 文件和 .dtsi 文件,生成完整的设备树
  2. 根据每个节点的 compatible 字符串,找到对应的 .yaml 绑定文件
  3. 用绑定文件里的定义,解析设备树节点的属性
  4. 生成 C 语言的宏定义(比如 DT_NODE_HAS_COMPATDT_REG_ADDR 等)
  5. 驱动代码通过这些宏来访问设备树数据

说白了,绑定文件就是设备树和 C 代码之间的翻译官。没有它,设备树里的数据就只是一堆看不懂的字符串和数字。

核心要点:绑定文件决定了设备树节点里的属性如何被解析成 C 语言的数据结构。写驱动之前,先确认绑定文件是否存在、定义是否正确。这是 Zephyr 驱动开发的第一步,也是最容易被忽略的一步。

4.6 实战建议:怎么写一个绑定文件?

如果你要为自己的硬件写一个绑定文件,我建议按这个步骤来:

  1. 先找现成的:去 dts/bindings/ 目录下看看有没有类似的。比如你要写一个 I2C 传感器,先看看 i2c-device.yaml 能不能 include
  2. 确定 compatible 字符串:这个字符串要跟设备树节点里写的一致,通常格式是 "厂商,设备型号"
  3. 列出所有需要的属性:哪些是必须的?哪些是可选的?每个属性的类型是什么?
  4. 加上约束:能用 enumconst 的地方尽量用,这样可以在编译时就发现问题
  5. 写 description:别偷懒,写清楚每个属性的含义和单位。几个月后你自己回来看也会感谢自己的

小技巧:Zephyr 提供了一个工具叫 dtlib,可以用来验证绑定文件的正确性。你可以在构建时加上 VERBOSE_DTS=1 来查看设备树解析的详细日志。如果绑定文件写错了,日志里会有明确的错误信息。

4.7 避坑指南

最后,分享几个我踩过的坑:

  • compatible 字符串大小写敏感:设备树里写的是 "nordic,nrf-uarte",绑定文件里就必须一模一样。多一个空格都不行
  • include 路径问题:include 的时候,路径是相对于 dts/bindings/ 目录的。别写绝对路径
  • type 要写对intarray 的区别要搞清楚。一个整数用 int,多个整数用 array
  • 别忘了 required:如果某个属性驱动里必须用到,就把它设为 required: true。这样用户忘了写,编译就会报错,而不是运行时崩溃

我曾经在一个项目里,因为绑定文件的 type 写成了 int 但实际设备树里传的是数组,结果驱动读到的数据全是错的。排查了整整两天才发现是绑定文件的问题。嗯,从那以后我每次写完绑定文件都会用工具验证一遍。

好了,绑定文件的内容就讲到这里。下一章我们会聊聊设备树宏 API,看看驱动代码里到底怎么用这些绑定文件生成的数据。