ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RK3588与FPGA的PCIe XDMA驱动开发实战:从设备树到DMA带宽调优

2026/9/19 4:42:30 拓冰建站 浏览量
RK3588与FPGA的PCIe XDMA驱动开发实战:从设备树到DMA带宽调优 从编译到调通RK3588与FPGA的PCIe XDMA驱动这条路我前前后后走了大概两周。中间踩的坑不算少但真正跑通DMA搬运那一刻前面所有的折腾都值了。这篇文章把整个流程拆开揉碎了讲从硬件链路、设备树配置、内核编译、驱动模块分析到枚举调试、DMA性能实测和常见故障定位尽量做到你照着走也能通。适合的读者比较明确手里正好有一块RK3588主板和带PCIe接口的FPGA开发板想自己把XDMA这套东西调通的人。或者虽然硬件不同但做Linux驱动、嵌入式异构开发想了解PCIe枚举和DMA驱动的完整链路也可以拿这篇当参考。先说下我手上的环境主控是RK3588内核用的官方Linux 5.10内核源码FPGA是Xilinx UltraScale系列PCIe IP用的XDMABMD模式FPGA侧逻辑基于Xilinx官方XDMA驱动源码也就是xdma字符设备驱动。RK3588这边跑的是Ubuntu 22.04文件系统内核自己编。1. RK3588侧PCIe控制器三个接口、两种速率、一套驱动框架RK3588的PCIe资源大部分人只知道“支持PCIe”但具体到项目选型和板卡设计里面的门道值得先说清楚。3588一共有3个PCIe控制器命名分别是PCIe 3.0 x4PCIE30、PCIe 3.0 x2PCIE30、PCIe 2.0 x1PCIE20。这三个控制器不是简单的速率差异它们挂在不同的总线上使用不同的DTS节点而且compatible字符串也有区别。PCIe 3.0的控制器compatible是rockchip,rk3588-pcie而PCIe 2.0的是rockchip,rk3568-pcie这种复用老IP的描述。换言之3588的PCIe2.0控制器和RK3568是同一个IP驱动可以直接复用。控制器通道数最高速率典型用途PCIE30 x44 lanes8GT/s/laneNVMe SSD、FPGA大带宽互联PCIE30 x22 lanes8GT/s/lane网卡、视频采集卡PCIE20 x11 lane5GT/s/lane低速设备、WiFi模块你用的FPGA接哪一个直接决定了理论带宽上限。我这边FPGA接的是PCIE30的x4控制器跑x4模式链路速率8GT/s单lane理论带宽约985MB/s考虑到8b/10b编码实际有效载荷大约这个值。4条lane合在一起理论极限在3.9GB/s左右但这是理想的物理层上限软件和DMA会有额外开销实测后面会讲。在RK3588的硬件层面PCIE30控制器的REFCLK通常是独立的100MHz差分时钟FPGA侧的PCIe PHY也需要外部输入100MHz参考时钟或者是用RK3588输出的REFCLK。这里有个很容易忽略的点如果用同一个时钟源要注意时钟缓冲器的扇出能力和走线长度匹配否则链路训练容易出现间歇性失败。另外RK3588的PCIE30控制器支持RC和EP两种模式但大多数主板默认工作在RC模式。如果FPGA侧想要做EPRK3588这边的驱动完全不用动它作为RC天然负责枚举EP设备。我项目里的FPGA就是EP角色RK3588是RC顺着这个思路整个软件架构就清晰了RK3588RC端运行Linux加载PCIe驱动负责枚举FPGA设备、分配资源、配置BAR空间并且运行XDMA字符设备驱动提供/dev/xdma0_*接口给应用层。FPGAEP端XDMA IP核作为Endpoint通过AXI接口连接FPGA内部的DMA逻辑和用户逻辑负责把数据搬运到DDR或者从DDR取数。了解这个拓扑结构后接下来的设备树配置就有了明确的方向。2. RK3588 DT配置单控制器双端口的隐藏坑位设备树配置是整个链路能否枚举成功的第一道关卡。RK3588的PCIE30控制器物理上有两个端口Port0/Port1对应dts里的pcie3x4和pcie3x2两个节点。更准确地说3588的PCIE30是一个支持 bifurcation 的控制器可以做x4、x2x2、x2、x1这些拆分组合。具体怎么拆由rockchip,bifurcation属性和主板硬件走线决定。我手上这块核心板把PCIE30 x4引出来给了FPGA所以设备树里主要关注pcie3x4节点。一个典型的配置片段类似这样pcie3x4 { status okay; reset-gpios gpio1 RK_PB2 GPIO_ACTIVE_HIGH; vpcie3v3-supply vcc3v3_pcie; pinctrl-names default; pinctrl-0 pcie30x4_perstn; max-link-speed 3; num-lanes 4; };几个容易出问题的地方reset-gpiosPERST#信号RC端拉高释放EP的复位FPGA侧必须正确响应这个复位时序。很多FPGA工程只做了PCIe硬核的初始化却没有把PERST#引入FPGA的逻辑里做复位控制结果就是RC一直等不到EP就绪链路训练失败。max-link-speed3代表Gen3如果FPGA侧XDMA IP生成时选的是Gen2这里设为3也能协商到2不冲突。但反过来如果FPGA只支持Gen1你设Gen3还可能偶尔被训练失败降低到2或1更稳妥。num-lanes4表示4条lane但这个必须和FPGA侧IP配置保持一致如果FPGA工程只例化了2条laneRK3588这边设置x4也能协商成x2但带宽只有一半。在3588的dts里还要留意pcie30_phy_grf和pcie30_pipe这些PHY相关节点时钟和PHY配置大多在pcie30-phy里。如果你用的是主线内核可能还需要确认rockchip,pcie30-phy驱动已编译进内核否则PHY不工作设备树配得再对也枚举不了。我踩过的具体坑PCIe节点配成status okay了但pinctrl-0里没有加上pcie30x4_perstn导致PERST引脚没有被正确配置为GPIO功能复位信号一直处于无效电平FPGA的EP始终无法起来。lspci里什么都看不到排查了老半天。3. XDMA驱动核心机制BMD模式寄存器、描述符和中断通道XDMAXilinx DMA/Bridge for PCIeIP核有两种工作模式BMDBus Master DMA模式和AXI Bridge模式。BMD模式下FPGA作为PCIe EP可以通过DMA读写主机内存不需要CPU逐字拷贝这是高性能数据传输的核心。XDMA IP在FPGA侧暴露了几块重要的寄存器空间和中断资源。首先BAR0通常映射的是用户逻辑寄存器BAR1映射的是XDMA控制寄存器如果你例化时有勾选而MSI中断则用于通知主机DMA完成事件。驱动侧核心的数据结构是描述符Descriptor也叫BDBuffer Descriptor。XDMA的BMD模式支持环形描述符Ring和聚合描述符Aggregation我用的驱动是Xilinx官方开源的xdma驱动它用的是环形描述符机制。描述符结构体dma_desc主要包含以下几个字段next指向下一个描述符的地址形成环形链。addr_lo/addr_hiDMA搬运的源或目的地址对于主机内存而言就是DMA缓冲区地址。len本次搬运的字节数。ctl控制和状态位包括DMA方向、完成中断使能等等。这里描述符的环形队列放在主机内存DMA BufferFPGA侧通过AXI MM2S/S2MM通道读取描述符。每次用户发起read或write驱动就往环形队列塞一个描述符然后写XDMA的寄存器来通知硬件处理。传输完成后FPGA通过MSI给CPU发中断驱动在中断处理函数里将对应的描述符标记为完成然后唤醒等待队列中的进程。这个逻辑看起来很线性但真正跑代码时你会发现描述符循环是用无锁方式改写的tail指针由驱动写、head指针由硬件更新。所以理解这两个指针的含义就很重要。还有XDMA的寄存器里有个H2C Channel Control和C2H Channel Control分别对应主机到FPGAHost-to-CardMM2S其实就是主机写和FPGA到主机Card-to-HostS2MM主机读。这张表建议收藏调试的时候会频繁用到方向XDMA术语等价AXI通道用户操作主机写FPGAH2C / MM2SAXI Writewrite(fd, buf, len)FPGA读主机C2H / S2MMAXI Readread(fd, buf, len)中断MSI-X / MSI——驱动中断处理XDMA支持最多4个H2C通道和4个C2H通道在IP配置里可选每个通道有独立的中断。驱动默认会注册/dev/xdma0_h2c_0和/dev/xdma0_c2h_0还有/dev/xdma0_control和/dev/xdma0_user。4. 编译内核和驱动模块版本一致性比想象中更重要RK3588的板级支持包BSP内核是最稳妥的起点。可以从Rockchip官方仓库拉取5.10内核也可以用你主板厂商提供的内核源码。这里有个核心前提内核源码版本必须和你板子实际运行的内核完全一致否则编译出来的驱动模块要么死活加载不进去要么加载了之后行为诡异。我自己第一次就是用了board vendor提供的内核源码但板子跑的是他们预编译的内核镜像版本号对不上编译模块时头文件指向了源码目录硬加载进去直接报version magic不匹配。后来干脆用源码自己编译整个内核镜像一了百了。编译内核前需要确保开启以下配置CONFIG_PCIy CONFIG_PCIE_DWy CONFIG_PCIE_DW_PLATy CONFIG_PCI_MSIy CONFIG_PCI_MSI_IRQ_DOMAINy CONFIG_DMA_ENGINEy CONFIG_DMA_OFy CONFIG_UIOy CONFIG_UIO_PCI_GENERICy其中CONFIG_UIO_PCI_GENERIC是给不带驱动的PCIe设备用的XDMA驱动用不上但有时排查BAR映射问题时会用到。XDMA官方驱动不依赖UIO它自己直接操作BAR0和BAR1。内核编译我用的命令make ARCHarm64 rockchip_linux_defconfig make ARCHarm64 menuconfig make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j16如果是交叉编译交叉工具链必须与目标架构匹配。然后单独编译XDMA驱动模块时需要用到编译内核时生成的Module.symvers否则modpost阶段会出现unknown symbol之类的错误。这里建议把XDMA驱动源码放到内核源码树的drivers/目录下或者至少确保KERNELDIR指向已编译好的内核源码树。XDMA驱动的Makefile大概长这样KERNELDIR ? /path/to/kernel-source PWD : $(shell pwd) obj-m : xdma.o xdma-objs : xdma_mod.o xdma_core.o xdma_linux.o c2h.o h2c.o libxdma.o xdma_proc.o all: $(MAKE) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -C $(KERNELDIR) M$(PWD) modules install: $(MAKE) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -C $(KERNELDIR) M$(PWD) modules_install实际编译时如果内核源码开启了CONFIG_MODVERSIONS强烈建议保留Module.symvers。遇到Unknown symbol的报错优先检查Module.symvers是否存在以及对应符号是否包含在内。另一种省事方法是直接把xdma编译进内核而不是作为模块把xdma相关文件加入drivers/dma/Kconfig和Makefile这也能绕开模块版本问题缺点就是每次改驱动都得重新烧内核调试效率低。我建议开发阶段用模块稳定后再考虑内建。5. 链路枚举与板级调试lspci看不到设备时按顺序查这几项驱动模块编译好dts也配了接下来就是激动人心的上电和枚举。把内核镜像烧进去系统起来后先执行lspci -vvv如果一切顺利你应该能看到类似输出01:00.0 Unassigned class [ff00]: Xilinx Corporation Device 90389038是XDMA常见的Device ID。如果这里什么都看不到不用慌链路协商失败的原因就那么几类按顺序排查PERST#信号用示波器查RK3588输出的PERST#是否拉高FPGA侧是否收到了有效的复位释放。时序上PERST#释放必须在REFCLK稳定之后且有一定的延时要求。最简单的方法是拿万用表测电平拉高正常但释放时机需要示波器才看得准。REFCLK信号100MHz差分时钟PCIe对REFCLK的抖动要求很严格但至少先确认频率对、幅度在合理范围约0.6Vpp-1.0Vpp。如果FPGA和RK3588共用时钟源确认时钟缓冲器供电正常。链路训练状态机FPGA例化XDMA IP后如果链路训练卡在某个状态可以在硬件里加入ILAIntegrated Logic Analyzer抓取PCIe PHY的LTSSM状态看看是卡在Detect、Polling还是Configuration。RK3588侧没有类似工具但FPGA侧用Xilinx的debug核很方便。设备树上电顺序RK3588的PCIe供电时序要求3.3V和1.8V电源、REFCLK、PERST#释放顺序符合PCIe规范。不少板子的电源轨由GPIO控制如果vpcie3v3-supply没配好导致供电晚于PERST释放就会枚举失败。我调试过程中最典型的一次FPGA侧XDMA IP的PCIe硬核没有正确引入PERST#导致EP永远处于复位状态。这个排查过程比较痛苦因为FPGA内部逻辑是正常的、JTAG能看到但PCIe的PHY状态一直不对。最后在FPGA里把PERST#作为一个普通信号引入逻辑接到XDMA IP的axi_resetn重新综合后链路直接训练成功。链路起来后还要确认BAR空间分配。用lspci -v看Region 0: Memory at 10000000 (32-bit, non-prefetchable) [size1M] Region 2: Memory at 10100000 (32-bit, non-prefetchable) [size64K]BAR0的地址就是驱动需要映射的用户寄存器空间XDMA控制寄存器也会映射在这附近。驱动加载后dmesg里会打印类似xdma 0000:01:00.0: enabling device (0000 - 0002) xdma: xdma_probe: xdma_dev 00000000abcdef12如果看到probe成功/dev/xdma0_*节点也就创建出来了。6. 驱动加载后不能立刻愉快DMA先解决dmesg里的这堆异常驱动加载成功只是第一步。/dev/xdma0节点出现了但调用read、write能不能真正完成DMA传输还取决于两件事DMA缓冲区映射是否成功、MSI中断是否注册成功。第一次调通时我执行了个最简单的测试——往FPGA写1KB数据echo test /dev/xdma0_h2c_0结果dmesg直接报错xdma 0000:01:00.0: DMA mapping failed: out of memory这个错误看着像内存不足实际上是DMA缓冲区地址超过了设备能访问的地址范围。XDMA这类PCIe设备默认可能只支持32位地址取决于IP配置和驱动代码里的dma_set_mask_and_coherent设置如果驱动没正确调用dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))内核就尝试分配高于4GB的缓冲区而设备寻址不了。这个问题的解决方法是修改驱动代码在probe函数里加上类似这样的调用if (dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64)) 0) { dev_info(dev, Using 64-bit DMA\n); } else { dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(32)); dev_info(dev, Using 32-bit DMA\n); }但注意FPGA侧的XDMA IP如果地址宽度只配了32位主机端设64位掩码也没用传输时地址会截断。所以IP配置和驱动需要双向对齐。还有一个高频报错是中断注册失败或MSI不可用xdma 0000:01:00.0: failed to enable MSI-X这多半是内核没开启CONFIG_PCI_MSI或者BIOS/U-Boot没有为PCIe正确初始化MSI控制器。RK3588这边U-Boot的PCIe初始化很关键如果U-Boot里PCIe node就是disabled内核即便能枚举中断资源也可能分配异常。检查一下U-Boot dts里PCIe节点的状态确保U-Boot阶段也做了PCIe初始化。还有一个很多人容易忽略的问题XDMA驱动默认的DMA缓冲区大小。在官方驱动里xdma分配DMA缓冲区用的参数在xdma_mod.c里默认可能只有几MB。如果你通过mmap映射用户缓冲区做非对齐传输性能会惨不忍睹。我后来把缓冲区调到64MB配合read/write一次性搬运大块数据吞吐量才真正上来了。xdma驱动支持mmap操作你在应用层把设备文件映射到用户空间然后直接读写映射区域对驱动而言就是DMA缓冲区的直接访问绕过了copy_to_user这一层拷贝。性能提升非常明显尤其是大数据量传输。7. 从BSP内核对接到自编译模块处理Mismatch和依赖问题前面提过内核版本一致性这节展开讲讲几种典型的模块加载失败场景和应对方法。场景一version magic不匹配。明明源码是同一个tag但编译出来的模块就是加载不了。原因通常是内核源码里的.config和板子实际运行内核的config不一样尤其CONFIG_LOCALVERSION字段会导致version magic变化。解决办法是用板子/proc/config.gz如果内核开启了CONFIG_IKCONFIG_PROC导出现有config再做编译。zcat /proc/config.gz .config make ARCHarm64 olddefconfig场景二符号依赖不满足。报错通常长这样xdma: Unknown symbol dma_set_mask_and_coherent (err -22)这种情况多半是Module.symvers缺失或路径不对。你可以把编译内核时生成的Module.symvers手动拷贝到XDMA驱动源码目录或者在Makefile里显式指定KBUILD_EXTRA_SYMBOLS。场景三设备树里中断配置和驱动不符。XDMA IP在生成时会默认上报MSI中断但有些旧版本驱动写死了INTx中断方式拉着irq号初始化结果中断处理函数一直没触发。排查方法是看/proc/interrupts里有没有注册对应的中断号。cat /proc/interrupts里面应该有类似xdma或者pcie相关的条目。没有的话多半是驱动在request_irq时拿到的irq号不对。新版xdma驱动用的是pci_alloc_irq_vectors和request_threaded_irq对MSI-X支持比较完善。如果你用的是老版本建议从Xilinx官方GitHub更新到最新版的xdma驱动源码。这些坑排查完之后驱动模块就可以稳定加载了。接下来进入最关键的环节——性能实测。8. 带宽实测和性能调优从理论3.9GB/s到实际性能只有六成链路跑通后大家最关心的肯定是带宽到底能到多少。我第一次测试时直接用dd从/dev/xdma0_c2h_0读数据结果惨不忍睹dd if/dev/xdma0_c2h_0 of/dev/null bs4k count1024速度只有150MB/s左右。这个数字离x4 Gen3的理论上限差太远了。问题出在几个环节逐个优化后有了明显改善。优化1DMA缓冲区对齐和大小XDMA对DMA地址的alignment要求比较严格。官方驱动内部用的缓冲区是kzalloc分配的但这种方式分配的内存不保证物理连续或满足设备对齐需求。应改用dma_alloc_coherent或者gen_pool分配大块连续内存。在驱动初始化时把每个通道的缓冲区按1MB对齐分配且支持mmap这样用户态可以用read/write直接批量DMA。优化2中断合并和DESC数量XDMA每个描述符可以携带一次中断请求但如果每个小包都中断CPU压力很大。解决方式是只在描述符链的最后一个描述符开启中断也就是把ctl里的stop_on_err和irq_enable合理设置。同时增加描述符数量让一次DMA操作尽量长。我后来把每个环的desc数量设为4096单次DMA长度可以做到几十MB中断频率大幅下降吞吐率提升明显。优化3使用io_uring或者异步IOXDMA驱动本质上支持阻塞IO和异步IO。但用户态最简单的read/write其实每次都会产生一次系统调用和上下文切换传输小包时开销相对大。如果应用允许尽量使用mmap映射DMA缓冲区然后批量填充数据不断触发DMA。或者用libaio提交多个请求让驱动和DMA引擎尽可能满负荷。优化后我的实测数据如下PIO读和DMA读分开测模式数据块大小实测带宽驱动读写read/write4KB260MB/s驱动读写read/write1MB1.8GB/smmap映射DMA1MB2.2GB/smmap映射DMA16MB2.5GB/s最终稳定在2.5GB/s左右也就是理论带宽的六成多。对于XDMA这种面向数据搬运的IP这个成绩已经算正常偏上。如果换算成PCIe Gen3 x4链路2.5GB/s意味着链路利用率超过60%说明瓶颈主要不在PCIe协议上而在DMA描述符处理、中断延迟和应用层拷贝。如果想要逼近3GB/s以上建议在FPGA侧把XDMA的Data Width做成512bit并在AXI互联上避免经过低速总线桥。另外用c2h连续读大块数据比h2c写大块数据更容易跑满因为读操作在PCIe协议上可以有更好的延迟隐藏。9. 常见故障排查与调试工具问题定位思路比工具本身更重要这篇文章到这里核心链路已经完整走了一遍。再补充一些调试中高频出现的故障类型和定位方法遇到问题的时候能少走弯路。9.1 设备枚举出来了但无法访问BAR空间lspci -v能看到设备但用户空间程序访问BAR地址时段错误Segmentation Fault。这个问题的根源通常不是内核驱动而是你没有将BAR映射到用户态。XDMA设备节点/dev/xdma0_user是专门用来访问BAR0用户空间的。如果你直接对/dev/xdma0_control做mmap需要确认偏移地址和BAR映射范围。最好的办法是先用pcim_iomap映射BAR0然后通过字符设备的mmap回调把这段iomem映射给应用层。9.2 DMA传输超时read调用一直阻塞最终返回Resource temporarily unavailable或者ETIMEDOUT。XDMA描述符环的tail指针没有成功写入硬件寄存器导致FPGA侧无法拾取描述符。排查时可以先用xdma驱动自带的proc接口查看cat /proc/xdma0/h2c0_desc正常情况可以看到可用的desc数和已处理的desc数。如果显示head0, tail0说明驱动压根没有提交描述符。9.3 中断风暴启动系统后CPU占用飙升到100%/proc/interrupts里某个中断计数狂涨。这个通常是因为FPGA侧在链路训练完成后持续产生了未处理的中断比如错误中断或MSI-X向量表被错误配置。解决办法是检查FPGA侧XDMA IP的irq_ack逻辑确保中断被正确清掉。如果只是调试驱动临时在中断处理函数里把中断屏蔽掉也能定位问题。9.4 用perf和ftrace辅助定位有时候问题出在内核路径上最好直接看函数耗时和调用栈perf record -g -a -- sleep 10 perf reportxdma驱动的关键函数会在输出里清晰展现比如xdma_irq_handler、xdma_h2c_write这些函数如果占用了太多CPU时间说明中断处理或者描述符操作优化空间还没挖尽。另外trace-cmd可以跟踪中断的进入和退出以及DMA映射是否一直在繁忙等待trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit trace-cmd report10. 对整体方案的几点个人体会串完整个项目有几个感受想最后提一下。第一fpga和RK3588的PCIe互联硬件设计的余量直接决定了软件调试的难度。如果板子走线不规范、时钟源设计不合理后面浮现的问题会让你怀疑驱动写错了实际上问题在物理层。有条件的话第一次打板就预留PCIe的调试探测点LTSSM的状态观测点也很重要。第二XDMA驱动代码虽然看起来复杂但核心就是描述符和中断两件事。建议拿官方驱动源码结合XDMA IP的寄存器手册PG195文档对照着看比盲目百度高效得多。尤其是H2C/C2H的通道控制寄存器Channel ID、Target Address这些字段的布局手册里写得明明白白驱动代码里的偏移量也都能对上。第三RK3588这种多控制器SoC做PCIe主控如果遇到枚举不稳定的情况优先怀疑电源和时钟时序不要一上来就改驱动。我实际调的过程中第一次上板100%失败后来发现是PERST#释放比供电晚根本原因是FPGA侧的电源监控芯片delay配置不对。最后分享一个小技巧驱动、硬件、FPGA逻辑三方联调时先在FPGA里写一个简单的寄存器回环模块用/dev/xdma0_user读写寄存器验证PCIe通路然后再跑DMA。把小问题消化在最小系统里能让你少熬好几个通宵。