第3章 缓存一致性问题产生原因:DMA直接访问内存导致脏数据

这一节我们聊聊为什么DMA传输会整出缓存一致性问题。说白了,就是CPU和DMA这两个家伙,一个靠缓存(Cache)过日子,一个直接跟内存打交道,两边看到的数据不一样了,就乱套了。

3.1 先搞清楚两边的数据通路

CPU读写内存的时候,大部分时间其实是在跟Cache亲热——Cache里存了一份副本,CPU改完数据先写到Cache里,等合适的时候再刷回内存。而DMA呢?它是个直肠子,直接读写内存,根本不理会Cache里有什么。

核心矛盾:CPU和DMA都在操作同一块物理内存区域,但CPU用的是缓存副本,DMA用的是内存本体——步调不一致,数据就脏了。

我刚开始做嵌入式的时候,以为内存是唯一的真相,直到被一个DMA传数据的bug折腾了两天。嗯,从那以后我养成了个习惯:但凡有DMA参与的内存区域,一定要搞清楚Cache策略。

3.2 经典场景:CPU写数据,DMA读数据

假设CPU往地址A写入一个数值0x55,这个数据先被写进了Cache里(如果用的是写回策略,内存里的地址A还是旧值0x00)。这时候DMA启动了,它去内存里读取地址A——好家伙,拿到的是0x00。CPU以为已经写好了,DMA却读了个寂寞。为什么?因为CPU修改的数据还趴在Cache里没刷出去,DMA直接读内存就拿到了脏数据(这里的“脏”是旧的意思)。

时刻 Cache中的数据 内存中的数据 发生的问题
T0(CPU写之前) 0x00 0x00 一致
T1(CPU写入0x55) 0x55(已更新) 0x00(未刷新) Cache和内存不一致
T2(DMA读内存) 0x55 0x00(落后) DMA拿到了脏数据

你看,这就是典型的脏Cache导致的内存陈旧问题。个人经验告诉我,这种场景最容易在查中断处理函数时出现——你在中断里修改了缓冲区,然后触发DMA去读,如果没手动清Cache,十有八九读到旧值。

3.3 反向场景:DMA写数据,CPU读数据

反过来也一样闹心。DMA从外设收到一批新数据,直接写到内存地址B。这时候CPU想去读地址B,但它可能发现Cache里已经有一个副本了——那个副本是之前读过的老数据。CPU直接读Cache,拿到的是旧数据,DMA刚写进去的新数据被忽略了。

时刻 Cache中的数据 内存中的数据 发生的问题
T0(DMA写之前) 0xAA(CPU此前读过的旧副本) 0xAA 一致
T1(DMA写入0xBB) 0xAA(未失效) 0xBB(已更新) Cache和内存不一致
T2(CPU读内存) 0xAA(命中Cache,拿到旧值) 0xBB CPU读了脏数据

我的小建议:在DMA传输完成后,先对相关内存区域执行一次Cache失效操作(Invalidate),再让CPU去读。反过来,在启动DMA写入前,先执行一次Cache Clean(写回),确保CPU写过的数据已经落地到内存。

3.4 根本原因:Cache写策略的“惯性”

其实问题的根子在于Cache写策略。如果用的是写直达(Write-Through),CPU每次写Cache的同时也写内存,DMA理论上能拿到最新数据,但性能损失很大——每次写都要穿透到内存,我一般只在关键安全场景下用它。实际大部分嵌入式系统用的是写回(Write-Back),数据先攒在Cache里,等被踢出或者显式刷回时才更新内存。这种策略高效,但给DMA挖了一个坑。

为什么说这是个坑?你想想看,CPU不知道DMA要读内存,DMA也不知道CPU在Cache里藏了私货。两者之间缺一个同步的握手,脏数据就这么出现了。

⚠️ 注意:有些MCU或SoC拥有硬件缓存一致性(比如ARM CCI或ACE总线),它们会在DMA访问时自动嗅探Cache并做一致性维护。但大多数低端嵌入式芯片没有这个功能——别指望硬件帮你擦屁股,必须软件手动处理。

3.5 一张图看懂根本原因

DMA缓存一致性冲突示意图 DMA与Cache数据不一致的两种场景 CPU Core Cache 内存 DMA 场景A:CPU先写,DMA后读 1. CPU修改数据 → 仅更新Cache 2. 数据留在Cache中,未写回内存 3. DMA读内存 → 拿到旧数据 场景B:DMA先写,CPU后读 1. DMA写内存 → 更新内存值 2. Cache中保存的是旧副本 3. CPU读Cache → 拿到旧数据 写回/失效 直接读写 不直接经过Cache

3.6 我曾经踩过的坑

我记得有一次调试一个音频播放的驱动,DMA负责把I2S进来的数据搬到一个环形缓冲,CPU在另一个任务里处理这个缓冲做音频解码。结果声音一直失真,查了两天才发现:CPU任务里读缓冲前没有做Cache无效(Invalidate),CPU一直在消费Cache里残留的旧数据,新数据被DMA写进内存后根本没被CPU看见。那次之后,我在所有DMA相关的代码开头都加了一条注释:/* 注意:此处需做Cache维护 */,算是给自己长个记性。

避坑指南:我曾经犯过一个低级错误——在DMA中断里直接访问DMA写过的内存,以为它已经是最新的了。结果因为在中断上下文里没有开缓存一致性的硬件控制,读到的还是Cache里的旧值。正确的做法是:在读取DMA写过的内存之前,先执行一次针对该地址(或内存区域)的Cache无效化操作——不同的架构API不同,比如ARM叫 SCB_InvalidateDCache_by_Addr,RISC-V常走平台的自定义函数。

3.7 总结性对比表

因素 对缓存一致性的影响 应对方案(我常用)
Cache写策略 写回策略下,Cache更新不立即反映到内存 DMA前做Clean;DMA后做Invalidate
CPU/DMA共享区域 系统不感知另一方的修改 使用非缓存(Non-cacheable)区域或手动维护
硬件支持 无硬件一致性(如低端MCU)必须软件处理 认清芯片手册,不要幻想硬件帮你搞定
中断上下文 中断中访问内存时,Cache状态不可预测 关键数据用volatile+内存屏障+显式Cache维护