STM32H7 串口 DMA 空闲中断接收不定长数据的缓存对齐与丢包排查

1. 背景与问题现象

STM32H7 主频高达 480MHz,带 D-Cache,串口波特率常达 921600 甚至更高。使用 DMA + 空闲中断 接收不定长数据是高效方案,但很多开发者会遇到:

  • 数据偶尔丢失前几个字节
  • 接收长度正确但内容错乱
  • 高速连续发送时丢包严重

根本原因往往不是串口配置,而是 DMA 缓存对齐 与 Cache 一致性 问题。

2. 原理讲解:Cache 与 DMA 的冲突

2.1 D-Cache 的工作机制

Cortex-M7 的 D-Cache 按 Cache Line(32 字节) 操作。当 CPU 访问数据时,若未命中,会从内存加载整个 Cache Line 到缓存。

2.2 DMA 直接访问内存

DMA 绕过 Cache 直接读写物理内存。若 CPU 之前访问过该区域,Cache 中可能有旧数据;DMA 写入新数据后,CPU 读到的仍是 Cache 中的旧值。

2.3 空闲中断的陷阱

空闲中断触发后,CPU 读取 DMA 剩余计数器(NDTR)计算长度,然后处理缓冲区。若缓冲区未做 Cache 无效化(Invalidate),或缓冲区未按 32 字节对齐,就会读到脏数据或触发总线错误。

关键结论:

  • DMA 接收缓冲区必须 32 字节对齐
  • 读取数据前必须 Invalidate D-Cache
  • 缓冲区大小最好是 32 的整数倍

3. CubeMX 配置步骤

  1. 串口配置:USART1 异步模式,波特率 921600,开启 DMA 接收(Normal 模式)。
  2. DMA 配置:USART1_RX 通道,优先级 High,数据宽度 Byte。
  3. NVIC 配置:使能 USART1 全局中断,使能 DMA 通道中断(可选)。
  4. Cache 配置:默认开启 D-Cache 和 I-Cache。

4. 完整代码示例

4.1 定义对齐的缓冲区

#include "main.h"

#define RX_BUFFER_SIZE  256

/* 强制 32 字节对齐 */
aligned(32) uint8_t rx_dma_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t  rx_flag = 0;

4.2 启动 DMA 接收

void UART_StartReceive(void)
{
    HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

4.3 空闲中断处理

void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
    {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);

        /* 停止 DMA 以获取准确长度 */
        HAL_UART_DMAStop(&huart1);
        uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        rx_len = RX_BUFFER_SIZE - remain;

        /* 无效化 Cache,确保 CPU 读到 DMA 写入的最新数据 */
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_dma_buffer, RX_BUFFER_SIZE);

        rx_flag = 1;

        /* 重新启动接收 */
        HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);
    }
    HAL_UART_IRQHandler(&huart1);
}

4.4 主循环处理数据

while (1)
{
    if (rx_flag)
    {
        rx_flag = 0;
        /* 处理 rx_dma_buffer 中前 rx_len 字节 */
        ProcessData(rx_dma_buffer, rx_len);
    }
}

5. 丢包排查清单

  • 检查对齐:rx_dma_buffer 是否 32 字节对齐?用 __attribute__((aligned(32))) 或 aligned(32)。
  • 检查 Cache 操作:读取前是否调用 SCB_InvalidateDCache_by_Addr?注意长度需覆盖整个缓冲区,且地址对齐。
  • 检查 DMA 模式:使用 Normal 模式,每次空闲后重启;若用 Circular 模式,需处理 NDTR 回绕。
  • 检查中断优先级:空闲中断优先级不宜过高,避免打断 DMA 传输。
  • 检查缓冲区大小:建议为 32 的整数倍,且大于最大帧长。
  • 检查时钟与波特率:H7 串口时钟源需正确,避免波特率误差累积。
  • 使用示波器:观察 RX 引脚波形,确认数据是否完整到达。

6. 注意事项

  • SCB_InvalidateDCache_by_Addr 的地址和长度必须 32 字节对齐,否则触发 HardFault。
  • 若使用 HAL_UARTEx_ReceiveToIdle_DMA(新版 HAL),内部已处理空闲中断,但仍需手动 Invalidate Cache。
  • 避免在中断中处理大量数据,建议置标志位由主循环处理。
  • 若数据量极大,可考虑使用 MDMA 或 BDMA 减轻 CPU 负担。
  • 调试时关闭 D-Cache 可快速定位是否为 Cache 问题。

7. 总结

STM32H7 的 Cache 与 DMA 协同工作必须谨慎处理对齐和一致性。遵循 32 字节对齐 + 读取前 Invalidate 原则,可解决绝大多数丢包问题。结合空闲中断,即可实现高效、稳定的不定长数据接收。