一、问题现象:偶发数据错乱
某工业数据采集项目使用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中的旧数据。
解决一致性有两种思路:
- 配置MPU将DMA缓冲区设为Write-Through或Non-Cacheable(简单但可能损失性能)
-
在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);
}
看似正确,但存在两个致命问题:
-
Invalidate的地址未按Cache Line对齐:Cortex-M7的Cache Line为32字节。若
rxBuf不是32字节对齐,SCB_InvalidateDCache_by_Addr会失效或误伤相邻数据。 - 在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维护,可以兼顾性能与数据正确性。希望本文的排查实录能帮助你少走弯路。