# 引言 在STM32等MCU上,UART接收数据通常采用中断或DMA方式。对于不定长数据,DMA+环形缓冲区是兼顾实时性与效率的优选方案。然而,DMA的半满中断(HT)和全满中断(TC)触发时机与数据帧边界往往不对齐,导致数据被截断或粘包。本文将深入探讨如何利用半满中断与环形缓冲区特性,实现精准的帧边界识别与数据提取。 # DMA环形缓冲区基础 ## 原理概述 DMA环形缓冲区(Circular Buffer)是一种固定大小的内存区域,DMA控制器在传输数据时自动回绕,实现连续写入。配合半满中断(Half Transfer)和全满中断(Transfer Complete),CPU可在数据达到一半或写满时介入处理,避免数据覆盖。 - **半满中断**:当DMA传输计数达到缓冲区大小的一半时触发。 - **全满中断**:当DMA传输计数达到缓冲区大小时触发(在循环模式下,即写满一圈)。 ## 为什么需要半满中断? 对于不定长数据,若仅使用全满中断,则必须等待缓冲区写满才能处理,延迟大且可能丢失数据。半满中断允许CPU在缓冲区半满时提前处理,降低延迟,同时配合环形特性,可处理任意长度数据。 # 帧边界检测策略 ## 核心问题 DMA只负责搬运数据,不关心帧结构。当一帧数据跨越半满或全满边界时,若简单按中断点切割,会导致帧被拆分。因此,我们需要一种机制来识别帧的起始和结束。 ## 常用方法 - **空闲中断(IDLE)**:UART的IDLE中断在总线空闲时触发,可标记一帧结束。这是最常用的帧边界检测手段。 - **帧头帧尾匹配**:在数据流中查找特定字节(如0xAA, 0x55)作为帧头,或使用长度字段。 - **超时判断**:通过定时器,若超过一定时间未收到新数据,则认为帧结束。 本文重点结合半满中断与IDLE中断,实现精准帧边界。 # 实现方案 ## 硬件配置(以STM32F4为例) 1. **UART配置**:使能DMA接收,开启IDLE中断。 2. **DMA配置**:设置为循环模式(Circular),缓冲区大小设为2的幂次(如256字节),使能半满和全满中断。 3. **中断优先级**:DMA中断优先级高于UART IDLE中断,确保数据不丢失。 ## 软件设计 ### 数据结构 ```c #define BUF_SIZE 256 // 必须是2的幂 #define BUF_MASK (BUF_SIZE-1) uint8_t dma_buf[BUF_SIZE]; volatile uint16_t last_pos = 0; // 上次处理位置 volatile uint8_t frame_ready = 0; // 帧完成标志 ``` ### DMA中断处理 ```c void DMA1_Stream5_IRQHandler(void) { uint16_t cur_pos; if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_HT)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_HT); cur_pos = BUF_SIZE/2; // 半满位置 process_data(cur_pos); // 处理从last_pos到cur_pos的数据 last_pos = cur_pos; } if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TC)) { DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TC); cur_pos = 0; // 回绕到0 process_data(cur_pos); // 处理从last_pos到缓冲区末尾的数据 last_pos = cur_pos; } } ``` ### IDLE中断处理 ```c void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE)) { USART_ClearITPendingBit(USART1, USART_IT_IDLE); // 读取当前DMA剩余计数,计算实际数据位置 uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Stream5); uint16_t cur_pos = BUF_SIZE - remaining; // 处理从last_pos到cur_pos的数据(可能包含完整帧) process_data(cur_pos); last_pos = cur_pos; frame_ready = 1; // 通知主循环处理帧 } } ``` ### 数据处理函数 ```c void process_data(uint16_t end_pos) { // 从last_pos到end_pos的数据,可能包含多个帧或半帧 // 需要根据帧协议解析,这里以帧头0xAA 0x55为例 static uint8_t in_frame = 0; static uint8_t frame_len = 0; static uint8_t frame_data[256]; static uint16_t frame_idx = 0; uint16_t i = last_pos; while (i != end_pos) { uint8_t byte = dma_buf[i]; if (!in_frame) { // 查找帧头 if (byte == 0xAA) { in_frame = 1; frame_idx = 0; frame_data[frame_idx++] = byte; } } else { frame_data[frame_idx++] = byte; // 假设帧格式:AA 55 LEN DATA... CHECK if (frame_idx == 2) { frame_len = byte; // 长度字段 } else if (frame_idx == frame_len + 3) { // 帧头2字节+长度1字节+数据+校验 // 帧完整,处理帧 handle_frame(frame_data, frame_idx); in_frame = 0; } } i = (i + 1) & BUF_MASK; // 环形回绕 } } ``` ## 注意事项 - **缓冲区大小**:必须大于最大帧长,且为2的幂,便于取模运算。 - **临界区保护**:在中断中修改last_pos时,主循环读取需关中断,防止数据竞争。 - **半满与全满中断处理**:在循环模式下,全满中断意味着DMA计数器回绕到0,此时last_pos可能大于cur_pos,需注意环形处理。 - **IDLE中断时机**:IDLE中断在总线空闲时触发,但可能发生在DMA中断处理期间,需确保顺序正确。 - **性能优化**:若帧率较高,可将数据处理放在主循环,中断只置标志位。 # 完整示例代码 以下是一个简化的STM32F4工程核心代码,展示如何整合以上逻辑。 ```c // main.c int main(void) { // 初始化UART、DMA、中断等 while (1) { if (frame_ready) { frame_ready = 0; // 处理完整帧,此时last_pos已更新 // 可调用解析函数 } } } // 中断服务函数已在上面给出,此处省略 ``` # 总结 通过DMA环形缓冲区结合半满中断和IDLE中断,我们能够高效、精准地处理UART不定长接收。关键在于理解DMA中断的触发点,并利用环形缓冲区的回绕特性,配合帧协议解析,确保帧边界不丢失。此方案在STM32等主流MCU上广泛适用,可大幅提升通信可靠性。