DMA描述符地址填什么?IOVA、PA还是VA?

发布时间:2026/9/13 18:51:53
DMA描述符地址填什么?IOVA、PA还是VA? 1. 这个问题不是在考概念而是在照镜子DMA描述符地址到底映射到哪层内存视图“DMA 描述符里写的是什么地址”——这句提问乍看像教科书习题但只要你在RK3588上调试过网卡驱动、在GD32E230上抓过ADC数据紊乱、或者被failed to reset the dma卡住超过两小时你就知道这不是选择题而是设备启动失败前最后一行dmesg日志的破译密钥。我第一次真正理解这个问题是在调试一块基于AXI UART16550的FPGA板卡。当时DMA传输始终卡在descriptor链表第二项寄存器显示DMA_STATUS_DESC_ERROR。我把描述符里写的地址用ioremap映射进内核空间一查发现它指向的是一片全零内存。可明明dma_alloc_coherent()返回的地址是有效的memset()也写入了数据。后来翻了三天Xilinx PG044和ARM IOMMU spec才明白设备看到的地址根本不是CPU写进描述符的那个物理地址而是经过IOMMU页表翻译后的IO虚拟地址IOVA。这个认知差就是90% DMA相关疑难杂症的根源。关键词里没给具体词但热搜词已经暴露了真实战场rk3588eth、gd32e230 adc dma数据紊乱、axi uart16550采用dma传输、dma continuous requests……这些全是嵌入式AI加速场景下的高频故障点。它们共同指向一个被严重低估的事实在现代SoC中“物理地址”早已不是单一平面而是CPU视角、IOMMU视角、设备硬件视角三重叠影。DMA描述符里填的地址必须精确匹配设备所处的内存视图层级否则就像给快递员一张手绘地图却忘了告诉他这是火星坐标系。这个理解直接决定你能否快速定位问题如果你填的是__pa(virt_addr)得到的DRAM物理地址而设备背后连着IOMMU如RK3588的MMU-600那设备看到的就是完全错误的地址必然超时或乱码如果你填的是dma_map_single()返回的IOVA但忘记调用dma_sync_single_for_device()同步cache那设备读到的就是旧数据GD32E230的ADC采样值就可能突然跳变如果你用dma_alloc_coherent()分配内存却在描述符里填了virt_to_phys()转换结果那在ARM64非一致性cache架构下设备可能读到未刷出的dirty cache行。所以这个问题的本质不是背诵“DMA描述符存物理地址”的教条而是建立一套内存视图对齐检查清单。接下来我会用RK3588以太网驱动、GD32E230 ADC采集、AXI UART16550三个真实案例一层层剥开设备眼里的内存真相。提示所有后续分析都基于ARMv8-A架构主流Linux内核5.10x86平台逻辑类似但页表结构不同此处不展开。2. RK3588以太网驱动实录当failed to reset the dma撞上IOMMU地址错位RK3588的GMAC控制器Realtek RTL8211F PHY Rockchip GMAC是AI边缘设备常用组合。去年帮一家做智能巡检机器人的客户调试时系统启动后dmesg反复刷出rk_gmac2: failed to reset the dma网卡无法up。我们按常规流程检查了时钟、PHY链接、寄存器复位序列全部正常。直到把DMA描述符环的内存dump出来才发现问题藏在最不起眼的地方。2.1 描述符地址的三重身份解剖RK3588的GMAC控制器通过AXI总线连接其DMA引擎直连IOMMUMMU-600。这意味着设备看到的地址必须是IOMMU翻译后的IOVA。我们来看一段典型驱动代码// 错误示范直接使用物理地址 struct rk_gmac_dma_desc *desc dma_alloc_coherent(pdev-dev, size, phys_addr, GFP_KERNEL); desc-des0 cpu_to_le32(phys_addr); // ❌ 这里填的是DRAM物理地址 // 正确做法使用IOMMU映射后的IOVA dma_addr_t dma_handle; void *cpu_addr dma_alloc_coherent(pdev-dev, size, dma_handle, GFP_KERNEL); desc-des0 cpu_to_le32(dma_handle); // ✅ 这里填的是IOVA关键差异在于phys_addr和dma_handle的来源phys_addr来自dma_alloc_coherent()的第三个参数它是DRAM芯片上的真实物理地址如0x8000_0000起始dma_handle是同一个函数的第四个参数它是IOMMU为这段内存分配的IO虚拟地址如0x1000_0000设备DMA引擎只认这个。为什么dma_alloc_coherent()能同时给出两个地址因为它内部完成了三件事在DRAM中分配一片连续内存返回cpu_addr和phys_addr在IOMMU页表中创建映射将dma_handleIOVA映射到phys_addrPA确保这片内存的cache属性设置为non-cacheable或write-through避免cache一致性问题。注意dma_alloc_coherent()在ARM64上默认启用IOMMU即使你没显式配置。因为Linux内核在arch/arm64/mm/dma-mapping.c中会自动检测并绑定IOMMU domain。如果你在dmesg里看到iommu: Adding device rk_gmac2 to group 12就说明IOMMU已介入。2.2failed to reset the dma的根因追踪链我们用devmem2工具读取GMAC的DMA描述符基址寄存器DBLAR# 读取当前描述符环基址 devmem2 0xff7b0010 w # DBLAR寄存器地址 # 返回值0x10000000 → 这是IOVA再用cat /sys/kernel/debug/iommu_groups/12/phys_addr查看IOMMU映射关系IOVA: 0x10000000 - PA: 0x80000000 (size: 0x1000) IOVA: 0x10001000 - PA: 0x80001000 (size: 0x1000) ...此时如果驱动错误地把phys_addr0x80000000写进DBLARGMAC控制器就会尝试从DRAM物理地址0x80000000读取描述符。但IOMMU页表里根本没有0x80000000到0x80000000的直通映射默认是禁止的结果就是AXI总线返回SLVERR响应DMA引擎卡死触发failed to reset the dma。2.3 实测对比地址错位导致的三种典型现象我们人为制造了三种地址填写错误在RK3588上观察现象错误类型描述符中填写的地址设备实际访问地址典型现象根本原因填纯物理地址0x80000000IOMMU拦截AXI SLVERRfailed to reset the dmaDMA引擎无法初始化IOMMU无对应IOVA→PA映射填虚拟地址0xffff000012345000设备总线尝试访问该VA触发AXI ERROR内核panic或设备挂死设备无MMU无法翻译虚拟地址填IOVA但未同步cache0x10000000正确IOVA访问正确物理内存但读到脏cache网络收包数据错乱校验和失败CPU写入数据在cache中未刷出其中第三种最隐蔽。我们在GD32E230上就遇到过类似问题ADC DMA采集的16位数据高位字节总是0xFF。最后发现是dma_map_single()后没调dma_sync_single_for_device()CPU写入描述符的buffer还在write-back cache里DMA引擎读到的是未更新的旧值。经验总结在RK3588等带IOMMU的SoC上永远用dma_handleIOVA填描述符永远不用phys_addr或virt_addr。这是血泪教训换来的铁律。3. GD32E230 ADC采集现场当adc dma数据紊乱暴露cache一致性漏洞GD32E230是国产Cortex-M3 MCU常用于AI传感器前端。去年调试一款气体分析仪时客户反馈ADC采集的12位数据高位字节随机变为0xFF尤其在高采样率1MSPS下更频繁。示波器显示模拟信号干净排除硬件干扰。最终定位到DMA描述符中的地址处理存在致命疏漏。3.1 M3架构的特殊性没有IOMMU但cache更危险GD32E230兼容STM32F103使用ARM Cortex-M3内核没有IOMMU也没有MMU。这意味着设备看到的地址就是CPU看到的物理地址。表面看比RK3588简单实则暗礁更多——因为M3的cache策略更原始且开发者容易忽略其影响。GD32E230的SRAM分为两块SRAM064KB位于0x2000_0000支持cachewrite-back模式SRAM116KB位于0x2000_1000不支持cache。ADC的DMA目标地址若放在SRAM0就面临经典cache一致性问题CPU执行memset(buffer, 0, size)数据写入cache未立即刷入SRAMADC DMA引擎从SRAM物理地址读取拿到的是旧数据0xFF填充的初始值结果采集缓冲区前半部分是0xFF后半部分才是有效数据。3.2 描述符地址的两种安全写法在无IOMMU的MCU上DMA描述符地址必须满足两个条件地址必须是物理地址因为设备无MMU内存区域必须禁用cache或使用write-through策略确保DMA读写与CPU一致。GD32标准外设库提供了两种安全方案方案A使用SRAM1无cache区// 分配在SRAM1物理地址0x20001000起 uint16_t adc_buffer[1024] __attribute__((section(.ram1))); // 链接脚本需定义.ram1段 // 填写描述符 DMA_Channelx-CMAR (uint32_t)adc_buffer; // 直接填虚拟地址M3中VAPA方案B禁用cache并手动同步// 分配在SRAM0但禁用cache SCB_EnableICache(); // 开指令cache SCB_DisableDCache(); // 关数据cache → 所有SRAM访问直通物理地址 // 或更精细控制对buffer所在页禁用cache需MPU // 填写描述符后强制同步 SCB_CleanInvalidateDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));我们实测发现仅关闭D-Cache就能解决90%的数据紊乱问题。但客户要求低功耗不能全局关cache。于是采用方案A将ADC buffer强制链接到.ram1段并在链接脚本中添加MEMORY { RAM1 (xrw) : ORIGIN 0x20001000, LENGTH 16K } SECTIONS { .ram1 (NOLOAD) : { *(.ram1) } RAM1 }3.3 从adc dma数据紊乱反推描述符设计缺陷这个案例揭示了一个关键原则DMA描述符地址的选择必须与内存子系统的特性深度耦合。很多开发者以为“填个地址就行”却忽略了GD32E230的SRAM0和SRAM1物理地址不同但虚拟地址空间重叠都映射到0x2000_0000起DMA_Channelx-CMAR寄存器接收的是CPU的虚拟地址M3内核会自动将其转换为物理地址因为无MMUVAPA但转换发生在CPU内部DMA引擎看到的仍是物理地址所以必须确保该物理地址对应的内存区域行为符合DMA要求。我们曾用逻辑分析仪抓取AXI总线信号证实当buffer在SRAM0时DMA读请求到达SRAM控制器但返回的数据是cache中的旧值切换到SRAM1后总线读请求直接命中SRAM数据完全正确。实操心得在Cortex-M系列MCU上永远优先选择无cache的SRAM区域存放DMA buffer。GD32E230的SRAM1、STM32F4的CCMRAM、ESP32-S3的DROM都是为此设计。不要试图用cache操作技巧去“修复”描述符地址问题那是本末倒置。4. AXI UART16550 FPGA实现离散式DMA Scatter-Gather描述符的地址编排艺术AXI UART16550是FPGA中常用的串口IP核支持DMA传输。去年为某AI训练数据回传模块开发FPGA固件时需要实现高速10Mbps串口DMA收发。我们采用Scatter-GatherSG模式即一个DMA事务可跨越多个不连续内存块。这时描述符里写的地址就不再是单个地址而是一个地址链表——每个节点包含“下一个描述符地址”和“数据缓冲区地址”。4.1 SG描述符的双地址结构解析AXI UART16550的SG描述符格式如下简化版字段偏移含义地址类型NEXT_DESC_ADDR0x00指向下一个描述符的地址IOVA设备视角BUFFER_ADDR0x04数据缓冲区起始地址IOVA设备视角BUFFER_LEN0x08缓冲区长度无地址属性CONTROL0x0C控制字中断使能等无地址属性关键点NEXT_DESC_ADDR和BUFFER_ADDR都必须是设备能直接访问的地址即IOVA。但在FPGA实现中我们发现一个陷阱描述符链表本身也需要DMA访问因此描述符内存也必须是dma_alloc_coherent()分配的。我们构建了一个4节点循环链表// 分配描述符内存16字节/描述符 × 4 64字节 struct sg_desc *desc_ring dma_alloc_coherent(dev, 64, desc_handle, GFP_KERNEL); // 初始化节点0 desc_ring[0].next_desc_addr desc_handle 16; // 节点1地址IOVA desc_ring[0].buffer_addr buf_handle_0; // 数据缓冲区IOVA desc_ring[0].buffer_len 1024; // 初始化节点1 desc_ring[1].next_desc_addr desc_handle 32; // 节点2地址IOVA desc_ring[1].buffer_addr buf_handle_1; desc_ring[1].buffer_len 1024; // ... 节点2、3同理 // 节点3指向节点0形成循环 desc_ring[3].next_desc_addr desc_handle; // 回到节点0IOVA这里desc_handle是描述符链表的IOVAbuf_handle_x是各数据缓冲区的IOVA。所有地址运算都必须基于IOVA而非物理地址。如果错误地用phys_addr计算next_desc_addr比如// ❌ 危险phys_addr是DRAM物理地址设备无法访问 desc_ring[0].next_desc_addr phys_addr 16;那么DMA引擎在完成节点0后会尝试从phys_addr16读取下一个描述符但该地址不在IOMMU映射范围内导致DMA挂起。4.2dma continuous requests的底层机制热搜词中的dma continuous requests指DMA引擎在SG模式下持续发出地址请求的行为。AXI UART16550的DMA状态机如下从DBLAR寄存器读取首描述符地址IOVA用该IOVA访问描述符内存获取BUFFER_ADDR和NEXT_DESC_ADDR用BUFFER_ADDR发起数据传输读/写传输完成后用NEXT_DESC_ADDR加载下一个描述符循环步骤2-4直到CONTROL字段指示结束。这个过程要求所有地址访问都必须在同一个IOVA地址空间内完成。我们曾因描述符链表和数据缓冲区用了不同的dma_alloc_coherent()调用导致IOVA不连续导致DMA在跳转时地址越界。解决方案是用一次大内存分配然后手动切分// ✅ 安全所有IOVA连续 void *all_mem dma_alloc_coherent(dev, 64 4*1024, all_handle, GFP_KERNEL); struct sg_desc *desc_ring all_mem; uint8_t *buf0 all_mem 64; uint8_t *buf1 buf0 1024; // ... 其余buffer // 填写描述符时地址计算基于all_handle desc_ring[0].next_desc_addr all_handle 16; desc_ring[0].buffer_addr all_handle 64;4.3 散列式DMA的地址对齐硬约束AXI总线协议要求DMA传输的地址必须对齐。UART16550的AXI接口规定BUFFER_ADDR必须4字节对齐因为AXI data width32bitNEXT_DESC_ADDR必须16字节对齐描述符大小16字节。我们曾因dma_alloc_coherent()返回的all_handle未对齐导致DMA传输异常。解决方案是在分配后手动对齐// 分配足够内存然后对齐 void *unalign_mem dma_alloc_coherent(dev, 128, unalign_handle, GFP_KERNEL); uint8_t *aligned_mem (uint8_t*)(((uintptr_t)unalign_mem 15) ~0xF); dma_addr_t aligned_handle unalign_handle (aligned_mem - (uint8_t*)unalign_mem); // 确保aligned_handle是16字节对齐的IOVA关键经验Scatter-Gather DMA不是简单的“地址数组”而是一个地址空间契约。描述符链表地址、数据缓冲区地址、甚至描述符内部的next字段都必须在同一IOVA空间内且满足总线对齐要求。任何一项不满足DMA引擎就会静默失败。5. 终极对照表设备眼中的内存视图决策树把前面所有案例提炼成一张可直接执行的决策表。当你面对一个新的DMA设备无论是RK3588的GMAC、GD32E230的ADC还是FPGA的AXI UART只需按此流程判断判断步骤检查项符合特征描述符应填地址类型验证方法典型错误Step 1查设备是否连IOMMUdmesggrep iommu或cat /sys/kernel/debug/iommu_groups/*/name显示设备在某个IOMMU group中如rk_gmac2在group 12IOVAdma_map_single()返回值readl(DBLAR)与dma_map_single()返回值对比Step 2查CPU架构是否有MMU/IOMMUcat /proc/cpuinfo查Features字段无iommu、mmu字段如Cortex-M3物理地址dma_alloc_coherent()的phys_addr参数readl(CMAR)与virt_to_phys(buffer)对比填虚拟地址或IOVAStep 3查内存区域cache属性查芯片手册SRAM章节或SCB-CCR寄存器SRAM支持write-back cache如GD32E230 SRAM0物理地址 禁用cache或选无cache区用逻辑分析仪抓AXI总线确认数据读取源在cacheable区放DMA bufferStep 4查DMA模式是否Scatter-Gather查设备手册DMA章节或驱动代码描述符含next_desc_addr字段所有地址包括next字段均为同一类型IOVA或PAhexdump -C描述符内存验证next字段值是否在IOVA/PA空间内next字段用物理地址其余用IOVAStep 5查总线对齐要求查AXI/AHB协议文档或IP核手册BUFFER_ADDR需4字节对齐DESC_ADDR需16字节对齐分配内存时强制对齐__attribute__((aligned(16)))printf(addr: %p\n, desc);检查地址末位地址未对齐导致DMA静默失败这张表不是理论推导而是我们踩过所有坑后焊死的checklist。例如RK3588rk_gmac2Step1成立 → 必须用IOVAGD32E230 ADCStep2成立无IOMMU Step3成立SRAM0有cache→ 必须用物理地址且禁用cache或选SRAM1AXI UART16550Step1成立连IOMMU Step4成立SG模式→ 所有地址必须是IOVA且next_desc_addr计算基于IOVA。最后分享一个压箱底技巧在Linux驱动中用dma_debug功能实时监控地址映射。开启后echo 1 /sys/module/dma_debug/parameters/enable dmesg | grep dma-debug你会看到每笔dma_map_single()的IOVA→PA映射记录。当DMA异常时立刻查这条日志比翻三天手册快十倍。设备眼里的内存从来不是一张静态地图而是一套动态的、分层的、受硬件约束的地址翻译系统。DMA描述符里写的那个数字是打开这扇门的唯一钥匙——填错一位整扇门就锁死。真正的AI Infra能力不在于调用多少高级API而在于你能否在dmesg的最后一行日志里一眼认出那个地址究竟是IOVA、PA还是一个不该存在的VA。