一、问题现象:为什么 DMA 数据总是“慢半拍”?
在 STM32H7 上,很多开发者会遇到这样的现象:
- DMA 从外设搬运数据到内存后,CPU 读到的还是旧值;
- CPU 修改缓冲区后启动 DMA 发送,对方收到的却是修改前的数据;
- 只有关闭 D-Cache 才正常,但性能大幅下降。
根本原因:Cortex-M7 的 D-Cache 与 DMA 控制器看到的内存视图不一致。DMA 直接访问物理内存(SRAM),而 CPU 可能读写的是 Cache 中的副本。若没有正确维护一致性,数据就会“错位”。
二、原理:D-Cache、写缓冲与 DMA 的冲突
Cortex-M7 的 D-Cache 是写回(Write-Back)+ 写分配(Write-Allocate) 策略:
- CPU 写数据时,若 Cache 命中,只更新 Cache,不立即写回 SRAM;
- CPU 读数据时,若 Cache 未命中,先从 SRAM 加载到 Cache 行,再返回。
DMA 不经过 Cache,直接读写 SRAM。因此:
- CPU 写 → DMA 读:数据可能还在 Cache 中未写回,DMA 读到旧数据;
- DMA 写 → CPU 读:SRAM 已更新,但 Cache 中仍是旧副本,CPU 读到旧数据。
此外,Cortex-M7 还有写缓冲(Write Buffer),即使执行了内存屏障,写操作也可能延迟到达 SRAM。
三、Cache 行对齐:容易被忽略的“隐形杀手”
D-Cache 以 Cache Line(通常 32 字节) 为单位操作。若缓冲区未按 32 字节对齐,Clean/Invalidate 可能影响相邻数据:
- Clean:将 Cache 行写回 SRAM。若缓冲区跨行,可能把无关数据也写回;
- Invalidate:丢弃 Cache 行。若缓冲区跨行,可能丢弃相邻变量尚未写回的数据,导致丢失。
避坑原则:
- DMA 缓冲区必须 32 字节对齐,且大小建议为 32 字节整数倍;
- 使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏; - 若无法对齐,需手动处理边界行,或使用 MPU 将该区域配置为 Non-Cacheable。
// 推荐:32 字节对齐的 DMA 缓冲区
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
四、正确使用 Clean 与 Invalidate
CMSIS 提供了维护 Cache 的函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 写回 SRAM; -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache,强制下次从 SRAM 读; -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃。
使用场景:
-
CPU 写 → DMA 读:启动 DMA 前执行
SCB_CleanDCache_by_Addr; -
DMA 写 → CPU 读:DMA 完成中断中执行
SCB_InvalidateDCache_by_Addr; -
双向缓冲:使用
SCB_CleanInvalidateDCache_by_Addr。
// 发送前:确保 CPU 写入的数据到达 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, DMA_BUF_SIZE);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, DMA_BUF_SIZE);
// 接收完成中断中:丢弃旧 Cache,读取最新数据
void DMA_RxComplete_Callback(void)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
// 此时 CPU 读取 dma_rx_buf 才是 DMA 写入的最新值
}
注意:
- 地址和大小都需 32 字节对齐,否则函数内部会按行处理,可能影响相邻数据;
- 调用前确保没有其他 CPU 访问该缓冲区,避免竞态;
- 中断中调用需注意执行时间,但通常可接受。
五、MPU 配置:一劳永逸的 Non-Cacheable 方案
若某块内存区域只用于 DMA,且 CPU 访问不频繁,可将其配置为 Non-Cacheable,彻底避免一致性问题。
使用 MPU 配置步骤:
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; - 禁用 MPU:
MPU->CTRL = 0; - 配置 Region:
- 选择区域号(0~15);
- 设置基地址、大小、属性;
- 属性设为 Normal, Non-Cacheable, Shareable;
- 使能 MPU:
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk; - 执行
__DSB(); __ISB();
void MPU_Config_DMA_NonCacheable(void)
{
MPU->CTRL = 0; // 禁用 MPU
MPU->RNR = 0; // 使用 Region 0
// 基地址:DMA 缓冲区起始地址,需 32 字节对齐
MPU->RBAR = (uint32_t)dma_rx_buf & MPU_RBAR_ADDR_Msk;
// 大小:256 字节,2^(SIZE+1),SIZE=7 表示 256 字节
// 属性:TEX=1, C=0, B=0, S=1 -> Normal, Non-Cacheable, Shareable
MPU->RASR = (0x7 << MPU_RASR_SIZE_Pos) |
(1 << MPU_RASR_TEX_Pos) |
(0 << MPU_RASR_C_Pos) |
(0 << MPU_RASR_B_Pos) |
(1 << MPU_RASR_S_Pos) |
(1 << MPU_RASR_AP_Pos) | // 全访问
(1 << MPU_RASR_ENABLE_Pos);
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
__DSB();
__ISB();
}
注意:
- MPU 区域不能与已有区域重叠,否则可能触发 MemManage 异常;
- 配置为 Non-Cacheable 后,CPU 访问速度会下降,但换来了简单可靠;
- 若使用 FreeRTOS 等 RTOS,需确保 MPU 配置在任务切换时不被破坏。
六、完整排查流程与代码模板
排查步骤:
- 确认 DMA 缓冲区是否 32 字节对齐;
- 确认是否在正确时机执行 Clean/Invalidate;
- 检查是否遗漏
__DSB()等内存屏障; - 若仍不稳定,尝试将缓冲区配置为 Non-Cacheable;
- 使用调试器查看 SRAM 与 Cache 内容,对比差异。
代码模板:
#include "stm32h7xx.h"
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
void DMA_Init(void)
{
// 初始化 DMA,源/目的地址为 rx_buf/tx_buf
// ...
}
void Send_Data(void)
{
// 填充 tx_buf
for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;
// 清理 Cache,确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, BUF_SIZE);
__DSB(); // 确保写操作完成
// 启动 DMA 发送
HAL_DMA_Start(&hdma_memtomem, (uint32_t)tx_buf, (uint32_t)dest, BUF_SIZE);
}
void DMA_RxComplete_Callback(void)
{
// 无效化 Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
__DSB();
// 处理 rx_buf 中的数据
Process_Data(rx_buf, BUF_SIZE);
}
七、总结与避坑清单
- 对齐:DMA 缓冲区必须 32 字节对齐;
- 时机:CPU 写后 Clean,DMA 写后 Invalidate;
-
屏障:Clean/Invalidate 后加
__DSB(); - MPU:对 DMA 专用内存可配置为 Non-Cacheable;
- 调试:关闭 D-Cache 可快速定位是否为一致性问题;
- 文档:参考 STM32H7 参考手册中 Cache 与 MPU 章节,以及 AN4838 应用笔记。
掌握这些要点,你就能在 STM32H7 上安全地使用 D-Cache 与 DMA,既享受高性能,又避免数据错乱。