一、为什么 D-Cache 会与 DMA 冲突?
STM32H7 的 Cortex-M7 内核运行频率高达 480MHz,为了匹配内核与存储器之间的速度差,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,若命中缓存则直接读写缓存,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM),不经过 Cache。
这就导致两种典型问题:
- DMA 读取(外设→内存):DMA 将新数据写入 SRAM,但 CPU 可能仍从 Cache 中读取旧数据。
- DMA 写入(内存→外设):CPU 修改了 Cache 中的数据,但尚未写回 SRAM,DMA 却从 SRAM 读取旧数据发送。
要解决此问题,必须正确使用 Cache 维护操作:Clean(将 Cache 数据写回 SRAM)和 Invalidate(丢弃 Cache 数据,强制从 SRAM 重新加载)。
二、地址对齐:32 字节边界是硬性要求
Cortex-M7 的 Cache 行大小为 32 字节。Clean 和 Invalidate 操作均以 Cache 行为单位。如果操作地址未按 32 字节对齐,或长度不是 32 的整数倍,会波及相邻数据,造成意外损坏。
正确做法:
- DMA 缓冲区首地址按 32 字节对齐(使用
__attribute__((aligned(32))))。 - 缓冲区大小设置为 32 的整数倍。
- 若无法满足,可手动扩展操作范围到 32 字节边界,但需确保扩展区域的数据不会被误改。
// 定义 32 字节对齐的 DMA 缓冲区
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];
三、Clean 与 Invalidate 的顺序
顺序错误是导致数据损坏的常见原因。牢记以下原则:
- CPU 写 → DMA 读(内存到外设):先 Clean,再启动 DMA。确保 SRAM 中是最新数据。
- DMA 写 → CPU 读(外设到内存):先 Invalidate,再启动 DMA?不!正确顺序是:启动 DMA 前 Invalidate(丢弃旧缓存),DMA 完成后 Invalidate 一次(丢弃 DMA 期间可能被预取的缓存),然后 CPU 读取。
- DMA 双向传输:先 Clean,再 Invalidate,再启动 DMA。
关键点:Invalidate 会丢弃未写回的数据,因此若缓冲区中有 CPU 尚未 Clean 的修改,必须先 Clean 再 Invalidate。
四、完整代码示例
以下示例使用 STM32H7 的 D-Cache 维护函数(需启用 SCB_EnableDCache())。
#include "stm32h7xx.h"
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];
// 内存到外设:CPU 填充数据,DMA 发送
void dma_send(void)
{
// 1. 填充数据
for (int i = 0; i < BUF_SIZE; i++) {
dma_buf[i] = i;
}
// 2. Clean 缓存,确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 3. 启动 DMA 发送
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_buf, (uint32_t)&periph_addr, BUF_SIZE);
}
// 外设到内存:DMA 接收数据,CPU 处理
void dma_receive(void)
{
// 1. Invalidate 缓存,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 2. 启动 DMA 接收
HAL_DMA_Start(&hdma_memtoperiph, (uint32_t)&periph_addr, (uint32_t)dma_buf, BUF_SIZE);
// 3. 等待 DMA 完成(中断或轮询)
while (!dma_done);
// 4. 再次 Invalidate,确保 CPU 读取到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 5. 处理数据
process_data(dma_buf);
}
五、实测数据损坏复现
为验证问题,我们编写一个简单测试:CPU 通过 DMA 将 SRAM 中的数组发送到另一个数组,比较结果。
未使用 Cache 维护:
uint8_t src[256] __attribute__((aligned(32)));
uint8_t dst[256] __attribute__((aligned(32)));
void test_no_cache_maintain(void)
{
for (int i = 0; i < 256; i++) src[i] = i;
// 启动 DMA 传输
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, 256);
while (!dma_done);
// 比较结果
for (int i = 0; i < 256; i++) {
if (src[i] != dst[i]) {
printf("Mismatch at %d: src=%d dst=%d\n", i, src[i], dst[i]);
}
}
}
运行后可能发现大量不匹配,因为 CPU 写入的 src 仍在 Cache 中,DMA 从 SRAM 读取到旧值。
加入 Clean 后:
void test_with_clean(void)
{
for (int i = 0; i < 256; i++) src[i] = i;
SCB_CleanDCache_by_Addr((uint32_t*)src, 256);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, 256);
while (!dma_done);
SCB_InvalidateDCache_by_Addr((uint32_t*)dst, 256);
// 比较结果,应全部匹配
}
六、注意事项与最佳实践
- 启用 D-Cache 后,所有 DMA 缓冲区必须按 32 字节对齐,否则维护操作会破坏相邻数据。
- 避免在 DMA 传输期间 CPU 访问同一缓冲区,否则可能引发不可预知的一致性错误。
- 使用 MPU 配置内存属性:将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,可简化维护,但会牺牲性能。
-
优先使用
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr,而非全局 Clean/Invalidate,以减少性能损失。 - DMA 传输完成后,若 CPU 要读取数据,必须再次 Invalidate,因为 DMA 期间 CPU 可能预取了旧数据到 Cache。
- 调试时若发现数据异常,首先检查 Cache 维护操作,这是 STM32H7 开发中最常见的陷阱之一。
掌握以上要点,即可在 STM32H7 上安全高效地使用 D-Cache 与 DMA,充分发挥芯片性能。