ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RK3568+FPGA PCIe DMA优化:从84MB/s到480MB/s的完整实战

RK3568+FPGA PCIe DMA优化:从84MB/s到480MB/s的完整实战 最近在调试一块RK3568PG2L50H的异构板卡ARM端跑Linux负责调度和协议处理FPGA端做图像预处理和高速接口适配。整条链路做到PCIe DMA数据通路时发现理论带宽和实际吞吐差了将近一个数量级。花了两周时间把DMA_memcpy路径从最初的84MB/s优化到480MB/s这里把完整的思路、代码框架和踩坑过程整理出来给正在做LinuxFPGA数据交互的朋友一个参考。先说结论RK3568和PG2L50H之间跑PCIe DMA性能瓶颈往往不在FPGA侧而在Linux侧的内存分配策略、Cache一致性处理、中断处理方式和CPU调频策略上。FPGA侧只要把DMA描述符逻辑写对带宽基本不会差太多。1. 为什么最终选择PCIe DMA对比了几种方案才下的决心1.1 需求背景FPGA预处理后的图像数据要怎么送到Linux项目需求是把FPGA采集到的1080p灰度图像经过FPGA内部的中值滤波和缩放后以30fps的帧率送到RK3568侧做AI推理。单帧大小约3MB30fps意味着每秒约90MB的有效数据量加上协议头、帧同步标志实际带宽需求在120MB/s左右。第一版方案用的是SPI波特率压到20MHz实测稳定吞吐也就1.8MB/s。这个速度跑控制指令可以跑图像数据完全不现实。后来又考虑了并口FMC但RK3568的GPIO直接操作数据总线很容易被Linux调度抖动影响实时性没法保证。真正让我下定决心用PCIe DMA的原因有三个RK3568原生支持PCIe 2.1 x1接口理论带宽500MB/sGen2速率下单向算上协议开销也能到400MB/s以上满足120MB/s的需求还有三四倍余量PG2L50H自带PCIe硬核和DMA控制器不需要额外搭逻辑省了大半FPGA开发工作量Linux侧对PCIe设备有完整的框架支持从设备树配置到驱动加载都是标准流程不像自定义并口协议要自己写一堆基础设施1.2 三种通信方案的实测对比我把实测数据整理成了表格这样在评审会上给需求方解释时也更有说服力方案接口类型实测有效带宽延迟驱动复杂度适用场景SPI主从模式1.8MB/s高低控制指令、小数据量并口FMCGPIO模拟8-12MB/s极高且抖动中低速传感器数据PCIe DMAGen2 x1400-480MB/s低且稳定中高图像流、大数据块传输从数据上看PCIe DMA几乎是为这个场景量身定做的。但当时我也没预料到实际做的时候Linux侧会有那么多需要调优的地方这部分内容留到后面详细展开。2. 硬件连接与PCIe枚举PG2L50H如何出现在RK3568的地址空间里2.1 板级设计的关键节点RK3568有两个PCIe控制器一个PCIe 3.0对应设备树节点pcie3x2实际只引出x1 lane和一个PCIe 2.1设备树节点pcie2x1。我这边用的是pcie2x1原因很简单它和PG2L50H的PCIe硬核速率匹配而且这个控制器在RK3568上支持的PIPE接口更稳定。板级连接上有三个地方容易出问题。REFCLK差分时钟线必须走阻抗控制RK3568的PCIe参考时钟要求100MHz差分我第一版PCB因为参考时钟走线过长导致眼图不合格PCIe链路经常训练失败PERST复位信号要接到RK3568的GPIO上由软件控制复位时序不能直接上拉到电源电平标准方面PG2L50H的PCIe硬核需要1.8V的PCIe AVDD供电如果和Bank供电混用会导致链路不稳定表现为偶尔枚举不到设备。2.2 设备树配置和枚举排查RK3568的设备树中pcie2x1节点默认是disable状态需要手动开启。我配置的关键字段如下pcie2x1 { status okay; reset-gpios gpio3 RK_PA1 GPIO_ACTIVE_HIGH; vpcie1v8-supply vcc3v3_sys; pinctrl-names default; pinctrl-0 pcie2x1_pins; };这里vpcie1v8-supply是很多人会忽略的配置。PCIe接口的电平转换电源必须正确指定否则Linux会认为设备未上电枚举阶段直接跳过。reset-gpios指定了PERST复位引脚注意这里用的是GPIO3_A1对应瑞芯微的GPIO编号规则。配置完成后Linux启动日志中应该能看到如下枚举信息pcieport 0001:00:00.0: PME: Signaling with IRQ 42 pcieport 0001:00:00.0: AER: enabled with IRQ 42 pcieport 0001:00:00.0: root port: 100.000 Gb/s (Max Payload Size 256, Max Read Request Size 512)这里Max Payload SizeMPS和Max Read Request SizeMRRS非常关键。PCIe协议栈默认协商的MPS可能是128字节但PG2L50H的DMA控制器如果支持256字节MPS我建议手动把MPS调到256能减少TLP包头开销。修改方法是在内核启动参数加pcipcie_bus_perf或者驱动里调用pcie_set_readrq()动态调整。如果lspci看不到PG2L50H设备排查链路顺序是先量REFCLK有没有100MHz时钟再看PERST复位是否释放然后检查PG2L50H的PCIe硬核配置寄存器比如Vendor ID和Device ID是否正确设置最后用瑞芯微的PCIe debugfs节点看链路状态cat /sys/kernel/debug/pcie_ports/0001:00:00.0/link3. DMA_memcpy驱动设计从内核态拷贝到用户态零拷贝3.1 第一版驱动设计为什么选择dma_alloc_coherent mmap刚开始写驱动时我犯了一个典型错误用kmalloc分配缓冲区再通过copy_to_user把数据从内核态拷贝到用户态。这样做的结果是每次read系统调用都要做一次额外拷贝CPU占用率高且带宽上不去。后来改用dma_alloc_coherent分配DMA缓冲区这个API能保证缓冲区物理地址连续、Cache一致而且在内核中可以直接映射到用户空间。关键代码框架如下static int fpga_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct fpga_dev *fpga; size_t buf_size 16 * 1024 * 1024; /* 16MB DMA 缓冲区 */ fpga kzalloc(sizeof(*fpga), GFP_KERNEL); /* 分配DMA一致性缓冲区 */ fpga-dma_virt dma_alloc_coherent(pdev-dev, buf_size, fpga-dma_phys, GFP_KERNEL); if (!fpga-dma_virt) return -ENOMEM; /* 使能PCIe总线主控 */ pci_set_master(pdev); }这里注意pci_set_master不能少否则DMA控制器发起bus master传输时会被拒绝。dma_alloc_coherent分配的缓冲区其物理地址在dma_phys中需要把这个地址通过BAR空间告诉PG2L50H的DMA控制器。3.2 用户态映射的实现细节在内核驱动的mmap回调中需要把DMA缓冲区的物理地址映射到用户空间static int fpga_pcie_mmap(struct file *file, struct vm_area_struct *vma) { struct fpga_dev *fpga file-private_data; unsigned long size vma-vm_end - vma-vm_start; unsigned long pfn __phys_to_pfn(fpga-dma_phys); /* 禁止Cache避免一致性问题 */ vma-vm_page_prot pgprot_noncached(vma-vm_page_prot); if (remap_pfn_range(vma, vma-vm_start, pfn, size, vma-vm_page_prot)) return -EAGAIN; return 0; }这里pgprot_noncached是必须的否则用户态读写DMA缓冲区时会出现Cache一致性问题。虽然dma_alloc_coherent已经将缓冲区设置为一致性内存但mmap映射时如果不显式设置noncached某些架构上内核会重新以Cacheable方式映射。3.3 性能测试方法不要用dd和cp测速很多人在Linux下测速第一反应就是用dd if/dev/xxx of/dev/null bs1M count100。实测发现这完全不能反映DMA的真实性能因为dd每次读写都要经过VFS层还有page cache干扰而且默认的bs设置会产生大量不连续的IO请求。正确的测速方式是写一个用户态基准测试程序直接打开设备节点mmap缓冲区然后循环read触发DMA传输int fd open(/dev/fpga_pcie, O_RDWR); void *buf mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); struct timeval start, end; gettimeofday(start, NULL); for (int i 0; i 100; i) { read(fd, buf, BUF_SIZE); } gettimeofday(end, NULL); double elapsed (end.tv_sec - start.tv_sec) (end.tv_usec - start.tv_usec) / 1e6; printf(average bandwidth: %.2f MB/s\n, 100.0 * BUF_SIZE / elapsed / 1024 / 1024);同时用perf和iostat监控CPU利用率这样才能准确定位瓶颈在CPU还是DMA链路。4. 性能瓶颈与优化过程从84MB/s到480MB/s的完整记录4.1 第一波优化去掉逐字读写改用DMA描述符链最初FPGA侧实现的DMA逻辑比较简单每收到一个主机read请求就用一个简单的FSM把数据从FPGA内部的FIFO搬到PCIe总线。而Linux侧的驱动每次read都直接操作BAR地址空间。这种方式有几个问题PCIe请求是以TLP包为单位传输的单个TLP最大能携带的有效数据只有MPS那么大通常是128字节或256字节远程一块3MB的数据需要几千个TLP包每个TLP包都有固定的12字节头部开销小数据传输时协议开销占比太高主机侧如果使用readl/writel这样的IO操作每读一个字都会产生一个PCIe事务性能极差所以第一版84MB/s根本不奇怪。随后我在FPGA侧实现了标准的DMA描述符链计算机侧把要传输的数据分成多个block每个block对应一个描述符描述符里包含源地址、目的地址、传输长度FPGA的DMA控制器会沿着描述符链依次搬运数据搬运完成后通过中断通知主机。描述符结构定义如下struct dma_desc { uint32_t src_addr; /* FPGA侧源地址 */ uint32_t dst_addr; /* 主机侧目的地址 */ uint32_t length; /* 传输长度 */ uint32_t ctrl; /* 控制字中断使能、下一描述符地址 */ };这样一个FPGA内部的DMA引擎可以连续搬运几十个block不用主机干预大大减少了PCIe事务数量。4.2 第二波优化Cache一致性、内存对齐与缓冲区管理优化到200MB/s左右时遇到了瓶颈我以为是FPGA侧DMA逻辑效率不够回头反复检查最后发现是Linux侧的Cache一致性和内存对齐问题。先看对齐。PG2L50H的DMA控制器要求源地址、目的地址、传输长度都按64字节对齐也就是2个cache line长度。如果不对齐DMA引擎会发起非对齐的PCIe请求导致性能下降30%左右。dma_alloc_coherent本身会按页对齐但如果你在驱动里又手动调整了缓冲区偏移就很容易破坏对齐。再看Cache一致性。虽然用dma_alloc_coherent分配的一致性缓冲区不需要显式flush但如果我在驱动里用dma_map_single来映射动态分配的内存就必须在DMA发送前调用dma_sync_single_for_device在DMA完成后调用dma_sync_single_for_cpu。这一步漏掉表面上不影响性能但会随机出现数据错乱后面避坑部分会详细讲。缓冲区管理上我做了两个优化。分配更大的DMA缓冲区从16MB增加到64MB减少用户态和内核态之间地址空间切换的频率将缓冲区拆分成多个block用环形buffer管理一个block在DMA传输时另一个block可以同时被用户态处理流水线化之后整个链路的带宽又有约10%的提升。4.3 第三波优化中断节流与performance governor优化到300MB/s后继续往上走变得困难。用perf监控发现CPU中断占比接近40%而且每次DMA传输完成都会抛出一个中断如果block比较小中断频率会非常高。之前做的block大小是256KB1GB数据传输会产生4096次中断每次中断处理最少也要几微秒加上上下文切换开销CPU被大量消耗在中断上。我采用了中断合并策略这是DMA性能优化中非常关键的一环。让FPGA的DMA控制器每完成N个block才产生一次中断或者在传输结束后延迟X微秒再发中断等待后续可能的block完成。梳理测试下来N取16时带宽提升最明显从300MB/s直接跳到380MB/sCPU占用率也降到了15%左右。/* 驱动中读取FPGA的中断合并配置寄存器 */ writel(16, fpga-bar0_virt FPGA_DMA_IRQ_COALESCE); writel(50, fpga-bar0_virt FPGA_DMA_IRQ_DELAY); /* 微秒级延迟 */另一个容易被忽视的点是CPU调频策略。RK3568的A55核心默认的schedutil调频器是动态调频的DMA是突发型负载CPU频率在高低之间频繁切换导致每次DMA启动都要付出额外的中断响应延迟。我把调频模式固定为performanceCPU持续运行在最高频率中断响应时间稳定整体吞吐又提升了约20MB/s。echo performance /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor有朋友问这样功耗会不会增加说实话DMA传输本来就是高性能场景那点功耗差异相对于性能提升完全可以接受。如果在意功耗保持ondemand模式然后给中断线程设置实时优先级也可以。4.4 关键参数优化对照表经过三轮优化我把每个优化点对性能的贡献整理成了表格后续项目可以直接照这个表来排查问题优化项优化前优化后性能提升备注逐字读写改DMA描述符链84MB/s168MB/s2倍减少TLP数量PCIe MPS调整到256168MB/s205MB/s22%减少协议头开销Cache一致性和对齐优化205MB/s260MB/s27%必须64字节对齐环形buffer流水线260MB/s300MB/s15%减少地址切换中断合并 performance governor300MB/s480MB/s60%中断次数降为1/16480MB/s已经是PCIe Gen2 x1协议层理论带宽的96%左右再往上升的空间已经不大了。这个数字意味着上层应用可用带宽完全满足120MB/s的需求甚至还有富余。5. 踩坑实录三个困扰我两周的问题5.1 中断风暴为什么CPU占用率高达90%第一次把DMA缓冲区加大到64MB后系统整体CPU占用率突然飙升到90%以上用top看ksoftirqd进程占用了大量CPU。排查链路是这样的先用cat /proc/interrupts看中断数量发现DMA中断每秒有23万次平均每个block传输完成都会产生一次中断。再配合perf top确认中断处理函数里大部分时间在等待spinlock。找到根因后启用了FPGA侧的中断合并寄存器中断频率从每秒23万次降到1.4万次CPU占用率立刻降下来了。这里有个小经验FPGA的DMA控制器在PCIe中断是传统的INTx还是MSI/MSI-X模式对中断性能影响非常大。MSI中断比INTx延迟低不少且不需要共享中断线。PG2L50H的PCIe硬核支持MSI设备树或驱动里请求MSI时要检查Linux是否成功分配了MSI号。/* 分配MSI中断 */ err pci_enable_msi(pdev); if (err) { dev_warn(pdev-dev, fall back to INTx\n); }5.2 Cache一致性偶发的0x00字节优化到260MB/s时发现用户态读取mmap区域的数据偶发出现一块一块的0x00字节。排查这个问题的过程非常痛苦因为它是偶发的没有固定的概率。第一反应是FPGA侧DMA传输出错于是加了大量的状态断言和错误标志。但反复验证后FPGA侧没有报错数据链路本身是通的。后来想到可能是Cache一致性问题。虽然mmap时用了pgprot_noncached但用户态程序中对这个内存区域执行了memcpy操作如果编译器优化后使用非cache操作的指令比如NEON的load/store仍然可能绕过noncached映射。解决办法是把mmap映射改为pgprot_writecombine写合并映射这样对于读操作是uncached对于写操作是write-combine既保证了DMA数据的一致性又保持了写性能。vma-vm_page_prot pgprot_writecombine(vma-vm_page_prot);这个坑的教训是在ARM64平台上DMA缓冲区即使由dma_alloc_coherent分配也必须在mmap映射时显式设置正确的页面属性不能依赖默认值。5.3 PCIe链路训练失败REFCLK与复位时序的那点事项目调试初期PG2L50H在Linux下经常枚举不到概率大约50%。每次板子重新上电后都要手动echo 1 /sys/bus/pci/rescan才能找到设备。这个问题排查了很久一度怀疑是PG2L50H的PCIe硬核配置问题。后来用示波器抓了REFCLK波形发现时钟信号幅值只有500mV而PCIe Gen2要求差分摆幅至少800mV。检查原理图发现REFCLK的源端串阻选得过大衰减了信号。换小阻值后引脚时钟恢复。同时软件侧的复位时序也要调整。PERST#信号低电平保持时间至少要有1ms释放后到设备就绪之间需要100ms左右的延时而RK3568的PCIe控制器枚举时机比这早导致设备还没有完成内部复位初始化就被枚举了。修改了GPIO控制逻辑确保PERST释放后延时200ms再开始扫描PCIe总线这个概率问题彻底消失。现象可能原因排查方法解决办法枚举不到设备REFCLK幅值不足示波器抓差分波形调整源端串阻阻值偶尔找不到设备复位时序不满足检查PERST时序延时200ms再扫描链路降速到Gen1信号完整性差查看链路协商速率检查走线阻抗和端接这块的教训是PCIe链路的稳定性问题有七成是硬件问题不要一上来就怀疑FPGA逻辑或Linux驱动。先用lspci -vvv查看LnkSta如果显示协商速率只有2.5GT/sGen1基本可以断定是硬件信号问题而不是软件配置问题。6. 写在最后的经验总结DMA_memcpy这类高速数据交互本质是把数据搬运工作从CPU交给DMA引擎但真正决定性能上限的往往不是DMA引擎本身而是整套系统里最弱的一环。我这边的优化经历了从FPGA逻辑到PCIe配置再到Linux内核策略的多轮迭代每一轮解决一个问题后下一轮的瓶颈才会暴露出来。如果你也正在做RK3568或类似ARM SoCFPGA的DMA交互项目我个人的建议是优先做这三件事首先把PCIe链路协商状态调到Gen2、MPS设为256字节其次在整个数据通路上使用一致性DMA缓冲区并配合mmap零拷贝最后用中断合并和performance governor把CPU侧的开销压到最低。这三步做完性能基本不会太差。另外调试过程中一定要先确认纯硬件层面的链路没问题再进入驱动和软件优化阶段。这个先后顺序搞反了会浪费大量时间在无意义的排查上。希望这篇内容能帮你少走一些弯路。
返回列表