摘要:承接第 06 篇的 Devicetree 实战,本篇进入 SoC BSP 的驱动层,核心主线是「Binding 定义规则、DTS 描述实例、Driver 执行操作」。通过一个完整 UART 实例,串联 Binding YAML → Devicetree 节点 → Driver 宏的编译期链路,并剖析 Config/Data 模式与多实例生成机制,为下一篇 UART Driver 最小实战铺路。
SoC Driver 与 Devicetree Binding
这一篇正好进入 Zephyr SoC BSP 真正的"驱动层"。
你前面已经建立了这条主线:
01 Zephyr Hardware Model
02 Architecture → SoC → Board
03 Zephyr SoC Porting
04 SoC Porting 实战
05 Zephyr SoC Devicetree
06 SoC Devicetree 实战
07 SoC Driver + Devicetree Binding ← 现在
对于你的最终目标——把公司自己的 SoC 加入 Zephyr 开发生态——这一篇尤其重要,因为从这里开始,你要理解:
本文核心结论速览
在深入正文之前,先把本篇最关键的几条结论放在这里,方便你带着主线阅读:
- 三者分工:Binding 定义规则(属性 schema),DTS 描述实例(硬件长什么样),Driver 执行操作(如何控制硬件)。三者通过 compatible 这条"钥匙"串成完整链路。
- compatible 匹配机制:Devicetree 节点里的 compatible = "mycompany,myuart" 与 Binding YAML 中的 compatible 完全一致时,Binding 才会被应用;Driver 再通过 #define DT_DRV_COMPAT mycompany_myuart(逗号转下划线)绑定到同一批节点。
- 编译期生成原理:DT_INST_* 宏(如 DT_INST_REG_ADDR、DT_INST_IRQ)不是运行时查询,而是 Devicetree Compiler 在编译期把节点属性展开成字面量常量,直接写进 config 结构体。
- 多实例机制:DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE) 在编译期遍历所有 status = "okay" 的同 compatible 节点,逐个展开宏,让一个 Driver 自动服务多个硬件实例(uart0 → inst=0,uart1 → inst=1)。
- Config/Data 设计模式:config 保存"这个硬件是什么"(编译期确定),data 保存"当前处于什么状态"(运行时可变),这是 Zephyr Driver 的核心思维。
- SoC BSP 职责边界:硬件描述问题看 Devicetree/Binding,硬件操作问题看 Driver,SoC 初始化问题看 SoC Porting,板级连接问题看 Board DTS——四类问题各有归属,不要混为一谈。
SoC 硬件资源如何通过 Devicetree 描述,再由 Driver 读取这些描述并控制硬件。
一、先建立整体认识
很多初学者会把这几个概念混在一起:
Devicetree
Binding
Driver
HAL
SoC
Board
实际上它们各自解决不同的问题。
可以先记住:
Zephyr Application
│
▼
Zephyr API
│
▼
Driver
│
┌──────────┴──────────┐
│ │
Devicetree HAL / Register
│ │
▼ ▼
硬件描述 硬件操作
│ │
└──────────┬──────────┘
▼
SoC
例如:
gpio_pin_set_dt(&led, 1);
Application 并不知道:
GPIO controller 在哪里
哪个寄存器
哪个 bit
clock 怎么开
pinmux 怎么配置
为了更直观地看清六者的完整调用链和数据流,这里给出一张总览图:
┌─────────────────────────────────────────────────────────────────────┐
│ Zephyr Application │
│ (调用 Zephyr API,不感知底层硬件细节) │
└───────────────────────────────┬─────────────────────────────────────┘
│ ① 调用标准 API(如 uart_poll_out)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Zephyr API │
│ (uart / gpio / spi 等子系统对外统一接口) │
└───────────────────────────────┬─────────────────────────────────────┘
│ ② 分发到具体 Driver 的 API 回调
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Driver │
│ ┌───────────────────────┐ ┌───────────────────────────┐ │
│ │ config(编译期常量) │ │ data(运行时状态) │ │
│ │ 来自 DT_INST_* 宏 │ │ 波特率、缓冲、标志位等 │ │
│ └───────────┬───────────┘ └───────────────────────────┘ │
│ │ ③ 读取编译期生成的硬件配置 │
└───────────────┼─────────────────────────────────────────────────────┘
│ ④ 通过 DT_INST_* 宏取地址/中断/时钟
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Devicetree (DTS) │
│ uart0: serial@40000000 { compatible = "mycompany,myuart"; ... } │
│ (描述硬件实例:地址、中断、时钟、引脚) │
└───────────────┬─────────────────────────────────────────────────────┘
│ ⑤ 节点属性由 Binding 校验合法性
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Binding YAML │
│ compatible: "mycompany,myuart" │
│ properties: reg / interrupts / clocks ... │
│ (定义属性 schema,编译期被 Devicetree Compiler 读取) │
└───────────────┬─────────────────────────────────────────────────────┘
│ ⑥ 校验通过后,节点属性被编译成宏
▼
┌─────────────────────────────────────────────────────────────────────┐
│ HAL │
│ (寄存器读写封装:sys_read32 / sys_write32,屏蔽位操作细节) │
└───────────────┬─────────────────────────────────────────────────────┘
│ ⑦ 最终读写 SoC 物理寄存器
▼
┌─────────────────────────────────────────────────────────────────────┐
│ SoC │
│ (UART / GPIO / SPI 等外设的物理寄存器,由 SoC Porting 提供时钟、 │
│ 中断控制器、低层启动等基础能力) │
└───────────────┬─────────────────────────────────────────────────────┘
│ ⑧ 引脚/板级连接由 Board DTS 决定
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Board │
│ (板级 DTS / overlay:UART0 接到哪个 pin、LED 接哪个 GPIO 等) │
└─────────────────────────────────────────────────────────────────────┘
这张图把六者的职责和相邻层之间的接口串成一条完整链路,可以这样理解:
- Application → Zephyr API:应用只调用标准 API,不感知底层硬件细节,接口是 uart_poll_out()、gpio_pin_set_dt() 这类统一函数。
- Zephyr API → Driver:API 层把调用分发到具体 Driver 的 API 回调(如 uart_driver_api 中的 poll_out),接口是 struct uart_driver_api。
- Driver → Devicetree → Binding:Driver 通过 DT_INST_* 宏在编译期读取 Devicetree 节点属性,而节点属性是否合法由 Binding 校验;接口是 DT_INST_REG_ADDR()、DT_INST_IRQ() 等宏。
- Driver → HAL → SoC:Driver 通过 HAL 的寄存器读写封装(sys_read32/sys_write32)最终操作 SoC 物理寄存器;接口是寄存器读写函数。
- SoC → Board:SoC 提供外设控制器,但具体引脚连接、板级外设由 Board DTS 决定;接口是板级 DTS 中的 pinctrl 和节点引用。
这些事情由下面几层共同完成:
Application
↓
GPIO API
↓
GPIO Driver
↓
Devicetree
↓
SoC GPIO registers
二、为什么 SoC BSP 一定会遇到 Driver?
假设你的公司 SoC 有:
UART0
UART1
GPIO0
GPIO1
I2C0
SPI0
TIMER0
PWM0
ADC0
那么 Zephyr 必须知道:
UART0 在哪里?
UART0 有哪些寄存器?
UART0 的 clock 怎么打开?
UART0 的 interrupt 是什么?
UART0 使用哪个 pin?
这些信息不能全部硬编码在 Driver 中。
否则你最后可能写出:
# define UART0_BASE 0x40000000
# define UART0_IRQ 32
# define UART0_CLK …
然后 Driver 里面到处都是:
# define …
这会导致 Driver 与具体 SoC 强耦合。
Zephyr 更希望:
Devicetree
↓
描述硬件实例
↓
Driver
↓
读取硬件配置
所以:
Devicetree Binding + Devicetree + Driver 是一组完整体系。
三、什么是 Devicetree Binding?
Binding 可以理解成:
告诉 Zephyr:一个 Devicetree 节点允许有哪些属性,以及这些属性分别是什么类型。
例如我们有:
uart0: serial@40000000 {
compatible = "company,foo-uart";
reg = <0x40000000 0x1000>;
interrupts = <32>;
clocks = <&clk UART0_CLK>;
status = "okay";
};
这里:
compatible
reg
interrupts
clocks
status
都是 Devicetree properties。
Binding 则描述:
compatible:
company,foo-uart
properties:
reg:
type: array
interrupts:
type: array
clocks:
type: phandle-array
所以:
DTS
│
│ "我有这些属性"
▼
Binding
│
│ "这些属性是否合法、是什么类型"
▼
Devicetree validation
四、Binding 最重要的东西:compatible
在 Zephyr Driver 中,最重要的连接点之一就是:
compatible = "company,foo-uart";
Driver 通过它识别硬件。
例如:
compatible = "mycompany,myuart";
对应:
compatible: "mycompany,myuart"
然后 Driver:
# define DT_DRV_COMPAT mycompany_myuart
注意这里有一个非常重要的转换:
"mycompany,myuart"
↓
mycompany_myuart
也就是:
vendor,device
↓
vendor_device
然后:
DT_INST_FOREACH_STATUS_OKAY(...)
就可以找到 Devicetree 中:
compatible = "mycompany,myuart";
的实例。
五、Binding 文件放在哪里?
Zephyr 中通常是:
dts/
└── bindings/
├── serial/
├── gpio/
├── i2c/
├── spi/
├── pwm/
└── ...
例如:
dts/bindings/serial/mycompany,myuart.yaml
可能写成:
description: MyCompany UART controller
compatible: "mycompany,myuart"
include:
\\– name: base.yaml
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
这里最重要的是:
compatible: "mycompany,myuart"
它把:
Binding
和:
Devicetree node
联系起来。
六、一个完整例子
假设公司 SoC 有一个 UART:
UART0
Base Address = 0x40000000
Size = 0x1000
IRQ = 32
那么我们可以设计:
1. Binding
dts/bindings/serial/mycompany,myuart.yaml
例如:
description: MyCompany UART
compatible: "mycompany,myuart"
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
2. Devicetree
uart0: serial@40000000 {
compatible = "mycompany,myuart";
reg = <0x40000000 0x1000>;
interrupts = <32>;
clocks = <&clk UART0_CLK>;
status = "okay";
};
3. Driver
# define DT_DRV_COMPAT mycompany_myuart
然后:
static const struct device \\*dev;
Driver 就可以通过 Devicetree 宏取得:
reg
interrupts
clocks
例如:
# define UART_BASE(inst) \\\\
DT_INST_REG_ADDR(inst)
最终:
UART_BASE(0)
可能得到:
0x40000000
这就是 Zephyr 非常核心的一种工作方式:
DTS
│
▼
Devicetree Compiler
│
▼
generated devicetree information
│
▼
DT_INST_REG_ADDR()
DT_INST_IRQ()
DT_INST_PROP()
DT_INST_CLOCKS_...
│
▼
Driver
七、Driver 到底在做什么?
以 UART 为例。
应用:
printk("Hello\\\\n");
最终需要 UART Driver。
Driver 的职责通常包括:
初始化
↓
clock
↓
pinmux
↓
UART registers
↓
interrupt
↓
buffer
↓
Zephyr UART API
例如:
static int myuart_init(const struct device \\*dev)
{
const struct myuart_config \\*config = dev->config;
/* enable clock */
/* configure registers */
/* configure interrupt */
return 0;
}
这里:
config
往往就是从 Devicetree 生成的。
八、Config 和 Data 是 Driver 设计中的核心
Zephyr Driver 中非常重要的一个模式:
struct myuart_config {
uintptr_t base;
int irq;
};
struct myuart_data {
...
};
通常:
config
↓
硬件配置
↓
编译期确定
而:
data
↓
运行时状态
例如:
struct myuart_config {
uintptr_t base;
};
struct myuart_data {
uint32_t baudrate;
};
可以理解成:
config
└── "这个 UART 是什么硬件?"
data
└── "这个 UART 当前处于什么状态?"
这是写 Zephyr Driver 时非常值得养成的思维方式。
九、Devicetree → Driver 的真正关系
你可以把它理解成:
Devicetree
│
│ describes
▼
Hardware
│
│ matched by
▼
compatible
│
▼
Driver
更完整一点:
Board DTS
│
▼
SoC DTSI
│
▼
peripheral node
│
│ compatible
▼
┌───────────────────┐
│ Binding YAML │
│ │
│ reg │
│ interrupts │
│ clocks │
│ pinctrl │
└─────────┬─────────┘
│
▼
DT macros
│
▼
Driver
│
▼
Hardware registers
这张图非常重要。
十、为什么不能只写 Driver?
假设你直接写:
# define UART0_BASE 0x40000000
然后:
volatile uint32_t \\*reg =
(volatile uint32_t \\*)UART0_BASE;
当然可以控制硬件。
但问题是:
UART0
UART1
UART2
怎么办?
你最终可能出现:
# define UART0_BASE …
# define UART1_BASE …
# define UART2_BASE …
然后大量:
# ifdef CONFIG_MY_SOC
...
# endif
这种 Driver 很快会失控。
Zephyr 的设计目标是:
同一个 Driver
│
├── UART0
├── UART1
├── UART2
└── UART3
通过:
Devicetree instances
生成不同实例。
十一、一个 SoC 有多个 UART
例如:
uart0: serial@40000000 {
compatible = "mycompany,myuart";
reg = <0x40000000 0x1000>;
interrupts = <32>;
status = "okay";
};
uart1: serial@40001000 {
compatible = "mycompany,myuart";
reg = <0x40001000 0x1000>;
interrupts = <33>;
status = "okay";
};
注意:
两个节点:
compatible 完全一样
但是:
reg 不一样
interrupt 不一样
这意味着:
一个 Driver 可以服务多个硬件实例。
这是 Zephyr SoC BSP 非常关键的能力。
十二、Driver 如何生成多个实例?
典型结构:
# define DT_DRV_COMPAT mycompany_myuart
然后:
# define MYUART_DEFINE(inst) \\
\\
static const struct myuart_config \\
myuart_config_##inst = { \\
.base = DT_INST_REG_ADDR(inst), \\
}; \\
\\
static struct myuart_data \\
myuart_data_##inst; \\
\\
DEVICE_DT_INST_DEFINE( \\
inst, \\
myuart_init, \\
NULL, \\
&myuart_data_##inst, \\
&myuart_config_##inst, \\
POST_KERNEL, \\
CONFIG_SERIAL_INIT_PRIORITY, \\
&myuart_driver_api);
DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE)
于是:
uart0
↓
inst = 0
uart1
↓
inst = 1
uart2
↓
inst = 2
自动生成:
myuart0
myuart1
myuart2
十三、这就是 Zephyr Driver 的"魔法"
表面上你看到:
DT_INST_REG_ADDR(inst)
但真正发生的是:
DTS
↓
Devicetree compiler
↓
generated headers
↓
DT macros
↓
C preprocessor
↓
compile-time constants
↓
Driver
所以:
Devicetree 不是运行时数据库。
这是一个非常重要的认识。
它主要是在:
编译阶段描述和配置硬件。
十四、Binding 不等于 Driver
这两个一定要分清。
Binding
描述:
这个硬件节点有什么属性?
例如:
compatible:
reg:
interrupts:
clocks:
pinctrl-0:
Driver
实现:
如何操作这个硬件?
例如:
write_register();
read_register();
enable_interrupt();
set_baudrate();
所以:
为了更直观地看清三者差异,这里用一张表格从五个维度对比 Binding、DTS、Driver:
| 定义 | 硬件节点的"数据字典",定义属性 schema | 描述具体硬件实例的源文件 | 操作硬件的代码实现 |
| 作用 | 校验 DTS 节点属性是否合法、类型是否正确 | 描述"硬件长什么样"(地址、中断、时钟等) | 实现"如何操作硬件"(读写寄存器、收发数据等) |
| 内容 | compatible、properties、required 等约束声明 | compatible、reg、interrupts、clocks、status 等节点属性 | config/data 结构体、初始化函数、API 回调、DEVICE_DT_INST_DEFINE 宏 |
| 编译期/运行期 | 编译期:被 Devicetree Compiler 读取,用于校验和生成宏 | 编译期:被解析后生成头文件(generated devicetree information) | 编译期生成 config 常量,运行期执行硬件操作 |
| 示例 | compatible: "mycompany,myuart" + properties: reg: required: true | uart0: serial@40000000 { compatible = "mycompany,myuart"; reg = <0x40000000 0x1000>; }; | #define DT_DRV_COMPAT mycompany_myuart + DT_INST_REG_ADDR(inst) + DEVICE_DT_INST_DEFINE(…) |
简单来说:Binding 定义规则,DTS 描述实例,Driver 执行操作。三者通过 compatible 这条"钥匙"串成一条完整的链路——Binding 校验 DTS 节点是否合法,DTS 为 Driver 提供编译期的硬件配置,Driver 则把这些配置变成真正控制硬件的代码。
Binding = 描述硬件
Driver = 操作硬件
十五、Binding 也不是 DTS
还有一个容易混淆的地方:
Binding
不是:
DTS
三者关系应该记成:
Binding
│
│ defines schema
▼
DTS
│
│ describes actual instance
▼
Driver
例如:
Binding
compatible: "mycompany,myuart"
properties:
reg:
required: true
DTS
uart0: serial@40000000 {
compatible = "mycompany,myuart";
reg = <0x40000000 0x1000>;
};
Driver
# define DT_DRV_COMPAT mycompany_myuart
十六、进一步理解 SoC BSP 的职责边界
到了这里,你可以开始建立下面这张地图:
Zephyr
│
┌─────────┴─────────┐
│ │
Application Kernel
│
▼
Zephyr API
│
▼
Driver
│
┌────┴─────┐
│ │
Devicetree HAL
│ │
└────┬─────┘
▼
SoC
│
┌─────┼─────┐
▼ ▼ ▼
UART GPIO SPI
而 SoC Porting 本身又包含:
Architecture
│
▼
SoC
│
├── clock
├── interrupt
├── timer
├── uart
├── gpio
├── spi
├── i2c
└── ...
十七、公司 SoC 真正进入 Zephyr 后,你会做什么?
假设你的公司 SoC 叫:
ACME-X1
那么最终目录可能逐渐形成:
zephyr/
├── arch/
│
├── soc/
│ └── acme/
│ └── acme-x1/
│ ├── CMakeLists.txt
│ ├── Kconfig
│ ├── Kconfig.defconfig
│ ├── soc.h
│ └── ...
│
├── dts/
│ ├── arm/
│ └── bindings/
│ ├── serial/
│ │ └── acme,uart.yaml
│ ├── gpio/
│ │ └── acme,gpio.yaml
│ └── timer/
│ └── acme,timer.yaml
│
└── drivers/
├── serial/
│ └── uart_acme.c
├── gpio/
│ └── gpio_acme.c
└── timer/
└── timer_acme.c
这时候你已经开始真正做:
公司 SoC → Zephyr BSP
而不是简单做一个 Board。
十八、一个非常重要的分界线
以后你看到一个外设问题,可以先问:
问题 1:这是硬件描述问题吗?
例如:
UART 地址是多少?
IRQ 是多少?
clock 是哪个?
GPIO controller 是哪个?
那么优先看:
Devicetree / Binding
问题 2:这是硬件操作问题吗?
例如:
UART 怎么发送?
GPIO 怎么设置方向?
SPI 怎么启动?
那么看:
Driver
问题 3:这是 SoC 初始化问题吗?
例如:
clock tree
interrupt controller
system timer
low-level startup
那么看:
SoC Porting
问题 4:这是板级连接问题吗?
例如:
UART0 接到了哪个 pin?
LED 接 GPIOA5?
SPI flash 接 SPI1?
那么看:
Board DTS / Board overlay
十九、你现在应该重点掌握的 5 个概念
这一篇不要急着背 Driver API。
先真正理解:
① compatible
② Binding
③ Devicetree instance
④ DT_\\* macros
⑤ DEVICE_DT_INST_DEFINE()
如果这五个东西搞明白,后面的 Zephyr Driver 会容易很多。
尤其是这条:
compatible
↓
Binding
↓
DTS node
↓
DT_INST_\\*
↓
Driver instance
↓
DEVICE_DT_INST_DEFINE()
这是以后你给公司 SoC 写 Driver 时最常见的一条链。
二十、建议下一篇:08-第一个 SoC Driver
十九点五、UART Driver 最小实战:从 Binding 到 Driver 的完整链路
前面我们拆解了概念,这里直接给出一套可运行的最小 UART Driver 完整代码,把 Binding YAML、Devicetree 节点、Driver 初始化函数和 DEVICE_DT_INST_DEFINE 宏串成一条完整的链路。建议你对照着逐行阅读,理解每一部分在整个体系中的位置。
1. Binding YAML:定义硬件节点的属性 schema
文件路径:dts/bindings/serial/mycompany,myuart.yaml
# Binding YAML 的作用:告诉 Zephyr 一个 compatible 为 "mycompany,myuart" 的
# Devicetree 节点允许有哪些属性,以及每个属性的类型和是否必填。
# 它相当于"硬件节点的数据字典",是 Devicetree 校验和生成宏的依据。
description: MyCompany UART controller
# compatible 是 Binding 与 Devicetree 节点之间的"钥匙"。
# 只有 compatible 完全匹配,这个 Binding 才会被应用到对应的节点上。
compatible: "mycompany,myuart"
# include 表示继承其他 Binding 的公共定义,例如 base.yaml 里通常包含
# reg、interrupts 等通用属性的基础 schema。
include:
– name: base.yaml
# properties 定义这个节点允许出现的属性及其约束。
properties:
reg:
# reg 描述硬件寄存器基地址和长度,类型是 array(数组)。
# 它会被编译成 DT_INST_REG_ADDR(inst) 等宏。
required: true
interrupts:
# interrupts 描述硬件中断号,类型是 array。
# 它会被编译成 DT_INST_IRQ(inst) 等宏。
required: true
clocks:
# clocks 描述时钟源,类型是 phandle-array(指向时钟控制器的句柄数组)。
# 它会被编译成 DT_INST_CLOCKS_…(inst) 等宏。
required: true
2. Devicetree 节点:描述具体的硬件实例
文件路径:dts/arm/mycompany/mycompany_x1.dtsi(SoC 级)或板级 DTS 中
// Devicetree 节点的作用:描述 SoC 上真实存在的 UART 硬件实例。
// 它告诉 Zephyr:"这里有一个 UART,它的地址、中断、时钟分别是什么"。
// 注意:这里只描述"硬件长什么样",不包含任何操作逻辑。
uart0: serial@40000000 {
// compatible 必须与 Binding YAML 中的 compatible 完全一致,
// 否则 Zephyr 无法把节点和 Binding、Driver 关联起来。
compatible = "mycompany,myuart";
// reg 描述寄存器基地址 0x40000000 和映射长度 0x1000。
// 对应 Binding 中的 reg 属性,会被编译成 DT_INST_REG_ADDR(0)。
reg = <0x40000000 0x1000>;
// interrupts 描述中断号 32。
// 对应 Binding 中的 interrupts 属性,会被编译成 DT_INST_IRQ(0)。
interrupts = <32>;
// clocks 描述时钟源,&clk 指向时钟控制器节点,UART0_CLK 是时钟编号。
// 对应 Binding 中的 clocks 属性。
clocks = <&clk UART0_CLK>;
// status = "okay" 表示该实例启用。
// 只有 status 为 "okay" 的节点才会被 DT_INST_FOREACH_STATUS_OKAY 遍历到。
status = "okay";
};
uart1: serial@40001000 {
// 第二个实例:compatible 相同,但 reg 和 interrupts 不同。
// 这正体现了"一个 Driver 服务多个硬件实例"的能力。
compatible = "mycompany,myuart";
reg = <0x40001000 0x1000>;
interrupts = <33>;
clocks = <&clk UART1_CLK>;
status = "okay";
};
3. Driver 初始化函数:实现硬件的具体操作
文件路径:drivers/serial/uart_mycompany.c
/*
* UART Driver 的作用:实现"如何操作这个硬件"。
* 它通过 Devicetree 生成的宏(DT_INST_*)读取硬件配置,
* 而不是把地址、中断号硬编码在代码里。
*/
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/devicetree.h>
/* 关键宏:声明本 Driver 服务的 compatible 字符串。
* 注意:逗号要替换成下划线,即 "mycompany,myuart" -> mycompany_myuart。
* 这个宏决定了 DT_INST_* 系列宏展开时查找哪个 compatible 的节点。 */
#define DT_DRV_COMPAT mycompany_myuart
/* config 结构体:保存"这个 UART 是什么硬件"的编译期常量。
* 这些值全部来自 Devicetree,由 DT_INST_REG_ADDR 等宏在编译期生成。 */
struct myuart_config {
uintptr_t base; /* 寄存器基地址,来自 reg 属性 */
int irq; /* 中断号,来自 interrupts 属性 */
};
/* data 结构体:保存"这个 UART 当前处于什么状态"的运行时数据。
* 例如当前波特率、收发缓冲状态等。 */
struct myuart_data {
uint32_t baudrate; /* 当前波特率,运行时由应用设置 */
};
/* 初始化函数:设备启动时被 Zephyr 内核调用一次。
* 职责:打开时钟、配置寄存器、注册中断,让硬件进入可用状态。 */
static int myuart_init(const struct device *dev)
{
const struct myuart_config *config = dev->config;
struct myuart_data *data = dev->data;
/* 1. 打开时钟:这里通过 Devicetree 的 clocks 属性拿到时钟句柄,
* 实际项目中会调用 clock 子系统 API 使能 UART 时钟。 */
/* clock_enable(config->clocks); */
/* 2. 配置寄存器:通过 config->base 访问硬件寄存器,
* 例如设置波特率、数据位、停止位等。 */
/* sys_write32(baudrate_value, config->base + UART_BAUD_REG); */
/* 3. 配置中断:注册中断处理函数并启用中断。 */
/* irq_connect_dynamic(config->irq, 0, myuart_isr, dev, 0); */
/* irq_enable(config->irq); */
/* 4. 初始化运行时数据。 */
data->baudrate = 115200;
return 0;
}
/* 寄存器偏移定义:这里以常见的 16550 兼容 UART 寄存器布局为例。
* 实际项目中应根据公司 SoC 的 TRM(技术参考手册)修改这些偏移值。 */
#define MYUART_REG_THR 0x00 /* 发送保持寄存器(写) */
#define MYUART_REG_RBR 0x00 /* 接收缓冲寄存器(读) */
#define MYUART_REG_LSR 0x14 /* 线路状态寄存器 */
#define MYUART_LSR_TX_READY BIT(5) /* 发送保持寄存器空 */
#define MYUART_LSR_RX_READY BIT(0) /* 接收数据就绪 */
/* 轮询发送一个字节:应用调用 uart_poll_out() 时进入此函数。
* 它通过 config->base 直接操作硬件寄存器,而不是硬编码地址。 */
static int myuart_poll_out(const struct device *dev, unsigned char c)
{
const struct myuart_config *config = dev->config;
volatile uint32_t *base = (volatile uint32_t *)config->base;
/* 1. 等待发送保持寄存器空闲(TX FIFO 有空位)。
* 轮询 LSR 的 TX_READY 位,直到硬件可以接收新数据。 */
while (!(sys_read32((uintptr_t)&base[MYUART_REG_LSR / 4]) &
MYUART_LSR_TX_READY)) {
/* 忙等待,直到发送寄存器空闲 */
}
/* 2. 把要发送的字节写入发送保持寄存器(THR)。
* 硬件会自动把该字节移位输出到 TX 引脚。 */
sys_write32(c, (uintptr_t)&base[MYUART_REG_THR / 4]);
return 0;
}
/* 轮询接收一个字节:应用调用 uart_poll_in() 时进入此函数。
* 返回值:0 表示成功收到一个字节并存入 *c;-1 表示当前没有数据可读。 */
static int myuart_poll_in(const struct device *dev, unsigned char *c)
{
const struct myuart_config *config = dev->config;
volatile uint32_t *base = (volatile uint32_t *)config->base;
/* 1. 检查接收数据就绪位(RX_READY)。
* 如果硬件还没有收到完整字节,直接返回 -1,不阻塞调用者。 */
if (!(sys_read32((uintptr_t)&base[MYUART_REG_LSR / 4]) &
MYUART_LSR_RX_READY)) {
return –1;
}
/* 2. 从接收缓冲寄存器(RBR)读出一个字节。 */
*c = (unsigned char)sys_read32((uintptr_t)&base[MYUART_REG_RBR / 4]);
return 0;
}
/* 配置 UART 参数:应用调用 uart_configure() 时进入此函数。
* 这里实现波特率、数据位、校验位、停止位的配置逻辑。
* 返回值:0 表示配置成功;-ENOTSUP 表示不支持的参数组合。 */
static int myuart_cfg_set(const struct device *dev,
const struct uart_config *cfg)
{
const struct myuart_config *config = dev->config;
struct myuart_data *data = dev->data;
volatile uint32_t *base = (volatile uint32_t *)config->base;
/* 1. 校验参数:本示例只支持 8 数据位、无校验、1 停止位。
* 实际 Driver 应根据 SoC 能力扩展支持范围。 */
if (cfg->data_bits != UART_CFG_DATA_BITS_8 ||
cfg->parity != UART_CFG_PARITY_NONE ||
cfg->stop_bits != UART_CFG_STOP_BITS_1) {
return –ENOTSUP;
}
/* 2. 计算波特率分频值:这里以 16MHz 外设时钟、16 倍过采样为例。
* 实际项目中应通过 Devicetree 的 clocks 属性获取真实时钟频率。 */
uint32_t clock_freq = 16000000u; /* 示例:16 MHz */
uint32_t divisor = clock_freq / (16u * cfg->baudrate);
/* 3. 写入波特率分频寄存器(DLL/DLH,这里简化为一个 32 位寄存器)。
* 实际 16550 兼容 UART 需要先设置 DLAB 位再写 DLL/DLH。 */
/* sys_write32(divisor, (uintptr_t)&base[MYUART_REG_DLL / 4]); */
/* 4. 记录当前波特率到运行时数据。 */
data->baudrate = cfg->baudrate;
return 0;
}
/* 查询当前 UART 配置:应用调用 uart_config_get() 时进入此函数。
* 它把 Driver 内部保存的运行时状态回填给调用者。 */
static int myuart_cfg_get(const struct device *dev, struct uart_config *cfg)
{
const struct myuart_data *data = dev->data;
/* 从运行时数据中读取当前配置。 */
cfg->baudrate = data->baudrate;
cfg->data_bits = UART_CFG_DATA_BITS_8;
cfg->parity = UART_CFG_PARITY_NONE;
cfg->stop_bits = UART_CFG_STOP_BITS_1;
cfg->flow_ctrl = UART_CFG_FLOW_CTRL_NONE;
return 0;
}
/* uart_driver_api:Zephyr UART 子系统要求 Driver 实现的标准接口。
* 这里补全了 poll_out、poll_in、cfg_set、cfg_get 四个必要回调,
* 使 Driver 可以通过 Zephyr UART API 被应用正常调用。 */
static const struct uart_driver_api myuart_driver_api = {
.poll_out = myuart_poll_out,
.poll_in = myuart_poll_in,
.cfg_set = myuart_cfg_set,
.cfg_get = myuart_cfg_get,
};
/* 实例生成宏:为每个 status = "okay" 的节点生成一个设备实例。
* 参数依次是:
* inst —— 实例编号(0、1、2…),由 DT_INST_FOREACH_STATUS_OKAY 自动传入
* myuart_init —— 初始化函数
* NULL —— 电源管理回调(本例不需要)
* &data —— 指向运行时数据
* &config —— 指向编译期配置
* POST_KERNEL —— 初始化优先级级别(内核启动后初始化)
* CONFIG_SERIAL_INIT_PRIORITY —— 同级别内的优先级
* &api —— 指向 Driver API 结构体 */
#define MYUART_DEFINE(inst) \\
static const struct myuart_config myuart_config_##inst = { \\
.base = DT_INST_REG_ADDR(inst), /* 从 Devicetree 取基地址 */ \\
.irq = DT_INST_IRQ(inst), /* 从 Devicetree 取中断号 */ \\
}; \\
\\
static struct myuart_data myuart_data_##inst; \\
\\
DEVICE_DT_INST_DEFINE(inst, \\
myuart_init, \\
NULL, \\
&myuart_data_##inst, \\
&myuart_config_##inst, \\
POST_KERNEL, \\
CONFIG_SERIAL_INIT_PRIORITY, \\
&myuart_driver_api);
/* 核心宏:遍历 Devicetree 中所有 compatible = "mycompany,myuart"
* 且 status = "okay" 的节点,对每个节点调用一次 MYUART_DEFINE(inst)。
* 例如 uart0 -> inst=0,uart1 -> inst=1,自动生成 myuart0、myuart1 两个设备。 */
DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE)
3.1 补全 DEVICE_DT_INST_DEFINE 宏与 DT_INST_FOREACH_STATUS_OKAY 调用
上面代码中的 DEVICE_DT_INST_DEFINE 宏和 DT_INST_FOREACH_STATUS_OKAY 调用在编辑器中可能被截断,这里给出完整、可直接编译的定义,并逐参数说明作用。
/* ========== DEVICE_DT_INST_DEFINE 宏完整定义 ==========
* 这是 Zephyr 设备模型中最核心的注册宏,它把「配置、数据、初始化函数、API」
* 打包成一个 struct device 实例,注册进 Zephyr 设备模型。
*
* 完整签名:
* DEVICE_DT_INST_DEFINE(inst, init_fn, pm_action_fn, data_ptr,
* config_ptr, level, priority, api_ptr);
*
* 参数逐一说明:
* inst —— 实例编号(0、1、2…),由 DT_INST_FOREACH_STATUS_OKAY 自动传入。
* 它决定了这个设备实例对应 Devicetree 中的哪个节点。
* init_fn —— 初始化函数,设备启动时被内核调用一次,返回 0 表示成功。
* pm_action_fn—— 电源管理回调函数,本例不需要,传 NULL。
* data_ptr —— 指向运行时数据(struct myuart_data),保存可变状态。
* config_ptr —— 指向编译期配置(struct myuart_config),值来自 Devicetree。
* level —— 初始化优先级级别,POST_KERNEL 表示内核启动后初始化。
* priority —— 同级别内的初始化顺序,数值越小越先执行。
* api_ptr —— 指向 Driver API 结构体(uart_driver_api),应用通过它调用硬件。
*/
#define MYUART_DEFINE(inst) \\
/* 第一段:编译期 config —— 每个实例一份,值来自 Devicetree 宏 */ \\
static const struct myuart_config myuart_config_##inst = { \\
.base = DT_INST_REG_ADDR(inst), /* 基地址:来自 reg 属性 */ \\
.irq = DT_INST_IRQ(inst), /* 中断号:来自 interrupts */ \\
}; \\
\\
/* 第二段:运行时 data —— 每个实例一份,保存运行状态 */ \\
static struct myuart_data myuart_data_##inst; \\
\\
/* 第三段:调用 DEVICE_DT_INST_DEFINE 注册设备实例 */ \\
DEVICE_DT_INST_DEFINE(inst, \\
myuart_init, /* init_fn:初始化函数 */ \\
NULL, /* pm_action_fn:无电源管理 */ \\
&myuart_data_##inst, /* data_ptr:运行时数据 */ \\
&myuart_config_##inst, /* config_ptr:编译期配置 */ \\
POST_KERNEL, /* level:内核启动后初始化 */ \\
CONFIG_SERIAL_INIT_PRIORITY, /* priority:同级别内顺序 */ \\
&myuart_driver_api); /* api_ptr:Driver API */
/* ========== DT_INST_FOREACH_STATUS_OKAY 调用 ==========
* 这是 Zephyr 多实例机制的「发动机」。
*
* 展开逻辑分三步:
* 第一步:收集节点 —— 找出 Devicetree 中所有 compatible = "mycompany,myuart"
* 且 status = "okay" 的节点;
* 第二步:按顺序编号 —— uart0(serial@40000000)→ inst = 0,
* uart1(serial@40001000)→ inst = 1;
* 第三步:逐个展开宏 —— 对每个节点调用一次 MYUART_DEFINE(inst)。
*
* 展开后的等价效果:
* MYUART_DEFINE(0) → 生成 myuart_config_0、myuart_data_0、设备实例 myuart0
* MYUART_DEFINE(1) → 生成 myuart_config_1、myuart_data_1、设备实例 myuart1
*
* 也就是说:uart0 节点自动生成 myuart0 设备,uart1 节点自动生成 myuart1 设备,
* 一个 Driver 源码无需复制,就能服务多个硬件实例。
*/
DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE)
3.2 uart0/uart1 如何自动生成 myuart0 和 myuart1
DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE) 在编译期完成展开,等价于手写下面两份代码:
/* —— DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE) 展开后的等价代码 —— */
/* inst = 0:对应 uart0(serial@40000000) */
static const struct myuart_config myuart_config_0 = {
.base = 0x40000000, /* DT_INST_REG_ADDR(0) 展开结果 */
.irq = 32, /* DT_INST_IRQ(0) 展开结果 */
};
static struct myuart_data myuart_data_0;
DEVICE_DT_INST_DEFINE(0, myuart_init, NULL,
&myuart_data_0, &myuart_config_0,
POST_KERNEL, CONFIG_SERIAL_INIT_PRIORITY,
&myuart_driver_api);
/* 注册结果:设备名 myuart0,应用可通过 DEVICE_DT_GET(DT_NODELABEL(uart0)) 获取 */
/* inst = 1:对应 uart1(serial@40001000) */
static const struct myuart_config myuart_config_1 = {
.base = 0x40001000, /* DT_INST_REG_ADDR(1) 展开结果 */
.irq = 33, /* DT_INST_IRQ(1) 展开结果 */
};
static struct myuart_data myuart_data_1;
DEVICE_DT_INST_DEFINE(1, myuart_init, NULL,
&myuart_data_1, &myuart_config_1,
POST_KERNEL, CONFIG_SERIAL_INIT_PRIORITY,
&myuart_driver_api);
/* 注册结果:设备名 myuart1,应用可通过 DEVICE_DT_GET(DT_NODELABEL(uart1)) 获取 */
关键点:DT_INST_FOREACH_STATUS_OKAY 在编译期完成展开,DT_INST_REG_ADDR(0) 会被替换成字面量 0x40000000,DT_INST_IRQ(0) 替换成 32。所以最终生成的 config 结构体是编译期常量,不占用运行时解析开销——这正是 Zephyr「Devicetree 不是运行时数据库」这一设计理念的落地体现。而 myuart0、myuart1 这两个设备名,正是由 DEVICE_DT_INST_DEFINE 内部根据 inst 编号自动拼接生成的。
3.1 补全 MYUART_DEFINE 宏的完整定义
上面代码中的 MYUART_DEFINE 宏在编辑器中可能被截断,这里给出完整、可直接编译的定义。它由三部分组成:config 结构体、data 结构体、DEVICE_DT_INST_DEFINE 注册调用。
/* ========== MYUART_DEFINE 宏完整定义 ==========
* 这个宏会被 DT_INST_FOREACH_STATUS_OKAY 对每个 status="okay" 的节点调用一次,
* 参数 inst 是实例编号(0、1、2…),由宏自动传入。
*
* 宏体分三段:
* 1) 定义该实例的编译期 config 结构体(从 Devicetree 取硬件配置);
* 2) 定义该实例的运行时 data 结构体;
* 3) 调用 DEVICE_DT_INST_DEFINE 把 config/data/init/api 打包注册成 Zephyr 设备。
*/
#define MYUART_DEFINE(inst) \\
/* 第一段:编译期 config —— 每个实例一份,值来自 Devicetree 宏 */ \\
static const struct myuart_config myuart_config_##inst = { \\
.base = DT_INST_REG_ADDR(inst), /* 基地址:来自 reg 属性 */ \\
.irq = DT_INST_IRQ(inst), /* 中断号:来自 interrupts */ \\
}; \\
\\
/* 第二段:运行时 data —— 每个实例一份,保存运行状态 */ \\
static struct myuart_data myuart_data_##inst; \\
\\
/* 第三段:注册设备实例 */ \\
DEVICE_DT_INST_DEFINE(inst, \\
myuart_init, \\
NULL, \\
&myuart_data_##inst, \\
&myuart_config_##inst, \\
POST_KERNEL, \\
CONFIG_SERIAL_INIT_PRIORITY, \\
&myuart_driver_api);
3.2 DEVICE_DT_INST_DEFINE 完整参数说明
DEVICE_DT_INST_DEFINE 是 Zephyr 设备模型中最核心的注册宏,它把「配置、数据、初始化函数、API」打包成一个 struct device 实例。完整签名如下:
DEVICE_DT_INST_DEFINE(inst, init_fn, pm_action_fn, data_ptr,
config_ptr, level, priority, api_ptr);
| inst | 实例编号,由 DT_INST_FOREACH_STATUS_OKAY 自动传入(0、1、2…) | inst |
| init_fn | 初始化函数,设备启动时被内核调用一次,返回 0 表示成功 | myuart_init |
| pm_action_fn | 电源管理回调函数,不需要时传 NULL | NULL |
| data_ptr | 指向运行时数据(struct myuart_data),保存可变状态 | &myuart_data_##inst |
| config_ptr | 指向编译期配置(struct myuart_config),值来自 Devicetree | &myuart_config_##inst |
| level | 初始化优先级级别,POST_KERNEL 表示内核启动后初始化 | POST_KERNEL |
| priority | 同级别内的初始化顺序,数值越小越先执行 | CONFIG_SERIAL_INIT_PRIORITY |
| api_ptr | 指向 Driver API 结构体(如 uart_driver_api),应用通过它调用硬件 | &myuart_driver_api |
3.3 DT_INST_FOREACH_STATUS_OKAY 的展开逻辑
DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE) 是 Zephyr 多实例机制的「发动机」。它的展开逻辑可以拆成三步理解:
第一步:收集节点
Devicetree 中所有 compatible = "mycompany,myuart"
且 status = "okay" 的节点
↓
第二步:按顺序编号
uart0(serial@40000000)→ inst = 0
uart1(serial@40001000)→ inst = 1
↓
第三步:逐个展开宏
MYUART_DEFINE(0) → 生成 myuart_config_0、myuart_data_0、设备实例 0
MYUART_DEFINE(1) → 生成 myuart_config_1、myuart_data_1、设备实例 1
展开后的等价代码(假设有 uart0、uart1 两个节点):
/* —— DT_INST_FOREACH_STATUS_OKAY(MYUART_DEFINE) 展开后的等价代码 —— */
/* inst = 0:对应 uart0(serial@40000000) */
static const struct myuart_config myuart_config_0 = {
.base = 0x40000000, /* DT_INST_REG_ADDR(0) 展开结果 */
.irq = 32, /* DT_INST_IRQ(0) 展开结果 */
};
static struct myuart_data myuart_data_0;
DEVICE_DT_INST_DEFINE(0, myuart_init, NULL,
&myuart_data_0, &myuart_config_0,
POST_KERNEL, CONFIG_SERIAL_INIT_PRIORITY,
&myuart_driver_api);
/* inst = 1:对应 uart1(serial@40001000) */
static const struct myuart_config myuart_config_1 = {
.base = 0x40001000, /* DT_INST_REG_ADDR(1) 展开结果 */
.irq = 33, /* DT_INST_IRQ(1) 展开结果 */
};
static struct myuart_data myuart_data_1;
DEVICE_DT_INST_DEFINE(1, myuart_init, NULL,
&myuart_data_1, &myuart_config_1,
POST_KERNEL, CONFIG_SERIAL_INIT_PRIORITY,
&myuart_driver_api);
关键点:DT_INST_FOREACH_STATUS_OKAY 在编译期完成展开,DT_INST_REG_ADDR(0) 会被替换成字面量 0x40000000,DT_INST_IRQ(0) 替换成 32。所以最终生成的 config 结构体是编译期常量,不占用运行时解析开销——这正是 Zephyr「Devicetree 不是运行时数据库」这一设计理念的落地体现。
4. 这条链路如何串起来?
把上面三部分对照着看,你会发现整条链路是编译期完成的:
Binding YAML(定义属性 schema)
│
│ compatible = "mycompany,myuart" 匹配
▼
Devicetree 节点(描述硬件实例 uart0/uart1)
│
│ Devicetree Compiler 解析
▼
生成的头文件(generated devicetree information)
│
│ DT_INST_REG_ADDR() / DT_INST_IRQ() 等宏
▼
Driver 源码(读取编译期常量,操作硬件寄存器)
│
│ DEVICE_DT_INST_DEFINE() 注册设备
▼
Zephyr 设备模型(应用通过 UART API 访问)
5. 关键点回顾
- Binding YAML 定义"节点允许有哪些属性",是 schema;
- Devicetree 节点描述"具体硬件实例的值",是 data;
- Driver 通过 DT_INST_* 宏在编译期读取这些值,生成 config 结构体;
- DEVICE_DT_INST_DEFINE() 把 config、data、init、api 打包注册成 Zephyr 设备;
- DT_INST_FOREACH_STATUS_OKAY() 让一个 Driver 自动服务多个硬件实例,无需复制代码。
这就是 Zephyr SoC BSP 中"描述硬件"与"操作硬件"分离的核心设计。下一篇 08,我们就基于这套代码,把它真正编译运行起来,输出 hello world。
6. Kconfig:让 Driver 可以被配置和编译
文件路径:drivers/serial/Kconfig.mycompany
# Kconfig 的作用:定义本 Driver 的编译开关和依赖关系。
# 只有对应的 CONFIG_* 被打开时,这个 Driver 才会被编译进 Zephyr。
# 定义一个名为 MYCOMPANY_UART 的配置项。
# 它继承自 Zephyr 串行子系统提供的公共配置项 SERIAL。
# "select SERIAL" 表示:只要打开 MYCOMPANY_UART,就自动打开 SERIAL。
config MYCOMPANY_UART
bool "MyCompany UART driver"
depends on DT_HAS_MYCOMPANY_MYUART_ENABLED
select SERIAL
help
This option enables the MyCompany UART driver.
The driver is instantiated for each devicetree node
with compatible "mycompany,myuart".
7. CMakeLists.txt:把 Driver 源码加入构建
文件路径:drivers/serial/CMakeLists.txt
# CMakeLists.txt 的作用:告诉 Zephyr 构建系统在什么条件下编译这个 Driver 源文件。
# 它通过 zephyr_library_sources_ifdef 把源文件与 Kconfig 配置项绑定。
# 当 CONFIG_MYCOMPANY_UART 被打开时,编译 uart_mycompany.c。
# 注意:文件名必须与 Driver 源文件一致,且放在 drivers/serial/ 目录下。
zephyr_library_sources_ifdef(CONFIG_MYCOMPANY_UART uart_mycompany.c)
8. 每个配置项的作用说明
| config MYCOMPANY_UART | 定义一个布尔配置项,作为本 Driver 的总开关。 |
| bool | 表示该配置项只有 y(打开)或空(关闭)两种状态。 |
| depends on DT_HAS_MYCOMPANY_MYUART_ENABLED | 只有当 Devicetree 中存在 compatible = "mycompany,myuart" 且 status = "okay" 的节点时,该配置项才可见。这是 Zephyr 中 Driver 与 Devicetree 联动的重要机制。 |
| select SERIAL | 自动打开串行子系统的基础配置,确保 UART API 框架被编译。 |
| help | 配置项的说明文字,会在 menuconfig 中显示。 |
| zephyr_library_sources_ifdef(CONFIG_MYCOMPANY_UART uart_mycompany.c) | 当 CONFIG_MYCOMPANY_UART=y 时,把 uart_mycompany.c 加入编译。 |
9. 如何让读者直接编译运行
把上面所有文件放到正确位置后,在应用工程的 prj.conf 中打开配置:
# prj.conf:应用工程的配置文件
# 打开 UART Driver 的编译开关
CONFIG_MYCOMPANY_UART=y
然后在应用目录下执行:
west build -b mycompany_x1_board .
Zephyr 构建系统会自动完成以下动作:
Kconfig(CONFIG_MYCOMPANY_UART=y)
│
│ zephyr_library_sources_ifdef 判断
▼
CMakeLists.txt(编译 uart_mycompany.c)
│
│ DT_HAS_MYCOMPANY_MYUART_ENABLED 校验
▼
Devicetree 节点(uart0/uart1)
│
│ DEVICE_DT_INST_DEFINE() 注册
▼
Zephyr 设备模型(应用可通过 UART API 访问)
10. 完整文件清单
到这里,一个最小 UART Driver 所需的全部文件已经齐了:
dts/bindings/serial/mycompany,myuart.yaml # Binding YAML:定义属性 schema
dts/arm/mycompany/mycompany_x1.dtsi # Devicetree:描述硬件实例
drivers/serial/uart_mycompany.c # Driver 源码:实现硬件操作
drivers/serial/Kconfig.mycompany # Kconfig:定义编译开关
drivers/serial/CMakeLists.txt # CMake:把源码加入构建
把这五个文件放进 Zephyr 工程对应目录,再在 prj.conf 打开 CONFIG_MYCOMPANY_UART=y,就可以编译运行了。下一篇 08,我们就基于这套完整代码,把它真正编译运行起来,输出 hello world。
我建议你的 BSP 专栏接下来不要马上继续讲一堆 Driver API,而是做一个完整的最小实战:
08 — 第一个 SoC Driver:UART
直接做:
Company SoC
│
▼
UART hardware
│
├── register definition
├── Devicetree node
├── Binding YAML
├── Kconfig
├── CMake
└── UART Driver
│
▼
Zephyr UART API
│
▼
hello world
最终形成一条真正完整的:
┌──────────────┐
│ Application │
└──────┬───────┘
│
UART API
│
┌──────▼───────┐
│ UART Driver │
└──────┬───────┘
│
DT_INST_* macros
│
┌──────▼───────┐
│ Devicetree │
└──────┬───────┘
│
┌──────▼───────┐
│ Binding YAML │
└──────┬───────┘
│
┌──────▼───────┐
│ SoC UART │
└──────────────┘
这会比单独学习 DEVICE_DT_DEFINE()、DT_INST_REG_ADDR() 等宏更有价值。
网硕互联帮助中心


评论前必须登录!
注册