4、UART接口规范:UART初始化、发送接收、DMA支持、流控制
UART 这个外设,说简单也简单,说复杂也复杂。简单是因为它就是个串行收发器,复杂在于——嗯,你一旦要把它放到 RTOS 里,还要考虑 DMA、流控、多任务并发,坑就来了。
我个人习惯把 UART 的 HAL 层分成四个维度:初始化配置、收发模型、DMA 集成、硬件流控。今天咱们一个一个拆开讲。
4.1 初始化接口设计
初始化是第一步,也是最容易出问题的一步。我在项目中遇到过好几次,芯片上电后 UART 没反应,查了半天发现是时钟没使能——这种低级错误,说白了就是初始化流程没规范好。
我建议的初始化接口长这样:
typedef struct {
uint32_t baudrate; // 波特率,如 115200
uint8_t data_bits; // 数据位:5/6/7/8
uint8_t stop_bits; // 停止位:1/2
uint8_t parity; // 校验:0=无, 1=奇, 2=偶
uint8_t flow_ctrl; // 流控:0=无, 1=RTS/CTS
uint8_t dma_tx; // TX DMA 通道号,0=禁用
uint8_t dma_rx; // RX DMA 通道号,0=禁用
uint32_t rx_timeout_ms; // 接收超时(毫秒)
} uart_config_t;
int uart_init(uart_port_t port, const uart_config_t *cfg);
这里有个细节:rx_timeout_ms 很多人会忽略。你想想看,如果接收端一直等数据,但对方发了一半停了,你的任务就卡死了。所以超时参数必须暴露给用户。
| 参数 | 典型值 | 说明 |
|---|---|---|
| baudrate | 115200 / 921600 | 根据总线负载选择,高速时注意信号完整性 |
| data_bits | 8 | 绝大多数场景用 8 位,7 位用于老协议 |
| stop_bits | 1 | 2 位停止位用于抗干扰差的链路 |
| parity | 0 | 工业总线常用偶校验,消费类多用无校验 |
4.2 发送与接收模型
收发接口的设计,我见过两种流派:阻塞式和回调式。我的建议是——都提供,但底层用同一套机制。
阻塞式接口适合简单任务:
int uart_send(uart_port_t port, const uint8_t *data, uint32_t len, uint32_t timeout_ms);
int uart_recv(uart_port_t port, uint8_t *buf, uint32_t max_len, uint32_t timeout_ms);
回调式接口适合高并发场景:
typedef void (*uart_tx_cb_t)(uart_port_t port, void *arg);
typedef void (*uart_rx_cb_t)(uart_port_t port, uint8_t *data, uint32_t len, void *arg);
int uart_send_async(uart_port_t port, const uint8_t *data, uint32_t len, uart_tx_cb_t cb, void *arg);
int uart_recv_async(uart_port_t port, uint8_t *buf, uint32_t max_len, uart_rx_cb_t cb, void *arg);
我曾经在一个项目里只提供了阻塞接口,结果三个任务同时发数据,互相阻塞,系统响应直接崩了。后来改成异步 + 内部队列,问题才解决。
4.3 DMA 支持
DMA 是 UART 高性能的关键。没有 DMA,CPU 就得一个字节一个字节地搬数据,尤其是在高波特率下(比如 921600),CPU 占用率能到 30% 以上。
DMA 的 HAL 层设计,我建议遵循以下流程:
// 1. 配置 DMA 通道(在 uart_init 中完成)
// 2. 启动 DMA 传输
int uart_dma_send(uart_port_t port, const uint8_t *data, uint32_t len);
int uart_dma_recv(uart_port_t port, uint8_t *buf, uint32_t max_len);
// 3. 注册 DMA 完成回调
int uart_dma_set_tx_done_cb(uart_port_t port, uart_tx_cb_t cb, void *arg);
int uart_dma_set_rx_done_cb(uart_port_t port, uart_rx_cb_t cb, void *arg);
这里有个容易踩的坑:DMA 缓冲区对齐。很多 MCU 的 DMA 控制器要求源地址和目的地址按 4 字节对齐。我之前用某款国产芯片,没注意这个,DMA 传输偶尔会丢数据,查了两天才发现是地址没对齐。
DMA 的环形缓冲区(Ring Buffer)也是常用技巧。接收端用 DMA 循环写入环形缓冲区,CPU 只在需要时读取,这样不会丢数据:
typedef struct {
uint8_t *pool; // 缓冲区池
uint32_t size; // 缓冲区大小(2的幂)
volatile uint32_t head; // DMA 写入位置
volatile uint32_t tail; // CPU 读取位置
} uart_ringbuf_t;
为什么 size 要是 2 的幂?因为可以用位运算代替取模,速度快很多。head & (size - 1) 比 head % size 快一个数量级。
4.4 硬件流控
硬件流控(RTS/CTS)在高速传输或长距离通信时几乎是必须的。没有流控,接收端缓冲区满了,发送端还在发,数据就丢了。
HAL 层的流控设计,我建议做到两点:
- 初始化时配置:通过
flow_ctrl字段使能 RTS/CTS - 运行时自动管理:RTS 引脚由 HAL 层根据 RX 缓冲区水位自动拉高/拉低
代码示例:
// 内部实现:当 RX 缓冲区剩余空间低于阈值时,拉高 RTS
static void uart_rts_manage(uart_port_t port) {
uart_ringbuf_t *rb = &uart_ctx[port].rx_ringbuf;
uint32_t free_space = rb->size - (rb->head - rb->tail);
if (free_space < UART_RTS_THRESHOLD) {
// 拉高 RTS,通知对方停止发送
GPIO_SetBits(uart_ctx[port].rts_pin);
} else {
// 拉低 RTS,通知对方可以发送
GPIO_ResetBits(uart_ctx[port].rts_pin);
}
}
阈值设多少合适?我个人习惯设为缓冲区大小的 25%。太小了容易频繁触发,太大了又失去流控意义。
4.5 整体架构图
下面这张图展示了 UART HAL 层的整体架构,从用户 API 到硬件寄存器的完整链路:
4.6 接口规范清单
最后,我把 UART HAL 层必须提供的接口整理成清单,方便你对照实现:
| 接口 | 功能 | 必选/可选 |
|---|---|---|
uart_init |
初始化 UART 外设,配置参数 | 必选 |
uart_deinit |
反初始化,释放资源 | 必选 |
uart_send |
阻塞发送 | 必选 |
uart_recv |
阻塞接收 | 必选 |
uart_send_async |
异步发送(回调) | 推荐 |
uart_recv_async |
异步接收(回调) | 推荐 |
uart_dma_send |
DMA 发送 | 可选 |
uart_dma_recv |
DMA 接收 | 可选 |
uart_set_flow_ctrl |
运行时配置流控 | 可选 |
嗯,UART 这块就讲到这里。接口设计没有标准答案,但有了这套规范,你在不同芯片之间移植时,至少能保证上层应用代码一行都不用改。这就是 HAL 层的价值所在。