为什么 STM32H7 的 Cache 会成为 DMA 的“猪队友”?
STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了弥补内核与存储器之间的速度鸿沟,芯片内部集成了 L1 Cache(I-Cache 和 D-Cache)。开启 D-Cache 后,CPU 访问 SRAM 或外部 SDRAM 时,数据会先被缓存到 Cache Line(32 字节)中。
问题来了:DMA 是独立于 CPU 的总线主设备,它直接访问物理内存,不经过 Cache。当 CPU 写数据到缓冲区后,数据可能还停留在 D-Cache 中未写回物理内存,此时启动 DMA 发送,DMA 读到的就是旧数据;反之,DMA 接收数据写入物理内存后,CPU 读到的却可能是 D-Cache 中的旧副本。这就是典型的 Cache 一致性(Coherency)问题。
核心原理:Clean 与 Invalidate 到底在做什么?
Cortex-M7 提供了两条关键维护指令:
- Clean(清理):将 D-Cache 中“脏”的数据写回物理内存。执行后,Cache 行变为“干净”状态,但数据仍在 Cache 中。
- Invalidate(无效化):将 D-Cache 中的对应行标记为无效。下次 CPU 读取时会强制从物理内存重新加载。
关键原则:
- CPU 写 → DMA 读(如发送):先 Clean,确保数据落盘。
- DMA 写 → CPU 读(如接收):先 Invalidate,丢弃旧缓存,让 CPU 重新加载。
- DMA 写 → CPU 读 → CPU 写 → DMA 读(双向):先 Invalidate,再 Clean,顺序不能反!
配置步骤:以 UART DMA 收发为例
1. 使能 D-Cache 并配置 MPU
默认情况下,STM32H7 的 SRAM 区域是 Write-Back, Write-Allocate 属性,这最容易引发一致性问题。推荐将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,可一劳永逸。
// MPU 配置:将 D2 SRAM 区域设为 Non-Cacheable
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
2. 若必须使用 Cache,手动维护
若缓冲区仍在 Cacheable 区域,则必须在 DMA 传输前后调用维护函数。
// 发送前:Clean 缓冲区
void DMA_Send_Prepare(uint8_t *buf, uint32_t len)
{
SCB_CleanDCache_by_Addr((uint32_t *)buf, len);
// 启动 DMA 发送...
}
// 接收后:Invalidate 缓冲区
void DMA_Recv_Complete(uint8_t *buf, uint32_t len)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
// 此时 CPU 可安全读取 buf
}
3. 完整收发示例(UART + DMA)
#define BUF_SIZE 64
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void UART_DMA_Init(void)
{
// 初始化 UART 和 DMA(略)
}
void UART_Send_DMA(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len); // 写回物理内存
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE); // 丢弃旧缓存
// 处理 rx_buf 数据...
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
实测波形:顺序错误导致的“幽灵数据”
我们使用逻辑分析仪抓取 UART TX 引脚波形,对比两种场景:
-
场景 A(正确):发送前 Clean,波形显示数据完整,首字节为
0xAA。 -
场景 B(错误):发送前未 Clean,波形显示首字节为
0x00(旧数据),后续字节错位。
实测发现,未 Clean 时,DMA 读到的前 32 字节(一个 Cache Line)全部为旧值,因为 CPU 写入的数据还在 Cache 中未同步。
注意事项与避坑指南
-
地址对齐:
SCB_CleanDCache_by_Addr要求地址 32 字节对齐,长度建议为 32 的整数倍,否则可能误伤相邻数据。 - 避免在中断中频繁维护:Cache 维护指令耗时较长,高频中断中调用会严重影响实时性。
- DMA 描述符也要注意:若使用链表式 DMA,描述符本身也需放在 Non-Cacheable 区域或手动 Clean。
- 双缓冲(Ping-Pong)陷阱:切换缓冲区时,务必对“即将被 DMA 读”的缓冲区 Clean,对“刚被 DMA 写”的缓冲区 Invalidate。
- 调试时关闭 Cache:若问题诡异,可先关闭 D-Cache 验证是否为一致性问题,再逐步开启优化。
掌握 Clean/Invalidate 的正确顺序,是 STM32H7 高性能应用开发的必修课。希望本文的代码与波形能帮你少走弯路。