一、为什么 H7 的 Cache 会成为 DMA 的“猪队友”?
STM32H7 的 Cortex-M7 内核带 16KB I-Cache 和 16KB D-Cache。D-Cache 让 CPU 访问 SRAM 时无需等待,但 DMA 直接访问物理内存,不经过 Cache。当 CPU 写数据到 Cache 而未同步到 SRAM 时,DMA 读到的就是旧数据;反之 DMA 写入新数据到 SRAM,CPU 却可能从 Cache 读到旧值。这就是经典的 Cache 一致性问题。
关键结论:
- CPU 写、DMA 读 → 必须 Clean(把 Cache 写回 SRAM)
- DMA 写、CPU 读 → 必须 Invalidate(丢弃 Cache 旧数据,强制从 SRAM 重读)
- 顺序错误会导致数据丢失或 HardFault
二、Clean 与 Invalidate 的正确操作顺序
2.1 发送方向(CPU→DMA)
// 1. CPU 填充缓冲区
fill_buffer(tx_buf, len);
// 2. Clean:将 Cache 内容写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
// 3. 启动 DMA 发送
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, len);
注意:Clean 必须在启动 DMA 之前,否则 DMA 可能读到未同步的旧数据。
2.2 接收方向(DMA→CPU)
// 1. 启动 DMA 接收(此时不要 Invalidate!)
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)rx_buf, len);
// 2. 等待 DMA 完成(中断或轮询)
while(!dma_done);
// 3. Invalidate:丢弃 Cache 旧数据,强制从 SRAM 重读
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
// 4. CPU 读取数据
process_buffer(rx_buf, len);
致命错误:在 DMA 传输过程中 Invalidate,可能把 DMA 刚写入但未完成的数据丢弃,导致数据错乱。
三、MPU 配置:让 DMA 缓冲区“绕过”Cache
每次手动 Clean/Invalidate 繁琐且易漏。更优雅的方案是用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,一劳永逸。
3.1 MPU 配置步骤
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
// 配置 SRAM1 的 0x30000000 起始 32KB 为 Non-Cacheable
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.BaseAddress = 0x30000000;
MPU_Init.Size = MPU_REGION_SIZE_32KB;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_Init.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:禁止 Cache
MPU_Init.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.2 链接脚本中放置 DMA 缓冲区
// 在 .ld 文件中定义段
MEMORY {
DMA_RAM (xrw) : ORIGIN = 0x30000000, LENGTH = 32K
}
SECTIONS {
.dma_buffer : { *(.dma_buffer) } > DMA_RAM
}
// C 代码中指定段
__attribute__((section(".dma_buffer"))) uint8_t adc_dma_buf[1024];
这样 DMA 缓冲区永远不经过 Cache,无需手动维护一致性。
四、完整实战:ADC + DMA 采集
#define ADC_BUF_LEN 256
__attribute__((section(".dma_buffer"))) uint16_t adc_buf[ADC_BUF_LEN];
void ADC_DMA_Init(void)
{
// ADC 配置(略)
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Init(&hadc1);
// DMA 配置
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_LEN);
}
// 处理数据时直接读,无需 Invalidate
void Process_ADC_Data(void)
{
uint32_t sum = 0;
for(int i = 0; i < ADC_BUF_LEN; i++) {
sum += adc_buf[i];
}
float avg = (float)sum / ADC_BUF_LEN;
}
五、避坑指南与注意事项
-
Invalidate 地址必须 32 字节对齐:Cortex-M7 Cache 行大小为 32 字节,非对齐地址会触发 HardFault。使用
SCB_InvalidateDCache_by_Addr时确保地址和长度都是 32 的倍数。 - 不要对同一区域同时 Clean 和 Invalidate:先 Clean 再 Invalidate 会丢失刚写回的数据,除非你明确知道自己在做什么。
- DMA 传输中禁止 Invalidate:这是最常见的 HardFault 来源,务必等 DMA 完成中断后再操作。
-
MPU 配置后需调用
SCB_EnableDCache():MPU 只定义属性,Cache 使能需单独调用。 - 调试时关闭 Cache:在 IDE 中可临时关闭 D-Cache 定位问题,但量产必须开启以发挥 H7 性能。
-
使用
__DSB()和__ISB():在关键操作后插入内存屏障,确保指令顺序执行。
掌握以上要点,你就能在 STM32H7 上安全地同时使用 Cache 和 DMA,既享受高性能,又避免数据一致性陷阱。