Zephyr UART 串口收发三种方式指南

本文通过三个完整的示例程序,演示 Zephyr 中 UART 数据收发的三种标准方式:轮询 (Polling)中断驱动 (Interrupt-driven)异步 (Asynchronous / DMA)。每个示例都包含发送和接收功能,并配有详细注释,帮助您快速理解和应用。

1. 方式对比

方式 核心 API CPU 占用 实时性 吞吐量 适用场景
轮询 uart_poll_out, uart_poll_in 高(忙等) 调试、简单命令、极少量数据
中断驱动 uart_irq_callback_user_data_set, uart_fifo_read 中(仅中断时) 通用推荐,如 GPS、4G 模块
异步 (DMA) uart_tx, uart_rx_enable, 事件回调 极低(DMA 搬运) 高速流数据(音频、文件)

2. 方式一:轮询 (Polling) ------ 最简单,适合调试

复制代码
/*
 * Copyright (c) 2025 Zephyr Project
 * SPDX-License-Identifier: Apache-2.0
 *
 * UART Polling Example
 * 发送:uart_poll_out 逐个字符发送
 * 接收:uart_poll_in 非阻塞读取(无数据时返回 -1)
 */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <string.h>

/* 使用 shell UART(通常为控制台串口) */
#define UART_DEVICE_NODE DT_CHOSEN(zephyr_shell_uart)

static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);

/* 发送字符串(轮询) */
void uart_poll_send(const char *str)
{
    if (!device_is_ready(uart_dev)) {
        printk("UART not ready\n");
        return;
    }
    for (int i = 0; str[i] != '\0'; i++) {
        uart_poll_out(uart_dev, str[i]);
    }
}

/* 接收一行数据(以换行结尾),轮询读取,超时返回 */
int uart_poll_receive(char *buf, int buf_len, int timeout_ms)
{
    int pos = 0;
    int64_t start = k_uptime_get();

    while (pos < buf_len - 1) {
        int c;
        if (uart_poll_in(uart_dev, &c) == 0) {
            if (c == '\r' || c == '\n') {
                break;          // 遇到换行结束
            }
            buf[pos++] = (char)c;
        }
        /* 超时检测 */
        if (k_uptime_get() - start > timeout_ms) {
            break;
        }
        k_yield();  // 避免完全占用 CPU
    }
    buf[pos] = '\0';
    return pos;
}

void main(void)
{
    char rx_buf[64];

    printk("UART Polling Example started\n");
    uart_poll_send("Type something and press Enter:\r\n");

    while (1) {
        int len = uart_poll_receive(rx_buf, sizeof(rx_buf), 5000);
        if (len > 0) {
            uart_poll_send("Echo: ");
            uart_poll_send(rx_buf);
            uart_poll_send("\r\n");
        }
        k_sleep(K_MSEC(100));
    }
}

关键点:

  • uart_poll_out 阻塞直到发送完成。

  • uart_poll_in 非阻塞,无数据立即返回 -1,需要轮询检查。

  • 适合简单交互,但 CPU 忙等效率低。

3. 方式二:中断驱动 (Interrupt-driven) ------ 最常用

复制代码
/*
 * Copyright (c) 2025 Zephyr Project
 * SPDX-License-Identifier: Apache-2.0
 *
 * UART Interrupt-driven Example
 * 接收:中断回调中读取 FIFO,放入消息队列
 * 发送:使用 uart_poll_out(也可改为中断发送)
 */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <string.h>

#define UART_DEVICE_NODE DT_CHOSEN(zephyr_shell_uart)
#define MSG_SIZE 64

static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);

/* 消息队列:用于从 ISR 向主线程传递接收到的行 */
K_MSGQ_DEFINE(uart_msgq, MSG_SIZE, 10, 4);

/* 接收缓冲区和当前字符位置 */
static char rx_buf[MSG_SIZE];
static int rx_pos;

/* UART 中断回调函数(在中断上下文执行) */
static void uart_irq_cb(const struct device *dev, void *user_data)
{
    uint8_t c;

    if (!uart_irq_update(dev)) {
        return;
    }

    /* 只处理接收就绪 */
    if (!uart_irq_rx_ready(dev)) {
        return;
    }

    /* 从 FIFO 中读取所有字符 */
    while (uart_fifo_read(dev, &c, 1) == 1) {
        if (c == '\n' || c == '\r') {
            /* 行结束,将缓冲区内容放入消息队列 */
            if (rx_pos > 0) {
                rx_buf[rx_pos] = '\0';
                k_msgq_put(&uart_msgq, &rx_buf, K_NO_WAIT);
                rx_pos = 0;
            }
        } else if (rx_pos < MSG_SIZE - 1) {
            rx_buf[rx_pos++] = (char)c;
        }
        /* 忽略超出缓冲区的字符 */
    }
}

/* 发送字符串(轮询方式,简单可靠) */
static void uart_send(const char *str)
{
    for (int i = 0; str[i]; i++) {
        uart_poll_out(uart_dev, str[i]);
    }
}

void main(void)
{
    char tx_buf[MSG_SIZE];

    if (!device_is_ready(uart_dev)) {
        printk("UART not ready\n");
        return;
    }

    /* 设置中断回调并启用接收中断 */
    int ret = uart_irq_callback_user_data_set(uart_dev, uart_irq_cb, NULL);
    if (ret < 0) {
        printk("Callback set failed: %d\n", ret);
        return;
    }
    uart_irq_rx_enable(uart_dev);

    uart_send("UART Interrupt Example\r\n");
    uart_send("Send a line, I will echo:\r\n");

    while (1) {
        /* 等待消息队列中有新行 */
        if (k_msgq_get(&uart_msgq, &tx_buf, K_FOREVER) == 0) {
            uart_send("Echo: ");
            uart_send(tx_buf);
            uart_send("\r\n");
        }
    }
}

关键点:

  • 中断回调 uart_irq_cb 中只做最轻量工作(读 FIFO、存队列),避免阻塞。

  • 主线程通过消息队列接收数据,处理逻辑与中断分离。

  • 发送仍用 uart_poll_out 简化,若需高性能可改为中断发送(参考异步示例)。

  • 这是 GPS/4G 模块等常见外设的首选方式。

特别说明,uart_fifo_fill() 是 Zephyr 中断驱动 UART 发送方式中的核心函数。下面详细解读它的作用、用法和注意事项。

uart_fifo_fill() 函数原型

复制代码
int uart_fifo_fill(const struct device *dev, const uint8_t *tx_data, int len);
  • 作用 :将数据写入 UART 硬件的 发送 FIFO 缓冲区

  • 返回值实际写入 FIFO 的字节数 (可能 0len 之间)。

  • 特点

    • 非阻塞:不会等待,立即返回。

    • 如果 FIFO 剩余空间小于 len,则只写入部分数据(返回值 < len)。

    • 通常需要配合 发送中断 来持续写入,否则可能发送不完。

典型使用模式(中断驱动发送)

模式 A:一次性填充(简单场景)

复制代码
int ret = uart_fifo_fill(uart, data, len);
if (ret < len) {
    // FIFO 满了,剩余数据需要等待中断再次填充
}
uart_irq_tx_enable(uart);  // 使能发送中断,当 FIFO 有空闲时会触发中断

在中断回调中:

复制代码
static void uart_isr(const struct device *dev, void *user_data)
{
    if (uart_irq_tx_ready(dev)) {
        // 继续填充剩余数据
        int ret = uart_fifo_fill(dev, send_buf + pos, remaining);
        pos += ret;
        if (pos >= total_len) {
            uart_irq_tx_disable(dev);  // 发送完成,关闭中断
        }
    }
}

4. 方式三:异步 (Asynchronous / DMA) ------ 高性能

复制代码
/*
 * Copyright (c) 2025 Zephyr Project
 * SPDX-License-Identifier: Apache-2.0
 *
 * UART Asynchronous (DMA) Example
 * 使用事件回调处理发送完成、接收就绪等。
 * 发送:uart_tx() 启动 DMA,完成后触发 UART_TX_DONE。
 * 接收:uart_rx_enable() 启动,数据到来时触发 UART_RX_RDY。
 */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/net_buf.h>
#include <string.h>

#define UART_DEVICE_NODE DT_CHOSEN(zephyr_shell_uart)

#define RX_BUF_LEN 64
#define TX_MAX_LEN 64

static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);

/* 接收双缓冲 */
static uint8_t rx_buf0[RX_BUF_LEN];
static uint8_t rx_buf1[RX_BUF_LEN];
static uint8_t active_rx_buf_index = 0;  // 0 或 1

/* 发送缓冲区(使用 net_buf 管理) */
NET_BUF_POOL_DEFINE(tx_pool, 4, TX_MAX_LEN, 0, NULL);
static struct net_buf *tx_pending = NULL;
static struct k_fifo tx_queue;   // 用于缓存待发送的数据包

/* UART 异步事件回调(在中断上下文) */
static void uart_async_cb(const struct device *dev, struct uart_event *evt, void *user_data)
{
    int rc;

    switch (evt->type) {
    case UART_TX_DONE:
        /* 发送完成,释放 buffer */
        if (tx_pending) {
            net_buf_unref(tx_pending);
            tx_pending = NULL;
        }
        /* 从队列中取出下一个待发送包 */
        tx_pending = k_fifo_get(&tx_queue, K_NO_WAIT);
        if (tx_pending) {
            rc = uart_tx(dev, tx_pending->data, tx_pending->len, 0);
            if (rc != 0) {
                net_buf_unref(tx_pending);
                tx_pending = NULL;
            }
        }
        break;

    case UART_RX_BUF_REQUEST:
        /* 接收缓冲区已满,请求新的缓冲区 */
        if (active_rx_buf_index == 0) {
            rc = uart_rx_buf_rsp(dev, rx_buf1, RX_BUF_LEN);
            active_rx_buf_index = 1;
        } else {
            rc = uart_rx_buf_rsp(dev, rx_buf0, RX_BUF_LEN);
            active_rx_buf_index = 0;
        }
        break;

    case UART_RX_RDY:
        /* 接收到数据,evt->data.rx 包含偏移和长度 */
        printk("RX: %.*s\n",
               evt->data.rx.len,
               (char *)(evt->data.rx.buf + evt->data.rx.offset));
        break;

    case UART_RX_BUF_RELEASED:
    case UART_RX_DISABLED:
        /* 可选处理 */
        break;

    default:
        break;
    }
}

/* 异步发送字符串(将字符串封装为 net_buf 并排队发送) */
void uart_async_send(const char *str)
{
    struct net_buf *buf = net_buf_alloc(&tx_pool, K_FOREVER);
    int len = strlen(str);
    if (len > net_buf_tailroom(buf)) {
        len = net_buf_tailroom(buf);
    }
    memcpy(buf->data, str, len);
    net_buf_add(buf, len);

    if (tx_pending == NULL) {
        /* 当前无发送任务,直接启动 */
        int rc = uart_tx(uart_dev, buf->data, buf->len, 0);
        if (rc == 0) {
            tx_pending = buf;
        } else {
            net_buf_unref(buf);
        }
    } else {
        /* 已有任务,放入队列等待 */
        k_fifo_put(&tx_queue, buf);
    }
}

void main(void)
{
    if (!device_is_ready(uart_dev)) {
        printk("UART not ready\n");
        return;
    }

    /* 注册异步回调 */
    uart_callback_set(uart_dev, uart_async_cb, NULL);

    /* 启动接收,提供第一个缓冲区 */
    active_rx_buf_index = 0;
    uart_rx_enable(uart_dev, rx_buf0, RX_BUF_LEN, 0);  // timeout=0 表示无限等待

    k_sleep(K_MSEC(100));
    uart_async_send("UART Async (DMA) Example\r\n");
    uart_async_send("Type something, I'll echo:\r\n");

    while (1) {
        /* 主线程可以执行其他任务,收发完全由 DMA 和中断处理 */
        k_sleep(K_SECONDS(1));
    }
}

关键点:

  • uart_callback_set 注册事件回调。

  • uart_tx 启动 DMA 发送,发送完成触发 UART_TX_DONE

  • uart_rx_enable 启动接收,硬件自动将数据存入提供的缓冲区,满时请求新缓冲区(UART_RX_BUF_REQUEST)。

  • 数据到达时触发 UART_RX_RDY,可在回调中处理。

  • 发送队列管理避免重叠发送(因为一次只能有一个 DMA 传输)。

5. 选择建议

  • 学习/调试:轮询方式最简单。

  • 常规外设(GPS、4G、传感器):中断驱动是标准选择,兼顾效率和复杂度。

  • 高速数据流(音频、大文件传输):异步 DMA 方式必不可少。

这三个示例均可直接复制到 Zephyr 工程中,修改 UART_DEVICE_NODE 以匹配您的硬件(如 DT_NODELABEL(uart1)),即可运行。希望这份指南能助您快速掌握 Zephyr UART 编程。