故障现象:偶发的数据错乱
某工业网关项目使用STM32H743,通过DMA2将ADC采样数据搬运到外部SDRAM缓冲区,CPU再读取处理。测试中发现:约每几百次采样会出现一次数据错乱,表现为个别通道值突变或旧数据残留。重启后故障消失,运行一段时间又复现。
初步排查排除了ADC配置、DMA通道冲突、电源干扰,最终定位到D-Cache与DMA一致性处理时,Clean/Invalidate的粒度选择错误。
原理:为什么Cache和DMA会打架
STM32H7的Cortex-M7内核带D-Cache(数据缓存),CPU访问内存时先查Cache,命中则直接读写Cache,不访问实际内存。而DMA是总线主设备,直接访问物理内存,完全绕过Cache。
当DMA向某块内存写入新数据时,如果该内存区域在D-Cache中有旧副本,CPU读到的仍是旧数据;反之,CPU修改了Cache中的数据但未写回内存,DMA搬运的则是旧数据。这就是Cache一致性问题。
解决手段有两个:
- Clean(清理):将Cache中已修改的数据写回内存,使DMA能读到最新值。
- Invalidate(无效化):将Cache中的副本标记为无效,强制CPU下次读时从内存重新加载,从而读到DMA写入的新值。
关键坑点:粒度选错
Cortex-M7的D-Cache按Cache Line操作,STM32H7的Cache Line大小为32字节。Clean和Invalidate的最小单位就是一行(32字节)。
很多开发者习惯用SCB_CleanDCache_by_Addr(addr, size)或SCB_InvalidateDCache_by_Addr(addr, size),认为传入任意地址和长度就能精确操作。但底层实现是按行对齐处理的:
- 如果
addr未32字节对齐,函数会从该地址所在行的起始处开始操作,可能误伤相邻数据。 - 如果
size不是32的倍数,函数会操作到覆盖末尾的整行,同样可能影响缓冲区之外的数据。
在故障代码中,DMA目标缓冲区定义如下:
uint8_t dma_buf[256]; // 未强制对齐
处理时调用:
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));
由于dma_buf起始地址可能不是32字节对齐(链接器随机分配),Invalidate操作会从该地址所在行的开头开始,把该行中位于dma_buf之前的数据也无效化了。如果那些数据恰好是其他变量的缓存副本,就会导致其他变量读取时从内存加载到旧值,引发偶发错乱。
正确配置步骤
1. 使能D-Cache
SCB_EnableICache();
SCB_EnableDCache();
2. 将DMA缓冲区对齐到Cache Line
使用__attribute__((aligned(32)))强制32字节对齐:
__attribute__((aligned(32))) uint8_t dma_buf[256];
3. 根据数据流向选择操作
- DMA写内存,CPU读(如ADC采样):DMA传输完成后,CPU读之前,执行Invalidate。
- CPU写内存,DMA读(如发送数据):CPU写完后,启动DMA前,执行Clean。
-
双向:先Clean再Invalidate,或使用
SCB_CleanInvalidateDCache_by_Addr。
4. 使用正确的API并确保长度对齐
// DMA接收完成后
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));
注意sizeof(dma_buf)必须是32的倍数,且dma_buf地址32字节对齐。
完整代码示例
#include "stm32h7xx.h"
#define BUF_SIZE 256
// 强制32字节对齐
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];
void DMA_Init(void) {
// 使能DMA时钟、配置DMA2 Stream0等(略)
// 设置外设地址、内存地址为dma_buf,传输长度BUF_SIZE
}
void Process_Data(void) {
// 等待DMA传输完成标志
while (!(DMA2->LISR & DMA_LISR_TCIF0)) {}
DMA2->LIFCR = DMA_LIFCR_CTCIF0;
// 无效化Cache,确保CPU读到DMA写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 安全读取数据
for (int i = 0; i < BUF_SIZE; i++) {
// 处理dma_buf[i]
}
}
void Send_Data(void) {
// 准备要发送的数据
for (int i = 0; i < BUF_SIZE; i++) {
dma_buf[i] = i;
}
// 清理Cache,确保DMA读到最新数据
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 启动DMA发送
DMA2_Stream0->CR |= DMA_SxCR_EN;
}
注意事项
- 对齐是前提:DMA缓冲区必须32字节对齐,长度建议为32的倍数。
-
避免频繁全Cache操作:
SCB_CleanInvalidateDCache()会操作整个Cache,影响性能,尽量用地址范围版本。 - MPU配置:可将DMA缓冲区配置为Write-Through或Non-Cacheable,从根本上避免一致性问题,但会牺牲性能。
- 中断中操作:在DMA完成中断中执行Clean/Invalidate是常见做法,但注意中断上下文时间。
- 多缓冲区:若使用多个DMA缓冲区,每个都要对齐并单独处理。
-
调试技巧:可在操作前后读取
SCB->CCR确认Cache状态,或使用__DSB()确保操作完成。
总结
STM32H7的D-Cache与DMA一致性处理,粒度对齐是核心。Clean/Invalidate按32字节行操作,地址和长度必须对齐,否则会误伤相邻数据,导致偶发、难复现的故障。正确做法是:缓冲区强制32字节对齐,长度取32倍数,根据数据流向选择Clean或Invalidate。掌握这些,就能避开这个经典的“坑”。