为什么 DMA 和 D-Cache 会“打架”?
STM32H7 的 Cortex-M7 内核主频高达 480MHz,为了匹配 CPU 速度,芯片内部集成了 D-Cache(数据缓存)。CPU 读写数据时,实际访问的是 Cache,而不是直接访问 SRAM。DMA 控制器则绕过 Cache,直接读写 SRAM。
这就导致一个经典问题:
- CPU 写数据到 SRAM:数据可能只停留在 Cache 中(write-back 模式),SRAM 里还是旧值。此时启动 DMA 发送,DMA 读到的是旧数据。
- DMA 从外设搬数据到 SRAM:SRAM 已更新,但 CPU 读同一地址时,Cache 命中旧数据,CPU 读到的是旧值。
解决思路只有两个:配置 MPU 将 DMA 缓冲区设为非缓存(Non-cacheable),或者在 DMA 传输前后手动 clean/invalidate Cache。
方法一:MPU 配置非缓存区域(推荐)
对于频繁 DMA 的大块缓冲区,最省心的方式是用 MPU 将这块内存配置为 Non-cacheable。这样 CPU 和 DMA 都直接访问 SRAM,无需任何 Cache 维护操作。
配置步骤
- 使能 MPU:
SCB_EnableMPU()之前先SCB_EnableICache()和SCB_EnableDCache()。 - 定义 MPU 区域,设置
Disable缓存属性。 - 注意对齐:MPU 区域基址和大小必须是 32 字节对齐,且大小是 2 的幂。
完整代码示例
#include "stm32h7xx.h"
// 假设 DMA 缓冲区放在 D2 SRAM 中,地址 0x30000000,大小 32KB
#define DMA_BUF_ADDR 0x30000000
#define DMA_BUF_SIZE 0x8000
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 DMA 缓冲区区域为非缓存、非共享
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
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 配置必须在使能 D-Cache 之前完成,否则可能触发异常。
方法二:手动 clean / invalidate Cache
如果缓冲区不能设为非缓存(比如需要 CPU 频繁读写),就必须在 DMA 传输前后手动维护 Cache。
核心 API
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中已修改的数据写回 SRAM。用于 DMA 发送前。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从 SRAM 读取。用于 DMA 接收后。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃,用于双向传输。
正确使用顺序
发送方向(CPU → 外设):
- CPU 填充缓冲区数据
-
SCB_CleanDCache_by_Addr()写回 SRAM - 启动 DMA 发送
接收方向(外设 → CPU):
- 启动 DMA 接收
- 等待 DMA 完成
-
SCB_InvalidateDCache_by_Addr()丢弃旧 Cache - CPU 读取缓冲区
代码示例
#define BUF_SIZE 1024
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
// DMA 发送
void DMA_Send(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len); // 写回
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE); // 丢弃
// 此时读取 rx_buf 才是 DMA 写入的新数据
ProcessData(rx_buf, BUF_SIZE);
}
实战避坑指南
-
地址和长度必须 32 字节对齐:Cortex-M7 的 Cache 行大小为 32 字节。如果地址或长度不对齐,clean/invalidate 会误伤相邻数据,导致其他变量被意外写回或丢弃。建议用
__attribute__((aligned(32)))修饰缓冲区。 - 不要对同一缓冲区同时 clean 和 invalidate:除非确实需要双向同步,否则分开使用。
- invalidate 前确保 DMA 已停止:如果 DMA 还在写,invalidate 可能丢弃刚写入的数据。
- MPU 配置区域大小必须是 2 的幂:32KB、64KB 可以,48KB 不行。
- 中断中调用 Cache 维护函数要小心:这些函数执行时间较长,可能影响实时性。
-
使用
SCB_InvalidateDCache_by_Addr后,CPU 不能再依赖 Cache 中的旧值:所有后续读取都会从 SRAM 重新加载。
总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的高频坑点。优先推荐用 MPU 将 DMA 缓冲区设为 Non-cacheable,简单可靠;若必须使用 Cache,则严格遵循“发送前 clean、接收后 invalidate”的原则,并注意 32 字节对齐。掌握这两招,就能让 H7 的性能与数据正确性兼得。