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 描述和兼容性
文件开头是 description 和 compatible。前者就是个给人看的说明,后者才是关键——它必须跟设备树节点里的 compatible 字符串完全一致。
我见过有人把 compatible 写成了 "nordic,nrf-uarte",但设备树里写的是 "nordic,nrf-uarte-v2"。结果绑定文件根本没被匹配上,驱动加载失败。这种低级错误,排查起来特别费时间。
4.3.2 include 机制
你注意到上面那个 include: [uart-controller.yaml, pinctrl-device.yaml] 了吗?这是 Zephyr 绑定文件的一个很实用的设计。你可以把通用的属性定义抽出来,放到一个基础绑定文件里,然后让具体的设备绑定文件去引用。
比如 uart-controller.yaml 里可能已经定义了 reg、interrupts、clocks 这些通用属性。你的 UARTE 绑定文件只需要定义自己特有的属性就行。这样既减少了重复代码,也保证了统一性。
我个人习惯是:先看看有没有现成的 include 可以用。Zephyr 在 dts/bindings/ 下提供了很多基础绑定文件,比如 base.yaml、gpio-controller.yaml、spi-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表示如果没写,默认就是 115200description说明了单位是 Hz
这样,驱动代码里就可以直接用 DT_PROP(node_id, current_speed) 来获取这个值,而且保证单位是 Hz。是不是很省心?
4.4 属性约束:别让用户乱写
绑定文件还有一个很重要的功能——约束。你可以通过 enum 和 const 来限制属性的取值范围。
比如一个 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 的构建系统会在编译时做这么几件事:
- 读取所有
.dts文件和.dtsi文件,生成完整的设备树 - 根据每个节点的
compatible字符串,找到对应的.yaml绑定文件 - 用绑定文件里的定义,解析设备树节点的属性
- 生成 C 语言的宏定义(比如
DT_NODE_HAS_COMPAT、DT_REG_ADDR等) - 驱动代码通过这些宏来访问设备树数据
说白了,绑定文件就是设备树和 C 代码之间的翻译官。没有它,设备树里的数据就只是一堆看不懂的字符串和数字。
核心要点:绑定文件决定了设备树节点里的属性如何被解析成 C 语言的数据结构。写驱动之前,先确认绑定文件是否存在、定义是否正确。这是 Zephyr 驱动开发的第一步,也是最容易被忽略的一步。
4.6 实战建议:怎么写一个绑定文件?
如果你要为自己的硬件写一个绑定文件,我建议按这个步骤来:
- 先找现成的:去
dts/bindings/目录下看看有没有类似的。比如你要写一个 I2C 传感器,先看看i2c-device.yaml能不能 include - 确定 compatible 字符串:这个字符串要跟设备树节点里写的一致,通常格式是
"厂商,设备型号" - 列出所有需要的属性:哪些是必须的?哪些是可选的?每个属性的类型是什么?
- 加上约束:能用
enum和const的地方尽量用,这样可以在编译时就发现问题 - 写 description:别偷懒,写清楚每个属性的含义和单位。几个月后你自己回来看也会感谢自己的
小技巧:Zephyr 提供了一个工具叫 dtlib,可以用来验证绑定文件的正确性。你可以在构建时加上 VERBOSE_DTS=1 来查看设备树解析的详细日志。如果绑定文件写错了,日志里会有明确的错误信息。
4.7 避坑指南
最后,分享几个我踩过的坑:
- compatible 字符串大小写敏感:设备树里写的是
"nordic,nrf-uarte",绑定文件里就必须一模一样。多一个空格都不行 - include 路径问题:include 的时候,路径是相对于
dts/bindings/目录的。别写绝对路径 - type 要写对:
int和array的区别要搞清楚。一个整数用int,多个整数用array - 别忘了
required:如果某个属性驱动里必须用到,就把它设为required: true。这样用户忘了写,编译就会报错,而不是运行时崩溃
我曾经在一个项目里,因为绑定文件的 type 写成了 int 但实际设备树里传的是数组,结果驱动读到的数据全是错的。排查了整整两天才发现是绑定文件的问题。嗯,从那以后我每次写完绑定文件都会用工具验证一遍。
好了,绑定文件的内容就讲到这里。下一章我们会聊聊设备树宏 API,看看驱动代码里到底怎么用这些绑定文件生成的数据。