一、问题背景与现象

在嵌入式开发中,使用STM32的DMA+串口空闲中断(IDLE)接收不定长数据是一种常见方案。但许多开发者发现,在高波特率或大数据量场景下,偶尔会出现数据丢包。典型现象:

  • 连续发送多帧数据时,部分帧丢失或数据错位。
  • 空闲中断触发后,读取到的数据长度小于实际发送长度。
  • 系统负载较高时,丢包概率显著增加。

二、丢包成因分析

2.1 空闲中断响应延迟

空闲中断在总线空闲后触发,但从中断发生到CPU响应存在延迟(尤其是高优先级中断抢占时)。若下一帧数据在延迟期间到达,DMA会继续写入原缓冲区,导致新旧数据混杂。

2.2 DMA缓冲区覆盖

HAL库的HAL_UARTEx_ReceiveToIdle_DMA函数在空闲中断回调中仅返回已接收长度,但DMA传输并未停止。若用户未及时处理数据,新数据会覆盖缓冲区头部,造成丢包。

2.3 HAL库内部处理缺陷

HAL库在空闲中断回调中会调用HAL_UART_IRQHandler,其中可能清除IDLE标志并重启DMA。若用户回调中耗时过长,可能错过下一次空闲事件。

2.4 单缓冲区限制

传统方案使用单缓冲区,在数据处理期间无法接收新数据。若数据处理时间大于帧间隔,必然丢包。

三、环形缓冲改造方案

3.1 设计思路

  • 使用两个缓冲区:DMA目标缓冲区(固定大小)和环形缓冲区(存储完整帧)。
  • DMA始终指向目标缓冲区,空闲中断触发后,将目标缓冲区数据拷贝至环形缓冲区,并立即重启DMA。
  • 应用层从环形缓冲区读取数据,实现接收与处理解耦。

3.2 环形缓冲区实现

#define RING_BUF_SIZE 1024

typedef struct {
    uint8_t buffer[RING_BUF_SIZE];
    volatile uint16_t head;
    volatile uint16_t tail;
} RingBuffer_t;

RingBuffer_t uart_ring;

void RingBuffer_Init(RingBuffer_t *rb) {
    rb->head = 0;
    rb->tail = 0;
}

uint16_t RingBuffer_Write(RingBuffer_t *rb, uint8_t *data, uint16_t len) {
    uint16_t i;
    for (i = 0; i < len; i++) {
        uint16_t next = (rb->head + 1) % RING_BUF_SIZE;
        if (next == rb->tail) break; // 缓冲区满
        rb->buffer[rb->head] = data[i];
        rb->head = next;
    }
    return i;
}

uint16_t RingBuffer_Read(RingBuffer_t *rb, uint8_t *data, uint16_t len) {
    uint16_t i;
    for (i = 0; i < len; i++) {
        if (rb->tail == rb->head) break; // 缓冲区空
        data[i] = rb->buffer[rb->tail];
        rb->tail = (rb->tail + 1) % RING_BUF_SIZE;
    }
    return i;
}

uint16_t RingBuffer_Available(RingBuffer_t *rb) {
    return (rb->head - rb->tail + RING_BUF_SIZE) % RING_BUF_SIZE;
}

3.3 DMA与空闲中断配置

使用HAL库的HAL_UARTEx_ReceiveToIdle_DMA启动接收,并重写空闲回调:

#define DMA_BUF_SIZE 256
uint8_t dma_buf[DMA_BUF_SIZE];

void UART_StartReceive(UART_HandleTypeDef *huart) {
    HAL_UARTEx_ReceiveToIdle_DMA(huart, dma_buf, DMA_BUF_SIZE);
    __HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT); // 关闭半传输中断
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
    if (huart->Instance == USART1) {
        // 将DMA缓冲区数据写入环形缓冲区
        RingBuffer_Write(&uart_ring, dma_buf, Size);
        // 重启DMA接收
        HAL_UARTEx_ReceiveToIdle_DMA(huart, dma_buf, DMA_BUF_SIZE);
        __HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT);
    }
}

3.4 应用层读取

在主循环中从环形缓冲区读取完整帧(需结合协议解析):

uint8_t frame[128];
while (1) {
    uint16_t len = RingBuffer_Available(&uart_ring);
    if (len > 0) {
        len = RingBuffer_Read(&uart_ring, frame, len);
        // 处理数据
    }
}

四、注意事项

  • 中断优先级:确保UART空闲中断和DMA中断优先级合理,避免被高优先级中断长时间阻塞。
  • 缓冲区大小:环形缓冲区应足够大,至少能容纳两帧最大数据,防止处理不及时导致覆盖。
  • 临界区保护:在读写环形缓冲区时,若存在中断并发,需使用__disable_irq()或原子操作保护head/tail指针。
  • DMA半传输中断:建议关闭半传输中断,仅使用空闲中断,减少中断频率。
  • 错误处理:在回调中检查DMA错误标志,必要时重新初始化DMA。
  • 内存对齐:DMA缓冲区建议使用__attribute__((aligned(4)))确保对齐,提高传输效率。

五、总结

通过环形缓冲区解耦DMA接收与数据处理,可有效解决HAL库DMA+空闲中断方案中的丢包问题。关键在于及时将DMA数据转移至环形缓冲区并重启DMA,同时保证应用层读取速度与数据产生速度匹配。该方案已在实际项目中验证,在115200bps下连续传输无丢包,适用于大多数嵌入式串口通信场景。