【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 是一对必须显式协商的伙伴。缓冲区对齐、方向分清、维护点放对,性能与正确性就能兼得。欢迎交流。 |
|