写定制驱动这半年我发现自己经常干的一件事就是打开调试日志盯着 DDR 里的描述符心里默念“你写的地址到底对不对”。尤其是在调 XDMA、GD32 网卡、以及 RK3588 的 ETH 这类外设时一旦报出 “failed to reset the DMA” 或者接收描述符里出现乱码十有八九是地址这个事没搞明白。今儿 Day 9咱们就把这层窗户纸捅破从 DMA 描述符里写的是什么地址到设备眼里的内存究竟长什么样用实际代码和踩坑记录把它讲透。这个系列面向的是搞 AI Infra、写驱动、调定制板卡的同学。如果你正被 DMA 连续请求、RX 描述符刷屏、外设内存映射看着一头雾水那么这篇内容大概率能让你少走几周的弯路。全文不和你兜圈子直接上干货。1. 先搞清楚DMA 描述符里写的到底是什么地址这个话题看着简单但我在技术群里问了一圈至少有一半人答不对。有人脱口而出“物理地址”有人纠正说“应该是总线地址”还有人直接说“反正把virt_to_phys得到的数写进去就对了”。这几个答案都有点意思但又都不完整。1.1 设备眼中的内存没有进程只有全局地址你的 CPU 平时看到的地址是经过 MMU 翻译之后的虚拟地址。每个进程都有自己的页表你以为的“0x10000”在物理上可能是 DDR 的任何一个位置。但是对于 DMA 控制器和外设来说事情简单粗暴得多它们拿到一个地址就直接往总线上发去访问这个地址对应的内存。它眼里没有进程、没有页表、没有虚拟地址这回事。打个比方CPU 像是个拿着地图查路的人地图上标注的“人民路 6 号”是虚拟地址真正的位置还得靠门牌和路网翻译。DMA 设备不一样它就像一个只看经纬度的快递无人机你给它 31.23, 121.47它就往那儿飞没人和它谈门牌号的浪漫。描述符里写的地址就是这种“经纬度坐标”。所以第一层结论先定下来描述符里绝对不写 CPU 虚拟地址写了必崩。这算是所有 DMA 编程里最基础的禁令。1.2 物理地址不是你以为的那个物理地址那写物理地址行不行行但对一半。这里有个隐蔽的点CPU 侧的“物理地址”和外设总线侧能直接寻址的地址并不总是同一个数字。大部分 ARM SoC 里DMA 设备看到的内存地址空间是一个被叫作“总线地址”Bus Address的概念。在没有 IOMMU/SMMU 的老方案里总线地址等于物理地址所以很多人早年写驱动直接virt_to_phys塞进去也能跑通。但一旦你上了带 SMMU 的平台或者设备挂在 PCIe 桥后面总线地址就和物理地址脱离了。设备报给驱动的地址是经过 IOMMU 翻译之后的结果驱动必须用dma_map_single或者dma_alloc_coherent这类 API 拿到的返回值才是设备能认的地址。这就能解释一个很典型的现象你在调试 XDMA 时描述符里明明填的是物理地址可硬件就是死等状态DMA 不搬数。原因往往就是你绕过了内核 DMA API自己拿物理地址填了进去而 SMMU 那头根本不认识这个数字。统一一下概念我建议你记住下面这张相当于“翻译对照”的表格地址类型谁在用怎么获得能不能写进描述符CPU 虚拟地址CPU、进程指针绝不能写CPU 物理地址CPU 访问 / 内核页表virt_to_phys()无 SMMU 时可以有 SMMU 时别用总线地址 / DMA 地址DMA 控制器、外设dma_alloc_coherent/dma_map_single这才是最规范的写法所以你问我“DMA 描述符里写的是什么地址”标准的完整回答是它写的是该设备所看到的、可用于 DMA 访问的地址这个地址一般等于物理地址但经过 IOMMU 后未必相等。2. 描述符的完整长相与编排逻辑说完了地址类型咱们来看看描述符本身。很多做 AI Infra 的同学平时不接触这一层只觉得“描述符就是一块内存填好地址和长度就行”。但真到调板子时描述符字段错一位硬件就有可能把数据写到天王老子那边去。2.1 一个典型描述符的字段结构以比较常见的 DMA 控制器为例一个描述符至少有下面这几个字段地址字段存放 DMA 要搬运的目标内存地址也就是咱们上面讨论的“总线地址”。长度字段本次传输的字节数。有一些控制器要求长度按 4/8/16/32 字节对齐不满足直接报错。控制字段指定传输方向、中断使能、是否是链尾、是否要触发下一次描述符等。状态字段传输结束后硬件回写完成标志、错误标志或者实际传输字节数。拿网上经常搜到的“XDMA 描述符”来说XDMA 有 H2C 和 C2H 两条搬运路径描述符里除了地址和长度外还会涉及一个关键字段BTTBytes To Transfer。这个字段在老版本驱动里出现过溢出问题一旦赋错DMA 一次搬运的字节就变了数据错位症状特别像“描述符被随机改写过”。你可以把描述符理解成外卖订单地址是收货人地址长度是点的菜量控制字段是加辣/免辣/不放香菜状态字段是骑手跑完单之后回的“已送达”。这套订单的格式是商家驱动和骑手DMA 引擎事先约定好的谁都不能改格式。2.2 环形队列描述符不是单个用的接口上见过的 DMA 描述符大多是梳状排列的也就是一个环形队列Ring。硬件控制器有一个“当前正在处理”的指针驱动把一批描述符写好之后会更新控制器的 tail 或 head 指针让它知道自己有新工作可做。这个环形队列的设计原因并不复杂DMA 引擎处理完一个描述符后如果直接结束传输那么高吞吐场景会频繁触发中断、频繁重新配置引擎根本跑不满一块网卡或 NVMe SSD 的带宽。改成环形队列之后驱动可以一次性交给硬件 N 个“任务”硬件闷头把这一圈干完再通知 CPU 处理结果。这也解释了为什么你在搜“DMA continuous requests”时会看到有人为了连续 DMA 传输会一次性铺几十上百个描述符。这里有个容易犯的错环形队列的每一项描述符都要占据一块物理连续的内存。如果你用kmalloc一个个分配往往分配出来的地址不连续环形队列就会出现“断裂”。所以驱动里通常的做法是一次性分配一整块描述符内存再用 struct 数组去切分绝对不要图省事搞多次kmalloc。2.3 地址字段为什么要对齐 64 字节这类细节描述符字段看着简单但很多 DMA 控制器接收到的地址如果不对齐会出现效率骤降或者直接触发Misaligned Address错误。之前在 GD32E230 的 ADC-DMA 采集上看到过类似问题描述符指向的数据缓冲区地址是 3 字节对齐的结果 DMA 搬运回来的数据隔几笔就乱一笔。原因在于不少 DMA 引擎的读突发burst长度是按总线位宽或缓存线大小设计的。地址没按 32/64 字节对齐硬件就需要用多个非对齐事务组合传输某些实现会直接丢弃末尾几个字节数据。所以在分配 DMA 缓冲区时最好带上ALIGN宏或者在设备树里声明dma-ranges时预留对齐余量。我的个人建议是分配 DMA 内存一律按 64 字节对齐打底长度按 4 字节倍数尽量拉满到 32 倍数。这不是强迫症是为了防止踩到不同硬件平台的对齐雷区。3. 从驱动代码看 DMA 地址怎么一步步“写到”描述符理论讲完咱们直接上一段实际流程。这是我在调试一块 PCIe DMA 采集卡时的真实路径完整的收发流程让很多朋友看了一头雾水这里把它拆开揉碎。3.1 内核给出的 DMA 地址是从哪来的第一步驱动里要准备一块专门给设备用的内存区。最常见的是dma_alloc_coherentdma_addr_t dma_handle; void *cpu_addr; cpu_addr dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL);这个 API 一次给了两个地址cpu_addr是 CPU 能访问的虚拟地址dma_handle是设备能认的 DMA 地址。别小看这个函数它背后做了不少事情它可能会分配带 IOMMU 映射的页、可能敲门进入一致性内存池、可能铺好 cache 同步所需的底层钩子唯一目的就是保证 CPU 写入的数据让设备 DMA 一方能看到。到了这一步你手里已经有了一个“设备认识的地址”描述符里要填的就是dma_handle。3.2 分配一致性映射与流式映射的区别不过dma_alloc_coherent只适合长时间持有、反复用作 DMA 缓冲的场景。如果你只是把网络报文或者磁盘块映射给设备用一下更推荐流式映射dma_addr_t addr; addr dma_map_single(dev, virt_addr, len, DMA_TO_DEVICE); // 填描述符... // 传输完成后 dma_unmap_single(dev, addr, len, DMA_TO_DEVICE);这个操作的核心是拿到 DMA 地址并把缓存刷新到内存。很多新手困在一件事上为什么我把数据写进缓冲区了设备 DMA 读到的还是旧数据原因多半是没有dma_map_single或者没有在写入后调用dma_map_single申请地址/刷缓存。用dma_alloc_coherent拿到的内存天然就是“一致映射”不需要每次手动同步 cache代价是分配开销大。流式映射则灵活但要严格配对 map/unmap顺序错了数据就花了。3.3 一个完整的数据发送流程这里以网卡 TX 方向为例给大家展示一个最小可运行的伪代码流程struct eth_desc *desc ring-desc_head idx; desc-addr dma_map_single(dev, skb-data, skb-len, DMA_TO_DEVICE); desc-len skb-len; desc-ctrl TX_DESC_CTRL_EN | (idx ring-last ? TX_DESC_CTRL_END : 0); wmb(); // 保证前面的写入对设备可见 writel(tail_new_idx, ring-tail_reg);这段流程里有一个关键操作wmb()写屏障。DMA 引擎通过总线访问内存时可能因为 CPU 乱序执行导致“地址已经写到描述符”的指令还没真正落到内存硬件就开始读取描述符。多数 DMA 控制器需要在更新 tail 寄存器前加wmb()确保描述符数据全部 reachable。常见错误就是漏掉wmb()症状表现为“跑了几十次才偶尔出错”排查起来极其头大。4. 我在实际项目里踩过的 DMA 地址坑写再多的结构理论都不如直接说几个真实场景让大家对照自查。下面这三个问题我分别在 RK3588 平台、Xilinx XDMA 和 GD32 上遇到过网络热词里也有人反复搜到了它们。4.1 “failed to reset the DMA”到底是什么关于“RK3588 ETH 报 failed to reset the DMA”这个错误我第一次见时也愣了一下。它并不是说 DMA 控制器损坏而是驱动在启动或链路复位时往 DMA 控制寄存器写复位命令硬件在规定时间内没有应答或者复位后标志位没有回到预期值。实际排查中出现这个报错的前置条件通常有两个一是描述符队列里有残留的异常地址导致 DMA 状态机卡在某个非法状态二是设备树里dma-ranges或者iommu-map配错导致内核把一个无效的总线地址写进了 DMA 配置寄存器。我那次解决的路径是把驱动卸载后手工读取 DMA 状态寄存器发现DMACSR里的RBReceive Busy位被卡住。清不掉。后来重新初始化描述符环形队列并确保通知硬件的 tail 指针是 0再触发软复位问题就消失了。所以遇到这个报错别急着换硬件先把描述符队列清干净再复位。4.2 高地址内存与 32 位设备老一点的外设比如某些 32 位 PCIe 桥设备DMA 地址只能支持 32 位范围也就是 4GB 以下。但现在的服务器/开发板内存动辄 8GB/16GB内核给驱动分配的dma_handle很可能落在 4GB 以上直接填进描述符就会出问题。这个场景下驱动要做的是让设备侧知道自己的地址能力比如在设备树里声明dma-ranges 0x0 0x0 0x0 0x0 0x0 0xffffffff;或使用DMA_BIT_MASK(32)做掩码设置。我当年在调一块 PCIe 数据采集卡时发现有时 DMA 能跑有时数据完全乱掉查了两天才意识到系统插了 16GB 内存默认 DMA 掩码是 64 位但设备实际只能处理 32 位描述符地址。需要在驱动初始化阶段做一次 mask 检查if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) { dev_err(dev, device cant do 32-bit DMA\n); return -EIO; }许多“偶发性”数据错误根源就在这里。排查问题时可以优先确认一下。4.3 不一致描述符里看到的全 0 地址或错乱数据还有一种特别玄学的故障打开调试寄存器发现描述符地址字段确实是 0但驱动明明设置了addr字段。后来发现是 cache 缓存没有回写。特别是dma_alloc_coherent的一致性映射只保证“内核帮你处理 cache 同步”但如果你用自己的方式将这块内存同时也做了普通映射然后往普通映射地址写入数据就有可能出现缓存不同步。这种情况在 GD32 这类单片机上尤其明显因为它的 ART 缓存和各总线主设备对内存可见性要求非常苛刻。建议在往描述符写入关键字段后无论是 ARM 还是 RISC-V都要按体系结构要求加wmb()/dmb()必要时调用dma_sync_single_for_device做一次显式同步。5. 调试 DMA 地址问题的几条实用路径最后一章把我这么多年定位 DMA 问题的方法论分享给你。不搞一把梭按顺序排查通常都能在小时内锁定问题。5.1 从 /proc/iomem 和内核日志确认地址空间你写完驱动第一件事不是上设备而是先cat /proc/iomem确认目标外设映射的物理地址范围。然后在内核里打印dma_handle的实际值确认它是否落在外设可寻址的范围之内。如果设备树里配了iommu-map还可以检查 SMMU 是否把设备侧地址重新映射到了另一个地址段这样调试时心里就有底。5.2 加打印与 ftrace 追踪 dma_map_ops在驱动里加dev_info打印dma_handle、物理地址、虚拟地址三者之间的对应关系是非常正常的操作。另外打开内核的CONFIG_FTRACE可以跟踪dma_map_page、dma_unmap_page等调用过程确认是不是在 map 阶段拿到异常地址。哪怕是临时加几千行日志也比盲目看逻辑靠谱。DMA 问题最怕的就是猜。5.3 物理层抓包DMA 数据验证如果手上有逻辑分析仪或总线协议分析仪可以挂在总线侧观察描述符读取地址和数据搬运地址。很多 DMA 控制器会把描述符读事务和实际搬运事务在总线上以可识别的模式呈现直接抓总线你就能看到硬件到底在尝试访问哪个地址。这个手段最硬核也最准。没有总线分析仪的也可以绕道用“数据验证”法把 DMA 接收缓冲区填上特定模式的种子数据比如 0xAA、0x55然后让设备 DMA 写一块内存写完后再 dump 出来检查目标地址区域里有没有对应模式。如果数据出现在了错误地址区域那你描述符里的地址字段 100% 有问题。我在实际调板时还喜欢在驱动里加一个 debugfs 接口用来随时 dump 描述符环形队列的内存内容。遇到问题直接cat /sys/kernel/debug/dma_ring/desc看一眼描述符里存着的地址对不对再和dma_handle比对基本一眼就能定位是驱动问题还是硬件问题。最后分享一个排查小技巧如果你经常调 DMA 相关驱动一定要养成一个好习惯在描述符初始化阶段往每一个描述符的“填充区”写入一个魔术脚印比如0xDEADBEEF。这样一旦硬件或软件改了描述符里的意外字段后面 dump 时就能看到哪个描述符被动过、被谁动过的痕迹。这招帮我在一次多核竞争问题上少排查了两天。DMA 描述符地址这件事说白了就是“设备眼中的内存是一张没有分层的地图”。搞清物理地址、总线地址、虚拟地址三者之间的关系再配合规范的内核 API 来管理 DMA 映射这个领域的大部分问题都能迎刃而解。希望这些内容能帮你避开我当年踩过的坑把更多的调试时间留给真正复杂的问题。
