一、为什么 D-Cache 会和 DMA 打架?
STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存)。CPU 访问内存时,数据可能只被缓存在 Cache 中,并未写回物理内存(SRAM)。而 DMA 控制器直接访问物理内存,它看不到 Cache 里的最新数据。
典型冲突场景:
- CPU 写,DMA 读:CPU 写数据到缓冲区,数据还在 Cache 中(Write-Back 策略),DMA 启动后从 SRAM 读到旧数据。
- DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 读时命中了 Cache 中的旧数据,读到过期值。
根本原因:Cache 对 CPU 透明,但对 DMA 不透明。
二、解决思路:MPU 配置 + Cache 维护
2.1 方案一:将 DMA 缓冲区配置为 Non-Cacheable
通过 MPU(Memory Protection Unit)将特定内存区域设为 Non-Cacheable,CPU 和 DMA 都直接访问物理内存,从根本上避免一致性问题。
优点:一劳永逸,无需手动维护 Cache。 缺点:该区域 CPU 访问性能下降。
2.2 方案二:保持 Cacheable,手动 Clean/Invalidate
在 DMA 传输前后调用 Cache 维护函数:
- Clean:将 Cache 中已修改的数据写回 SRAM(CPU 写 → DMA 读之前)。
- Invalidate:丢弃 Cache 中的旧数据,强制从 SRAM 重新加载(DMA 写 → CPU 读之前)。
优点:CPU 访问性能高。 缺点:容易遗漏或时机错误,且 Invalidate 有副作用(见注意事项)。
三、MPU 配置实战(以 Non-Cacheable 为例)
假设 DMA 缓冲区位于 0x30000000(D1 SRAM),大小 32KB。
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置 D1 SRAM 区域为 Non-Cacheable, Non-Shareable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 配置必须在使能 Cache 之前完成,且区域不能与已有配置重叠。
四、手动 Clean/Invalidate 完整示例
若选择方案二,以 UART DMA 接收为例:
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 1. Invalidate:丢弃 Cache 旧数据,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
// 2. 此时可安全处理 dma_rx_buf 中的数据
process_data(dma_rx_buf, DMA_BUF_SIZE);
// 3. 重新启动 DMA 接收
HAL_UART_Receive_DMA(huart, dma_rx_buf, DMA_BUF_SIZE);
}
// DMA 发送前
void uart_dma_send(uint8_t *data, uint16_t len)
{
// 先 Clean:确保 CPU 写入的数据真正到达 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
五、关键注意事项与排查技巧
-
地址必须 32 字节对齐:
SCB_CleanDCache_by_Addr和Invalidate要求地址按 Cache Line(32 字节)对齐,否则可能误伤相邻数据。使用__attribute__((aligned(32)))。 - Invalidate 会丢弃未写回的数据:如果 CPU 刚写过缓冲区,直接 Invalidate 会丢失这些修改。正确顺序是:先 Clean,再 Invalidate。
- DMA 传输期间不要访问缓冲区:否则可能触发 Cache 与 DMA 的竞争。
-
使用
SCB_InvalidateDCache_by_Addr而非SCB_InvalidateDCache:后者会清空整个 D-Cache,影响系统性能。 - 调试时关闭 Cache 对比:若关闭 D-Cache 后问题消失,基本可确定是一致性问题。
- 注意 DMA 突发长度与 Cache Line 的关系:若 DMA 传输长度不是 32 字节整数倍,Invalidate 可能影响缓冲区外的数据,建议缓冲区大小按 32 字节对齐。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑位。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠;若追求性能,则必须严格遵循 Clean/Invalidate 的时机与对齐要求。理解原理、善用工具,才能让 H7 的高性能真正为我所用。