STM32 HAL库DMA+空闲中断接收不定长串口数据的环形缓冲区实现与溢出防护

一、为什么需要DMA+空闲中断+环形缓冲区

传统HAL_UART_Receive()是阻塞式接收,必须预先知道数据长度,且会长时间占用CPU。常见改进方案有三种:

  • 逐字节中断接收:每来一个字节进一次中断,高波特率下中断开销大,易丢数据。
  • DMA+固定长度:长度固定,无法处理不定长帧。
  • DMA+空闲中断:DMA自动搬运数据,总线空闲时触发IDLE中断,一次性取走整帧,CPU占用极低。

再叠加环形缓冲区,可把"接收"与"处理"解耦:中断只负责把数据写入环形区,主循环按需读取,天然适配生产者-消费者模型。

二、原理讲解

2.1 空闲中断(IDLE)

USART在检测到接收引脚持续一个字节时间的高电平(即总线空闲)后置位IDLE标志。此时DMA已把本帧数据搬入内存,我们在中断中读取NDTR寄存器得到剩余计数,即可算出本帧长度:

本帧长度 = 缓冲区总长 - __HAL_DMA_GET_COUNTER(hdma)

2.2 环形缓冲区

用head(写指针,仅中断写)和tail(读指针,仅主循环写)两个索引管理固定大小数组,通过取模实现回绕。单生产者单消费者场景下,无需加锁,只需保证读写指针的原子性(32位MCU上对uint16_t的读写是原子的)。

2.3 溢出防护要点

  • 写溢出:剩余空间不足时丢弃新数据或覆盖最旧数据,并置溢出标志。
  • DMA半满/全满中断:作为IDLE的补充,防止一帧数据超过DMA缓冲长度时丢包。
  • 错误恢复:ORE(溢出错误)、FE、NE等错误发生后需清除标志并重启DMA,否则接收会永久停止。

三、CubeMX配置步骤

  1. 使能USART1,模式设为Asynchronous,波特率115200。
  2. 在USART1的NVIC中勾选USART1 global interrupt。
  3. 在DMA Settings中添加USART1_RX,模式选Circular(循环模式),优先级Medium。
  4. 使能DMA对应通道中断(用于半满/全满回调)。
  5. 生成代码,确认MX_DMA_Init()在MX_USART1_UART_Init()之前调用。

四、完整代码示例

4.1 环形缓冲区定义

#define RB_SIZE 512

typedef struct {
    uint8_t  buf[RB_SIZE];
    volatile uint16_t head;   // 写指针,仅中断修改
    volatile uint16_t tail;   // 读指针,仅主循环修改
    volatile uint8_t  overflow;
} ringbuf_t;

static ringbuf_t s_rb;
static uint8_t   s_dma_buf[64];   // DMA 搬运缓冲

4.2 写入与读取接口

/* 中断上下文调用:写入 len 字节,返回实际写入数 */
uint16_t rb_write(ringbuf_t *rb, const uint8_t *data, uint16_t len)
{
    uint16_t free = RB_SIZE - ((rb->head - rb->tail + RB_SIZE) % RB_SIZE) - 1;
    if (len > free) {
        rb->overflow = 1;
        len = free;              // 丢弃超出部分,也可改为覆盖最旧数据
    }
    for (uint16_t i = 0; i < len; i++) {
        rb->buf[rb->head] = data[i];
        rb->head = (rb->head + 1) % RB_SIZE;
    }
    return len;
}

/* 主循环调用:读取最多 len 字节 */
uint16_t rb_read(ringbuf_t *rb, uint8_t *data, uint16_t len)
{
    uint16_t avail = (rb->head - rb->tail + RB_SIZE) % RB_SIZE;
    if (len > avail) len = avail;
    for (uint16_t i = 0; i < len; i++) {
        data[i] = rb->buf[rb->tail];
        rb->tail = (rb->tail + 1) % RB_SIZE;
    }
    return len;
}

4.3 启动接收与空闲中断处理

void uart_rx_start(void)
{
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    HAL_UART_Receive_DMA(&huart1, s_dma_buf, sizeof(s_dma_buf));
}

/* 在 stm32f1xx_it.c 的 USART1_IRQHandler 中调用 */
void uart_idle_handler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        uint16_t len = sizeof(s_dma_buf) - remain;
        if (len > 0) {
            rb_write(&s_rb, s_dma_buf, len);
        }
        /* 循环模式下 DMA 自动重装,无需重启;若为 Normal 模式需重新启动 */
    }
}

4.4 DMA半满/全满回调(防超长帧丢包)

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
        rb_write(&s_rb, s_dma_buf, sizeof(s_dma_buf) / 2);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
        rb_write(&s_rb, s_dma_buf + sizeof(s_dma_buf) / 2,
                 sizeof(s_dma_buf) / 2);
}

注意:半满/全满回调与IDLE处理存在数据重叠风险。工程上常用两种做法:一是只用IDLE,DMA缓冲足够大;二是只用半满/全满,按块处理。二者混用时需在IDLE中记录已消费偏移,避免重复写入。

4.5 错误恢复

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        __HAL_UART_CLEAR_OREFLAG(huart);
        __HAL_UART_CLEAR_FEFLAG(huart);
        __HAL_UART_CLEAR_NEFLAG(huart);
        HAL_UART_AbortReceive(huart);
        uart_rx_start();   // 重新挂载 DMA
    }
}

4.6 主循环消费

uint8_t frame[128];
while (1) {
    uint16_t n = rb_read(&s_rb, frame, sizeof(frame));
    if (n > 0) {
        protocol_parse(frame, n);   // 交给协议层处理
    }
    if (s_rb.overflow) {
        s_rb.overflow = 0;
        log_warn("uart rx overflow");
    }
}

五、注意事项

  • DMA模式选择:循环模式免去每帧重启DMA,但IDLE与半满/全满回调会重复搬运同一段数据,务必二选一或做偏移管理。
  • 指针原子性:head/tail用volatile修饰;若在Cortex-M0等不支持字节对齐原子访问的平台,需关中断保护。
  • 缓冲区大小:环形区应大于"最大帧长×最大待处理帧数",DMA缓冲建议不小于最大单帧长度。
  • IDLE标志清除顺序:先读SR再读DR,HAL宏已封装,勿手动乱序。
  • 错误标志必须清除:ORE不清会导致后续接收全部失效,这是现场最常见故障。
  • 调试观察:可用__HAL_DMA_GET_COUNTER在调试器中实时查看剩余计数,快速定位丢包位置。

六、小结

DMA负责"搬",空闲中断负责"切帧",环形缓冲区负责"解耦",三者组合是STM32串口不定长接收的经典高效方案。真正决定工程稳定性的,往往是溢出判断、错误恢复和回调重叠这些细节。把上述防护补齐,即可在115200乃至更高波特率下稳定接收不定长数据。