## 引言 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 干预的串口通信。空闲中断解决了不定长数据的边界问题,而软件发送缓冲区则平滑了数据发送的突发性。这套驱动架构在工业控制、物联网网关等场景中表现出色,可轻松应对高波特率下的持续数据流。希望本文能帮助你构建更高效的串口驱动,提升系统整体性能。