一、为什么 Cache 会让 DMA 数据错乱?
STM32H7 基于 Cortex-M7,主频高达 480MHz,为了匹配内核速度,芯片内部集成了 L1 Cache(指令 Cache 与数据 Cache)。CPU 访问数据时,会先查 Cache,命中则直接读写,未命中才访问实际内存。
而 DMA 是“旁路”CPU 直接访问内存的。当 CPU 写数据到 Cache 但尚未写回内存时,DMA 从内存读到的就是旧数据;反之,DMA 把新数据写入内存,CPU 却可能从 Cache 中读到旧数据。这就是缓存一致性问题。
典型场景:
- 串口 DMA 接收数据,CPU 读到的总是第一次的数据。
- ADC 连续采样,DMA 搬运到数组,CPU 读到的值不变。
- 以太网发送,DMA 发出的数据包内容错乱。
二、解决思路:MPU 配置 + Cache 维护
2.1 两种策略
- 将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable:从根源上避免 Cache 与内存不一致。
- 使用 Clean/Invalidate 操作:在 DMA 传输前后手动维护 Cache。
实际工程中常结合使用:对频繁小数据量传输用 Non-Cacheable 省去维护开销;对大数据块用 Write-Back 加手动维护提升性能。
2.2 MPU 配置步骤(以 Non-Cacheable 为例)
- 使能 MPU:
SCB_EnableMPU()前需先SCB_EnableICache()和SCB_EnableDCache()。 - 定义 MPU 区域:设置基地址、大小、属性。
- 属性中
TEX=1, C=0, B=0表示 Normal Memory, Non-Cacheable。 - 注意 32 字节对齐,且区域不能重叠。
#include "stm32h7xx.h"
// 定义 DMA 缓冲区,必须 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[1024];
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 DMA 缓冲区区域为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_buffer;
MPU_InitStruct.Size = MPU_REGION_SIZE_1KB; // 根据实际大小调整
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_LEVEL1; // TEX=1, C=0, B=0
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
三、Clean 与 Invalidate 的正确用法
若缓冲区配置为 Write-Back(默认),则必须手动维护。
- Clean:将 Cache 中已修改的数据写回内存。用于 CPU 写、DMA 读 之前。
- Invalidate:丢弃 Cache 中的旧数据,强制从内存重新加载。用于 DMA 写、CPU 读 之前。
- Clean+Invalidate:既写回又丢弃,用于双向传输。
CMSIS 提供函数:
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize) -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize) -
SCB_CleanInvalidateDCache_by_Addr(...)
关键点:地址必须 32 字节对齐,大小建议为 32 的整数倍。
// 发送前:CPU 填充数据后,Clean 确保内存数据最新
void DMA_Send(uint8_t *data, uint32_t len)
{
memcpy(dma_buffer, data, len);
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, len);
HAL_UART_Transmit_DMA(&huart1, dma_buffer, len);
}
// 接收后:DMA 完成中断中,Invalidate 丢弃旧 Cache
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, RX_LEN);
// 此时读取 dma_buffer 才是 DMA 写入的新数据
ProcessData(dma_buffer, RX_LEN);
}
四、实战避坑要点
- 对齐是生命线:所有 DMA 缓冲区必须 32 字节对齐,长度建议 32 的倍数。否则 Clean/Invalidate 可能破坏相邻数据。
- Invalidate 前先 Clean:若缓冲区部分被 CPU 修改,直接 Invalidate 会丢失修改。安全做法:先 Clean 再 Invalidate。
- 中断中调用需谨慎:Cache 维护函数执行时间较长,在高速中断中频繁调用可能影响实时性。
- 多缓冲区管理:使用多个 DMA 缓冲区时,每个都要单独维护,不能只维护首地址。
- MPU 区域覆盖完整:确保所有 DMA 缓冲区都在配置的 MPU 区域内,否则属性不生效。
- 调试时关闭 Cache:初期调试可暂时关闭 D-Cache 验证逻辑,确认无误后再开启并加入维护代码。
-
注意共享性:若系统有多主设备(如 MDMA、以太网 DMA),需设置
IsShareable为MPU_ACCESS_SHAREABLE。
五、总结
STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的必经之路。核心原则:要么让 DMA 缓冲区不经过 Cache,要么在传输前后正确 Clean/Invalidate。推荐对关键 DMA 缓冲区使用 MPU 配置为 Non-Cacheable,简单可靠;对性能敏感的大数据块,采用 Write-Back + 手动维护。牢记 32 字节对齐、先 Clean 后 Invalidate、中断中慎用等要点,即可避开绝大多数坑。