4、Zephyr设备树绑定(Binding):YAML绑定文件的结构、compatible属性、required与optional属性
好,咱们接着聊设备树。上一章我讲了设备树源文件(.dts)怎么写,但有个问题你肯定遇到过:你写了一个节点,比如 my_led: led@0,Zephyr 怎么知道这个节点对应哪个驱动?它的寄存器地址是什么格式?哪些属性是必须的?
答案就在绑定文件里。说白了,绑定文件就是设备树节点的“身份证”加“说明书”。它告诉 Zephyr 的构建系统:这个节点是什么硬件,需要哪些信息才能正常工作。
4.1 绑定文件长什么样?
绑定文件是 YAML 格式的,后缀是 .yaml。我习惯把它们放在 dts/bindings/ 目录下,按厂商或外设类型分文件夹存放。比如 GPIO 的绑定在 dts/bindings/gpio/ 里。
来看一个最简单的例子。这是我手头一个项目中用到的 LED 绑定文件:
# dts/bindings/led/mycompany,myled.yaml
description: MyCompany's custom LED controller
compatible: "mycompany,myled"
include: [gpio.yaml]
properties:
reg:
required: true
type: array
description: Register base address and size
label:
required: true
type: string
description: Human-readable device label
max-brightness:
required: false
type: int
default: 255
description: Maximum brightness value
嗯,这里要注意几个关键点。第一行 description 是给人看的,写清楚这个绑定是干什么的。我见过有些团队直接写“LED binding”,太敷衍了。你想想看,半年后你自己回来看,可能都忘了这个 LED 是 I2C 的还是 SPI 的。
4.2 compatible 属性——匹配的钥匙
compatible 是绑定文件里最重要的字段。它就像一把钥匙,把设备树节点和驱动代码连接起来。
在设备树里,你写:
/ {
myled: led@40001000 {
compatible = "mycompany,myled";
reg = <0x40001000 0x1000>;
label = "board-led0";
max-brightness = <200>;
};
};
在驱动代码里,你用 DT_DRV_COMPAT(mycompany_myled) 来匹配。注意这里下划线代替了逗号,这是 Zephyr 的命名规则。
我曾经踩过一个坑:compatible 字符串写错了大小写。设备树里写的是 "mycompany,MyLED",绑定文件里写的是 "mycompany,myled"。结果驱动死活加载不上,查了半天才发现是大小写不匹配。Zephyr 对大小写是敏感的,这一点千万记住。
"mycompany,my-led"。
4.3 required 与 optional 属性
绑定文件里,每个属性都要声明是 required: true 还是 required: false。这直接决定了设备树节点能不能通过编译。
我个人的习惯是:能设为 optional 的就尽量设为 optional。为什么?因为硬件设计经常变。比如某个芯片的版本 A 有中断引脚,版本 B 把它砍了。如果你把中断属性设为 required,那版本 B 的设备树就编译不过。
来看一个更完整的例子,这是我做的一个 I2C 传感器绑定:
description: MyCompany temperature sensor
compatible: "mycompany,tempsensor"
include: [i2c-device.yaml]
properties:
vdd-supply:
required: false
type: phandle
description: Power supply regulator
measurement-interval-ms:
required: false
type: int
default: 1000
description: Measurement interval in milliseconds
alert-gpios:
required: false
type: phandle-array
description: Alert interrupt GPIO
temp-offset:
required: false
type: int
default: 0
description: Temperature calibration offset in millidegrees
你看,这里只有 reg 和 label 是 required 的(从 i2c-device.yaml 继承而来),其他都是 optional。这样设计的好处是:
- 基本功能只需要 I2C 地址就能工作
- 高级功能(如中断、校准)按需配置
- 兼容不同硬件版本
4.4 属性类型详解
Zephyr 支持多种属性类型。我整理了一个表格,方便你查阅:
| 类型 | 说明 | 示例 |
|---|---|---|
boolean |
布尔值,存在即为 true | status = "okay"; |
int |
32 位整数 | max-brightness = <255>; |
string |
字符串 | label = "board-led0"; |
array |
整数数组 | reg = <0x40001000 0x1000>; |
phandle |
指向另一个节点的引用 | vdd-supply = <®ulator>; |
phandle-array |
带参数的 phandle 数组 | alert-gpios = <&gpio0 1 GPIO_ACTIVE_LOW>; |
string-array |
字符串数组 | compatible = "mycompany,tempsensor", "generic,tempsensor"; |
dts/bindings/gpio/ 目录下的文件,那里有最权威的示例。
4.5 include 机制——复用已有的绑定
你注意到前面的例子里有 include: [gpio.yaml] 和 include: [i2c-device.yaml] 吗?这是 Zephyr 绑定文件的一大特色——支持继承。
比如你的设备是 I2C 接口的,直接 include: [i2c-device.yaml],就能自动获得 reg(I2C 地址)和 label 这两个 required 属性。你只需要写自己特有的属性就行。
我建议你:
- 优先使用 Zephyr 自带的绑定文件作为基础
- 只添加硬件特有的属性
- 不要重复定义已经存在的属性
这样做的好处是,当 Zephyr 升级时,基础绑定的更新会自动应用到你的设备上。
4.6 实战:写一个完整的绑定文件
好,咱们动手写一个。假设你有一个 SPI 接口的 DAC 芯片,型号是 ACME1234。我会这样写:
# dts/bindings/dac/acme,acme1234.yaml
description: ACME ACME1234 12-bit SPI DAC
compatible: "acme,acme1234"
include: [spi-device.yaml]
properties:
vref-mv:
required: true
type: int
description: Reference voltage in millivolts
settling-time-us:
required: false
type: int
default: 10
description: Output settling time in microseconds
power-down-gpios:
required: false
type: phandle-array
description: Power-down control GPIO
对应的设备树节点:
&spi1 {
dac0: dac@0 {
compatible = "acme,acme1234";
reg = <0>; // SPI chip select 0
spi-max-frequency = <10000000>;
vref-mv = <3300>;
settling-time-us = <15>;
power-down-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
};
};
你看,spi-max-frequency 是从 spi-device.yaml 继承来的,我不需要再定义。这就是 include 机制的威力。
- 绑定文件是设备树节点和驱动代码之间的桥梁
- compatible 字符串必须完全匹配
- required 属性必须提供,否则编译报错
- optional 属性按需配置,尽量给默认值
- 善用 include 机制,避免重复劳动
最后说一句,绑定文件写得好不好,直接影响到你的驱动代码能不能优雅地获取设备信息。我见过太多人图省事,把所有属性都设为 required,结果硬件改版时改得焦头烂额。记住:设计绑定文件时,多想想未来可能的变化。