## 为什么需要 DMA + 空闲中断? 传统串口接收通常采用逐字节中断(RXNE),每收到一个字节就触发一次中断,CPU 频繁进出中断上下文,在高波特率或大数据量时极易导致丢包。而 DMA(直接内存访问)可以在无需 CPU 干预的情况下将串口接收数据搬运到内存缓冲区,配合**空闲中断(IDLE)**——当总线空闲超过一个字节时间时触发——即可实现“一次中断接收一帧完整数据”,极大降低 CPU 负载。 ## 核心原理:空闲中断与 DMA 的协同 - **空闲中断(IDLE)**:串口检测到 RX 线上持续空闲(即一个字节的传输时间)时,硬件置位 IDLE 标志。注意,它不同于“空闲总线”检测,而是基于字节间隙。 - **DMA 循环模式**:将 DMA 配置为循环模式,持续将串口 RX 数据写入内存环形缓冲区。当空闲中断发生时,我们只需计算当前 DMA 写入位置与上次处理位置的差值,即可得到本次帧长度。 - **关键寄存器**:`USART_ISR` 的 `IDLE` 位,`DMA_NDTR`(剩余传输计数)用于计算已接收字节数。 ## 基础配置步骤(以 STM32F1/F4 为例) ### 1. 串口与 DMA 时钟使能 ```c __HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); ``` ### 2. 配置串口参数(115200-8-N-1) ```c UART_HandleTypeDef huart2; huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; HAL_UART_Init(&huart2); ``` ### 3. 配置 DMA 接收(循环模式) ```c DMA_HandleTypeDef hdma_usart2_rx; hdma_usart2_rx.Instance = DMA1_Channel6; // 根据芯片映射 hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; // 循环模式关键 hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_usart2_rx); __HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx); ``` ### 4. 使能空闲中断并启动 DMA 接收 ```c __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 使能空闲中断 HAL_UART_Receive_DMA(&huart2, rx_buffer, RX_BUFFER_SIZE); // 启动 DMA ``` ### 5. 中断服务函数处理 ```c void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 必须先清除标志 uint16_t len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); // 此处 len 即为本次接收到的字节数(从上次处理位置到当前) process_frame(rx_buffer, len); // 用户处理函数 } } ``` ## 深度优化策略 ### 1. 处理环形缓冲区边界 当 DMA 写入位置跨越缓冲区末尾时,数据会被分成两段。优化方案:使用**双缓冲区交替**或**内存拷贝拼接**。推荐双缓冲: ```c #define BUF_SIZE 256 uint8_t rx_buf[2][BUF_SIZE]; volatile uint8_t active_buf = 0; // 在空闲中断中切换 active_buf,并重新配置 DMA 目标地址 ``` ### 2. 防止 DMA 溢出 当数据帧长度超过缓冲区大小时,DMA 会覆盖未处理数据。优化:在 DMA 传输完成中断(TC)中及时处理或扩大缓冲区,并设置帧长度上限检查。 ### 3. 低功耗与中断优先级 - 在空闲中断中尽快处理数据,避免长时间占用中断; - 将串口中断优先级设为最高(NVIC),确保实时性; - 若进入低功耗模式,需在唤醒后重新初始化 DMA 和空闲中断。 ### 4. 使用 HAL 库的扩展函数 HAL 库提供 `HAL_UARTEx_ReceiveToIdle_DMA()`,它自动处理空闲中断和回调,简化代码: ```c HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_buffer, RX_BUFFER_SIZE); // 回调函数:HAL_UARTEx_RxEventCallback void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { // Size 为本次接收长度 } ``` ## 完整代码示例(基于 HAL 库,STM32F4) ```c // 全局变量 #define RX_BUF_SIZE 512 uint8_t rx_buffer[RX_BUF_SIZE]; volatile uint16_t rx_len = 0; // 初始化函数 void UART_Init(void) { // 1. 使能时钟 __HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 2. GPIO 配置(PA2 TX, PA3 RX) GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF7_USART2; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 串口配置(略,见上文) // 4. DMA 配置(略,见上文) // 5. 使能空闲中断 __HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 6. 启动 DMA 接收 HAL_UART_Receive_DMA(&huart2, rx_buffer, RX_BUF_SIZE); // 7. 设置中断优先级 HAL_NVIC_SetPriority(USART2_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USART2_IRQn); } // 中断处理 void USART2_IRQHandler(void) { if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 计算接收长度 rx_len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); // 注意:若使用循环模式,需要记录上次位置,这里简化处理 // 实际应用中应使用环形索引 process_data(rx_buffer, rx_len); // 重新启动 DMA(循环模式无需重启,但若用普通模式需重新调用) } } // 数据处理示例 void process_data(uint8_t *data, uint16_t len) { // 例如:回显 HAL_UART_Transmit(&huart2, data, len, 1000); } ``` ## 注意事项 - **清除 IDLE 标志**:必须先读 `USART_ISR` 再写 `USART_ICR`,或直接调用 `__HAL_UART_CLEAR_IDLEFLAG()`,否则会反复进入中断。 - **DMA 计数器是递减的**:`__HAL_DMA_GET_COUNTER` 返回剩余未传输数量,因此已接收长度 = 缓冲区大小 - 计数器值。 - **环形缓冲区索引**:在循环模式下,每次接收后需要记录当前 DMA 位置(通过计数器计算),下次接收时用新位置减去旧位置,并处理回绕。 - **中断与主循环竞争**:若主循环也访问接收缓冲区,需加临界区保护或使用无锁环形队列。 - **调试技巧**:在中断中临时翻转 GPIO 或使用逻辑分析仪观察时序,确保帧间隔正确。 ## 总结 通过 DMA + 空闲中断,STM32 串口接收不定长数据变得高效且可靠。本文从原理到实现,再到优化,给出了完整方案。实际项目中,建议结合具体芯片型号(如 F1/F4/H7)查阅参考手册,并灵活运用 HAL 库的扩展 API。掌握这一技术,你将能轻松应对各种串口通信场景。