[STM32H7] STM32H7 的 D-Cache 与 DMA 数据一致性实战:缓冲区对齐与维护方向

[复制链接]
25|10
【STM32H7 的 D-Cache 与 DMA 数据一致性实战】

STM32H7 主频 480 MHz、带 I-Cache 和 D-Cache,性能很香,但也是玄学 bug 的高发区:串口 DMA 收到的数据不对、ADC DMA 缓冲区半数是旧值、以太网收包随机丢帧,十有**是 Cache 与 DMA 的一致性问题。

一、问题的本质

Cortex-M7 的 D-Cache 是写回(write-back)加写分配(write-allocate)的。CPU 读写内存时,数据其实先落在 32 字节的 Cache 行里,物理 SRAM 可能还是旧值。

而 DMA 是旁路总线的独立主设备,它直接读写 SRAM,不经过 Cache。于是出现两种错配:

1、DMA 写入 SRAM 后,CPU 从 Cache 读到旧数据,典型现象是缓冲区数据慢一拍;
2、CPU 写数据只落在 Cache 里没有回写,DMA 拿到的还是旧值,典型现象是发送数据错乱。

二、三条解法,各有取舍

方案一:把 DMA 缓冲区放到不经过 Cache 的区域

STM32H7 的 SRAM1/2/3、SRAM4 都可以通过 MPU 或链接脚本映射为 non-cacheable。最省事的做法是在链接脚本里单独开一段:

    .dma_buffer (NOLOAD) : {
      . = ALIGN(32);
      *(.dma_buffer)
    } > RAM_D2

然后在代码里用 section 属性声明缓冲区:

    __attribute__((section(".dma_buffer"), aligned(32))) uint8_t rx_buf[1024];

注意必须 32 字节对齐,否则一次 Cache 维护会波及相邻数据。

方案二:手动维护 Cache,按传输方向区分

- CPU 准备发送数据(CPU 写、DMA 读):先干净 clean
  SCB_CleanDCache_by_Addr((uint32_t*)buf, len);
- DMA 接收数据(DMA 写、CPU 读):先无效化 invalidate
  SCB_InvalidateDCache_by_Addr((uint32_t*)buf, len);

invalidate 要放在 DMA 启动之前,并在传输完成、读数据之前不再覆盖 CPU 已缓存的内容,否则可能丢掉刚写入的数据。

方案三:小缓冲区可用 DMA 完成中断加短临界区

如果缓冲区小、频率不高,可以在 DMA 完成中断里做维护,避免 CPU 访问与维护操作竞争。

三、几个容易踩的坑

1、地址必须 32 字节对齐,长度最好向上取整到 32 的倍数,否则维护操作会漏掉边界几字节;
2、clean 与 invalidate 的顺序不能反。发送用 clean、接收用 invalidate,这是最容易记错的地方;
3、用非缓存区方案时,别把频繁访问的大数组也丢进去,性能会明显下降;
4、以太网和 USB 的 DMA 描述符同样需要处理,很多人只处理了数据缓冲区却忘了描述符;
5、Cache 上电默认关闭,别忘记在初始化里使能,否则前面的优化等于没做。

四、快速定位方法

如果怀疑是 Cache 问题,最直接的验证方式是先把 D-Cache 关掉跑一遍。如果关掉就正常,基本可以锁定方向。然后再按传输方向补 clean 或 invalidate,最后再考虑用 MPU 划分非缓存区来换取性能。

结语:H7 的 Cache 不是开了就完事的开关,它和 DMA 是一对必须显式协商的伙伴。缓冲区对齐、方向分清、维护点放对,性能与正确性就能兼得。欢迎交流。
公羊子丹 发表于 2026-9-17 14:04 | 显示全部楼层
总结得很系统,赞同'缓冲区对齐、方向分清、维护点放对'这三条。我补充个血泪点:invalidate 用 SCB_InvalidateDCache_by_Addr 时,如果 CPU 之前改过这个 buffer 还没回写,invalidate 会直接丢弃脏数据。接收场景要么先 clean 再 invalidate,要么保证 CPU 不碰,顺序错了数据就悄悄没了。
周半梅 发表于 2026-9-17 14:05 | 显示全部楼层
楼主提到的'先关 D-Cache 跑一遍定位'这个技巧太实用了。我以前查 H7 串口 DMA 收到半旧数据,关 Cache 就正常,一秒锁定方向。建议新手都先用这招二分法,比盯着寄存器猜快十倍。
帛灿灿 发表于 2026-9-17 14:08 | 显示全部楼层
想问下楼主,你用的方案一(非缓存区)是把整个 RAM_D2 都设 non-cacheable,还是只圈一小段?我之前只圈缓冲区,结果以太网描述符忘了圈,照样出问题。而且把大数组放非缓存区确实拖性能,得权衡。
童雨竹 发表于 2026-9-17 14:08 | 显示全部楼层
补充个坑:H7 里 DMA 能访问的 RAM 是分域的,DMA1/DMA2 各自能到的区域有限。你放缓冲区的 SRAM 区域必须和 DMA 控制器所在域匹配,不然配得再对也搬不动。这个和 Cache 无关但经常一起踩。
万图 发表于 2026-9-17 14:10 | 显示全部楼层
从 Cache 行为看,M7 的 D-Cache 是 write-back + write-allocate,32 字节行。所以维护时长度一定要向上取整到 32 的倍数,地址对齐到 32。楼主写了'最好取整',我想强调这不是最好,是必须,否则会波及相邻变量,那种偶发 bug 最难查。
Wordsworth 发表于 2026-9-17 14:11 | 显示全部楼层
我赌很多人的 USB/以太网 DMA 描述符问题就是这么来的。描述符通常是小结构体,放缓存区里,DMA 改了 CPU 读旧值,或者 CPU 改了 DMA 读旧值。楼主第 4 个坑点出这个很关键,很多人只处理数据缓冲忘了描述符。
Bblythe 发表于 2026-9-17 14:12 | 显示全部楼层
分享个替代思路:如果嫌手动 clean/invalidate 麻烦,可以给关键 DMA 缓冲配 MPU 属性为'Device'或'Normal Non-cacheable',一劳永逸免维护。代价是访问变慢,但对低频访问的缓冲无所谓,换来的是代码简单不易错。
Pulitzer 发表于 2026-9-17 14:12 | 显示全部楼层
想问下楼主用的是 HAL 的 DMA 还是 LL?HAL 的某些 DMA 收发回调内部其实没帮你做 Cache 维护,得自己在正确时机调。如果用 LL + 自己的传输管理,反而更容易把维护点插准。这个取决于项目架构。
Uriah 发表于 2026-9-17 14:14 | 显示全部楼层
从调试经验,遇到 H7 这种'数据偶尔错'的软错误,第一反应就该怀疑 Cache 一致性,而不是怀疑外设或线缆。楼主总结的'串口数据错、ADC 半数旧值、以太网丢帧'这三类现象基本是 Cache 问题的经典脸谱,收藏了。
Clyde011 发表于 2026-9-17 14:14 | 显示全部楼层
补充一个容易忽略的:开了 D-Cache 后,对同一块内存的'双缓冲/乒乓'操作也要小心。如果 CPU 在处理一个 buffer 时 DMA 已经在写另一个,两个 buffer 的维护时机要分别处理,别用同一个维护点覆盖,不然乒乓切换那一下最容易出数据错乱。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

397

主题

3248

帖子

4

粉丝
快速回复 在线客服 返回列表 返回顶部
0