摘要:本文是 Zephyr BSP 开发系列的第 27 篇,深入讲解 Devicetree Binding 的核心概念。Binding 是硬件在 Zephyr 中的"数据结构定义",它规定了
compatible对应的硬件节点允许哪些属性、每个属性的类型与是否必填。文章从 Binding 与 DTS 的"类型与对象"关系讲起,逐步展开compatible的匹配机制、properties与required的语义、属性类型约束、include继承复用,以及 Binding 如何通过 DT macros 最终影响 Driver 的 C 代码生成。理解 Binding,是建立"数据驱动" BSP 开发思想、完成从硬件规格到软件驱动契约的关键一步。
Binding:告诉 Zephyr “我的硬件是什么”
在上一篇 26 — Devicetree:把 Company SoC 描述给 Zephyr 之后,我们已经能写:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5 0>;
status = "okay";
};
但这里有一个关键问题:
Zephyr 怎么知道 company,uart 允许哪些属性?
reg 是什么?
interrupts 是什么?
clock-frequency 能不能写?
current-speed 是不是合法?
compatible = “company,uart” 到底对应什么硬件?
答案就是:
Devicetree Binding
Binding 可以理解成:
Devicetree Binding = 你的硬件在 Zephyr 中的"数据结构定义"。
如果 Devicetree 是:
“这块板子上有什么硬件?”
那么 Binding 就是:
“这种硬件应该长什么样、有哪些属性、每个属性是什么类型?”
一、先把整个关系图建立起来
到了 BSP 开发阶段,你应该形成这样一张脑图:
Company SoC
│
▼
┌──────────────────┐
│ Hardware IP │
│ │
│ UART / SPI / GPIO│
│ Timer / I2C ... │
└────────┬─────────┘
│
│ 硬件规格
▼
┌──────────────────┐
│ Devicetree │
│ Binding │
│ │
│ company,uart │
│ company,spi │
│ company,timer │
└────────┬─────────┘
│
│ compatible
▼
┌──────────────────┐
│ Board .dts │
│ │
│ uart0: uart@... │
│ { │
│ compatible = │
│ "company,uart";
│ reg = ... │
│ interrupts=... │
│ }; │
└────────┬─────────┘
│
▼
Devicetree Compiler
│
▼
generated devicetree
│
▼
C / C++ macros
│
▼
DEVICE_DT_DEFINE()
│
▼
struct device
│
▼
UART Driver
所以:
Binding
↓
规定 DTS 节点可以描述什么
DTS
↓
描述具体板子上的实例
Driver
↓
真正操作硬件
这三个东西不要混在一起。
二、Binding 本质上是什么?
一个最简单的 Binding:
description: Company UART controller
compatible: "company,uart"
properties:
reg:
required: true
interrupts:
required: true
clock-frequency:
type: int
required: false
current-speed:
type: int
required: false
你可以把它理解成类似 C 的:
struct company_uart_dt {
reg;
interrupts;
clock_frequency;
current_speed;
};
当然,Binding 并不是 C struct。
它实际上是一个 YAML schema。
它告诉 Zephyr:
compatible = "company,uart"
这种节点:
必须有 reg
必须有 interrupts
可以有 clock-frequency
可以有 current-speed
为了帮你快速建立概念边界,这里把 **Binding、DTS、Driver** 三者放在一起对比:
| 维度 | Binding | DTS | Driver |
| --- | --- | --- | --- |
| **作用** | 定义某类硬件"允许有哪些属性、每个属性是什么类型、是否必填" | 描述某块具体板子上"有哪些硬件实例、每个实例的具体配置值" | 真正操作硬件寄存器,实现读写、初始化、中断处理等行为 |
| **定义位置** | `dts/bindings/` 下的 YAML 文件,如 `company,uart.yaml` | `boards/` 或 `soc/` 下的 `.dts` / `.dtsi` 文件,如 `my_board.dts` | `drivers/` 下的 C 源文件,如 `uart_company.c` |
| **语法格式** | YAML schema(`compatible` + `properties` + `required` + `include`) | Devicetree 源语法(节点 + `compatible` + `reg` + `interrupts` 等属性赋值) | C 语言(`struct device`、`DEVICE_DT_DEFINE()`、`static int xxx_init()`) |
| **与硬件关系** | 对应"硬件类型"(如 Company UART 这一类 IP) | 对应"硬件实例"(如这块板子上的 `uart0`) | 对应"硬件操作"(读写寄存器、配置波特率等) |
| **影响范围** | 全局:影响所有 `compatible` 匹配该类型的节点 | 局部:只影响当前板子/SoC 的实例配置 | 局部:只影响该驱动所服务的设备实例 |
一句话总结:
```bash
Binding 定义"硬件长什么样"
DTS 描述"这块板子上具体是什么配置"
Driver 负责"真正去操作硬件"
这些属性分别是什么类型
**三、Binding 和 DTS 是"类型"和"对象"的关系**
这个概念非常重要。
假设 Binding:
```bash
compatible: "company,uart"
properties:
reg:
required: true
interrupts:
required: true
current-speed:
type: int
那么 DTS:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5 0>;
current-speed = <115200>;
status = "okay";
};
可以类比成:
struct CompanyUart {
uintptr_t reg;
int irq;
int current_speed;
};
struct CompanyUart uart0 = {
.reg = 0x40000000,
.irq = 5,
.current_speed = 115200,
};
所以:
Binding
↓
定义“Company UART 是什么”
DTS
↓
定义“我的板子上这个 UART 是什么配置”
四、为什么 Zephyr 需要 Binding?
因为 DTS 本身只是描述硬件。
例如:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5 0>;
};
如果没有 Binding,工具链很难知道:
reg
到底应该是什么。
也不知道:
interrupts
是不是这个硬件允许的属性。
更不知道:
current-speed
是不是合法。
Binding 就提供了这个语义。
五、Binding 文件放在哪里?
对于你自己的 SoC BSP,典型结构会逐渐变成:
zephyr/
├── boards/
│ └── company/
│ └── my_board/
│ ├── my_board.dts
│ ├── my_board.yaml
│ └── ...
│
├── dts/
│ ├── bindings/
│ │ ├── serial/
│ │ │ └── company,uart.yaml
│ │ │
│ │ ├── spi/
│ │ │ └── company,spi.yaml
│ │ │
│ │ └── timer/
│ │ └── company,timer.yaml
│
└── soc/
└── company/
└── ...
例如:
dts/bindings/serial/company,uart.yaml
六、最重要的字段:compatible
Binding 的核心就是:
compatible: "company,uart"
而 DTS:
compatible = "company,uart";
两者通过字符串连接。
也就是说:
DTS
│
│ compatible
▼
"company,uart"
│
│ matching
▼
Binding
│
▼
company,uart.yaml
所以:
compatible 是 Devicetree 世界中硬件类型的"身份证"。
七、compatible 为什么不是随便写?
比如你写:
compatible = "abc,uart";
那么 Zephyr 会去找对应 Binding。
通常会寻找:
abc,uart.yaml
因此最好按照:
vendor,device
形式命名。
例如:
company,uart
company,gpio
company,spi
company,i2c
company,timer
company,watchdog
如果公司名字是:
acme
那么:
acme,uart
acme,gpio
acme,timer
八、Binding 不只是 compatible
一个完整 Binding 往往会描述很多内容。
例如:
description: Company UART controller
compatible: "company,uart"
include:
- name: uart-controller.yaml
property-allowlist:
- current-speed
- parity
- stop-bits
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
clock-frequency:
type: int
required: false
这里出现了几个非常重要的概念。
九、properties
这是 Binding 最核心的部分。
例如:
properties:
clock-frequency:
type: int
required: true
表示:
这个设备有一个属性:
clock-frequency
类型:
int
必须存在:
true
于是:
uart0 {
clock-frequency = <50000000>;
};
合法。
十、required
例如:
properties:
reg:
required: true
意味着:
uart0 {
compatible = "company,uart";
};
缺少:
reg
就不符合这个 Binding。
而:
properties:
clock-frequency:
required: false
则意味着:
uart0 {
compatible = "company,uart";
reg = <...>;
};
可以没有:
clock-frequency
十一、Binding 还能规定属性类型
例如:
properties:
fifo-size:
type: int
dma-enabled:
type: boolean
label:
type: string
clocks:
type: phandle-array
reset:
type: phandle
pinctrl-0:
type: phandles
所以 Binding 不只是:
“这个属性存在。”
而是:
“这个属性存在,而且它应该是什么数据类型。”
十二、reg 为什么经常不用自己定义?
这是初学者非常容易困惑的地方。
你可能看到:
properties:
reg:
required: true
但 Zephyr 的 Binding 经常会:
include:
- base.yaml
或者:
include:
- name: base.yaml
通过 include 复用标准 Binding 定义。
因为:
reg
status
interrupts
compatible
...
这些 Devicetree 基础概念不应该每个 Binding 都重新定义。
十三、Binding 可以继承/复用
例如 UART。
你的硬件:
Company UART
本质上还是一个:
UART controller
所以可以复用 Zephyr 已有的 UART controller Binding。
概念上:
uart-controller.yaml
│
│ include
▼
company,uart.yaml
│
▼
uart0.dts
这样你不需要自己重新定义所有 UART 通用属性。
十四、这就是 Zephyr Binding 非常强大的地方
假设 Zephyr 已经知道:
UART controller
具有:
current-speed
parity
stop-bits
data-bits
那么你的 SoC UART:
company,uart
可以在这个基础上增加自己的东西:
fifo-size
clock-frequency
dma
最终形成:
标准 UART 属性
+
Company SoC 特有属性
↓
company,uart
这比从零定义一个 UART 更合理。
十五、举一个完整的 Company UART Binding
例如:
description: Company SoC UART controller
compatible: "company,uart"
include:
- uart-controller.yaml
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
clock-frequency:
type: int
required: false
fifo-size:
type: int
required: false
然后 DTS:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5 0>;
clocks = <&clk UART0_CLK>;
clock-frequency = <50000000>;
fifo-size = <32>;
current-speed = <115200>;
status = "okay";
};
现在:
Binding
↓
告诉 Zephyr:
↓
company,uart 有哪些属性
↓
DTS
↓
填写这些属性的具体值
十六、Binding 和 Driver 到底是什么关系?
这是 BSP 开发最关键的一点。
很多人会误认为:
Binding = Driver
不是。
实际上:
Binding
│
│ 描述硬件数据
▼
Devicetree
│
│ 提供实例配置
▼
Driver
│
│ 操作寄存器
▼
Hardware
例如:
company,uart.yaml
描述:
reg
interrupts
clocks
fifo-size
而:
uart_company.c
里面才会有:
# define UART_STATUS 0x00
# define UART_CTRL 0x04
# define UART_TXDATA 0x08
# define UART_RXDATA 0x0c
然后:
static int uart_company_init(...)
{
...
}
Binding 不负责:
读 UART_STATUS
写 UART_TXDATA
配置波特率
这些属于 Driver。
十七、最终它们如何汇合?
这才是你前面 17~26 篇一直在追踪的主线。
现在完整链路可以画成:
company,uart.yaml
│
│ Binding
▼
┌─────────────────┐
│ Devicetree Node │
│ │
│ uart0: uart@... │
│ │
│ compatible │
│ reg │
│ interrupts │
│ clocks │
└────────┬────────┘
│
▼
Devicetree Generator
│
▼
devicetree_generated.h
│
▼
DT_NODELABEL(uart0)
│
▼
DEVICE_DT_DEFINE(...)
│
▼
struct device
│
▼
UART Driver
│
▼
UART Hardware
这就是:
Binding → DTS → DT macros → Device → Driver
十八、Binding 还会影响 C 宏
这一点特别重要。
假设 DTS:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
current-speed = <115200>;
};
Driver 可以写:
#define UART_INIT(node_id) \
static const struct uart_company_config \
uart_config_##node_id = { \
.base = DT_REG_ADDR(node_id), \
.baudrate = DT_PROP(node_id, current_speed), \
};
然后:
UART_INIT(DT_NODELABEL(uart0));
最终相当于获得:
static const struct uart_company_config uart_config_uart0 = {
.base = 0x40000000,
.baudrate = 115200,
};
注意:
Driver 没有硬编码 0x40000000。
地址来自:
DTS
↓
DT_REG_ADDR()
而:
baudrate
来自:
DTS
↓
DT_PROP()
Binding 定义了:
current-speed
这个属性。
十九、这就是 BSP 的"数据驱动"思想
你以后做 Company SoC BSP,尽量形成这种结构:
不要:
# define UART0_BASE 0x40000000
# define UART1_BASE 0x40001000
然后:
if (board == XXX) {
...
}
而是:
Hardware
↓
DTS
↓
Binding
↓
DT macros
↓
Driver
例如不同芯片:
SoC A
UART0 = 0x40000000
SoC B
UART0 = 0x50000000
Driver 本身不需要修改。
只需要:
reg = <0x40000000 ...>;
或者:
reg = <0x50000000 ...>;
二十、Binding 的真正价值
因此不要把 Binding 理解成:
“一个 YAML 配置文件。”
它实际上承担了一个非常重要的角色:
Hardware Specification
│
▼
Devicetree Binding
│
┌──────────┴──────────┐
│ │
Property Schema Hardware Type
│ │
▼ ▼
reg / irq / clock company,uart
│ │
└──────────┬──────────┘
▼
Devicetree
│
▼
Driver
它是:
硬件描述世界和软件 Driver 世界之间的契约。
二十一、回到你的 Company SoC BSP
到目前为止,我们已经逐步建立了:
20 理解 Zephyr BSP
↓
21 SoC Port Skeleton
↓
22 CPU / Architecture
↓
23 Startup
↓
24 Interrupt Controller
↓
25 Clock / Reset
↓
26 Devicetree
↓
27 Binding
现在你的 BSP 已经开始具备完整形态:
Company SoC
│
├── ARCH
│
├── SOC
│ ├── startup
│ ├── clock
│ ├── reset
│ └── interrupt controller
│
├── DTS
│ ├── SoC nodes
│ └── board nodes
│
├── Bindings
│ ├── company,uart
│ ├── company,gpio
│ ├── company,timer
│ └── company,spi
│
└── Drivers
├── uart
├── gpio
├── timer
└── spi
这时你已经不是在"学习怎么使用 Zephyr"。
而是在:
为一个真实 SoC 建立 Zephyr Hardware Abstraction。
转载自 CSDN-专业IT技术社区




