摘要:本文完整走通 Zephyr 从 Devicetree 的 compatible = "company,uart" 到真正可运行的 UART Driver 全链路。文章从整体架构出发,依次定义寄存器、Config、Data,实现 poll_out() 与 poll_in(),注册 uart_driver_api,并通过 DT_INST_REG_ADDR() 从 Devicetree 自动获取 base address,最终用 DEVICE_DT_INST_DEFINE() 生成设备实例。全文以 Company SoC 的 UART0(base = 0x40000000)为例,给出可直接落地的最小 Driver 骨架,并说明 Polling 模式为何暂不使用 interrupts 属性,帮助读者理解 Zephyr Driver Model 的核心闭环。
目录
- 一、这一篇我们最终要得到什么
- 二、先建立整体架构
- 三、第一步:定义 UART 寄存器
- 四、第二步:定义 Driver Config
- 五、第三步:定义 Driver Data
- 六、第四步:实现 poll_out()
- 七、为什么使用 sys_read32()?
- 八、第五步:实现 poll_in()
- 九、第六步:定义 Zephyr UART API
- 十、uart_poll_out() 到底发生了什么?
- 十一、第七步:Driver Init
- 十二、第八步:从 Devicetree 获取 base address
- 十三、真正重要的一点:DT_INST_*
- 十四、第九步:生成 Device Instance
- 十五、完整 Driver 骨架
- 十五点五、完整 Driver 文件结构说明
- 十六、它和第 27 篇 Binding 到底怎么连接?
- 十七、但是现在有一个"坑"
- 十八、Polling 与 Interrupt 模式对比
- 十九、如何从当前骨架扩展为中断驱动 UART
- 二十、完整的中断驱动 UART 代码示例
- 二十一、与 Polling 骨架的对比
- 二十二、如何验证 Driver 是否工作
Company UART Driver:从 compatible 到真正的 uart_poll_out()
这一篇正式把前面 SoC BSP + Devicetree + Binding 串起来,写出第一个真正可以工作的 Company UART Driver。
到第 27 篇为止,我们已经解决了:
Board
↓
Devicetree
↓
Binding
↓
SoC
↓
地址 / IRQ / clock / status
现在要解决最后一个关键问题: Zephyr 怎么把 compatible = “company,uart” 变成一个真正的 UART Driver,并最终让应用程序调用 uart_poll_out()?
一、这一篇我们最终要得到什么
假设 Company SoC 有一个 UART:
UART0
base address = 0x40000000
IRQ = 5
clock = 24 MHz
Devicetree:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5>;
status = "okay";
};
应用:
const struct device *uart = DEVICE_DT_GET(DT_NODELABEL(uart0));
uart_poll_out(uart, 'H');
uart_poll_out(uart, 'i');
uart_poll_out(uart, '\\n');
最终硬件行为:
uart_poll_out()
↓
struct device
↓
uart_api
↓
company_uart_poll_out()
↓
UART_REG_TXDATA
↓
0x40000000 + offset
↓
Company UART
↓
TX pin
↓
"Hi\\\\n"
这就是我们这一篇要完整走通的链路。
二、先建立整体架构
Company UART Driver 的位置:
Application
│
│ uart_poll_out()
▼
┌─────────────────────┐
│ Zephyr UART API │
│ uart_poll_out() │
└──────────┬──────────┘
│
▼
struct device
│
├── config
├── data
└── api
│
▼
┌────────────────────┐
│ Company UART Driver│
│ │
│ poll_out() │
│ poll_in() │
│ init() │
└─────────┬──────────┘
│
▼
UART registers
│
▼
Company SoC
注意: UART Driver 本身并不负责"定义 UART 是什么"。 UART 硬件已经由 Devicetree 描述:
compatible
reg
interrupts
clocks
status
Driver 负责:
如何操作这个硬件。
三、第一步:定义 UART 寄存器
假设 Company UART 的寄存器如下:
offset register
0x00 DATA
0x04 STATUS
0x08 CTRL
0x0C BAUD
STATUS:
bit 0 = TX_READY
bit 1 = RX_READY
那么:
# define COMPANY_UART_DATA 0x00
# define COMPANY_UART_STATUS 0x04
# define COMPANY_UART_CTRL 0x08
# define COMPANY_UART_BAUD 0x0C
# define COMPANY_UART_STATUS_TX_READY BIT(0)
# define COMPANY_UART_STATUS_RX_READY BIT(1)
这里的 offset 是:
UART base
+
register offset
例如:
0x40000000 + 0x04
就是:
UART_STATUS
四、第二步:定义 Driver Config
Zephyr Driver 通常会把硬件静态信息放进:
struct company_uart_config
例如:
struct company_uart_config {
uintptr_t base;
};
为什么不直接写:
# define UART_BASE 0x40000000
? 因为我们的 Driver 要支持:
UART0
UART1
UART2
例如:
UART0 → 0x40000000
UART1 → 0x40001000
UART2 → 0x40002000
所以:
struct company_uart_config {
uintptr_t base;
};
可以让每个 Device Instance 有自己的 base。
五、第三步:定义 Driver Data
先做最简单的 polling UART。 可以暂时:
struct company_uart_data {
};
甚至:
struct company_uart_data {
uint32_t baudrate;
};
例如:
struct company_uart_data {
uint32_t baudrate;
};
这里需要理解一个非常重要的原则:
Config
通常放: 编译时、硬件固定的信息 例如:
base address
IRQ
clock
pin configuration
Data
通常放: 运行时可能变化的信息 例如:
current baudrate
runtime state
buffers
locks
所以:
struct company_uart_config
│
├── base
└── irq
struct company_uart_data
│
├── baudrate
└── runtime state
六、第四步:实现 poll_out()
这是整个 Driver 最重要的第一个函数。
Zephyr UART API:
static void company_uart_poll_out(
const struct device *dev,
unsigned char c)
首先获取 config:
const struct company_uart_config \\*config =
dev->config;
然后:
uintptr_t base = config->base;
检查 TX ready:
while (!(sys_read32(base + COMPANY_UART_STATUS) &
COMPANY_UART_STATUS_TX_READY)) {
}
最后:
sys_write32(c, base + COMPANY_UART_DATA);
完整逻辑:
static void company_uart_poll_out(
const struct device *dev,
unsigned char c)
{
const struct company_uart_config *config = dev->config;
uintptr_t base = config->base;
while (!(sys_read32(base + COMPANY_UART_STATUS) &
COMPANY_UART_STATUS_TX_READY)) {
}
sys_write32(c, base + COMPANY_UART_DATA);
}
于是:
uart_poll_out(dev, 'A');
最终变成:
UART API
↓
company_uart_poll_out()
↓
STATUS
↓
等待 TX_READY
↓
DATA = 'A'
七、为什么使用 sys_read32()?
不要直接:
*(volatile uint32_t \\*)(base + offset)
虽然很多时候也能工作。 Zephyr Driver 通常使用:
sys_read32()
sys_write32()
或者某些情况下:
sys_set_bit()
sys_clear_bit()
原因是: Zephyr 提供了一层统一的 MMIO 访问抽象。 例如:
uint32_t status;
status = sys_read32(base + COMPANY_UART_STATUS);
写:
sys_write32(value, base + COMPANY_UART_DATA);
这样 Driver 更符合 Zephyr 的硬件访问模型。
八、第五步:实现 poll_in()
TX 是:
CPU → UART → TX pin
RX 则反过来:
RX pin → UART → CPU
实现:
static int company_uart_poll_in(
const struct device *dev,
unsigned char *c)
{
const struct company_uart_config *config =
dev->config;
uintptr_t base = config->base;
if (!(sys_read32(base + COMPANY_UART_STATUS) &
COMPANY_UART_STATUS_RX_READY)) {
return –1;
}
*c = sys_read32(base + COMPANY_UART_DATA);
return 0;
}
这里:
return 0;
表示:
成功收到一个字符
而:
return -1;
表示:
目前没有数据
实际 Driver 中通常会使用更合适的 errno,例如:
-EAGAIN
所以可以写成:
return –EAGAIN;
九、第六步:定义 Zephyr UART API
现在我们已经有:
company_uart_poll_out()
company_uart_poll_in()
但是 Zephyr 还不知道它们是 UART API。 所以定义:
static const struct uart_driver_api company_uart_api = {
.poll_in = company_uart_poll_in,
.poll_out = company_uart_poll_out,
};
这一步非常重要。 它建立:
Zephyr UART API
│
▼
company_uart_api
│
├── poll_in
│
└── poll_out
十、uart_poll_out() 到底发生了什么?
应用:
uart_poll_out(uart, 'A');
Zephyr API 内部最终类似:
dev->api->poll_out(dev, c);
所以:
uart_poll_out()
│
▼
dev->api
│
▼
company_uart_api
│
▼
poll_out
│
▼
company_uart_poll_out()
这就是 Zephyr Driver Model 最核心的一层:
struct device
│
└── api
│
└── function pointers
十一、第七步:Driver Init
接下来需要初始化 UART。
static int company_uart_init(const struct device *dev)
{
const struct company_uart_config *config =
dev->config;
uintptr_t base = config->base;
/* enable UART */
sys_write32(1, base + COMPANY_UART_CTRL);
return 0;
}
真实 SoC 通常会复杂得多:
UART init
│
├── enable clock
│
├── release reset
│
├── configure pinmux
│
├── configure baudrate
│
├── configure FIFO
│
└── enable UART
这也是为什么前面的 BSP 章节必须先学:
Clock
Reset
Pinmux
Interrupt Controller
Devicetree
UART Driver 只是把这些 SoC 基础设施真正组合起来。
十二、第八步:从 Devicetree 获取 base address
现在进入非常关键的一步。
Devicetree:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
status = "okay";
};
Driver 不应该再写:
# define UART0_BASE 0x40000000
而应该:
DT_INST_REG_ADDR(0)
例如:
#define COMPANY_UART_CONFIG(inst) \\
{ \\
.base = DT_INST_REG_ADDR(inst), \\
}
然后:
#define COMPANY_UART_CONFIG(inst) \\
{ \\
.base = DT_INST_REG_ADDR(inst), \\
}
最终:
DT_INST_REG_ADDR(0)
↓
0x40000000
↓
config_0.base
十三、真正重要的一点:DT_INST_*
如果只有:
uart0
可以使用:
DT_NODELABEL(uart0)
但是写通用 Driver 时,更推荐:
DT_INST_REG_ADDR(inst)
因为 Driver 需要支持:
instance 0
instance 1
instance 2
例如:
DT_INST(0, company_uart)
DT_INST(1, company_uart)
DT_INST(2, company_uart)
所以:
DT_INST_FOREACH_STATUS_OKAY(...)
就可以自动生成:
UART0
UART1
UART2
这和我们前面的多 Device Instance 章节完全连接起来了。
十四、第九步:生成 Device Instance
现在我们可以写:
#define COMPANY_UART_DEFINE(inst) \\
\\
static const struct company_uart_config \\
company_uart_config_##inst = { \\
.base = DT_INST_REG_ADDR(inst), \\
}; \\
\\
static struct company_uart_data \\
company_uart_data_##inst; \\
\\
DEVICE_DT_INST_DEFINE( \\
inst, \\
company_uart_init, \\
NULL, \\
&company_uart_data_##inst, \\
&company_uart_config_##inst, \\
PRE_KERNEL_1, \\
CONFIG_SERIAL_INIT_PRIORITY, \\
&company_uart_api \\
);
最后:
DT_INST_FOREACH_STATUS_OKAY(COMPANY_UART_DEFINE)
于是 Devicetree:
uart0
uart1
uart2
就会生成:
struct device uart0
struct device uart1
struct device uart2
十五、完整 Driver 骨架
现在把前面的东西放到一起。
例如:
drivers/serial/company_uart.c
核心结构:
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/sys_io.h>
#include <zephyr/dt-bindings/dt-util.h>
#define COMPANY_UART_DATA 0x00
#define COMPANY_UART_STATUS 0x04
#define COMPANY_UART_CTRL 0x08
#define COMPANY_UART_STATUS_TX_READY BIT(0)
#define COMPANY_UART_STATUS_RX_READY BIT(1)
struct company_uart_config {
uintptr_t base;
};
struct company_uart_data {
uint32_t baudrate;
};
然后:
static void company_uart_poll_out(
const struct device *dev,
unsigned char c)
{
const struct company_uart_config *config =
dev->config;
uintptr_t base = config->base;
while (!(sys_read32(base + COMPANY_UART_STATUS) &
COMPANY_UART_STATUS_TX_READY)) {
}
sys_write32(c, base + COMPANY_UART_DATA);
}
RX:
static int company_uart_poll_in(
const struct device *dev,
unsigned char *c)
{
const struct company_uart_config *config =
dev->config;
uintptr_t base = config->base;
if (!(sys_read32(base + COMPANY_UART_STATUS) &
COMPANY_UART_STATUS_RX_READY)) {
return –EAGAIN;
}
*c = sys_read32(base + COMPANY_UART_DATA);
return 0;
}
API:
static const struct uart_driver_api company_uart_api = {
.poll_in = company_uart_poll_in,
.poll_out = company_uart_poll_out,
};
Init:
static int company_uart_init(const struct device *dev)
{
const struct company_uart_config *config =
dev->config;
uintptr_t base = config->base;
sys_write32(1, base + COMPANY_UART_CTRL);
return 0;
}
Instance:
#define COMPANY_UART_DEFINE(inst) \\
\\
static const struct company_uart_config \\
company_uart_config_##inst = { \\
.base = DT_INST_REG_ADDR(inst), \\
}; \\
\\
static struct company_uart_data \\
company_uart_data_##inst; \\
\\
DEVICE_DT_INST_DEFINE( \\
inst, \\
company_uart_init, \\
NULL, \\
&company_uart_data_##inst, \\
&company_uart_config_##inst, \\
PRE_KERNEL_1, \\
CONFIG_SERIAL_INIT_PRIORITY, \\
&company_uart_api \\
);
DT_INST_FOREACH_STATUS_OKAY(COMPANY_UART_DEFINE)
十五点五、完整 Driver 文件结构说明
上面的骨架把代码按逻辑拆开讲解,但一个真实的 Driver 文件应该怎么组织?下面给出 drivers/serial/company_uart.c 的完整文件结构,从文件头部注释到函数实现顺序,再到配套的 Kconfig 与 CMakeLists.txt。
1. 文件头部注释
每个 Zephyr Driver 源文件都应该以 SPDX 许可证标识开头,并附上简要说明:
/*
* Copyright (c) 2026 Company
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Company SoC UART Driver
*
* Polling-mode UART driver for Company SoC.
* Supports UART0/UART1/UART2 via Devicetree instances.
*/
2. 包含的头文件列表
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/sys_io.h>
#include <zephyr/dt-bindings/dt-util.h>
#include <zephyr/kernel.h>
各头文件的作用:
| zephyr/device.h | 提供 struct device、DEVICE_DT_INST_DEFINE() 等设备模型核心定义 |
| zephyr/drivers/uart.h | 提供 uart_driver_api、uart_poll_out() 等 UART API 定义 |
| zephyr/sys/sys_io.h | 提供 sys_read32() / sys_write32() 等 MMIO 访问接口 |
| zephyr/dt-bindings/dt-util.h | 提供 DT_INST_REG_ADDR() 等 Devicetree 宏 |
| zephyr/kernel.h | 提供 BIT()、CONFIG_SERIAL_INIT_PRIORITY 等基础定义 |
3. 宏定义
#define COMPANY_UART_DATA 0x00
#define COMPANY_UART_STATUS 0x04
#define COMPANY_UART_CTRL 0x08
#define COMPANY_UART_BAUD 0x0C
#define COMPANY_UART_STATUS_TX_READY BIT(0)
#define COMPANY_UART_STATUS_RX_READY BIT(1)
宏定义放在头文件之后、结构体之前,集中管理寄存器偏移和状态位,避免魔法数字散落在函数中。
4. 结构体定义
struct company_uart_config {
uintptr_t base;
};
struct company_uart_data {
uint32_t baudrate;
};
config 放编译时固定的硬件信息(base address),data 放运行时可变的状态(baudrate)。
5. 函数实现顺序
一个规范的 Driver 文件,函数按以下顺序排列:
drivers/serial/company_uart.c
│
├── 1. 头文件
├── 2. 宏定义
├── 3. 结构体定义
├── 4. 静态函数实现
│ ├── company_uart_poll_out()
│ ├── company_uart_poll_in()
│ └── company_uart_init()
├── 5. uart_driver_api 实例
│ └── company_uart_api
├── 6. Device Instance 生成宏
│ └── COMPANY_UART_DEFINE(inst)
└── 7. 实例展开
└── DT_INST_FOREACH_STATUS_OKAY(...)
顺序原则:先定义、后使用。poll_out() / poll_in() 先实现,company_uart_api 才能引用它们;company_uart_api 先定义,DEVICE_DT_INST_DEFINE() 才能引用它。
6. 对应的 Kconfig 配置
在 drivers/serial/Kconfig.company 中:
config COMPANY_UART
bool "Company SoC UART driver"
depends on DT_HAS_COMPANY_UART_ENABLED
help
Enable the Company SoC UART driver.
This driver provides polling-mode UART support.
然后在 drivers/serial/Kconfig 中引入:
source "drivers/serial/Kconfig.company"
7. 对应的 CMakeLists.txt 配置
在 drivers/serial/CMakeLists.txt 中:
zephyr_library_sources_ifdef(CONFIG_COMPANY_UART company_uart.c)
这样,只有当 CONFIG_COMPANY_UART=y 时,company_uart.c 才会被编译进固件。
8. 完整文件结构一览
drivers/serial/
│
├── company_uart.c # Driver 实现
├── Kconfig.company # Driver 的 Kconfig 配置
└── CMakeLists.txt # 构建配置(追加一行)
这就是一个 Zephyr Driver 文件的完整组织方式:源文件负责实现,Kconfig 负责开关,CMakeLists.txt 负责编译,三者配合才能让 Driver 真正进入 Zephyr 构建系统。
这已经是一个非常重要的 Company SoC UART Driver 最小骨架。
十六、它和第 27 篇 Binding 到底怎么连接?
第 27 篇:
Binding
例如:
compatible: "company,uart"
properties:
reg:
required: true
interrupts:
required: true
Devicetree:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5>;
status = "okay";
};
Driver:
DT_INST_REG_ADDR(0)
于是:
"company,uart"
│
▼
Binding
│
▼
Devicetree node
│
▼
DT_INST(0, company_uart)
│
▼
DT_INST_REG_ADDR(0)
│
▼
0x40000000
│
▼
company_uart_config_0
│
▼
struct device
│
▼
uart_poll_out()
这就是完整闭环。
十七、但是现在有一个"坑"
上面的 Driver:
struct company_uart_config {
uintptr_t base;
};
只使用了:
reg
但是 Devicetree 还有:
interrupts = <5>;
目前完全没有使用。 为什么? 因为我们现在实现的是: Polling UART 而不是: Interrupt-driven UART 所以:
polling driver
CPU
│
├── 检查 TX_READY
├── 等待
├── 写 DATA
└── 返回
不需要 IRQ。
所以: Devicetree 描述"硬件在哪里、有什么资源";Driver 描述"如何操作这个硬件";struct device 把两者连接起来;Zephyr API 则让应用程序不需要知道 Company UART 的寄存器细节。
十八、Polling 与 Interrupt 模式对比
既然当前实现的是 Polling UART,而 Devicetree 中已经声明了 interrupts = <5>,那么很自然会产生一个问题:为什么不用中断? 要回答这个问题,先要理解 Polling 与 Interrupt 两种模式在本质上的差异。
| CPU 占用 | 高。CPU 必须持续轮询 STATUS 寄存器,等待 TX_READY / RX_READY,期间无法做其他事情 | 低。CPU 只在数据就绪时被中断唤醒,其余时间可以执行其他任务或进入低功耗 |
| 实时性 | 差。响应延迟取决于轮询频率,数据到达后可能无法被及时处理 | 好。硬件事件立即触发中断,CPU 可以第一时间响应 |
| 代码复杂度 | 低。只需 poll_out() / poll_in() 两个函数,无需注册 IRQ、编写 ISR | 高。需要注册 IRQ、编写 ISR、处理中断标志、管理环形缓冲区,还要考虑并发与锁 |
| 适用场景 | 调试、简单数据发送、低速率、对 CPU 占用不敏感的场景 | 高速率通信、长时间运行、需要低功耗或高实时性的产品级场景 |
用一句话概括:
Polling 用 CPU 时间换代码简单;Interrupt 用代码复杂度换 CPU 时间。
这也是为什么当前这一篇先实现 Polling 模式——它足够简单,能让我们把注意力集中在 Zephyr Driver Model 的核心闭环 上,而不被中断处理的细节干扰。
十九、如何从当前骨架扩展为中断驱动 UART
理解了两种模式的差异之后,后续文章将基于现有的最小骨架,逐步把它扩展为中断驱动 UART。扩展路径大致如下:
当前骨架(Polling)
│
├── 1. 在 config 中增加 irq 字段
│ .irq = DT_INST_IRQN(inst)
│
├── 2. 在 init() 中注册 ISR
│ IRQ_CONNECT(...)
│ irq_enable(...)
│
├── 3. 实现 ISR
│ company_uart_isr()
│ ├── 读取 STATUS
│ ├── 判断 RX_READY
│ ├── 读取 DATA
│ └── 写入环形缓冲区
│
├── 4. 在 data 中增加环形缓冲区
│ struct company_uart_data {
│ uint32_t baudrate;
│ uint8_t rx_buf[...];
│ ...
│ };
│
└── 5. 在 uart_driver_api 中补充
.irq_callback_set(...)
.irq_is_pending(...)
.irq_update(...)
其中最关键的一步,是把 Devicetree 中目前"闲置"的 interrupts = <5> 真正用起来:
#define COMPANY_UART_CONFIG(inst) \\
{ \\
.base = DT_INST_REG_ADDR(inst), \\
.irq = DT_INST_IRQN(inst), \\
}
这样,interrupts = <5> 就会通过 DT_INST_IRQN(0) 变成 config_0.irq = 5,再配合 IRQ_CONNECT() 与 irq_enable(),就能让 UART 在数据到达时主动通知 CPU,而不再需要 CPU 反复轮询。
到那时,Polling 与 Interrupt 两种模式会共存于同一个 Driver 中:poll_out() / poll_in() 保留用于调试和简单场景,中断路径则负责产品级的高速收发。这也正是 Zephyr 中许多真实 UART Driver 的最终形态。
二十、完整的中断驱动 UART 代码示例
上一节给出了扩展路径,这一节我们把它真正落地。下面是一个完整的中断驱动 UART Driver,它在前面的 Polling 骨架基础上,增加了 IRQ 注册、ISR 实现和环形缓冲区管理。
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/sys_io.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/dt-bindings/dt-util.h>
#include <zephyr/irq.h>
#define COMPANY_UART_DATA 0x00
#define COMPANY_UART_STATUS 0x04
#define COMPANY_UART_CTRL 0x08
#define COMPANY_UART_STATUS_TX_READY BIT(0)
#define COMPANY_UART_STATUS_RX_READY BIT(1)
#define COMPANY_UART_RX_BUF_SIZE 256
struct company_uart_config {
uintptr_t base;
uint32_t irq;
};
struct company_uart_data {
uint32_t baudrate;
uint8_t rx_buf[COMPANY_UART_RX_BUF_SIZE];
struct ring_buf rx_ring;
uart_irq_callback_user_data_t callback;
void *callback_data;
};
1. 在 init() 中注册 IRQ 并启用中断
static int company_uart_init(const struct device *dev)
{
const struct company_uart_config *config =
dev->config;
struct company_uart_data *data = dev->data;
uintptr_t base = config->base;
/* 初始化环形缓冲区 */
ring_buf_init(&data->rx_ring, sizeof(data->rx_buf),
data->rx_buf);
/* 使能 UART */
sys_write32(1, base + COMPANY_UART_CTRL);
/* 使能 UART 接收中断(假设 CTRL bit 1 = RX_IE) */
sys_write32(sys_read32(base + COMPANY_UART_CTRL) | BIT(1),
base + COMPANY_UART_CTRL);
/* 注册 ISR 并启用中断 */
IRQ_CONNECT(config->irq, 0, company_uart_isr,
dev, 0);
irq_enable(config->irq);
return 0;
}
2. 实现 ISR
static void company_uart_isr(const struct device *dev)
{
const struct company_uart_config *config =
dev->config;
struct company_uart_data *data = dev->data;
uintptr_t base = config->base;
/* 只要 RX 有数据就持续读取 */
while (sys_read32(base + COMPANY_UART_STATUS) &
COMPANY_UART_STATUS_RX_READY) {
uint8_t c = sys_read32(base + COMPANY_UART_DATA);
/* 写入环形缓冲区,满则丢弃 */
ring_buf_put(&data->rx_ring, &c, 1);
}
/* 通知上层应用有数据到达 */
if (data->callback) {
data->callback(dev, data->callback_data);
}
}
3. 在 uart_driver_api 中补充中断相关回调
static int company_uart_irq_update(const struct device *dev)
{
return 0;
}
static int company_uart_irq_is_pending(const struct device *dev)
{
return 0;
}
static int company_uart_irq_enable(const struct device *dev)
{
const struct company_uart_config *config =
dev->config;
irq_enable(config->irq);
return 0;
}
static int company_uart_irq_disable(const struct device *dev)
{
const struct company_uart_config *config =
dev->config;
irq_disable(config->irq);
return 0;
}
static int company_uart_irq_callback_set(
const struct device *dev,
uart_irq_callback_user_data_t callback,
void *user_data)
{
struct company_uart_data *data = dev->data;
data->callback = callback;
data->callback_data = user_data;
return 0;
}
static const struct uart_driver_api company_uart_api = {
.poll_in = company_uart_poll_in,
.poll_out = company_uart_poll_out,
.irq_callback_set = company_uart_irq_callback_set,
.irq_update = company_uart_irq_update,
.irq_is_pending = company_uart_irq_is_pending,
.irq_enable = company_uart_irq_enable,
.irq_disable = company_uart_irq_disable,
};
4. 在 config 中通过 DT_INST_IRQN() 获取 IRQ
#define COMPANY_UART_CONFIG(inst) \\
{ \\
.base = DT_INST_REG_ADDR(inst), \\
.irq = DT_INST_IRQN(inst), \\
}
5. 生成 Device Instance
#define COMPANY_UART_DEFINE(inst) \\
\\
static const struct company_uart_config \\
company_uart_config_##inst = \\
COMPANY_UART_CONFIG(inst); \\
\\
static struct company_uart_data \\
company_uart_data_##inst; \\
\\
DEVICE_DT_INST_DEFINE( \\
inst, \\
company_uart_init, \\
NULL, \\
&company_uart_data_##inst, \\
&company_uart_config_##inst, \\
PRE_KERNEL_1, \\
CONFIG_SERIAL_INIT_PRIORITY, \\
&company_uart_api \\
);
DT_INST_FOREACH_STATUS_OKAY(COMPANY_UART_DEFINE)
二十一、与 Polling 骨架的对比
把上面的中断驱动版本和第十五节的 Polling 骨架放在一起,差异一目了然:
| config 字段 | 只有 base | base + irq(通过 DT_INST_IRQN() 获取) |
| data 字段 | 只有 baudrate | baudrate + 环形缓冲区 + 回调指针 |
| init() | 只使能 UART | 初始化环形缓冲区 + 使能 UART + IRQ_CONNECT() + irq_enable() |
| 接收方式 | poll_in() 主动查询 STATUS | ISR 在数据到达时自动读取并写入环形缓冲区 |
| 数据暂存 | 无,读不到就返回 -EAGAIN | 环形缓冲区暂存,应用可稍后读取 |
| API 回调 | 只有 poll_in / poll_out | 增加 irq_callback_set / irq_update / irq_is_pending / irq_enable / irq_disable |
| CPU 占用 | 高,需持续轮询 | 低,仅在数据到达时被唤醒 |
| 代码量 | 约 100 行 | 约 200 行 |
核心差异可以概括为一句话:
Polling 骨架是"CPU 主动去问硬件有没有数据";中断驱动版本是"硬件主动告诉 CPU 有数据了"。
环形缓冲区在这里扮演了关键角色:ISR 运行在中断上下文,不能长时间阻塞,所以它把收到的字节快速写入环形缓冲区就立即返回;应用程序在自己的上下文里通过 uart_fifo_read() 或直接读取 data->rx_ring 来消费数据。这样 ISR 与应用之间通过缓冲区解耦,既不会丢数据,也不会阻塞中断处理。
这就是从 Polling 骨架走向中断驱动 UART 的完整落地过程。后续文章会进一步讨论 FIFO 深度、流控、DMA 以及多实例下的 ISR 参数传递等进阶话题。
二十二、如何验证 Driver 是否工作
写完 Driver 之后,最重要的一步就是验证它真的能工作。下面给出从配置、编写应用、到用串口工具观察输出的完整验证流程。
1. 在 prj.conf 中启用 CONFIG_COMPANY_UART=y
Driver 的 Kconfig 开关是 CONFIG_COMPANY_UART,只有把它设为 y,company_uart.c 才会被编译进固件。在应用的 prj.conf 中加上:
CONFIG_COMPANY_UART=y
CONFIG_SERIAL=y
其中 CONFIG_SERIAL 是 Zephyr 串口子系统的基础开关,CONFIG_COMPANY_UART 则是我们自定义 Driver 的开关。两者都打开后,drivers/serial/CMakeLists.txt 中的 zephyr_library_sources_ifdef(CONFIG_COMPANY_UART company_uart.c) 才会生效。
2. 编写一个简单的 main.c 调用 uart_poll_out() 发送字符串
在应用目录下创建 src/main.c,通过 DEVICE_DT_GET(DT_NODELABEL(uart0)) 拿到 UART0 设备,然后循环调用 uart_poll_out() 发送字符串:
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#define MSG "Hello from Company UART!\\r\\n"
int main(void)
{
const struct device *uart =
DEVICE_DT_GET(DT_NODELABEL(uart0));
if (!device_is_ready(uart)) {
printk("UART0 not ready\\n");
return –1;
}
while (1) {
for (const char *p = MSG; *p; p++) {
uart_poll_out(uart, *p);
}
k_sleep(K_MSEC(1000));
}
return 0;
}
这里用 DT_NODELABEL(uart0) 直接引用 Devicetree 中的 uart0 节点,device_is_ready() 会检查设备是否成功初始化。程序每秒通过 uart_poll_out() 发送一次 "Hello from Company UART!"。
3. 使用 minicom 或 screen 连接 UART0 观察输出
编译烧录后,把开发板的 UART0 TX/RX 引脚通过 USB 转串口模块连接到电脑,然后查看设备节点:
ls /dev/ttyUSB*
假设识别为 /dev/ttyUSB0,用 minicom 连接(波特率与 Driver 配置一致,这里以 115200 为例):
minicom -D /dev/ttyUSB0 -b 115200
或者用 screen:
screen /dev/ttyUSB0 115200
连接成功后,复位开发板,应该能在终端里周期性看到:
Hello from Company UART!
Hello from Company UART!
Hello from Company UART!
如果看不到输出,按以下顺序排查:
| 完全没有输出 | CONFIG_COMPANY_UART 未启用 | 检查 prj.conf 是否包含 CONFIG_COMPANY_UART=y |
| 完全没有输出 | 波特率不匹配 | 确认 minicom/screen 的波特率与 Driver 配置一致 |
| 完全没有输出 | TX/RX 接线错误 | 确认开发板 TX 接模块 RX、开发板 RX 接模块 TX、共地 |
| 输出乱码 | 波特率不匹配 | 重新设置 minicom/screen 的波特率 |
| 输出乱码 | 时钟配置错误 | 检查 SoC 的 UART 时钟是否按 24 MHz 正确配置 |
验证通过后,就说明从 compatible = "company,uart" 到 uart_poll_out() 的整条链路真正跑通了——Devicetree 描述了硬件,Binding 定义了属性,Driver 实现了操作,应用通过 Zephyr API 完成了发送。这也是后续扩展为中断驱动 UART 之前,最值得先做的一次端到端验证。
这就是 Company SoC BSP 从"能被 Zephyr 识别"真正走向"能运行外设"的关键一步。
二十三、总结与下篇预告
到这里,我们已经完整走通了从 Devicetree 的 compatible = "company,uart" 到应用程序真正调用 uart_poll_out() 的整条链路。回顾一下这个闭环的关键节点:
compatible = "company,uart"
│
▼
Binding(定义属性)
│
▼
Devicetree node(描述硬件资源)
│
▼
DT_INST_REG_ADDR(0)(获取 base address)
│
▼
company_uart_config(编译时静态信息)
│
▼
struct device(连接 config / data / api)
│
▼
uart_driver_api(注册 poll_out / poll_in)
│
▼
uart_poll_out()(应用程序调用)
本文要点回顾:
为什么先实现 Polling 模式?
因为 Polling 模式足够简单,能让我们把注意力集中在 Zephyr Driver Model 的核心闭环 上。它用 CPU 时间换代码简单,适合调试和低速率场景;但代价是 CPU 必须持续轮询 STATUS 寄存器,无法处理高速率通信或低功耗需求。
下篇预告:扩展为中断驱动 UART
下一篇将基于当前的最小骨架,把它扩展为完整的中断驱动 UART,具体内容包括:
到那时,Polling 与 Interrupt 两种模式会共存于同一个 Driver 中:poll_out() / poll_in() 保留用于调试和简单场景,中断路径则负责产品级的高速收发。这也正是 Zephyr 中许多真实 UART Driver 的最终形态。
从 compatible 到 uart_poll_out(),我们已经迈出了 Company SoC BSP 从"能被 Zephyr 识别"到"能运行外设"的关键一步;而中断驱动,则是让这个 Driver 真正走向产品级的下一个里程碑。
网硕互联帮助中心


评论前必须登录!
注册