一、为什么 Cache 会让 DMA 数据“错乱”?
STM32H7 的 Cortex-M7 内核配有 L1 数据 Cache(D-Cache) 和 指令 Cache(I-Cache)。D-Cache 的存在让 CPU 访问频繁使用的数据时无需每次都访问 SRAM,从而大幅提升性能。但 DMA 控制器是独立于 CPU 的外设,它直接访问物理内存(SRAM),不经过 Cache。
这就产生了经典的一致性问题:
- CPU 写内存 → DMA 读内存:CPU 写入的数据可能还停留在 Cache 中(Write-Back 模式),尚未写回 SRAM。此时启动 DMA 发送,DMA 读到的就是旧数据。
- DMA 写内存 → CPU 读内存:DMA 已将新数据写入 SRAM,但 CPU 的 Cache 中可能还保留着该地址的旧数据副本。CPU 读取时命中 Cache,得到的是旧值。
简单说:Cache 与 DMA 各看各的,谁也不知道对方改了数据。
二、Cache 维护操作:Clean 与 Invalidate
解决一致性问题的核心是 Cache 维护操作,Cortex-M7 提供了两条关键指令:
- Clean(清理):将 Cache 中“脏”数据(已修改但未写回)强制写回 SRAM。
- Invalidate(无效化):将 Cache 中的对应行标记为无效,下次读取时强制从 SRAM 重新加载。
2.1 典型场景与操作选择
| 场景 | 方向 | 操作 | |------|------|------| | CPU 准备数据,DMA 发送 | CPU→DMA | 发送前 Clean | | DMA 接收数据,CPU 读取 | DMA→CPU | 接收完成后 Invalidate | | CPU 写、DMA 读、CPU 再写 | 双向 | Clean + Invalidate |
2.2 按地址范围维护
CMSIS 提供了按地址范围操作的函数,需注意 32 字节对齐(Cache Line 大小):
// 清理指定范围 D-Cache(写回 SRAM)
SCB_CleanDCache_by_Addr((uint32_t *)addr, size);
// 无效化指定范围 D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size);
// 清理并无效化
SCB_CleanInvalidateDCache_by_Addr((uint32_t *)addr, size);
注意:地址必须 32 字节对齐,长度建议向上取整到 32 字节的倍数,否则可能误伤相邻数据。
三、MPU 配置:从根源上规避问题
频繁的 Cache 维护会带来性能开销,且容易遗漏。更优雅的方案是使用 MPU(Memory Protection Unit) 将 DMA 缓冲区所在的内存区域配置为 Non-Cacheable(非缓存) 或 Write-Through(写通)。
3.1 推荐的内存属性
- Non-Cacheable:CPU 访问不经过 Cache,彻底消除一致性问题,但性能略低。
- Write-Through:写操作同时更新 Cache 和 SRAM,读仍可缓存,适合 CPU 频繁读、DMA 频繁写的场景。
3.2 MPU 配置步骤
- 使能 MPU 并配置 Region。
- 设置基地址、大小、访问权限。
- 指定内存属性(TEX、C、B、S 位)。
- 使能 Region 与 MPU。
#include "stm32h7xx.h"
// 将 0x30000000 起始的 64KB 配置为 Non-Cacheable
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 Region 0
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
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 串口接收 + Cache 维护
下面以 UART DMA 接收为例,展示“MPU 非缓存 + Cache 维护”双保险方案。
#define RX_BUF_SIZE 128
// 必须 32 字节对齐
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
void UART_DMA_Init(void)
{
// 1. 配置 MPU,将 rx_buf 所在区域设为 Non-Cacheable
MPU_Config();
// 2. 启动 DMA 接收前,无效化 Cache(防止旧数据残留)
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
// 3. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 4. 接收完成后再次无效化,确保 CPU 读到最新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
// 5. 处理数据
ProcessData(rx_buf, RX_BUF_SIZE);
}
}
五、注意事项与避坑指南
- 对齐是生命线:所有 Cache 维护操作和 DMA 缓冲区必须 32 字节对齐,否则会破坏相邻变量。
- Invalidate 前先 Clean:若缓冲区同时被 CPU 写过,直接 Invalidate 会丢失未写回的数据,应先 Clean 再 Invalidate。
- 不要对栈上变量做 DMA:栈变量地址不固定且可能未对齐,应使用全局或静态缓冲区。
- MPU 与 Cache 维护可共存:即使配置了 Non-Cacheable,显式维护操作也无害,可作为双重保险。
- 性能权衡:Non-Cacheable 区域 CPU 访问变慢,仅将 DMA 缓冲区设为非缓存,其余内存保持缓存以兼顾性能。
- I-Cache 一般无需维护:DMA 通常不修改代码区,除非实现自修改代码或 Bootloader 跳转,才需考虑 I-Cache 无效化。
掌握 Cache 维护与 MPU 配置,是驾驭 STM32H7 高性能特性的必修课。理解原理、选对策略,才能让 DMA 与 Cache 和谐共处。