## 一、为什么需要空闲中断 + DMA? 串口接收数据时,我们往往不知道一帧数据有多长。常见做法有: - **固定长度接收**:简单但灵活性差,无法应对变长帧。 - **逐字节中断**:每收一个字节就进一次中断,高频数据下CPU占用高,且容易丢失数据。 **空闲中断(IDLE)** 是STM32 USART的一个硬件事件:当接收完一个字节后,总线保持空闲(电平持续为高)超过一个字节时间,就会触发中断。利用它,可以精准判断一帧数据结束。 **DMA(直接内存访问)** 负责把串口接收数据自动搬运到内存缓冲区,不占用CPU。两者结合,可实现:**DMA持续接收,空闲中断标记帧结束**,CPU只在帧完整时处理一次数据,效率极高。 ## 二、工作原理 1. 开启USART的DMA接收请求,设置DMA为循环模式(Circular Mode)。 2. 数据到达时,DMA自动将字节存入缓冲区,并更新DMA计数器。 3. 当一帧数据结束后,串口检测到线空闲,触发IDLE中断。 4. 在IDLE中断中,读取DMA当前剩余计数(NDTR),即可算出本次接收到的数据长度:`len = buffer_size - NDTR`。 5. 主程序或回调函数中处理这帧数据,然后DMA继续循环接收下一帧。 关键点:DMA是循环模式,意味着它可以不间断地写入缓冲区,不会在接收完一帧后停止;IDLE中断只是告知“有完整帧可处理”。 ## 三、CubeMX 配置步骤 假设使用STM32F103系列,USART1,DMA通道。 ### 1. 配置串口参数 - 打开USART1,模式选择`Asynchronous`,波特率115200,8位数据,无校验,1停止位。 ### 2. 配置DMA - 在DMA Settings中添加`USART1_RX`,方向`PeripheralToMemory`,模式`Circular`,数据宽度均为`Byte`,优先级`High`。 - 注意:不需要配置TX DMA(若需要发送也可加)。 ### 3. 开启中断 - 在NVIC Settings中使能`USART1 global interrupt`(这同时会覆盖IDLE中断和错误中断)。 - 不使能DMA中断(循环模式无需DMA传输完成中断)。 ### 4. 生成代码 - 生成初始化代码,后续只需在`main.c`中补充逻辑。 ## 四、完整代码示例 ### 1. 定义缓冲区与接收标志 ```c #define RX_BUF_SIZE 256 uint8_t rx_buffer[RX_BUF_SIZE]; volatile uint8_t rx_frame_complete = 0; // 帧完成标志 volatile uint16_t rx_frame_len = 0; // 帧长度 ``` ### 2. 启动DMA接收(放在main初始化后) ```c // 启动DMA循环接收,数据会持续填充rx_buffer HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE); // 开启USART空闲中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); ``` ### 3. 空闲中断处理(在中断回调里) ```c void USART1_IRQHandler(void) { uint32_t isr = READ_REG(huart1.Instance->SR); // 检查空闲中断标志 if ((isr & UART_FLAG_IDLE) != 0) { // 清除IDLE标志(先读SR再读DR) __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 停止DMA传输,读取当前剩余计数 HAL_UART_DMAStop(&huart1); uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx); // 计算已接收字节数 rx_frame_len = RX_BUF_SIZE - remain; // 标记帧完成 rx_frame_complete = 1; // 重新启动DMA接收,准备下一帧 HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE); } HAL_UART_IRQHandler(&huart1); } ``` > **注意**:上面的代码中,`HAL_UART_DMAStop`会停止DMA,但也会关闭接收。重新调用`HAL_UART_Receive_DMA`可以再次开启。简单场景下也可不调用DMAStop,直接读取计数器再重启,但为了严谨,建议按上述方式。 ### 4. 主循环处理帧数据 ```c while (1) { if (rx_frame_complete) { rx_frame_complete = 0; // 现在rx_buffer中有效数据为前rx_frame_len个字节 // 例如打印十六进制数据 for (uint16_t i = 0; i < rx_frame_len; i++) { printf("%02X ", rx_buffer[i]); } printf("\r\n"); // 业务处理... } } ``` 这段代码的精髓在于:**DMA循环写入缓冲区,帧边界由IDLE事件标定**,CPU仅在帧结束时参与一次计算和业务处理。 ## 五、注意事项与常见坑 - **缓冲区大小**:必须大于最大可能帧长度,否则数据会覆盖。若帧可能超过缓冲区,可加大缓冲区或分段处理。 - **读取计数的时序**:在IDLE中断中,务必先清标志再读DMA计数器,避免再次触发中断导致计数错误。 - **DMAStop的副作用**:调用`HAL_UART_DMAStop`会同时清空DMA配置,因此必须重新调用`HAL_UART_Receive_DMA`。注意检查返回值,确保启动成功。 - **多字节接收竞态**:如果主循环读取缓冲区时,新数据又开始写入,可能导致数据交错。建议在读取前先关闭DMA(或使用双缓冲),本项目为演示简单场景,主循环处理及时则问题不大。 - **IDLE中断标志清除**:在STM32F1系列上,清除IDLE标志是通过“先读SR,再读DR”实现的。HAL库提供了`__HAL_UART_CLEAR_IDLEFLAG`宏,使用它更安全。 - **其他串口事件**:防止溢出错误(ORE)中断干扰,可在开启IDLE前使能`UART_IT_ERR`并用HAL_UART_ErrorCallback处理,或直接置位`huart1.RxState`恢复接收。 ## 六、扩展:双缓冲与多帧处理 若需要连续处理大量数据,可使用两个缓冲区交替使用(乒乓缓冲)。当IDLE中断时,切换DMA目标到另一缓冲区,同时将当前缓冲区数据交付处理,这样接收和处理完全并行,避免丢帧。实现方式为:在IDLE中断里,用`HAL_UART_DMAStop` + 修改`huart1.hdmarx->Instance->CMAR`指向另一缓冲区,再启动。有兴趣的读者可自行实现。 ## 结语 空闲中断+DMA方案是STM32串口接收不定长数据的经典高效解法。理解其原理后,你可以轻松应用于各种串口协议解析。记住核心要点:**DMA负责搬数据,IDLE负责划帧界**。配合CubeMX快速配置,代码清晰可维护,非常适合工程落地。