STM32H7 的 D-Cache 与 DMA 数据一致性:从 MPU 配置到 Clean/Invalidate 的完整避坑指南
一、为什么 DMA 遇上 D-Cache 会出错?
STM32H7 的 Cortex-M7 内核带有一级 D-Cache(通常 16KB 或 32KB),它缓存的是物理内存的副本。当 CPU 写数据时,若命中 Cache,新数据只写入 Cache,并未立即写回 SRAM;当 CPU 读数据时,若命中 Cache,直接返回 Cache 中的旧值,而不去 SRAM 取新值。
DMA 控制器则直接访问 SRAM,完全绕过 Cache。于是出现两类经典错误:
- CPU 写,DMA 读:CPU 写入的数据还在 Cache 中(Write-Back 模式),DMA 从 SRAM 读到的是旧数据。
- DMA 写,CPU 读:DMA 已将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据。
解决思路只有两个:要么让该内存区域不走 Cache(MPU 配置为 Device/Strongly Ordered 或 Write-Through),要么在恰当时机手动维护 Cache 一致性(Clean/Invalidate)。
二、MPU 配置:为 DMA 缓冲区划定“安全区”
最稳妥的做法是将 DMA 缓冲区所在的内存区域通过 MPU 配置为 Non-Cacheable(或 Write-Through,但 Non-Cacheable 更简单)。这样 CPU 和 DMA 看到的都是 SRAM 的真实内容,无需任何 Clean/Invalidate 操作。
2.1 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;然后MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk; - 选择一个空闲的 MPU Region(0~15)。
- 设置基地址、大小、属性(TEX=0, C=0, B=0 即 Non-Cacheable, Normal memory)。
- 使能该 Region。
2.2 代码示例
#include "stm32h7xx.h"
// 将地址 0x30000000 开始的 32KB 区域配置为 Non-Cacheable
void MPU_Config_DMA_Buffer(void)
{
MPU->CTRL = 0; // 先关闭 MPU
// 选择 Region 0
MPU->RNR = 0;
// 基地址 0x30000000,必须按区域大小对齐
MPU->RBAR = 0x30000000 | MPU_RBAR_VALID_Msk | 0;
// 大小 32KB (2^(15+1)=32KB? 实际 RASR SIZE 字段:size=14 表示 32KB)
// 属性:TEX=0, C=0, B=0, S=0, AP=011(全访问), XN=0
MPU->RASR = (0x0 << MPU_RASR_TEX_Pos) |
(0 << MPU_RASR_C_Pos) |
(0 << MPU_RASR_B_Pos) |
(3 << MPU_RASR_AP_Pos) |
(0 << MPU_RASR_XN_Pos) |
(14 << MPU_RASR_SIZE_Pos) | // 2^(14+1)=32KB
MPU_RASR_ENABLE_Msk;
// 使能 MPU 和默认背景区域
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
__DSB();
__ISB();
}
注意:MPU 配置必须在访问该内存区域之前完成,通常放在
main()开头或 SystemInit 中。
三、Clean 与 Invalidate:手动维护一致性
如果因性能原因必须使用 Cache(例如缓冲区很大,频繁访问),则需在 DMA 传输前后手动操作 Cache。
3.1 关键 API
CMSIS 提供了以下函数:
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中已修改的数据写回 SRAM。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):丢弃 Cache 中的内容,强制下次读取从 SRAM 获取。 -
SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。
3.2 操作时机
| 传输方向 | 操作顺序 | |---------|----------| | CPU 写 → DMA 读 | 写数据后,启动 DMA 前:Clean | | DMA 写 → CPU 读 | DMA 完成后,CPU 读之前:Invalidate | | DMA 读 → CPU 写(双向) | 启动前 Clean,完成后 Invalidate |
3.3 代码示例:UART DMA 发送
#define TX_BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t txBuffer[TX_BUFFER_SIZE]; // 32字节对齐
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
memcpy(txBuffer, data, len);
// 1. Clean D-Cache,确保数据写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)txBuffer, len);
// 2. 启动 DMA 传输
HAL_UART_Transmit_DMA(&huart1, txBuffer, len);
}
3.4 代码示例:ADC DMA 接收
#define RX_BUFFER_SIZE 256
__attribute__((aligned(32))) uint16_t rxBuffer[RX_BUFFER_SIZE];
void ADC_DMA_Start(void)
{
// 启动前无需 Clean,但若缓冲区之前被 CPU 写过,最好 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, sizeof(rxBuffer));
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)rxBuffer, RX_BUFFER_SIZE);
}
// DMA 完成回调中
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
// 3. Invalidate D-Cache,丢弃旧缓存,强制从 SRAM 读取新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rxBuffer, sizeof(rxBuffer));
// 现在可以安全读取 rxBuffer
ProcessData(rxBuffer, RX_BUFFER_SIZE);
}
四、避坑清单与注意事项
-
地址对齐:Clean/Invalidate 操作的地址和长度必须是 32 字节对齐,否则可能误伤相邻数据。建议缓冲区使用
__attribute__((aligned(32)))。 - 长度对齐:长度也建议向上取整到 32 字节的倍数,避免遗漏。
- 不要过度 Invalidate:Invalidate 会丢弃 Cache 中所有未写回的数据,若之前有 CPU 写入未 Clean,会导致数据丢失。
- DMA 传输期间禁止 CPU 访问缓冲区:否则可能引发 Cache 与 DMA 的竞争,导致不可预知结果。
- 多缓冲区(Ping-Pong):每个缓冲区独立维护,回调中只 Invalidate 当前完成的缓冲区。
- MPU 配置优先:若对性能要求不苛刻,强烈建议将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
-
使用
__DSB()和__ISB():在 MPU 配置或 Cache 操作后插入屏障指令,确保指令顺序执行。 - 调试时关闭 Cache:若问题诡异,可临时关闭 D-Cache 验证是否为一致性问题。
五、总结
STM32H7 的 D-Cache 是一把双刃剑:用好了大幅提升性能,用不好则调试到怀疑人生。核心原则是:明确数据流向,在正确的时间做正确的 Cache 维护。对于 DMA 缓冲区,优先考虑 MPU 配置为 Non-Cacheable;若必须使用 Cache,则严格遵循“写后 Clean,读前 Invalidate”的规则,并注意对齐与屏障。掌握这些,你就能在 H7 上自如地驾驭 DMA 与 Cache,让系统既快又稳。