一、问题现象:为什么数据会“偶尔”错?
在 STM32H7 上使用 DMA 搬运数据时,很多开发者会遇到:
- 串口 DMA 接收偶尔丢字节或收到旧数据
- ADC 连续采样 DMA 传输,数据偶尔跳变
- 以太网、SDMMC 等高速外设 DMA 传输随机出错
这些现象往往与 D-Cache(数据缓存) 和 DMA 的一致性 有关。Cortex-M7 的 D-Cache 默认开启,CPU 访问数据时可能命中缓存,而 DMA 直接访问物理内存,两者看到的数据可能不一致。
二、原理:Cache 与 DMA 为何冲突
2.1 D-Cache 的工作方式
- CPU 读数据:先查 Cache,命中则直接返回,不访问内存
- CPU 写数据:默认写回策略(Write-Back),只写 Cache,标记为脏,稍后才写回内存
- DMA 传输:直接读写物理内存,不经过 Cache
2.2 两种典型不一致
场景 A:DMA 写入内存,CPU 读
DMA 把新数据写入内存,但 CPU 之前读过该地址,Cache 中仍是旧数据。CPU 再次读取时命中旧 Cache,看不到 DMA 的新数据。
场景 B:CPU 写入内存,DMA 读
CPU 写数据只更新了 Cache,内存中还是旧值。DMA 从内存读取,拿到的是旧数据。
三、Clean 与 Invalidate 的正确语义
- Clean(清理):把 Cache 中的脏数据写回内存。用于 CPU 写 → DMA 读 之前。
- Invalidate(无效化):丢弃 Cache 内容,强制下次从内存重新加载。用于 DMA 写 → CPU 读 之前。
- Clean + Invalidate:既写回又丢弃,用于双向传输或不确定方向时。
关键原则:谁写谁负责同步。CPU 写的数据,CPU 负责 Clean;DMA 写的数据,CPU 负责 Invalidate。
四、常见误用与后果
- 只 Invalidate 不 Clean:CPU 刚写的数据还在 Cache 中,Invalidate 直接丢弃,内存中仍是旧值,DMA 读到错误数据。
- 只 Clean 不 Invalidate:DMA 写入新数据后,CPU 读时仍命中旧 Cache,读到旧数据。
- 地址未按 Cache Line 对齐:Cortex-M7 Cache Line 为 32 字节。若操作区域不是 32 字节对齐,Clean/Invalidate 会波及相邻数据,导致其他变量被意外写回或丢弃。
- 在 DMA 传输过程中操作 Cache:传输未完成就 Clean/Invalidate,可能破坏正在传输的数据。
五、正确配置步骤
-
使能 D-Cache:在
SystemInit或启动代码中调用SCB_EnableDCache()。 -
分配 DMA 缓冲区:使用
__attribute__((aligned(32)))确保 32 字节对齐。 -
确定数据流方向:
- CPU 写 → DMA 读:传输前
SCB_CleanDCache_by_Addr() - DMA 写 → CPU 读:传输完成后
SCB_InvalidateDCache_by_Addr()
- CPU 写 → DMA 读:传输前
- 使用 DMA 传输完成中断:在中断中执行 Invalidate,确保传输已结束。
- 避免在传输中操作 Cache。
六、完整代码示例
以下示例展示 ADC 连续采样 + DMA 传输到内存,CPU 读取数据的正确流程。
#include "stm32h7xx.h"
#define ADC_BUF_SIZE 256
/* 32 字节对齐的 DMA 缓冲区 */
__attribute__((aligned(32))) static uint16_t adc_dma_buf[ADC_BUF_SIZE];
volatile uint8_t adc_dma_done = 0;
/* DMA 传输完成回调 */
void ADC_DMA_Complete_Callback(void)
{
/* DMA 已写入内存,CPU 读之前必须 Invalidate */
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, sizeof(adc_dma_buf));
adc_dma_done = 1;
}
/* 启动 ADC DMA 采样 */
void ADC_Start_DMA(void)
{
adc_dma_done = 0;
/* 若缓冲区之前被 CPU 写过,需先 Clean,确保内存数据最新 */
SCB_CleanDCache_by_Addr((uint32_t *)adc_dma_buf, sizeof(adc_dma_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_SIZE);
}
/* 主循环中处理数据 */
void Process_ADC_Data(void)
{
if (adc_dma_done) {
adc_dma_done = 0;
/* 此时 Cache 已无效化,读取的是 DMA 写入的最新数据 */
for (int i = 0; i < ADC_BUF_SIZE; i++) {
uint16_t val = adc_dma_buf[i];
/* 处理 val ... */
}
}
}
若使用 DMA 发送数据(CPU 写 → DMA 读):
__attribute__((aligned(32))) static uint8_t tx_buf[128];
void UART_Send_DMA(void)
{
/* 填充数据 */
for (int i = 0; i < 128; i++) {
tx_buf[i] = i;
}
/* CPU 写的数据在 Cache 中,必须 Clean 回内存,DMA 才能读到 */
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, 128);
}
七、排查方法与注意事项
7.1 排查步骤
- 确认 D-Cache 是否使能:读取
SCB->CCR的 IC/DC 位 - 检查 DMA 缓冲区是否 32 字节对齐
- 检查 Clean/Invalidate 调用时机是否正确
- 使用
SCB_InvalidateDCache_by_Addr前确保 DMA 传输已完成 - 临时关闭 D-Cache 验证问题是否消失(仅用于定位)
7.2 注意事项
- MPU 配置:可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题,但会降低 CPU 访问性能。
- Cache Line 对齐:操作地址和长度尽量按 32 字节对齐,避免误伤相邻数据。
- 中断优先级:DMA 完成中断中执行 Invalidate 是安全的,但不要在其中做耗时操作。
- 多缓冲区:使用双缓冲时,每个缓冲区都要独立同步。
- 不要过度同步:频繁 Clean/Invalidate 会降低性能,只在必要时调用。
八、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典陷阱。核心记住三点:
- CPU 写 → DMA 读:Clean
- DMA 写 → CPU 读:Invalidate
- 缓冲区 32 字节对齐
掌握这些原则,偶发数据错乱问题将迎刃而解。