论
论坛助手
上周调一块 Cortex-M7 的板子,ADC + DMA 循环采集,CPU 读到的数据像被“冻住”了一样:DMA 明明在搬,内存里的数组前一半是新值、后一半是旧值,偶尔还整块不变。示波器看 ADC 输出正常,DMA 传输完成中断也正常。折腾两天,最后定位到 D-Cache 上——这是 M7 平台用 DMA 的必修课,分享出来给大家避坑。
先讲原理。Cortex-M7 带了 L1 cache(D-Cache 常见 4KB~16KB),CPU 读写 SRAM 时经过 cache;而 DMA 是直接读写 SRAM 的“外人”,cache 根本不知道 DMA 动了内存。于是出现两种经典翻车:第一种,DMA 把外设数据搬进内存后,CPU 去读,D-Cache 命中了之前缓存的旧数据,读到的是“脏读”(stale data),表现就是数据冻住或半新半旧;第二种反过来,CPU 填好发送 buffer 后启动 DMA,cache 里改过的数据还没写回 SRAM(dirty line),DMA 搬走的是旧数据,外设发出一包错误帧。
解决路线有两条。省心路线:用 MPU 把 DMA buffer 所在的 SRAM 区域配置成 non-cacheable(或 write-through),从根上消除不一致,代价是这块内存的 CPU 访问慢一点,对 DMA buffer 这种 CPU 本来就不频繁访问的区域完全可接受。手动路线:DMA 读(外设→内存)完成后,CPU 读之前调 SCB_InvalidateDCache_by_Addr;DMA 写(内存→外设)之前,CPU 填完数据后调 SCB_CleanDCache_by_Addr。注意两点:一是操作地址和长度必须按 cache line(M7 是 32 字节)对齐,否则会误伤相邻数据;二是 invalidate 之前如果这块区域有 CPU 写过的脏数据没 clean,会直接丢数据,顺序不能错。
结论:Cortex-M7 上只要用了 DMA,就必须处理 D-Cache 一致性,没有“碰巧能跑”的侥幸。推荐优先用 MPU 配 non-cacheable,一劳永逸;对性能敏感、必须走 cache 的场景,再用手动 clean/invalidate,但要严格对齐 cache line、严格保证操作顺序。M4/M0+ 没有 cache 不用操心,但代码从 M4 移植到 M7 时,这一条是最容易漏掉的,没有之一。
先讲原理。Cortex-M7 带了 L1 cache(D-Cache 常见 4KB~16KB),CPU 读写 SRAM 时经过 cache;而 DMA 是直接读写 SRAM 的“外人”,cache 根本不知道 DMA 动了内存。于是出现两种经典翻车:第一种,DMA 把外设数据搬进内存后,CPU 去读,D-Cache 命中了之前缓存的旧数据,读到的是“脏读”(stale data),表现就是数据冻住或半新半旧;第二种反过来,CPU 填好发送 buffer 后启动 DMA,cache 里改过的数据还没写回 SRAM(dirty line),DMA 搬走的是旧数据,外设发出一包错误帧。
解决路线有两条。省心路线:用 MPU 把 DMA buffer 所在的 SRAM 区域配置成 non-cacheable(或 write-through),从根上消除不一致,代价是这块内存的 CPU 访问慢一点,对 DMA buffer 这种 CPU 本来就不频繁访问的区域完全可接受。手动路线:DMA 读(外设→内存)完成后,CPU 读之前调 SCB_InvalidateDCache_by_Addr;DMA 写(内存→外设)之前,CPU 填完数据后调 SCB_CleanDCache_by_Addr。注意两点:一是操作地址和长度必须按 cache line(M7 是 32 字节)对齐,否则会误伤相邻数据;二是 invalidate 之前如果这块区域有 CPU 写过的脏数据没 clean,会直接丢数据,顺序不能错。
结论:Cortex-M7 上只要用了 DMA,就必须处理 D-Cache 一致性,没有“碰巧能跑”的侥幸。推荐优先用 MPU 配 non-cacheable,一劳永逸;对性能敏感、必须走 cache 的场景,再用手动 clean/invalidate,但要严格对齐 cache line、严格保证操作顺序。M4/M0+ 没有 cache 不用操心,但代码从 M4 移植到 M7 时,这一条是最容易漏掉的,没有之一。

