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

轻松学习Zephyr BSP: 28-Company UART 驱动

摘要:本文完整走通 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 两种模式在本质上的差异。

维度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 骨架放在一起,差异一目了然:

对比项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()(应用程序调用)

本文要点回顾:

  • Devicetree 描述"硬件在哪里":compatible、reg、interrupts、status 共同定义了 UART0 的资源与状态。
  • Binding 定义"属性怎么解析":让 DT_INST_REG_ADDR() 等宏能够从 Devicetree 节点中提取出 base address。
  • Driver 描述"如何操作硬件":company_uart_poll_out() 通过 sys_read32() / sys_write32() 访问寄存器,实现真正的数据收发。
  • struct device 是连接枢纽:它把 config(静态信息)、data(运行时状态)和 api(函数指针表)三者绑定在一起。
  • Zephyr API 屏蔽硬件差异:应用程序只需要 DEVICE_DT_GET(DT_NODELABEL(uart0)) 拿到设备,再调用 uart_poll_out() 即可,完全不需要关心 Company UART 的寄存器细节。
  • 为什么先实现 Polling 模式?

    因为 Polling 模式足够简单,能让我们把注意力集中在 Zephyr Driver Model 的核心闭环 上。它用 CPU 时间换代码简单,适合调试和低速率场景;但代价是 CPU 必须持续轮询 STATUS 寄存器,无法处理高速率通信或低功耗需求。

    下篇预告:扩展为中断驱动 UART

    下一篇将基于当前的最小骨架,把它扩展为完整的中断驱动 UART,具体内容包括:

  • 在 config 中增加 irq 字段:通过 DT_INST_IRQN(inst) 把 Devicetree 中"闲置"的 interrupts = <5> 真正用起来。
  • 在 init() 中注册 ISR:使用 IRQ_CONNECT() 绑定中断服务函数,再通过 irq_enable() 使能中断。
  • 实现 company_uart_isr():读取 STATUS、判断 RX_READY、读取 DATA,并写入环形缓冲区。
  • 引入环形缓冲区:在 data 中增加 struct ring_buf,让 ISR 与应用之间通过缓冲区解耦,既不丢数据也不阻塞中断处理。
  • 补充中断相关 API:在 uart_driver_api 中增加 irq_callback_set、irq_update、irq_is_pending、irq_enable、irq_disable 等回调。
  • 到那时,Polling 与 Interrupt 两种模式会共存于同一个 Driver 中:poll_out() / poll_in() 保留用于调试和简单场景,中断路径则负责产品级的高速收发。这也正是 Zephyr 中许多真实 UART Driver 的最终形态。

    从 compatible 到 uart_poll_out(),我们已经迈出了 Company SoC BSP 从"能被 Zephyr 识别"到"能运行外设"的关键一步;而中断驱动,则是让这个 Driver 真正走向产品级的下一个里程碑。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 轻松学习Zephyr BSP: 28-Company UART 驱动
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!