一、为什么 DCache 会让 DMA 数据“失灵”?
Cortex-M7 的 L1 DCache 采用写回(write-back)策略。CPU 写数据时,数据先停留在 Cache 中,并未立即写入 SRAM;CPU 读数据时,若 Cache 命中则直接返回旧值,不会去 SRAM 取新值。
而 DMA 是“总线主设备”,它直接访问 SRAM,完全绕过 Cache。于是出现两类经典问题:
- CPU 写、DMA 读:CPU 写入的数据还在 Cache 里(脏行),DMA 从 SRAM 读到的是旧数据。
- DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧副本。
解决思路只有两条:要么让这段内存不经过 Cache(MPU 配置为 Non-Cacheable),要么在恰当时机手动维护 Cache(clean / invalidate)。
二、MPU 配置:最省心的方案
对于频繁与 DMA 交互的缓冲区,推荐用 MPU 将其配置为 Non-Cacheable, Non-Shareable,彻底规避一致性问题。代价是 CPU 访问该区域变慢,但通常可接受。
2.1 关键配置项
- TEX = 0b001, C = 0, B = 0:Normal memory, Non-cacheable。
-
AP:根据读写权限设置,例如全访问
0b011。 - XN:若不需要执行代码,设为 1 更安全。
2.2 完整 MPU 配置代码
#include "stm32h7xx.h"
#define DMA_BUF_ADDR 0x30000000UL /* AXI SRAM 起始 */
#define DMA_BUF_SIZE 0x2000UL /* 8KB */
void MPU_Config_DMA_Buffer(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
/* 配置 Region 0 为 Non-Cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_8KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; /* TEX=1 */
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; /* C=0 */
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;/* B=0 */
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 配置必须在使能 Cache 之前完成,且 Region 地址与大小必须对齐(大小是 2 的幂,基地址按大小对齐)。
三、clean / invalidate:必须掌握的边界条件
如果缓冲区仍需 Cache(例如 CPU 要频繁读写),就必须手动维护。CMSIS 提供三个函数:
-
SCB_CleanDCache_by_Addr(addr, size):把 Cache 脏行写回 SRAM。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,下次读从 SRAM 取。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃。
3.1 使用时机
- CPU 写 → DMA 读:DMA 启动前执行 Clean。
- DMA 写 → CPU 读:DMA 完成后执行 Invalidate。
- CPU 读写混合:DMA 完成后执行 Invalidate(若 CPU 之前写过该区域,需先 Clean 再 Invalidate)。
3.2 边界条件(最容易踩坑)
- 地址与大小必须 32 字节对齐。Cortex-M7 Cache Line 为 32 字节,若地址不对齐,操作会波及相邻数据,可能误伤其他变量。
- Invalidate 会丢弃未写回的数据。如果 CPU 刚写过缓冲区且未 Clean,直接 Invalidate 会丢失数据。
- DMA 传输期间禁止操作 Cache。否则可能读到半新半旧的数据。
- 多缓冲区共享 Cache Line:若两个变量位于同一 32 字节行,对其中一个 Invalidate 会影响另一个。建议缓冲区按 32 字节对齐并填充。
3.3 对齐的缓冲区定义
/* 强制 32 字节对齐,避免 Cache Line 伪共享 */
__attribute__((aligned(32))) uint8_t dma_tx_buf[1024];
__attribute__((aligned(32))) uint8_t dma_rx_buf[1024];
四、完整示例:UART DMA 收发
以 UART DMA 发送为例,展示 clean 的正确位置。
#include "stm32h7xx.h"
extern UART_HandleTypeDef huart1;
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
/* 1. 拷贝数据到 DMA 缓冲区 */
memcpy(dma_tx_buf, data, len);
/* 2. 确保 CPU 写入的数据写回 SRAM */
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);
/* 3. 启动 DMA 传输 */
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
}
/* DMA 接收完成回调 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
/* DMA 已写入 SRAM,使 Cache 失效以读取新数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
/* 此时可安全处理 dma_rx_buf 中的数据 */
}
}
提示:
SCB_CleanDCache_by_Addr的 size 参数建议传入实际长度,但函数内部会按 Cache Line 对齐处理,因此缓冲区最好整体对齐。
五、注意事项与最佳实践
- 优先使用 MPU Non-Cacheable:对 DMA 缓冲区,这是最不易出错的方式。
- 若必须用 Cache:严格遵循“写后 Clean、读前 Invalidate”的顺序,并保证 32 字节对齐。
- 避免在中断中频繁操作 Cache:clean/invalidate 有一定开销,高频小数据量传输可考虑 Non-Cacheable。
- DMA 描述符与缓冲区分离:描述符通常也需 Non-Cacheable 或正确维护。
- 调试时关闭 Cache 验证:若数据异常,先关 DCache 确认是否为一致性问题。
- 注意 AXI SRAM 与 DTCM:DTCM 不被 Cache,可直接用于 DMA,但带宽有限;AXI SRAM 需按上述规则处理。
六、总结
STM32H7 的 DCache 与 DMA 一致性没有“银弹”,核心在于理解 Cache 行为并选择合适策略:能不用 Cache 就不用(MPU),必须用时严格对齐并正确 clean/invalidate。掌握这些边界条件,你就能在 H7 上兼顾高性能与数据可靠性。