一、为什么 DMA 与 D-Cache 会“打架”?
STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,为了匹配高速内核与相对较慢的 SRAM/外部存储器,芯片默认开启了 D-Cache(数据缓存)。D-Cache 位于 CPU 与总线矩阵之间,CPU 读写数据时实际访问的是 Cache,而非直接访问物理内存。
DMA 则绕过 CPU,直接在内存与外设之间搬运数据。当 CPU 修改了 Cache 中的数据但尚未写回内存(写回策略),或 DMA 更新了内存但 Cache 中仍是旧数据时,就会产生数据一致性问题。典型现象:
- DMA 发送的数据不是最新值(Cache 未写回)
- DMA 接收的数据被 CPU 读成旧值(Cache 未失效)
- 数据“随机”出错,调试时正常、全速运行异常
二、Cache 维护操作:Clean 与 Invalidate
解决一致性问题的核心是在正确的时机执行 Cache 维护操作。Cortex-M7 提供以下指令:
- Clean(清理):将 Cache 中已修改的数据写回物理内存。用于 CPU 写、DMA 读 的场景(如 DMA 发送)。
- Invalidate(无效化):将 Cache 行标记为无效,下次读取时从内存重新加载。用于 DMA 写、CPU 读 的场景(如 DMA 接收)。
- Clean & Invalidate:先写回再无效化,用于双向传输或不确定方向的场景。
CMSIS 提供了封装函数(需包含 core_cm7.h):
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
关键点:地址必须按 32 字节对齐(Cache 行大小),长度建议向上取整到 32 字节的倍数,否则可能误伤相邻数据。
三、DMA 传输中的正确操作顺序
3.1 DMA 发送(内存 → 外设)
- CPU 填充发送缓冲区
- Clean D-Cache(确保数据写回内存)
- 启动 DMA 发送
- 等待传输完成
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
void dma_send(uint8_t *data, uint16_t len) {
memcpy(tx_buf, data, len);
// 清理 D-Cache,将 tx_buf 写回内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
// 启动 DMA(以 UART 为例)
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
3.2 DMA 接收(外设 → 内存)
- Invalidate D-Cache(丢弃旧缓存,避免读到脏数据)
- 启动 DMA 接收
- 等待传输完成
- CPU 读取数据(此时从内存加载新数据)
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void dma_receive(void) {
// 无效化 D-Cache,确保后续读取从内存获取
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 传输完成后再次无效化,保证数据可见
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 此时读取 rx_buf 是 DMA 写入的最新数据
process_data(rx_buf, BUF_SIZE);
}
四、MPU 配置:一劳永逸的方案
频繁的 Cache 维护会带来性能开销,且容易遗漏。更优雅的方式是使用 MPU(内存保护单元) 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable(非缓存) 或 Write-Through(写透)。
4.1 配置步骤
- 使能 MPU:
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); - 定义 MPU 区域,设置基地址、大小、属性
- 将 DMA 缓冲区放入该区域
4.2 完整代码示例
#include "stm32h7xx_hal.h"
// 定义非缓存区域,用于 DMA 缓冲区
#define DMA_BUFFER_ADDR 0x30000000 // 例如 D2 SRAM 起始地址
#define DMA_BUFFER_SIZE MPU_REGION_SIZE_64KB
void MPU_Config(void) {
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 配置区域 0:DMA 缓冲区,非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = DMA_BUFFER_SIZE;
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);
}
// 将 DMA 缓冲区放在非缓存区域
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_buffer[4096];
在链接脚本中定义 .dma_buffer 段,将其定位到 0x30000000:
.dma_buffer (NOLOAD) : {
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2
五、注意事项与避坑清单
- 地址对齐:Cache 维护函数要求地址 32 字节对齐,长度建议为 32 的倍数。
-
内存屏障:在 Cache 维护后、启动 DMA 前,插入
__DSB()确保操作完成。 - 避免栈上缓冲区:DMA 缓冲区不要放在栈上,栈可能被 Cache 覆盖且地址不固定。
- 多缓冲区管理:若使用多个 DMA 缓冲区,确保每个都正确对齐和维护。
- 性能权衡:非缓存区域会降低 CPU 访问速度,仅将 DMA 缓冲区设为非缓存,其余内存保持缓存。
- 调试陷阱:调试器可能自动执行 Cache 维护,导致问题在调试时消失,务必全速运行验证。
- H7 的 AXI SRAM:默认映射为 Cacheable,若用作 DMA 缓冲区,必须通过 MPU 重配置。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典“坑”。掌握 Clean/Invalidate 的适用场景、正确的操作顺序,并利用 MPU 将 DMA 缓冲区配置为非缓存,即可从根本上避免数据错乱。建议在项目初期就规划好内存布局,将 DMA 缓冲区统一放在非缓存区域,既保证性能又提升可靠性。