STM32H7 的 D-Cache 与 DMA 数据一致性:从 Cache 行对齐到 Clean/Invalidate 的实战配置
1. 为什么 D-Cache 会与 DMA 冲突?
STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与存储器之间的速度差距,引入了 D-Cache(数据缓存)。D-Cache 以 Cache Line(通常为 32 字节)为单位缓存数据。当 CPU 访问内存时,若数据已在 Cache 中,则直接读取,无需访问慢速的 SRAM/SDRAM。
然而,DMA 控制器直接访问物理内存,不经过 Cache。这就导致:
- CPU 写数据到内存:数据可能只停留在 Cache 中,尚未写回物理内存。此时启动 DMA 发送,DMA 读到的是旧数据。
- DMA 从外设读数据到内存:数据被写入物理内存,但 CPU 可能仍从 Cache 中读取旧数据。
因此,必须通过 Clean(将 Cache 内容写回内存)和 Invalidate(丢弃 Cache 内容,强制从内存重新加载)来维护一致性。
2. Cache 行对齐:不可忽视的细节
Cortex-M7 的 D-Cache 行大小为 32 字节。Clean 和 Invalidate 操作的最小单位是 一行,而非单个字节。如果 DMA 缓冲区地址或长度未按 32 字节对齐,会导致:
- 误 Clean:将相邻的不相关数据也写回内存,可能覆盖 DMA 刚写入的新数据。
- 误 Invalidate:丢弃相邻的、CPU 尚未写回的数据,造成数据丢失。
解决方案:
- 将 DMA 缓冲区定义为
__attribute__((aligned(32))),确保起始地址 32 字节对齐。 - 缓冲区大小设置为 32 字节的整数倍。
- 若无法对齐,可将缓冲区放在非缓存区域(如通过 MPU 配置),或使用
SCB_InvalidateDCache_by_Addr时手动处理边界。
3. Clean 与 Invalidate 操作详解
CMSIS 提供了以下函数(需包含 core_cm7.h):
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将指定地址范围的数据从 Cache 写回内存。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):使指定地址范围的 Cache 行无效,下次读取时从内存加载。 -
SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate,适用于 DMA 双向传输。
使用场景:
- CPU 写 → DMA 读(发送):先 Clean,确保数据写入内存,再启动 DMA。
- DMA 写 → CPU 读(接收):DMA 完成后,先 Invalidate,再读取数据。
- DMA 双向传输:使用 CleanInvalidate。
4. 实战配置步骤
以 STM32H743 为例,使用 DMA 将 ADC 数据搬运到内存,并通过串口发送。
4.1 定义对齐的 DMA 缓冲区
#include "stm32h7xx.h"
#define BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t dma_buffer[BUFFER_SIZE];
4.2 配置 MPU(可选但推荐)
若不想手动维护 Cache,可将 DMA 缓冲区所在区域配置为 Non-cacheable。通过 MPU 设置:
void MPU_Config(void) {
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_buffer & 0xFFFFFFE0; // 32字节对齐
MPU_InitStruct.Size = MPU_REGION_SIZE_256B; // 根据实际大小调整
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
4.3 DMA 发送流程(CPU 写 → DMA 读)
void DMA_SendData(uint8_t *data, uint16_t len) {
// 1. 确保数据已写入内存
SCB_CleanDCache_by_Addr((uint32_t *)data, len);
// 2. 启动 DMA 传输
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)data, (uint32_t)&USART1->TDR, len);
}
4.4 DMA 接收流程(DMA 写 → CPU 读)
void DMA_ReceiveCompleteCallback(void) {
// 1. 使 Cache 无效,强制从内存加载新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buffer, BUFFER_SIZE);
// 2. 安全读取数据
ProcessData(dma_buffer, BUFFER_SIZE);
}
5. 注意事项与常见陷阱
- 对齐是前提:所有 Clean/Invalidate 操作必须基于 32 字节对齐的地址和长度,否则可能损坏相邻数据。
- 避免频繁操作:Clean/Invalidate 本身有开销,对于小数据量可考虑使用非缓存内存。
- 中断上下文:在 DMA 完成中断中执行 Invalidate 是安全的,但需确保没有其他 CPU 访问同一缓冲区。
- 多主设备冲突:若 CPU 和 DMA 同时访问同一缓冲区,必须使用软件互斥或双缓冲机制。
- MPU 配置:将 DMA 缓冲区设为 Non-cacheable 可简化代码,但会降低 CPU 访问速度,需权衡。
- 调试技巧:若数据异常,可先关闭 D-Cache 验证是否为一致性问题。
6. 总结
STM32H7 的 D-Cache 与 DMA 数据一致性是高性能嵌入式开发的必修课。核心原则:CPU 写后 Clean,DMA 写后 Invalidate,地址长度 32 字节对齐。通过合理的 MPU 配置和严谨的代码流程,可以充分发挥 H7 的性能,同时确保数据可靠传输。