一、为什么 D-Cache 会和 DMA 打架?
STM32H7 主频高达 480MHz,为了匹配 CPU 速度,芯片内部集成了 L1 D-Cache(数据缓存)。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的搬运工,它直接访问物理内存(SRAM),完全不知道 Cache 的存在。
这就导致两个典型问题:
- DMA 写入的数据,CPU 读不到:DMA 把新数据写入 SRAM,但 CPU 之前读过该地址,旧数据还在 Cache 里,CPU 再次读取时命中 Cache,拿到的是旧值。
- CPU 写入的数据,DMA 读不到:CPU 修改了变量,但只写入了 Cache(写回策略),SRAM 中还是旧数据,DMA 从 SRAM 搬运,发出去的是错误内容。
因此,任何被 DMA 访问的内存区域,都必须保证 Cache 与 SRAM 的一致性。
二、一致性维护的三种手段
- 禁用 D-Cache:简单粗暴,但牺牲性能,不推荐。
-
软件维护 Cache:在 DMA 传输前后调用
SCB_CleanDCache_by_Addr()或SCB_InvalidateDCache_by_Addr()。 - MPU 配置内存属性:将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,从硬件层面避免一致性问题。
实际项目中,推荐 MPU 配置 + 必要时软件维护 的组合方案。
三、地址对齐:最容易被忽视的坑
Cortex-M7 的 Cache 行大小为 32 字节。SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr() 操作的是整个 Cache 行,而不是单个字节。
如果 DMA 缓冲区起始地址或长度不是 32 字节对齐,维护操作会波及相邻数据,导致其他变量被意外清除或写回。
避坑规则:
- DMA 缓冲区必须 32 字节对齐(推荐用
__attribute__((aligned(32))))。 - 缓冲区大小最好是 32 的整数倍。
- 若无法对齐,需手动扩展维护范围到 Cache 行边界,并确保相邻数据不被误伤。
// 推荐:强制 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
四、MPU 配置实战
以 STM32H743 为例,将 SRAM 中某块区域(如 0x30000000 起始的 64KB)配置为 Non-Cacheable,专供 DMA 使用。
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 区域 0:DMA 缓冲区,Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
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);
}
配置后,该区域所有访问都绕过 D-Cache,DMA 与 CPU 看到的内存完全一致,无需再手动维护 Cache。
五、完整示例:DMA 串口接收 + Cache 维护
若不想用 MPU,也可在 DMA 传输前后手动维护 Cache。以下以 UART DMA 接收为例。
#define RX_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
void UART_DMA_Init(void)
{
// 启动 DMA 接收前,先无效化 Cache,确保 CPU 读到 SRAM 新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 处理前再次无效化,保证读到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, RX_BUF_SIZE);
ProcessData(rx_buf, RX_BUF_SIZE);
}
}
// DMA 发送前,先清理 Cache,把 CPU 写入的数据同步到 SRAM
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
SCB_CleanDCache_by_Addr((uint32_t*)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
注意:SCB_InvalidateDCache_by_Addr 会丢弃 Cache 中未写回的数据,因此只能用于 DMA 写入方向(CPU 只读)。对于 CPU 会写入的缓冲区,应先 Clean 再 Invalidate,或直接使用 MPU Non-Cacheable 方案。
六、排查清单与注意事项
遇到 DMA 数据异常时,按以下顺序排查:
- 确认缓冲区地址是否 32 字节对齐,长度是否为 32 的倍数。
- 确认是否在 DMA 传输前后正确维护 Cache,方向是否搞反。
- 检查 MPU 配置是否覆盖了 DMA 缓冲区,属性是否为 Non-Cacheable 或 Write-Through。
- 注意多缓冲区场景:多个 DMA 通道共用 Cache 行时,维护一个缓冲区可能影响另一个。
- 中断优先级:Cache 维护操作应在 DMA 传输完成中断中尽早执行,避免 CPU 先读取旧数据。
- 调试技巧:可临时禁用 D-Cache 验证问题是否由 Cache 引起,确认后再针对性优化。
七、总结
STM32H7 的 D-Cache 与 DMA 冲突本质是 缓存一致性 问题。核心解决思路有三条:地址对齐是基础,软件维护是补救,MPU 配置是根治。对于高频 DMA 传输,强烈建议用 MPU 将 DMA 缓冲区设为 Non-Cacheable,既保证数据正确,又避免频繁维护带来的性能开销。掌握这些要点,就能在 H7 上放心使用 DMA,不再被“玄学”数据错乱困扰。