一、问题现象:偶发数据错乱

某工业数据采集项目使用STM32H743,通过DMA将ADC采样数据搬运到内存缓冲区,CPU再读取缓冲区进行滤波和打包。系统运行几分钟到几小时后,偶尔出现采样值跳变、数据包错位,重启后恢复。

  • 错误发生无规律,与温度、电压无关
  • 使用调试器单步运行几乎不复现
  • 关闭D-Cache后问题消失

这三点几乎可以断定:DMA与D-Cache的一致性被破坏。

二、原理:为什么D-Cache会与DMA冲突

Cortex-M7的D-Cache位于CPU与总线矩阵之间。CPU访问内存时,数据可能被缓存在D-Cache中,而DMA直接访问物理内存(SRAM/SDRAM),两者看到的数据可能不一致。

  • CPU写,DMA读:CPU写入的数据可能还在Cache中未写回内存,DMA读到旧数据。
  • DMA写,CPU读:DMA将新数据写入内存,但CPU读到的却是Cache中的旧数据。

解决一致性有两种思路:

  1. 配置MPU将DMA缓冲区设为Write-Through或Non-Cacheable(简单但可能损失性能)
  2. 在DMA传输前后手动维护Cache:
    • DMA发送前(CPU写→DMA读):执行 Clean(将Cache写回内存)
    • DMA接收后(DMA写→CPU读):执行 Invalidate(丢弃Cache中旧数据,强制从内存重读)

三、排查过程:误用Clean/Invalidate的典型场景

检查原代码,发现如下操作:

// DMA接收完成后,在中断中处理
void DMA_RxComplete_Callback(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuf, BUF_SIZE);
    process_data(rxBuf);
}

看似正确,但存在两个致命问题:

  1. Invalidate的地址未按Cache Line对齐:Cortex-M7的Cache Line为32字节。若rxBuf不是32字节对齐,SCB_InvalidateDCache_by_Addr会失效或误伤相邻数据。
  2. 在DMA接收前未Clean:如果CPU在DMA启动前写过该缓冲区(例如初始化或上一轮处理),这些数据可能还在Cache中,DMA启动后直接覆盖内存,但Cache中的脏数据可能在后续被写回,导致数据错乱。

更隐蔽的是,原代码在DMA发送前只调用了SCB_CleanDCache_by_Addr,但缓冲区大小不是32的整数倍,导致最后一个Cache Line未被完全Clean,DMA读到了部分旧数据。

四、正确配置步骤

4.1 缓冲区对齐与大小

  • 使用__attribute__((aligned(32)))确保缓冲区32字节对齐
  • 缓冲区大小向上取整到32字节的整数倍
#define BUF_SIZE  1024
__attribute__((aligned(32))) uint8_t dmaRxBuf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t dmaTxBuf[BUF_SIZE];

4.2 MPU配置(可选但推荐)

将DMA缓冲区所在区域配置为Normal, Non-cacheable,可彻底避免一致性问题,但会牺牲CPU访问速度。若追求性能,可保持Cacheable,严格使用Clean/Invalidate。

void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // D1 SRAM
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.3 正确的Cache维护函数

// DMA发送前:Clean,确保CPU写的数据到达内存
void DMA_Send_Prepare(uint8_t *buf, uint32_t len)
{
    uint32_t aligned_len = (len + 31) & ~31;
    SCB_CleanDCache_by_Addr((uint32_t*)buf, aligned_len);
}

// DMA接收后:Invalidate,丢弃Cache旧数据
void DMA_Receive_Complete(uint8_t *buf, uint32_t len)
{
    uint32_t aligned_len = (len + 31) & ~31;
    SCB_InvalidateDCache_by_Addr((uint32_t*)buf, aligned_len);
}

4.4 完整DMA收发流程

// 发送流程
void send_data(uint8_t *data, uint32_t len)
{
    memcpy(dmaTxBuf, data, len);
    DMA_Send_Prepare(dmaTxBuf, len);
    HAL_DMA_Start(&hdma_tx, (uint32_t)dmaTxBuf, (uint32_t)&UART->TDR, len);
}

// 接收流程(DMA循环模式)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    DMA_Receive_Complete(dmaRxBuf, BUF_SIZE);
    process_data(dmaRxBuf, BUF_SIZE);
    // 重新启动DMA接收前,再次Invalidate,防止CPU处理期间Cache污染
    HAL_DMA_Start(&hdma_rx, (uint32_t)&UART->RDR, (uint32_t)dmaRxBuf, BUF_SIZE);
}

五、注意事项与避坑指南

  • 地址与长度必须32字节对齐:否则Clean/Invalidate可能无效或破坏相邻数据。
  • Clean与Invalidate不可互换:发送前Clean,接收后Invalidate。
  • Invalidate前确保DMA已停止:若DMA仍在写,Invalidate可能丢失新数据。
  • 避免在中断中频繁调用:Cache维护操作耗时,可考虑双缓冲或MPU非缓存区。
  • 使用SCB_InvalidateDCache_by_Addr而非SCB_InvalidateDCache:后者会清空整个Cache,影响性能。
  • 调试时关闭D-Cache可快速定位:若问题消失,基本可确定是一致性问题。

六、总结

STM32H7的D-Cache与DMA一致性问题是嵌入式开发中的经典陷阱。核心原则:谁写谁Clean,谁读谁Invalidate,地址长度32字节对齐。通过合理的MPU配置或严格的Cache维护,可以兼顾性能与数据正确性。希望本文的排查实录能帮助你少走弯路。