STM32 串口 DMA 收发环形缓冲区:高性能串口驱动设计
👁 1 阅读 · 2026-08-17 · STM32 串口
在嵌入式系统中,串口是调试与通信的核心接口,但传统中断收发在高波特率或大数据量下会频繁占用 CPU,导致实时性下降。本文深入讲解基于 STM32 的串口 DMA 收发机制,结合环形缓冲区(Ring Buffer)设计,实现零中断开销的连续数据流处理。文章涵盖 DMA 原理、环形缓冲区实现、双缓冲策略、空闲中断检测,并提供完整可移植的代码示例,帮助开发者构建高性能、低延迟的串口驱动。
## 引言
STM32 的串口(USART/UART)在嵌入式项目中无处不在,但很多开发者仍采用逐字节中断收发,导致 CPU 在每次收发时都被打断,尤其在 921600 甚至更高波特率下,中断频率可达 90kHz,系统实时性严重受损。DMA(直接内存访问)可以解放 CPU,而环形缓冲区则解决数据连续性与边界问题。本文将带你设计一套基于 DMA + 环形缓冲区的串口驱动,实现高效、可靠的数据收发。
## 一、DMA 收发原理
### 1.1 DMA 工作模式
STM32 的 DMA 支持两种模式:
- **Normal 模式**:传输完成后停止,需重新配置。
- **Circular 模式**:传输完成后自动回绕到起始地址,持续工作,适合流式数据。
对于串口接收,我们使用 **Circular 模式**,DMA 将外设数据寄存器(DR)的数据源源不断写入内存缓冲区,无需 CPU 干预。发送则使用 **Normal 模式**,每次发送指定长度后停止,由 CPU 或中断触发下一次发送。
### 1.2 空闲中断(IDLE)
串口空闲中断在接收线上检测到空闲(即一帧数据结束)时触发,配合 DMA 可判断当前接收到的数据长度。这是实现不定长数据接收的关键。
## 二、环形缓冲区设计
环形缓冲区(Ring Buffer)是一种先进先出(FIFO)的数据结构,通过读写指针实现循环存储。在 DMA 接收中,缓冲区被 DMA 硬件写入,而应用程序通过读指针读取,两者互不干扰。
### 2.1 数据结构
```c
#define UART_RX_BUF_SIZE 256 // 接收缓冲区大小(需为2的幂)
#define UART_TX_BUF_SIZE 256 // 发送缓冲区大小
typedef struct {
uint8_t rx_buf[UART_RX_BUF_SIZE]; // DMA 接收缓冲区
volatile uint16_t rx_read_index; // 应用读指针
uint16_t rx_last_index; // 上次处理时的 DMA 写指针
uint8_t tx_buf[UART_TX_BUF_SIZE]; // 软件发送缓冲区
volatile uint16_t tx_write_index; // 应用写指针
volatile uint16_t tx_read_index; // DMA 发送读指针
volatile uint8_t tx_busy; // 发送忙标志
} UART_Handle;
```
### 2.2 接收逻辑
DMA 在 Circular 模式下持续将数据写入 `rx_buf`,硬件写指针可通过 `__HAL_DMA_GET_COUNTER()` 获取剩余计数,从而算出当前写位置。我们利用空闲中断来更新 `rx_last_index`,并计算新数据长度。
```c
void UART_RxIdleCallback(UART_Handle *huart) {
uint16_t current_index = UART_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
uint16_t len;
if (current_index >= huart->rx_last_index) {
len = current_index - huart->rx_last_index;
} else {
len = UART_RX_BUF_SIZE - huart->rx_last_index + current_index; // 回绕
}
// 将新数据拷贝到应用层或直接处理
ProcessReceivedData(&huart->rx_buf[huart->rx_last_index], len);
huart->rx_last_index = current_index;
}
```
注意:`rx_last_index` 必须由中断更新,而 `rx_read_index` 用于应用层读取,两者分离避免竞争。
### 2.3 发送逻辑
发送采用软件缓冲区 + DMA Normal 模式。应用层将数据写入 `tx_buf`,然后启动 DMA 发送。发送完成中断中更新读指针,并检查是否有新数据待发。
```c
void UART_SendData(UART_Handle *huart, uint8_t *data, uint16_t len) {
// 将数据拷贝到 tx_buf(需处理回绕)
for (uint16_t i = 0; i < len; i++) {
huart->tx_buf[huart->tx_write_index++] = data[i];
huart->tx_write_index &= (UART_TX_BUF_SIZE - 1); // 取模
}
if (!huart->tx_busy) {
UART_StartTxDMA(huart);
}
}
void UART_StartTxDMA(UART_Handle *huart) {
uint16_t len;
if (huart->tx_write_index >= huart->tx_read_index) {
len = huart->tx_write_index - huart->tx_read_index;
} else {
len = UART_TX_BUF_SIZE - huart->tx_read_index; // 先发送到缓冲区尾
}
if (len > 0) {
huart->tx_busy = 1;
HAL_UART_Transmit_DMA(&huart->huart, &huart->tx_buf[huart->tx_read_index], len);
}
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
UART_Handle *my_uart = GetMyUart(huart); // 关联自定义结构
my_uart->tx_read_index += my_uart->last_tx_len;
my_uart->tx_read_index &= (UART_TX_BUF_SIZE - 1);
if (my_uart->tx_write_index != my_uart->tx_read_index) {
UART_StartTxDMA(my_uart); // 继续发送剩余数据
} else {
my_uart->tx_busy = 0;
}
}
```
注意:`last_tx_len` 需在启动 DMA 前保存,因为 DMA 完成后无法获取传输长度。
## 三、配置步骤(以 STM32CubeMX + HAL 库为例)
1. **CubeMX 配置**:
- 使能 USARTx,设置波特率、数据位等。
- 添加 DMA 请求:USARTx_RX 选择 Circular 模式,USARTx_TX 选择 Normal 模式。
- 使能 USARTx 全局中断和 DMA 中断。
2. **初始化代码**:
- 在 `main()` 中调用 `MX_USARTx_UART_Init()` 和 `MX_DMA_Init()`。
- 启动 DMA 接收:`HAL_UART_Receive_DMA(&huart, rx_buf, UART_RX_BUF_SIZE)`。
- 使能空闲中断:`__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE)`。
3. **中断处理**:
- 在 `USARTx_IRQHandler` 中调用 `HAL_UART_IRQHandler()`,然后检测空闲中断标志。
```c
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
UART_RxIdleCallback(&my_uart);
}
}
```
## 四、完整代码示例
以下是一个简化的完整驱动实现(基于 STM32F4 HAL 库):
```c
// uart_ring.h
#ifndef UART_RING_H
#define UART_RING_H
#include "stm32f4xx_hal.h"
#define RX_BUF_SIZE 256
#define TX_BUF_SIZE 256
typedef struct {
UART_HandleTypeDef *huart;
DMA_HandleTypeDef *hdma_rx;
DMA_HandleTypeDef *hdma_tx;
uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t rx_last_index;
uint8_t tx_buf[TX_BUF_SIZE];
volatile uint16_t tx_write_index;
volatile uint16_t tx_read_index;
volatile uint8_t tx_busy;
uint16_t last_tx_len;
} UART_Ring;
void UART_Ring_Init(UART_Ring *ring, UART_HandleTypeDef *huart);
void UART_Ring_StartRx(UART_Ring *ring);
void UART_Ring_Send(UART_Ring *ring, uint8_t *data, uint16_t len);
void UART_Ring_HandleIdle(UART_Ring *ring);
void UART_Ring_TxCplt(UART_Ring *ring);
#endif
```
```c
// uart_ring.c
#include "uart_ring.h"
void UART_Ring_Init(UART_Ring *ring, UART_HandleTypeDef *huart) {
ring->huart = huart;
ring->hdma_rx = huart->hdmarx;
ring->hdma_tx = huart->hdmatx;
ring->rx_last_index = 0;
ring->tx_write_index = 0;
ring->tx_read_index = 0;
ring->tx_busy = 0;
}
void UART_Ring_StartRx(UART_Ring *ring) {
HAL_UART_Receive_DMA(ring->huart, ring->rx_buf, RX_BUF_SIZE);
__HAL_UART_ENABLE_IT(ring->huart, UART_IT_IDLE);
}
void UART_Ring_HandleIdle(UART_Ring *ring) {
uint16_t current = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(ring->hdma_rx);
uint16_t len;
if (current >= ring->rx_last_index) {
len = current - ring->rx_last_index;
} else {
len = RX_BUF_SIZE - ring->rx_last_index + current;
}
// 此处将数据交给应用层,例如通过回调函数
if (len > 0) {
// 示例:直接回显
UART_Ring_Send(ring, &ring->rx_buf[ring->rx_last_index], len);
}
ring->rx_last_index = current;
}
void UART_Ring_Send(UART_Ring *ring, uint8_t *data, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
ring->tx_buf[ring->tx_write_index++] = data[i];
ring->tx_write_index &= (TX_BUF_SIZE - 1);
}
if (!ring->tx_busy) {
uint16_t send_len;
if (ring->tx_write_index >= ring->tx_read_index) {
send_len = ring->tx_write_index - ring->tx_read_index;
} else {
send_len = TX_BUF_SIZE - ring->tx_read_index;
}
if (send_len > 0) {
ring->tx_busy = 1;
ring->last_tx_len = send_len;
HAL_UART_Transmit_DMA(ring->huart, &ring->tx_buf[ring->tx_read_index], send_len);
}
}
}
void UART_Ring_TxCplt(UART_Ring *ring) {
ring->tx_read_index += ring->last_tx_len;
ring->tx_read_index &= (TX_BUF_SIZE - 1);
if (ring->tx_write_index != ring->tx_read_index) {
uint16_t send_len;
if (ring->tx_write_index >= ring->tx_read_index) {
send_len = ring->tx_write_index - ring->tx_read_index;
} else {
send_len = TX_BUF_SIZE - ring->tx_read_index;
}
ring->last_tx_len = send_len;
HAL_UART_Transmit_DMA(ring->huart, &ring->tx_buf[ring->tx_read_index], send_len);
} else {
ring->tx_busy = 0;
}
}
// 中断回调示例(在 stm32f4xx_it.c 中)
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
UART_Ring_HandleIdle(&my_ring);
}
}
void DMA1_Stream4_IRQHandler(void) { // 根据实际 DMA 通道
HAL_DMA_IRQHandler(&hdma_usart1_tx);
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
UART_Ring_TxCplt(&my_ring);
}
}
```
## 五、注意事项
- **缓冲区大小**:必须为 2 的幂,以便用位与运算取模,提高效率。
- **内存对齐**:DMA 缓冲区建议按 4 字节对齐,避免某些 DMA 控制器对齐要求。
- **竞争条件**:`rx_last_index` 在中断中更新,应用层读取时需关中断或使用临界区保护。
- **溢出处理**:如果 DMA 写指针追上读指针,数据会覆盖,需在设计中考虑溢出标志或增大缓冲区。
- **空闲中断清除**:在 STM32F4 中,清除空闲中断需先读 SR 再读 DR,或使用 `__HAL_UART_CLEAR_IDLEFLAG` 宏。
- **发送 DMA 的 Normal 模式**:每次传输后需重新配置,因此发送完成回调中必须处理剩余数据。
- **调试建议**:先用回环测试验证收发,再接入实际外设。
## 六、总结
通过 DMA 的 Circular 模式接收和 Normal 模式发送,配合环形缓冲区,我们实现了几乎零 CPU 干预的串口通信。空闲中断解决了不定长数据的边界问题,而软件发送缓冲区则平滑了数据发送的突发性。这套驱动架构在工业控制、物联网网关等场景中表现出色,可轻松应对高波特率下的持续数据流。希望本文能帮助你构建更高效的串口驱动,提升系统整体性能。