DMA 调试:缓存一致性问题,比传输失败更阴

发布时间:2026/7/3 1:49:16
DMA 调试:缓存一致性问题,比传输失败更阴 DMA 调试缓存一致性问题比传输失败更阴一、深度引言DMA 能解放 CPU也能把调试逼疯DMADirect Memory Access是现代嵌入式系统中不可或缺的硬件机制。它让外设直接与内存交换数据CPU 不需要逐字节搬运从而把算力留给更有价值的任务。一个典型的视频采集场景摄像头通过 MIPI CSI 输出数据流DMA 引擎将每一帧搬进 DDRCPU 只在前处理阶段介入。没有 DMA640×480 30fps 的 YUV 数据就能把 Cortex-A7 的中断全部消耗在 memcpy 上。但 DMA 调试是嵌入式开发中最容易让人怀疑人生的领域。问题不在传输失败——传输失败至少有个明确的错误码。真正折磨人的是数据看起来是对的但偶尔会错连续跑 1000 次都正常第 1001 次 image 上出现几行错位压力测试时没问题温度升高后边沿出现随机位翻转。这类问题的根因绝大多数指向同一个地方缓存一致性Cache Coherency。CPU 有 L1/L2 CacheDMA 控制器直接操作物理内存。当 CPU 写了一个 buffer 交给 DMA 去发送但数据还在 CPU Cache 中未刷到 DDR——DMA 读到的就是旧数据。反过来DMA 把接收数据写进 DDR但 CPU 的 Cache 中还缓存着这个地址的旧值——CPU 读到的也是旧数据。数据没有丢也没有损坏但 正确版本的数据在错误的地方这就解释了为什么问题看起来是随机的。本文从 Cache Line 对齐、DMA mapping API 选型、Scatter-Gather 传输和完整环形 DMA 接收代码四个方面把 DMA 调试的核心方法系统化。二、原理剖析缓存一致性、DMA API 选型与 Scatter-Gather 传输2.1 Cache Line 对齐的隐性影响CPU Cache 以 Cache Line通常 32 或 64 字节为最小操作单位。当 CPU 写入一个字节时整个 Cache Line 被标记为 dirty。如果 DMA buffer 的起始地址不是 Cache Line 对齐的会出现一个经典问题DMA buffer: [ ... | A | B | C | D | ... ] ← DMA 写入新数据 CPU cache: [ ... | a | b | c | d | ... ] ← CPU 缓存旧数据即使 DMA 完成了传输CPU 读取 buffer 时命中的是 Cache 中的旧值cache hit。只有等到 Cache 被逐出或显式 invalidateCPU 才看到新数据。但如果 buffer 和另一个变量共享同一个 Cache LineCache Line 边界: [ DMA_buf_tail | unrelated_var ]对unrelated_var的写操作触发 Cache Line 回写可能意外覆盖 DMA 刚刚写入的数据。这种隔空打击极难复现——它只发生在特定地址布局、特定 Cache 状态和特定写操作时序的交叉点上。2.2 dma_alloc_coherent vs dma_map_singleLinux DMA API 提供两种基本范式一致性 DMACoherent DMAvoid *buf dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL);从内核的 coherent pool 中分配内存硬件保证这段内存的 CPU Cache 与 DMA 访问之间的一致性通常在页表层面标记为 non-cacheable 或使用硬件一致性协议。优点是无需手动 sync缺点是 coherent pool 容量有限通常几 MB分配大 buffer 可能失败。流式 DMAStreaming DMAdma_addr_t dma dma_map_single(dev, buf, size, direction); // ... 触发 DMA 传输 ... dma_unmap_single(dev, dma, size, direction);使用普通 kmalloc 分配的内存通过dma_map_single建立映射。传输前后 kernel 自动处理 cache flush/invalidate。优点是不受 coherent pool 限制缺点是需要手动管理映射生命周期且每次 map/unmap 有 cache 操作开销。选择策略小 buffer64KB且频繁访问使用 coherent DMA避免反复 flush。大 buffer1MB或偶尔使用使用 streaming DMA节约 coherent pool。需要 CPU 和 DMA 同时读写同一 buffer必须 streaming DMA 精确的 sync 控制。2.3 Scatter-Gather 传输当数据在物理内存中不连续时用户态 buffer、网络包碎片等Scatter-Gather DMA 允许一个 DMA 事务操作多段不连续的物理内存struct scatterlist sg[N]; sg_init_table(sg, N); sg_set_buf(sg[0], buf1, len1); sg_set_buf(sg[1], buf2, len2); int n dma_map_sg(dev, sg, N, direction);DMA 控制器逐一处理 scatterlist 的每个 entry无需 CPU 把不连续的 buffer 拷贝到一个连续大 buffer 中。对视频流、音频流和网络数据包处理Scatter-Gather 可以消除大量不必要的 memcpy。flowchart TD A[CPU 分配 buffer\n(kmalloc/vmalloc/用户态)] -- B{buffer 大小\n和访问模式} B --|小 buffer,高频读写| C[dma_alloc_coherent\ncoherent pool] B --|大 buffer,低频| D[dma_map_single\nstreaming mapping] B --|不连续物理内存| E[Scatter-Gather\nsg_init_table dma_map_sg] C -- F{DMA 方向} D -- F E -- F F --|DMA_TO_DEVICE| G[CPU→设备\nmap前: flush dcache\nunmap后: 无操作] F --|DMA_FROM_DEVICE| H[设备→CPU\nmap前: invalidate dcache\nunmap后: invalidate dcache] F --|DMA_BIDIRECTIONAL| I[双向\nmap前: flush\nunmap后: invalidate] G -- J[启动 DMA 传输] H -- J I -- J J -- K[DMA 完成中断] K -- L[dma_unmap_* 或 sync] L -- M[CPU 安全访问数据]2.4 环形 DMA BufferRing DMA对于连续数据流音频、视频、传感器数据使用环形 DMA buffer 是最常见的模式分配 N 个固定大小 bufferDMA 循环填充CPU 轮流处理。环形 buffer 的核心挑战是读写指针的同步——DMA 写指针由硬件维护CPU 读指针由软件维护两者必须在没有锁的情况下正确同步锁会阻塞中断上下文。三、代码实现完整环形 DMA 接收代码/** * 环形 DMA 接收引擎 * * 适用场景连续数据流接收音频、视频、高速传感器 * 设计要点 * 1. Buffer 数量为 2 的幂利用位掩码代替取模 * 2. 每个 buffer 独立 dma_map互不影响 * 3. 读写指针均由 DMA 硬件状态推导无锁设计 * 4. 完整的错误处理和超时机制 */ #include linux/module.h #include linux/kernel.h #include linux/dma-mapping.h #include linux/dmaengine.h #include linux/completion.h #include linux/circ_buf.h #include linux/interrupt.h #include linux/err.h #define RING_BUF_COUNT 16 /* 必须是 2 的幂 */ #define RING_BUF_SIZE 4096 /* 每个 buffer 大小 */ #define RING_MASK (RING_BUF_COUNT - 1) #define DMA_TIMEOUT_MS 1000 /* Buffer 描述符 */ struct dma_buf_desc { void *cpu_addr; /* CPU 虚拟地址 */ dma_addr_t dma_addr; /* DMA 总线地址 */ size_t size; /* 实际传输大小 */ bool ready; /* CPU 已消费完毕 */ }; /* 环形 DMA 引擎 */ struct ring_dma_engine { struct device *dev; struct dma_chan *chan; struct dma_buf_desc bufs[RING_BUF_COUNT]; volatile unsigned int write_idx; /* DMA 下一次写入的 buffer 索引 */ volatile unsigned int read_idx; /* CPU 下一次读取的 buffer 索引 */ struct completion transfer_done; struct dma_async_tx_descriptor *active_desc; bool running; /* 统计信息 */ unsigned long total_bytes; unsigned long overrun_count; unsigned long dma_error_count; }; /** * 初始化环形 DMA 引擎 * param dev 设备指针用于 DMA mapping * param chan DMA 通道通过 dma_request_chan 获取 * return 成功返回引擎指针失败返回 ERR_PTR */ struct ring_dma_engine *ring_dma_init(struct device *dev, struct dma_chan *chan) { struct ring_dma_engine *eng; int i; if (!dev || !chan) return ERR_PTR(-EINVAL); if (RING_BUF_COUNT (RING_BUF_COUNT - 1)) { dev_err(dev, RING_BUF_COUNT 必须是 2 的幂\n); return ERR_PTR(-EINVAL); } eng kzalloc(sizeof(*eng), GFP_KERNEL); if (!eng) return ERR_PTR(-ENOMEM); eng-dev dev; eng-chan chan; init_completion(eng-transfer_done); /* 为每个 buffer 分配 coherent DMA 内存 */ for (i 0; i RING_BUF_COUNT; i) { eng-bufs[i].cpu_addr dma_alloc_coherent(dev, RING_BUF_SIZE, eng-bufs[i].dma_addr, GFP_KERNEL); if (!eng-bufs[i].cpu_addr) { dev_err(dev, 无法分配 DMA buffer[%d]\n, i); /* 回滚已分配的内存 */ while (--i 0) { dma_free_coherent(dev, RING_BUF_SIZE, eng-bufs[i].cpu_addr, eng-bufs[i].dma_addr); } kfree(eng); return ERR_PTR(-ENOMEM); } eng-bufs[i].size 0; eng-bufs[i].ready true; /* 初始状态全部可被 DMA 写入 */ } eng-write_idx 0; eng-read_idx 0; eng-running false; dev_info(dev, 环形 DMA 引擎初始化完成: %d x %d bytes\n, RING_BUF_COUNT, RING_BUF_SIZE); return eng; } /** * 提交单个 buffer 的 DMA 传输请求 * return 0成功, -EBUSY队列满, 其他错误 */ static int ring_dma_submit_one(struct ring_dma_engine *eng) { struct dma_buf_desc *buf; struct dma_async_tx_descriptor *desc; dma_cookie_t cookie; unsigned int idx; idx eng-write_idx; buf eng-bufs[idx]; /* 检查该 buffer 是否已被 CPU 释放 */ if (!buf-ready) { /* 环形队列满CPU 消费跟不上 */ eng-overrun_count; return -EBUSY; } buf-ready false; buf-size 0; /* 准备 DMA 传输描述符 */ desc dmaengine_prep_slave_single(eng-chan, buf-dma_addr, RING_BUF_SIZE, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); if (!desc) { dev_err(eng-dev, dmaengine_prep_slave_single 失败\n); buf-ready true; eng-dma_error_count; return -EIO; } desc-callback NULL; /* 使用 completion 同步 */ desc-callback_param eng; cookie dmaengine_submit(desc); if (dma_submit_error(cookie)) { dev_err(eng-dev, dmaengine_submit 失败: %d\n, cookie); buf-ready true; eng-dma_error_count; return -EIO; } dma_async_issue_pending(eng-chan); return 0; } /** * DMA 传输完成中断处理 * 硬件完成一个 buffer 的传输后驱动需要推进 write_idx 并提交下一个 buffer */ irqreturn_t ring_dma_irq_handler(int irq, void *data) { struct ring_dma_engine *eng (struct ring_dma_engine *)data; unsigned int next; if (!eng || !eng-running) return IRQ_NONE; /* 获取当前完成的传输信息 */ enum dma_status status dmaengine_tx_status(eng-chan, eng-chan-cookie, NULL); if (status DMA_COMPLETE) { /* 更新当前 buffer 的大小 */ /* 实际工程中从硬件寄存器读取实际传输字节数 */ eng-bufs[eng-write_idx].size RING_BUF_SIZE; /* 推进写指针 */ next (eng-write_idx 1) RING_MASK; eng-write_idx next; eng-total_bytes RING_BUF_SIZE; /* 提交下一个 buffer */ if (eng-running) { int ret ring_dma_submit_one(eng); if (ret ! 0 ret ! -EBUSY) { dev_err(eng-dev, 提交下一个 DMA buffer 失败: %d\n, ret); } } } complete(eng-transfer_done); return IRQ_HANDLED; } /** * 启动环形 DMA 传输 * 预填充 N-1 个 buffer留一个给 CPU 作为安全边界 */ int ring_dma_start(struct ring_dma_engine *eng) { int ret; if (!eng || eng-running) return -EINVAL; eng-running true; eng-read_idx 0; eng-write_idx 0; /* 预提交 (RING_BUF_COUNT - 1) 个 buffer */ for (int i 0; i RING_BUF_COUNT - 1; i) { ret ring_dma_submit_one(eng); if (ret ! 0) { dev_err(eng-dev, 预填充 buffer[%d] 失败: %d\n, i, ret); eng-running false; return ret; } } dev_info(eng-dev, 环形 DMA 启动: %d buffers 就绪\n, RING_BUF_COUNT - 1); return 0; } /** * 停止环形 DMA 传输 */ void ring_dma_stop(struct ring_dma_engine *eng) { if (!eng) return; eng-running false; dmaengine_terminate_sync(eng-chan); dev_info(eng-dev, 环形 DMA 停止: 总传输 %lu bytes, overrun%lu, error%lu\n, eng-total_bytes, eng-overrun_count, eng-dma_error_count); } /** * CPU 读取一个已完成的数据 buffer * param buf_out 输出指向 CPU 可读数据的指针直接引用 DMA buffer不拷贝 * param timeout_ms 超时时间 * return 实际数据长度0超时无数据负值错误 */ ssize_t ring_dma_read(struct ring_dma_engine *eng, void **buf_out, unsigned int timeout_ms) { unsigned int read_idx, write_idx; struct dma_buf_desc *buf; if (!eng || !buf_out || !eng-running) return -EINVAL; /* 等待数据可用write_idx ! read_idx */ unsigned long timeout msecs_to_jiffies(timeout_ms); unsigned long elapsed 0; while (elapsed timeout) { write_idx eng-write_idx; smp_rmb(); /* 内存屏障确保读到最新的 write_idx */ if (write_idx ! eng-read_idx) break; if (!eng-running) return -ESHUTDOWN; msleep(1); elapsed 1; } if (write_idx eng-read_idx) return 0; /* 超时无数据 */ read_idx eng-read_idx; buf eng-bufs[read_idx]; // 确保 DMA 写入对 CPU 可见coherent buffer 通常不需要但统一 sync 更安全 dma_sync_single_for_cpu(eng-dev, buf-dma_addr, buf-size, DMA_FROM_DEVICE); *buf_out buf-cpu_addr; ssize_t data_len buf-size; /* 消费完成后将其归还给 DMA */ buf-ready true; eng-read_idx (read_idx 1) RING_MASK; return data_len; } /** * 销毁环形 DMA 引擎释放所有资源 */ void ring_dma_destroy(struct ring_dma_engine *eng) { if (!eng) return; ring_dma_stop(eng); for (int i 0; i RING_BUF_COUNT; i) { if (eng-bufs[i].cpu_addr) { dma_free_coherent(eng-dev, RING_BUF_SIZE, eng-bufs[i].cpu_addr, eng-bufs[i].dma_addr); } } kfree(eng); }四、边界分析DMA 缓存一致性的七种阴间场景场景一Cache Line 共享导致的写覆盖。DMA buffer 的尾部 8 字节与某个内核变量的头部共享同一个 64B Cache Line。CPU 更新该变量时整个 Cache Line 被加载并标记 dirty随后逐出时将 DMA buffer 尾部覆盖为旧值。对策DMA buffer 必须是 Cache Line 对齐且大小是 Cache Line 的整数倍或使用 kmem_cache_create 专用 slab。场景二CONFIG_DMA_API_DEBUG 没开时的静默错误。没有启用 DMA debug 的情况下方向写错FROM_DEVICE 写成 TO_DEVICE、buffer 未 map 就传给 DMA、buffer 已释放但仍被 DMA 访问——这些错误不会立即崩溃只会在特定条件下表现为数据偶发错误。对策开发阶段始终启用CONFIG_DMA_API_DEBUGy。场景三IOMMU 映射后的地址空间隔离。启用 SMMU/IOMMU 后DMA API 返回的 dma_addr 不再是物理地址而是 IO 虚拟地址IOVA。如果驱动里用virt_to_phys硬算地址直接写入 DMA 控制器寄存器跳过了 IOMMU地址完全不匹配。对策永远通过 DMA API 获取 dma_addr永远不手动计算物理地址。场景四CPU 乱序访问和内存屏障。ARM 处理器是 weakly-ordered memory model。DMA 完成中断到来时CPU 可能还在执行中断前的指令——此时读取 DMA buffer 可能读到旧 Cache 值。即使 coherent buffer 也需要dma_sync_single_for_cpu或至少rmb()内存屏障。场景五burst 传输被 4K 边界截断。很多 DMA 控制器的 burst 传输不允许跨越 4K 物理页边界。如果 buffer 起始地址后的剩余空间不足一次 burst 长度DMA 可能静默截断传输或产生总线错误。对策分配 buffer 时确保物理地址连续用 coherent DMA 或 CMA并检查 burst 对齐。场景六non-cacheable 内存的性能损失。coherent DMA buffer 通常标记为 non-cacheable 或 write-combine。CPU 逐字节访问这类内存时性能极差每次访问都是 uncached read/write。如果 CPU 需要对 DMA buffer 做大量处理如校验和、解压缩应该先 memcpy 到 cacheable buffer 再操作。场景七多核 CPU 的 cache coherency 协议边界。不同 CPU 核的 L1 Cache 可能不一致。CPU0 的 L1 中有 buffer 的缓存CPU1 发起 DMA 传输并处理完成中断但 CPU1 的 L1 中没有该 buffer 的旧缓存——而 CPU0 在读取时命中自己的 L1 旧值。对策在 SMP 系统中buffer 的消费者和 DMA 中断处理应在同一 CPU 核心上或使用 coherent buffer 显式跨核 cache 维护。五、总结DMA 调试的核心挑战不在传输失败而在缓存一致性的静默欺骗。CPU Cache 和 DMA 直访内存之间的不一致会让数据在正确与错误之间随机摇摆。调试策略从简到繁先验证 buffer 的对齐和大小 → 确认 DMA API 使用正确coherent vs streaming→ 检查 map/unmap 方向 → 开启 DMA debug → 用硬件 trace如 DS-5/Trace32抓总线事务。工程上建立三个规则可以避免大部分问题DMA buffer 只通过 DMA API 分配/映射绝不裸用 kmalloc → DMA buffer 是 Cache Line 对齐且大小是 Cache Line 整数倍 → 每次 DMA 完成后显式 sync即使 coherent。连续传输 1000 次正确不是 DMA 稳能在各种 Cache 状态、温度、总线负载下持续正确才算稳。