一、为什么 Cache 与 DMA 会“打架”?
STM32H7 的 Cortex-M7 内核带有 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。CPU 访问内存时,数据可能暂存在 Cache 中,而 DMA 直接读写物理内存(SRAM/SDRAM)。若 CPU 写数据后未同步到内存,DMA 读到的就是旧数据;若 DMA 写入新数据,CPU 读到的却是 Cache 中的旧副本。这就是Cache 一致性问题。
- 写通(Write-through):CPU 写操作同时更新 Cache 和内存,但 H7 默认是写回(Write-back),只有 Cache 行被替换时才写回内存。
- 读分配(Read-allocate):CPU 读未命中时,先从内存加载到 Cache,后续读直接命中 Cache。
因此,DMA 传输前后必须手动维护 Cache:发送前 Clean(清理),接收后 Invalidate(无效化)。
二、Clean 与 Invalidate 的准确时机
2.1 发送方向(CPU → 外设)
- CPU 填充发送缓冲区(数据可能还在 Cache 中)。
- Clean D-Cache:将 Cache 行写回内存,确保 DMA 读到最新数据。
- 启动 DMA 发送。
- 等待 DMA 完成(此时无需 Invalidate,因为 CPU 不再读该缓冲区)。
// 发送前 Clean
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
2.2 接收方向(外设 → CPU)
- 启动 DMA 接收(此时缓冲区内容未知,建议先 Invalidate,防止 Cache 中有脏数据)。
- 等待 DMA 完成。
- Invalidate D-Cache:丢弃 Cache 中的旧副本,强制 CPU 从内存读取 DMA 写入的新数据。
- CPU 处理数据。
// 接收前 Invalidate(可选但推荐)
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// 等待完成...
// 接收后 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf));
注意:
SCB_InvalidateDCache_by_Addr的地址和长度必须按 32 字节对齐(Cache 行大小),否则可能误伤相邻数据。
三、MPU 配置:一劳永逸的非缓存区域
频繁手动 Clean/Invalidate 容易遗漏,且影响性能。更优雅的方案是用 MPU(Memory Protection Unit) 将 DMA 缓冲区所在内存区域配置为 Non-cacheable,CPU 和 DMA 直接访问物理内存,无需任何 Cache 维护。
3.1 MPU 配置步骤(以 SRAM1 为例)
- 使能 MPU:
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT)。 - 配置 Region:基地址、大小、属性(Normal, Non-cacheable, Shareable)。
- 使能该 Region。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
// 假设 DMA 缓冲区位于 0x30000000 (SRAM1),大小 64KB
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.BaseAddress = 0x30000000;
MPU_Init.Size = MPU_REGION_SIZE_64KB;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_Init.IsShareable = MPU_ACCESS_SHAREABLE; // 共享,保证一致性
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 非缓存
MPU_Init.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.2 链接脚本调整
将 DMA 缓冲区放到非缓存区域,需修改链接脚本(.ld)或使用 __attribute__((section(".dma_buffer"))) 指定段,并在 MPU 中覆盖该段地址。
// 定义非缓存缓冲区
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[1024];
四、完整实战:UART DMA 收发 + MPU 非缓存
#include "stm32h7xx_hal.h"
#define DMA_BUF_SIZE 256
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t tx_buf[DMA_BUF_SIZE];
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t rx_buf[DMA_BUF_SIZE];
void MPU_Config(void) { /* 如上 */ }
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 Cache 前调用
SCB_EnableICache();
SCB_EnableDCache();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 填充发送数据
for (int i = 0; i < DMA_BUF_SIZE; i++) tx_buf[i] = i;
// 由于 MPU 已配置为非缓存,无需 Clean/Invalidate
HAL_UART_Transmit_DMA(&huart1, tx_buf, DMA_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, DMA_BUF_SIZE);
while (1) {
// 处理接收数据
}
}
五、注意事项与避坑指南
-
MPU 配置必须在
SCB_EnableDCache()之前,否则已缓存的数据可能不一致。 -
Cache 行大小 32 字节:手动维护时地址和长度必须 32 字节对齐,否则用
SCB_CleanInvalidateDCache_by_Addr并确保不跨越其他数据。 - DMA 描述符:若使用链表模式,描述符本身也需放在非缓存区或手动 Clean。
- 性能权衡:非缓存区域会降低 CPU 访问速度,适合仅 DMA 频繁访问的缓冲区;其他内存仍保持 Cache 开启。
- 多主设备:若以太网、SDMMC 等多个 DMA 同时访问,建议统一规划非缓存区域。
- 调试时:可临时关闭 D-Cache 验证问题是否由一致性引起。
掌握 Clean/Invalidate 时机与 MPU 配置,就能在 STM32H7 上安全高效地使用 DMA,充分发挥 480MHz 的性能优势。