嵌入式驱动开发:DMA与Cache一致性问题的三种解法

发布时间:2026/10/19 1:40:44
嵌入式驱动开发:DMA与Cache一致性问题的三种解法 做嵌入式驱动的人大概率都撞见过这么个诡异现象DMA 传输明明完成了可外设收到的数据总是旧版本或者 CPU 读回来的一直是上一次的内容。第一反应往往是怀疑 DMA 配置错了、地址写偏了查了一圈寄存器、示波器都正常最后才发现问题出在 Cache 身上。这个坑我前前后后踩过好几回也从“靠猜靠试”慢慢变成“按套路出牌”。借这个机会把思路彻底梳理一遍DMA 为什么会读到旧数据以及我从实际项目中总结出来的三招解决方法。1. 先弄清楚DMA 为什么会被 Cache“坑”1.1 CPU、Cache、DMA 三者的内存视图不一样现代处理器里CPU 访问内存不是直接去 DRAM而是先经过一层 Cache。CPU 写一个变量很多时候只是把这个值写进了 Cache 行Cache 行被标记成 dirty真正写回 DRAM 是后面某个时刻才发生由写回策略决定。也就是说CPU 认为的“最新数据在内存里”在物理层面其实只存在于 Cache 中。DMA 控制器是另一个总线主设备它访问内存时通常不经过 CPU 的 Cache直接去 DRAM 或总线内存空间拿数据。于是就有了两条数据通路CPU → Cache → DRAM和 DMA → DRAM。打个比方CPU 像是车间里记账的工人先在小本子上记下最新数字但最终要交给仓库的账本DMA 则是仓库管理员负责去翻账本。如果工人记完小本子没抄到仓库账本上仓库管理员当然只能查到旧数字这就是“DMA 总读旧数据”最直接的原因。1.2 两个方向的问题并不对称很多人以为“DMA 读旧数据”只有一种形态其实恰恰相反这个坑有正反两个方向而且症状完全不同。CPU 写、DMA 读CPU 填充缓冲区后最新内容还在 Cache 里没写回 DRAM。DMA 启动后从 DRAM 读拿到的全是旧数据。这是真正的“DMA 读旧数据”。DMA 写、CPU 读DMA 已经把新数据写进了 DRAM但 CPU 去读同一块地址时Cache 命中的是旧数据读出来的还是老内容。严格说这是“CPU 读到旧数据”但很多人调试时也会把它归类成“DMA 数据不对”。判断问题方向非常关键。数据从 CPU 流向设备要在 DMA 启动前做 Cache 写回数据从设备流向 CPU要在 DMA 完成后做 Cache 失效让 CPU 重新从 DRAM 拉取。搞反了操作结果只会更乱。1.3 为什么是“总”读旧数据而不是“偶尔”嵌入式场景里这个现象往往不是偶发抖动而是稳定复现。原因在于如果 CPU 写完后没有触发写回Cache 里的脏行会一直保留DMA 每次去 DRAM 拿到的都是同一个旧值。缓冲区越频繁被 CPU 复用越容易出现“永远拿到上一轮数据”的情况。另外编译器也可能插一脚。CPU 写缓冲区后紧接着启动 DMA 的代码中间如果没有内存屏障或 volatile 约束编译器优化后可能把写操作延后甚至让 CPU 感觉不到需要写回。所以看到一个“稳定复现”的旧数据问题先不要怀疑硬件坏大概率是 Cache 一致性的锅。2. 第一招用一致性 DMA 映射从源头规避2.1 dma_alloc_coherent 到底做了什么最省心的方案是让这块缓冲区在硬件层面就保持一致。Linux 内核提供了一组一致性 DMAcoherent DMAAPI最常用的是dma_alloc_coherentdma_addr_t dma_handle; void *cpu_addr dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL);返回两个地址cpu_addr是 CPU 访问这块内存的虚拟地址dma_handle是给 DMA 控制器用的总线地址。系统在底层会保证这块内存要么被映射为 non-cacheable要么由硬件一致性机制维护CPU 和 DMA 访问它时看到的始终是同一份数据。使用完成后调用dma_free_coherent释放dma_free_coherent(dev, size, cpu_addr, dma_handle);2.2 使用姿势与注意事项这个方案适合“长生命周期、CPU 和设备都会频繁访问”的缓冲区比如 DMA 描述符、控制结构体、稳定的数据缓冲池。我通常会在驱动的 probe 阶段分配remove 阶段释放而不是每次传输都临时申请避免分配开销和内存碎片。几个实操中的关键点先设置 DMA 掩码。dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))这类调用要放在分配之前。有些平台对一致性内存有地址上限不设置掩码dma_alloc_coherent可能直接失败。不要在原子上下文里分配大块内存。GFP_ATOMIC虽然可以调用但大尺寸分配容易失败应该提前在 probe 阶段准备好。CPU 访问性能会打折。non-cacheable 区域对 CPU 来说没有缓存加速如果这块内存主要用于大批量数据搬运且 CPU 很少碰就不适合用 coherence 方式应该走流式映射。这套方法的本质是“釜底抽薪”不解决 Cache 写回时机而是直接取消这块内存的缓存属性。代价是 CPU 访问速度下降但换来的是软件逻辑简单可靠。对描述符、控制块这类高频小数据而言这笔交易非常划算。3. 第二招流式映射 在正确时机同步3.1 流式映射三件套map / sync / unmap很多场景里数据缓冲区是从kmalloc来的普通内存不可能全部改成 non-cacheable。这时候要用流式 DMA 映射streaming DMA mapping把 Cache 维护的责任交给明确的 API 调用。最典型的三步流程dma_addr_t addr; addr dma_map_single(dev, buf, len, DMA_TO_DEVICE); if (dma_mapping_error(dev, addr)) { // 映射失败处理 } // 将 addr 写入 DMA 控制器的源地址寄存器启动传输 write_dma_reg(..., addr); start_dma(); // DMA 传输完成后 dma_unmap_single(dev, addr, len, DMA_TO_DEVICE);dma_map_single做的事情远比“翻译地址”多它会根据方向把 Cache 里属于这块缓冲区的脏数据写回内存或者让对应的 Cache 行失效。dma_unmap_single在传输结束时做反向操作。名字听着简单但方向参数填错或者调用时机错误行为就会完全拧着来。3.2 方向选择与四个关键时机方向参数有三种搞清楚它们比记 API 本身重要DMA_TO_DEVICECPU 写、DMA 读。map 时内核做 Cache clean即把脏行写回内存。DMA_FROM_DEVICEDMA 写、CPU 读。map 时内核做 Cache invalidate即丢弃对应的 Cache 行让 CPU 后续读的时候重新从 DRAM 加载。DMA_BIDIRECTIONAL两边都可能读写。map 时 clean invalidate 都做但代价是每次都要等内存同步吞吐会受影响。对于复用缓冲区的场景不能用一次 map 就撒手不管。每次传输方向切换时要用dma_sync_single_for_device和dma_sync_single_for_cpu补做同步// 初始化时做一次映射 addr dma_map_single(dev, buf, len, DMA_BIDIRECTIONAL); // 每一轮CPU 写数据准备交给 DMA dma_sync_single_for_device(dev, addr, len, DMA_TO_DEVICE); start_dma(); // DMA 完成中断里CPU 读取数据前 dma_sync_single_for_cpu(dev, addr, len, DMA_FROM_DEVICE); // 现在可以安全地访问 buf 了四个关键时机总结时机调用作用DMA 启动前CPU 刚写完数据dma_sync_single_for_device(..., DMA_TO_DEVICE)把 CPU 写的脏 Cache 写回内存DMA 启动前设备即将写内存dma_sync_single_for_device(..., DMA_FROM_DEVICE)丢弃 CPU 侧旧 Cache避免后续冲突DMA 完成中断CPU 读取数据前dma_sync_single_for_cpu(..., DMA_FROM_DEVICE)让 CPU 看到 DMA 写入的最新数据DMA 完成中断数据不再被 CPU 使用时dma_sync_single_for_cpu(..., DMA_TO_DEVICE)确保 CPU 侧视图一致方便复用3.3 缓冲区复用与多核的坑流式映射最容易出问题的是“复用同一块缓冲区”的场景。我见过一个很典型的案例一块 4KB 发送缓冲区CPU 填完数据启动 DMA第一轮正常第二轮开始外设收到的还是第一轮的数据。代码里反复查发现缓冲区地址没变、长度没变、DMA 寄存器没配错唯一的问题是第二轮启动前没有再调dma_sync_single_for_device。第一轮完成后新数据又被 CPU 写进 CacheDMA 再去 DRAM 读读到的还是第一轮写回内存的旧内容。多核环境还有更隐蔽的问题。如果写缓冲区的 CPU 和发起 DMA 的 CPU 不是同一个核Cache 行的 dirty 状态可能停留在另一个核上。单纯在当前核做 sync不一定能把远程核的脏行刷回来。这种情况没有特别取巧的办法只能靠正确使用 DMA API 并配合内存屏障或者干脆把缓冲区每个核独立分配避免共享。4. 第三招从内存属性和硬件层面釜底抽薪4.1 为什么 API 选对了还是会出问题有一类问题很让人恼火代码里dma_alloc_coherent也用了、sync 也调用得很勤但偶发还是读到旧数据。这时候就要怀疑是不是有“中间层”介入。常见的原因有几个缓冲区被重新映射成 cacheable比如有人拿到物理地址后又用ioremap或内核虚拟地址去访问它导致同一块内存有多个映射属性还是普通缓存型。有二次 DMA 或硬件加速器某些 SoC 里外设到内存之间还夹着一层硬件它的内部缓存也需要刷新单纯刷新 CPU 侧的 Cache 完全无效。IOMMU/SMMU 生效后地址翻译页表属性不一致设备侧通过 IOMMU 访问内存IOMMU 页表里对这块区域标成 cacheable那么即便 CPU 侧已经 clean设备经 IOMMU 读到的也可能是旧数据。4.2 预留专用 non-cacheable 内存区一种常见做法是在设备树里预留一块 DMA 专用的内存并让驱动声明为一致性 DMA 堆。例如reserved-memory { #address-cells 1; #size-cells 1; dma_reserved: dma_buffer80000000 { compatible shared-dma-pool; reusable; size 0x100000; alignment 0x1000; }; };配套在驱动里用of_reserved_mem_device_init或dma_declare_coherent_memory做绑定后续dma_alloc_coherent就会从这块预留区域分配。这种方式适合 DMA 控制器对地址范围有硬性要求、或者默认一致性内存堆不够用的场景。它把“一致性”从软件约定变成了内存布局的硬约束调试起来反而简单。4.3 弄清谁在“缓存”而不是盲目加 sync我个人的经验是如果常规 API 都用了问题还在就不要再反复调 sync 接口“碰运气”。应该静下心来看整体内存路径。一个有效的方法是查系统里所有映射这块缓冲区的地址逐一确认页表属性。在内核里可以通过ptdump、/sys/kernel/debug/kernel_page_tables类似的调试接口检查 PTE 标志位确认区域是否为 non-cacheable。如果设备侧经过了 IOMMU那么 IOMMU 驱动侧映射也需要显式配置属性不能只盯着 CPU 的 MMU。另外不要自己手工调用架构相关的 cache 维护函数除非确认那一段代码永远不会跨平台。通用 DMA API 里已经处理了不同架构的差异自己插入cache_op之类的函数等于是绕过内核的一致性协议很容易制造出更难排查的问题。5. 调试一个“DMA 读旧数据”问题我的排查思路5.1 第一步判断方向再动手改代码拿到这类问题我通常不急着改代码先做两个小实验确定方向。实验一在 DMA 启动前用printk打印缓冲区内容。如果打印出来的内容已经是最新的说明 CPU 侧写到了 Cache再抓设备实际收到的数据如果设备收到的是旧数据就能确认是 CPU → DMA 方向的一致性缺失。实验二DMA 完成中断里打印缓冲区内容。如果 DMA 明明把新数据传进了同一块区域CPU 打印出来却是旧的方向就是 DMA → CPU需要 invalidate。方向判断错了代码是越改越乱。我曾经见过有人把DMA_TO_DEVICE和DMA_FROM_DEVICE来回换越改越玄其实第一步就应该先确认哪一侧看到的数据是旧的。5.2 调试验证与常见错误速查下面这张表是我平时排查时对照用的基本覆盖了这个领域最典型的几种情况现象可能原因排查重点DMA 发出去的数据总是旧内容CPU 写完数据后没做 Cache clean检查 map/sync 时机是否在 DMA 启动前调用CPU 读回的数据是旧内容DMA 写完内存后没做 Cache invalidate确认完成中断里先 sync_for_cpu 再访问缓冲区偶发出错与 CPU 负载相关Cache 脏行写回时序不确定或屏障缺失检查是否有内存屏障能否改用一致性 DMA更换 CPU 核后问题出现不同核的 Cache 行状态迁移导致一致性丢失确认共享缓冲区是否真的必须考虑核间通信开启 IOMMU 后数据全是旧值IOMMU 页表缓存属性未配置检查设备侧映射属性CPU 侧 sync 无法覆盖DMA API 调试打印报错map/unmap 次数不配对或方向用错开启 CONFIG_DMA_API_DEBUG运行后看 dmesg5.3 我的几个调试习惯和建议第一个习惯内核配置里直接打开CONFIG_DMA_API_DEBUG。这个选项能检测出大量 DMA API 使用错误包括 map/unmap 不配对、映射后越界访问、用没映射的地址启动 DMA。开发阶段开着它能省下很多看波形的时间。第二个习惯保证缓冲区地址和长度对齐到 Cache Line。以常见的 64 字节 Cache Line 为例如果缓冲区地址没有对齐sync 操作会波及到相邻的合法数据在DMA_FROM_DEVICE方向尤其危险invalidate 可能把还没写回内存的脏数据直接丢掉。与其赌硬件宽容不如在分配时用ALIGN强制对齐或者让 DMA 只搬运缓冲区中间对齐的部分。第三个习惯不在中断上下文里做复杂操作。DMA 完成中断里调用dma_sync_single_for_cpu是必要的但不要接着去做耗时的数据处理更不要再发起新的 map 操作。正确姿势是中断里只做必要同步和唤醒数据处理放到线程或 workqueue 里。6. 写在最后的一点个人体会这三招不是互相替代的关系而是三个层面的防线。一致性 DMA 映射解决的是“怎么分配这块内存”流式映射的 sync 解决的是“每次传输怎么维护 Cache”内存属性的配置解决的是“整个系统还有没有隐藏的缓存路径”。调试时从第一个开始排查通常能解决八成问题剩下两成就要靠对内存路径的理解去逐个击破。我个人的习惯是能不用流式映射就不用除非明确知道这块缓冲区会被高频复用且 CPU 很少碰。把所有缓冲区按“谁拥有、谁访问、何时访问”列成一张表再决定用哪种映射方式比写到哪算哪要稳得多。特别是以后还要维护的驱动别人接手时看到代码里全是 sync 但没注释八成会改出问题来。如果刚开始接触这个问题建议先在一块测试板上反复做小实验一块缓冲区分别用三种方式做一遍 CPU 到 DMA、DMA 到 CPU 的传输把每种方式的 latency、正确性、代码复杂度记录下来。这个实验做下来你对 Cache 一致性的理解会比看十篇文章都深。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询