轻松学习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 的核心闭环。

目录

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 deviceDEVICE_DT_INST_DEFINE() 等设备模型核心定义
zephyr/drivers/uart.h提供 uart_driver_apiuart_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 字段只有 basebase + irq(通过 DT_INST_IRQN() 获取)
data 字段只有 baudratebaudrate + 环形缓冲区 + 回调指针
init()只使能 UART初始化环形缓冲区 + 使能 UART + IRQ_CONNECT() + irq_enable()
接收方式poll_in() 主动查询 STATUSISR 在数据到达时自动读取并写入环形缓冲区
数据暂存无,读不到就返回 -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,只有把它设为 ycompany_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()(应用程序调用)

本文要点回顾:

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

为什么先实现 Polling 模式?

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

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

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

  1. 在 config 中增加 irq 字段:通过 DT_INST_IRQN(inst) 把 Devicetree 中"闲置"的 interrupts = <5> 真正用起来。
  2. 在 init() 中注册 ISR:使用 IRQ_CONNECT() 绑定中断服务函数,再通过 irq_enable() 使能中断。
  3. 实现 company_uart_isr():读取 STATUS、判断 RX_READY、读取 DATA,并写入环形缓冲区。
  4. 引入环形缓冲区:在 data 中增加 struct ring_buf,让 ISR 与应用之间通过缓冲区解耦,既不丢数据也不阻塞中断处理。
  5. 补充中断相关 API:在 uart_driver_api 中增加 irq_callback_setirq_updateirq_is_pendingirq_enableirq_disable 等回调。

到那时,Polling 与 Interrupt 两种模式会共存于同一个 Driver 中poll_out() / poll_in() 保留用于调试和简单场景,中断路径则负责产品级的高速收发。这也正是 Zephyr 中许多真实 UART Driver 的最终形态。

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值