云计算百科
云计算领域专业知识百科平台

轻松学习Zephyr BSP: 07-SoC驱动与设备树绑定

摘要:承接第 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:

维度BindingDTS(Devicetree)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 = &lt;0x40000000 0x1000&gt;;
};

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() 等宏更有价值。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 轻松学习Zephyr BSP: 07-SoC驱动与设备树绑定
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!