一、为什么 Cache 与 DMA 会“打架”?
STM32H7 的 Cortex-M7 内核包含 L1 数据缓存(D-Cache)和指令缓存(I-Cache)。D-Cache 的存在让 CPU 访问频繁使用的数据时无需每次都读/写外部 SRAM 或 SDRAM,从而大幅提升性能。然而,DMA 控制器是独立于 CPU 的总线主设备,它直接访问物理内存,不经过 Cache。
这就导致两个典型问题:
- CPU 写数据,DMA 读:CPU 将数据写入 Cache(写回策略下可能未同步到内存),DMA 从内存读取旧数据,导致传输错误。
- DMA 写数据,CPU 读:DMA 将新数据写入内存,但 CPU 读取时命中 Cache 中的旧数据,同样出错。
解决思路有两种:禁用 Cache(牺牲性能)或正确维护 Cache 一致性(推荐)。后者需要结合 MPU 配置和 Cache 维护操作。
二、MPU 配置:为 DMA 缓冲区划定“安全区”
MPU(Memory Protection Unit)可以将特定内存区域配置为非缓存(Non-cacheable)或写通(Write-through),从而避免一致性问题。对于 DMA 缓冲区,推荐以下两种策略:
- 完全非缓存:CPU 访问该区域时不使用 Cache,性能略低但无需手动维护。
- 写通 + 读分配:写操作直接穿透到内存,读操作仍可缓存,适合读多写少的场景。
2.1 MPU 配置步骤(以 SDRAM 地址 0xC0000000 为例)
- 使能 MPU 并配置 Region。
- 设置基地址、大小、属性。
- 使能 Region 和 MPU。
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 SDRAM 区域为 Write-through, no write allocate
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64MB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:若将区域设为 Non-cacheable,则 IsCacheable 应设为 MPU_ACCESS_NOT_CACHEABLE,且无需后续 Clean/Invalidate 操作。
三、Clean 与 Invalidate:顺序决定成败
当无法使用 MPU 非缓存区域时,必须手动维护 Cache。核心操作有两个:
- Clean:将 Cache 中已修改的数据写回内存。
- Invalidate:将 Cache 中的对应行标记为无效,下次读取时从内存重新加载。
3.1 正确顺序
- CPU 写 → DMA 读:先 Clean,再启动 DMA。
- DMA 写 → CPU 读:先 Invalidate(在 DMA 启动前),DMA 完成后再次 Invalidate(可选但推荐),再读取数据。
关键陷阱:Invalidate 操作会丢弃 Cache 中未写回的数据。如果 CPU 在 DMA 传输前写入了缓冲区,且未 Clean 就 Invalidate,这些数据将永久丢失。因此,Invalidate 前必须确保没有脏数据,或先 Clean 再 Invalidate。
3.2 完整代码示例(DMA 接收)
#define DMA_BUFFER_SIZE 1024
__attribute__((aligned(32))) uint8_t dma_rx_buffer[DMA_BUFFER_SIZE];
void DMA_Receive_Start(void)
{
// 1. 若缓冲区之前被 CPU 写过,先 Clean 确保数据写回
SCB_CleanDCache_by_Addr((uint32_t*)dma_rx_buffer, DMA_BUFFER_SIZE);
// 2. Invalidate 缓冲区,确保 DMA 写入后 CPU 读到新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buffer, DMA_BUFFER_SIZE);
// 3. 启动 DMA 接收
HAL_DMA_Start(&hdma_usart1_rx, (uint32_t)&USART1->RDR, (uint32_t)dma_rx_buffer, DMA_BUFFER_SIZE);
}
void DMA_Receive_Complete_Callback(void)
{
// 4. DMA 完成后,再次 Invalidate,确保 CPU 读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buffer, DMA_BUFFER_SIZE);
// 5. 安全读取数据
process_data(dma_rx_buffer, DMA_BUFFER_SIZE);
}
3.3 发送场景(CPU 写 → DMA 读)
void DMA_Transmit_Start(uint8_t *data, uint32_t len)
{
memcpy(dma_tx_buffer, data, len);
// 必须 Clean,确保数据写回内存
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buffer, len);
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)dma_tx_buffer, (uint32_t)&USART1->TDR, len);
}
四、注意事项与最佳实践
- 地址对齐:Cache 操作以 32 字节为单位,缓冲区地址和长度应 32 字节对齐,否则可能影响相邻数据。
- 避免频繁维护:Clean/Invalidate 有一定开销,尽量将 DMA 缓冲区集中管理,减少调用次数。
- 使用 MPU 优先:若性能允许,将 DMA 缓冲区设为 Non-cacheable 是最简单可靠的方式。
- 双缓冲区技巧:对于高速数据流,可采用双缓冲,一个用于 DMA,一个用于 CPU 处理,交替使用并配合 Cache 维护。
- 调试技巧:若数据异常,可先禁用 D-Cache 验证是否为一致性问题,再逐步启用并添加维护操作。
五、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典坑。核心解决思路是:MPU 配置非缓存区域或手动 Clean/Invalidate。牢记顺序:写后 Clean,读前 Invalidate,且 Invalidate 前确保无脏数据。结合对齐和缓冲区管理,即可在享受 Cache 性能的同时保证数据可靠传输。