一、为什么 H7 上 DMA 会踩 Cache 的坑?
STM32H7 基于 Cortex-M7,主频高,为了弥补内存访问延迟,芯片内置了 D-Cache(数据缓存)。CPU 访问 SRAM 时,数据可能只存在于 Cache 中,并未写回物理内存。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存,完全看不到 Cache。
于是出现两类经典问题:
- 发送方向(内存→外设):CPU 写好缓冲区,数据还在 Cache 里(Write-Back 策略),DMA 从物理内存读到的却是旧数据。
- 接收方向(外设→内存):DMA 把新数据写入物理内存,但 CPU 读缓冲区时命中了 Cache 里的旧数据,读到“脏”内容。
解决手段就是 Clean(清理/写回) 和 Invalidate(无效化)。但顺序一旦搞反,问题更隐蔽。
二、Clean 与 Invalidate 到底做了什么?
- Clean(CleanDCache):把 Cache 中“脏”行写回物理内存,Cache 内容保留。
- Invalidate(InvalidateDCache):把 Cache 行标记为无效,下次读取强制从物理内存加载。
- Clean+Invalidate:先写回再无效,常用于接收缓冲区。
关键原则:
- 发送前:对缓冲区做 Clean,确保物理内存是最新数据。
- 接收后:对缓冲区做 Invalidate(或 Clean+Invalidate),丢弃 Cache 旧副本。
三、顺序错了会怎样?
3.1 发送时先 Invalidate 再 Clean
Invalidate 会把尚未写回的脏数据直接丢弃,物理内存仍是旧值,DMA 发出错误数据。更糟的是,若该 Cache 行后续被 CPU 再次修改,可能触发不可预测的写回,造成数据错乱。
3.2 接收时先 Invalidate 再启动 DMA
如果在 DMA 启动之前就 Invalidate,看似没问题,但若 CPU 在 Invalidate 后、DMA 完成前又访问了该缓冲区,Cache 会重新加载旧数据并可能标记为脏,DMA 完成后这些脏行可能覆盖新数据。正确做法是:DMA 启动前不要动 Cache,DMA 完成中断里再 Invalidate。
3.3 最危险的:接收后只 Clean 不 Invalidate
Clean 只写回,不丢弃 Cache。CPU 读到的仍是 Cache 里的旧数据,DMA 新数据被“屏蔽”,表现为数据永远不变。
四、正确配置步骤
- 使能 D-Cache(默认开启),并确保 MPU 将 DMA 缓冲区所在 SRAM 配置为 Non-Cacheable 或 Write-Through 可简化问题;若坚持用 Cacheable,必须手动维护。
- 发送缓冲区建议用
__attribute__((aligned(32)))对齐到 Cache 行(32 字节),避免伪共享。 - 发送流程:填充数据 →
SCB_CleanDCache_by_Addr()→ 启动 DMA。 - 接收流程:启动 DMA → 等待完成 →
SCB_InvalidateDCache_by_Addr()→ 读取数据。 - 若接收缓冲区在 DMA 期间可能被 CPU 访问,改用
SCB_CleanInvalidateDCache_by_Addr()。
五、完整代码示例
#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"
#define BUF_SIZE 256
#define CACHE_LINE 32
/* 32 字节对齐,避免跨 Cache 行 */
__attribute__((aligned(CACHE_LINE))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(CACHE_LINE))) uint8_t rx_buf[BUF_SIZE];
/* 发送:Clean 后启动 DMA */
void dma_send(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
/* 关键:把 Cache 脏行写回物理内存 */
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
/* 启动 DMA 传输(以 SPI 为例) */
HAL_SPI_Transmit_DMA(&hspi1, tx_buf, len);
}
/* 接收完成回调:Invalidate 后再读 */
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1) {
/* 关键:丢弃 Cache 旧副本,强制从物理内存加载 */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
/* 此时读取才是 DMA 写入的新数据 */
process_data(rx_buf, BUF_SIZE);
}
}
/* 启动接收 */
void dma_receive(void)
{
/* 启动前不要 Invalidate,避免 CPU 误访问 */
HAL_SPI_Receive_DMA(&hspi1, rx_buf, BUF_SIZE);
}
六、注意事项与避坑清单
-
地址与长度必须 32 字节对齐:
SCB_*DCache_by_Addr要求地址对齐,长度建议向上取整到 32 的倍数,否则可能漏刷或越界。 - 不要对栈上变量做 DMA:栈变量地址不固定且可能跨行,维护困难。
- 中断里避免频繁 Invalidate 大块内存:影响实时性,可分段处理。
- MPU 配置 Non-Cacheable 最省心:若 DMA 缓冲区固定,直接配成 Non-Cacheable,无需手动维护,但会牺牲 CPU 访问性能。
- 双缓冲/环形缓冲要小心:DMA 正在写的区域不能被 CPU 访问,否则 Invalidate 会丢数据。
- 调试时关 Cache 正常、开 Cache 出错:基本可判定是 Cache 一致性问题。
七、总结
STM32H7 的 Cache 与 DMA 一致性没有捷径:发送先 Clean,接收后 Invalidate,顺序不能反,地址要对齐。理解“Cache 是 CPU 的私有副本,DMA 只认物理内存”这一本质,就能避开这个 H7 开发中最隐蔽的坑。