一、为什么 D-Cache 会和 DMA 打架?
STM32H7 基于 Cortex-M7 内核,主频可达 480MHz,为了弥补 CPU 与存储器之间的速度差距,引入了 L1 D-Cache。CPU 访问数据时,会先将数据从 SRAM 加载到 Cache 行(通常 32 字节)中,后续访问直接命中 Cache,从而大幅提升性能。
但 DMA 是“独立于 CPU”的搬运工,它直接访问 SRAM,不经过 Cache。这就导致两个问题:
- CPU 写数据到 Cache,但未写回 SRAM:DMA 从 SRAM 读到的还是旧数据。
- DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据:CPU 看不到 DMA 的更新。
因此,必须手动维护 Cache 一致性:在 DMA 读取前,将 CPU 写的数据 Clean(写回 SRAM);在 DMA 写入后,将 CPU 中对应的 Cache 行 Invalidate(丢弃,强制从 SRAM 重新加载)。
二、地址对齐:32 字节的硬性要求
Cortex-M7 的 Cache 行大小为 32 字节。Clean 和 Invalidate 操作都是以 Cache 行为单位的。如果 DMA 缓冲区地址或长度没有 32 字节对齐,就会误伤相邻数据。
-
错误示例:
uint8_t buf[100]定义在0x20000004,长度 100 字节。Clean 时,CPU 会写回包含0x20000000~0x2000001F的整行,可能把其他变量也写回,若其他变量被 DMA 修改过,就会覆盖新值。 -
正确做法:使用
__attribute__((aligned(32)))将 DMA 缓冲区强制 32 字节对齐,且长度建议为 32 的整数倍。
// 定义 DMA 缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buf[256];
三、Clean 与 Invalidate 的时序
正确的操作顺序取决于数据流向:
- CPU 写 → DMA 读(发送):先 Clean,再启动 DMA。
- DMA 写 → CPU 读(接收):先启动 DMA,等 DMA 完成后再 Invalidate。
绝对禁止:在 DMA 传输过程中 Invalidate,因为此时 Cache 中可能还有未写回的数据,Invalidate 会直接丢弃,导致数据丢失。
四、实测数据损坏复现
下面通过一个简单实验复现问题:使用 DMA 将内存数据搬到另一块内存,CPU 先写入源缓冲区,然后启动 DMA,最后检查目标缓冲区。
4.1 错误代码(无 Cache 维护)
__attribute__((aligned(32))) uint8_t src[256];
__attribute__((aligned(32))) uint8_t dst[256];
void test_dma_no_cache(void) {
for (int i = 0; i < 256; i++) src[i] = i; // CPU 写 src
// 启动 DMA 搬运 src -> dst
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, 256);
HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000);
// 检查 dst
for (int i = 0; i < 256; i++) {
if (dst[i] != i) {
printf("Error at %d: expected %d, got %d\n", i, i, dst[i]);
break;
}
}
}
现象:由于 src 在 Cache 中,DMA 从 SRAM 读到的 src 是旧值(可能全 0),导致 dst 数据错误。
4.2 正确代码(Clean + Invalidate)
__attribute__((aligned(32))) uint8_t src[256];
__attribute__((aligned(32))) uint8_t dst[256];
void test_dma_with_cache(void) {
for (int i = 0; i < 256; i++) src[i] = i;
// 1. Clean src,确保 SRAM 中数据最新
SCB_CleanDCache_by_Addr((uint32_t*)src, 256);
// 2. 启动 DMA
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, 256);
HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000);
// 3. Invalidate dst,丢弃 Cache 中旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dst, 256);
// 4. 检查 dst
for (int i = 0; i < 256; i++) {
if (dst[i] != i) {
printf("Error at %d: expected %d, got %d\n", i, i, dst[i]);
break;
}
}
}
注意:SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,长度建议为 32 的倍数。
五、配置步骤与注意事项
5.1 配置步骤
-
使能 D-Cache:在
SystemInit或main开头调用SCB_EnableDCache()。 -
定义 DMA 缓冲区:使用
__attribute__((aligned(32)))对齐,长度设为 32 的倍数。 -
根据数据流向维护 Cache:
- 发送:先
SCB_CleanDCache_by_Addr,再启动 DMA。 - 接收:启动 DMA,等待完成,再
SCB_InvalidateDCache_by_Addr。
- 发送:先
- 避免在 DMA 传输中操作 Cache。
5.2 注意事项
-
不要使用
SCB_InvalidateDCache()全局无效化:会清掉所有 Cache,影响性能,且可能误伤其他数据。 - DMA 描述符也要对齐:如果使用链表模式,描述符本身也需 32 字节对齐并维护 Cache。
- MPU 配置:可将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,但会牺牲性能,需权衡。
- 调试时关闭 Cache:如果问题难以复现,可暂时关闭 D-Cache 对比测试。
六、总结
STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。核心原则:谁写谁 Clean,谁读谁 Invalidate,地址对齐 32 字节,时序不能乱。掌握这些,就能在享受 H7 高性能的同时,保证数据一致性。