本文通过三个完整的示例程序,演示 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 的字节数 (可能
0到len之间)。 -
特点:
-
非阻塞:不会等待,立即返回。
-
如果 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 编程。