搞高速数据采集的人多半都经历过这种尴尬:FPGA侧ADC采得飞快,数据堆在FIFO里快溢出了,CPU却迟迟拿不走。要么拆成一次次中断搬运,CPU占用率高得吓人;要么绕开操作系统直接操作物理地址,性能倒是上去了,可驱动和上层应用又变成一团乱麻。
hs_dma_framework这个项目,就是专门来收拾这个局面的。它把FPGA、ARM64处理器和Linux系统串成一条完整的采集链路,核心里是一套不依赖厂商闭源IP的DMA框架:FPGA负责把高速数据灌进AXI4-Stream总线,ARM64侧通过Linux驱动的描述符环形队列接管这批数据,整个搬运过程CPU几乎不参与,用户态拿到手的已经是规整的、可以直接丢给算法处理的数据块。
这篇文章会把这套框架从硬件选型、FPGA逻辑、Linux驱动到ARM64适配从头拆一遍,把你最关心的几个问题讲透:DMA描述符环形队列到底怎么设计才算可靠、ARM64缓存一致性为什么和x86不一样、设备树里那几行配置为什么能坑你一整天。如果你是做FPGA数据采集、边缘计算网关或者软件无线电的工程师,这篇可以直接当参考设计来读。
1. hs_dma_framework到底解决了什么问题
1.1 高速数据采集的常见痛点
做采集类项目的朋友应该都有体会,数据搬运往往是整个系统里最难啃的骨头。ADC采样率上去了,数据率轻松到几百Mbps甚至上Gbps,这时候如果还靠CPU一条条读寄存器取数据,基本就是灾难。我见过不少项目组第一步用GPIO模拟接口,第二步用简单FIFO加中断,第三步发现中断太频繁CPU被打满,第四步才开始想DMA。
麻烦还不止这些。用厂商自带DMA IP的工程一般都能跑通,但问题在于这些IP往往和具体芯片绑定,换一颗FPGA或者换一个操作系统版本,整个链路就得重新调试。而且底层细节被封装起来,出了问题你根本不知道是描述符写错了还是地址翻译出了岔子。hs_dma_framework的思路是:把DMA这套机制自己实现一遍,从描述符管理到中断处理全部透明化,出了问题能直接顺着链路排查。
1.2 为什么选FPGA加ARM64加Linux这个组合
先说FPGA。高速数据采集的前端必然是FPGA,因为ADC的LVDS、JESD204B这类接口,以及数据的预处理、滤波、触发逻辑,用处理器做不现实。FPGA的并行架构决定了它天生适合做这种高吞吐的数据管道。
再说ARM64。放在几年前,很多人可能还在纠结用ARM Cortex-A9还是FPGA内嵌的软核。但现在嵌入式平台上ARM64已经非常成熟,四核A53/A72的价格和功耗都控制得很好,性能完全撑得起协议解析、FFT、存盘这类后续处理。更重要的是,ARM64能跑完整版的Linux,这意味着网络、文件系统、调试工具这些现成的生态都能直接用。
最后是Linux。Linux带来的最大好处是把内存管理、中断处理、设备模型这些东西都标准化了。你不需要自己管物理内存的分配和释放,不需要考虑怎么把物理地址暴露给用户态,驱动框架已经把这些事情安排好了。平台选型本质上就是在实时性和生态之间找平衡,FPGA兜底实时采集,Linux兜底应用生态,ARM64负责把这两者连接起来,这套组合在数据采集场景里确实是最稳的。
1.3 平台的总体架构
整个平台的数据流是这样走的:ADC采样数据经过前端接口进入FPGA逻辑,FPGA内部做必要的格式转换和缓冲后,以AXI4-Stream协议把数据送给DMA写引擎;写引擎根据CPU预先配置好的描述符,把数据直接写到ARM64侧的内存里;写完一整块描述符指向的数据后,写引擎产生中断,驱动在中断里处理这块数据并归还描述符。
反过来,CPU要下发控制命令时,通过AXI4-Lite接口走寄存器通道,配置采样率、触发条件、DMA启动停止等参数。两条通道分开设计,控制面低带宽高实时,数据面高带宽低干预,互不干扰。
ARM64侧的Linux驱动注册成平台设备,在设备树里匹配FPGA挂在总线上的设备节点。驱动负责分配DMA缓冲区、维护描述符队列、注册中断处理函数,然后把缓冲区通过mmap暴露给用户态应用。用户态拿到的是物理连续的内存映射,可以直接读写,避免了内核态到用户态的一次拷贝。
2. 硬件选型与数据通路设计
2.1 FPGA侧的资源评估与选型
FPGA的选型往往不是先定芯片,而是先算资源。你需要先明确前端接口的类型和数据率,再根据数据率反推内部需要的缓冲深度和处理逻辑复杂度。
以我常用的配置为例:前端是双通道250MSPS的ADC,16bit位宽,通过LVDS接口接入FPGA,总数据率约1Gbps。FPGA内部需要做双通道的数据对齐、格式转换、然后打包成AXI4-Stream的64bit数据通路。加上DMA写引擎、寄存器控制模块,以及后续可能加的滤波逻辑,LUT大概要占到中端芯片的3成左右,DSP Slice反而用得很少。
这里有个容易被忽视的坑是FIFO深度。很多人想省资源,把FIFO开得很浅,结果DMA搬运稍微慢一点就开始溢出丢数。实际上,只要DMA描述符队列设计合理,写入侧FIFO只需要能扛住一次总线仲裁的延迟就够了,不需要做得特别深。真正决定系统稳定性的是DMA引擎本身的背压处理——能不能在AXI4-Stream握手信号里正确地把FIFO满的状态反馈回采集前端。
如果项目里要用到多Die的中大型FPGA,还要特别注意跨Die走线的约束问题。数据通路尽量安排在同一个Die内,跨Die的信号要手动约束到特定的Super Logic Region,否则时序收敛会让你调到头秃。这个问题在后面的踩坑章节会展开说。
2.2 ARM64主控的选型思路
ARM64这边要考虑的东西不太一样。首先是接口,必须得有PCIe或AXI总线和FPGA对接,这不是USB和网口能替代的,因为只有总线级接口才能提供足够带宽和低延迟。其次是内存带宽,DMA要写内存,四核A53如果配的是DDR4-2400单通道,理论带宽已经足够,但你要注意实际平台的内存分配策略,别让DMA缓冲区和普通应用的内存互相抢占。
操作系统支持也是硬指标。选主控的时候一定要确认它的BSP对Linux的支持程度,比如主线内核是否已经包含对应的设备树和驱动,还是需要厂商提供补丁。我吃过一次亏,选了一颗很新的芯片,主线内核根本不认识它的串口和网卡,只能等厂商发布全量BSP,前后拖了一个多月。所以宁可选成熟一两年的型号,也不要追新。
常见的搭配有两种:一种是独立的ARM64核心板和FPGA板通过PCIe连接,适合产品迭代和模块化设计;另一种是SoC FPGA,ARM核和FPGA逻辑封装在同一颗芯片里,比如Xilinx的Zynq UltraScale+ MPSoC,这种方案集成度高、时序可控性好,但灵活性差一些。hs_dma_framework在设计上对这两种模式都做了兼容,核心区别只是总线接口的配置方式不同。
2.3 前端采集接口的取舍
前端接口的选择直接决定FPGA逻辑的复杂度,也是最容易返工的地方。说几个我实际对比过的接口:
- UART:这个主要是低速调试场景。FPGA实现UART RX很简单,但有几个细节要注意,比如波特率时钟需要16倍过采样才能保证稳定的采样点,FIFO深度至少要做成2的幂来配合DMA搬运。UART对hs_dma_framework来说更多是验证链路用的最小用例,跑通它再切换到高速接口,排查问题会舒服很多。
- SPI与ADC:FPGA控制SPI接口的ADC非常常见。SPI模式决定数据字节序,一般ADC都是MSB First,如果你的FPGA逻辑没对齐时序,采出来的数据会整体错位,表现出来就是数值完全不对但波形形状又像。SPI数据率上到几十MSPS就已经很吃力了,所以它适合中低速高精度的采集场景。
- LVDS:中高速ADC的标准接口。LVDS接收端要注意动态对齐的问题,采样时钟和数据之间的相位差会随温度和电压漂移,所以要用FPGA内部的延迟链做自动训练。这个训练逻辑务必在数据采集前完成,否则高低温环境下数据会随机错位。
- JESD204B:吉比特采样率的ADC基本都用这个。它的链路层、传输层如果用厂商IP还好,自己实现的话工作量巨大,而且必须处理多通道的同步问题。hs_dma_framework的DMA部分不关心你前端是什么接口,只要输出标准AXI4-Stream就行,但不同接口的适配模块还是得单独写。
这里给个建议:不管前端用什么接口,FPGA侧统一转成AXI4-Stream协议再送给DMA引擎,这条规则能让你后续接任何ADC都不用改DMA核心逻辑。
3. DMA框架的核心设计
3.1 描述符环形队列是怎么工作的
DMA框架的心脏是描述符环形队列。理解它之前,先丢掉“DMA就是把数据从一个地址搬到另一个地址”这种简单认知。实际的高速DMA由一组描述符驱动,每个描述符记载一段数据搬运任务的元信息:源地址、目标地址、数据长度、控制标志、下一个描述符的地址等。CPU负责准备这组描述符,DMA硬件按顺序执行它们。
环形队列的意思是这个描述符数组在逻辑上是首尾相接的。CPU在队尾追加新的描述符,DMA在队头取描述符执行,两者通过一个硬件生产的“尾巴索引”和软件维护的“头索引”来同步。我设计hs_dma_framework的队列深度是256项,每项描述符最大搬运1MB数据,这样整个环最多可承载256MB的任务量,实际项目中足够覆盖绝大多数突发采集场景。
描述符环形队列相比传统链式描述符有个实在的好处:分配和回收都是固定的内存块,不会出现链表碎片化问题,而且DMA引擎可以用简单的循环索引判断队列是否为空或已满。只要CPU追加描述符的速度跟得上DMA执行的速度,系统就能稳定跑满带宽。
3.2 为什么不用简单的FIFO加中断方式
这是个值得展开的问题。很多FPGA采集项目用的是这种方式:ADC数据不断写入FIFO,FIFO水位超过阈值就拉高中断,CPU响应中断后把FIFO数据搬走。看着没什么问题,但数据率一高就露馅了。
中断的代价是很高的,一次完整的Linux中断处理包括硬件中断、中断线程化、上下文切换,最少也要几十微秒。如果FIFO很浅,数据会频繁触发中断,CPU大部分时间都在响应中断,真正处理应用的算力所剩无几。如果FIFO很深,延迟又上去了,对实时性要求高的场景根本不合格。
DMA描述符方案的本质是用内存换中断次数。每执行完一个描述符才产生一次中断,一次中断对应一整块数据的交接,中断频率下降了两个数量级。实测下来,同样是200MB/s的采集数据率,FIFO加中断方式CPU占用大概在一核半左右,而描述符环形队列方案只需要不到三分之一核,而且数据块越大这个优势越明显。
3.3 ARM64缓存一致性问题的处理
这是整个项目里最阴险的坑,x86平台上搞DMA的人很容易在这里翻车。x86的CPU缓存和DMA引擎之间有硬件缓存一致性协议,DMA写进内存的数据CPU能看到,CPU改过的数据DMA也能读到,不需要软件介入。但ARM64平台的很多实现不具备完整的硬件缓存一致性,DMA直接写物理内存时,可能会绕过CPU的Cache,导致CPU读到的还是旧数据。
解决方案是在软件层面对DMA缓冲区的Cache操作做显式管理。Linux内核里对应的API是dma_map_single、dma_alloc_coherent这一族。dma_alloc_coherent分配的缓冲区保证了CPU视角和DMA视角的一致性,但代价是该区域不能使用Cache,读写性能会打折扣。dma_alloc_attrs可以灵活调整这个策略。
hs_dma_framework里我做了个折中:DMA描述符队列本身用dma_alloc_coherent分配,因为描述符的读写频率不高,一致性优先;真正的数据缓冲区用dma_map_single动态映射,配合DMA_BIDIRECTIONAL标志,在DMA操作前执行dma_map_single,操作完成后再dma_unmap_single。这样既保证了数据通路的高速缓存命中,又避免了缓存一致性问题。驱动的每次DMA操作都要严格按我上面说的顺序执行,漏掉一步,采集数据就会出现“看起来没什么规律但偶尔跳变”的灵异现象。
4. Linux驱动与用户态数据通路
4.1 字符设备驱动还是UIO
驱动架构的选择直接影响开发效率。两条常见的路线,一是传统字符设备驱动,二是UIO(Userspace I/O)。我用过两种,各有适用场景。
传统字符设备驱动的优势是符合Linux设备模型,数据可以走内核网络栈、V4L2框架等成熟路径,适合要对接现成应用框架的场合。缺点也很明显,内核态编程风险高,一个指针错误就是整个系统崩溃,调试全靠printk和trace。而且每改一次需求可能就要重新编译内核模块加重启,迭代效率低。
UIO则把大部分驱动逻辑搬到了用户态。内核侧只需要一个极简的UIO驱动把物理地址映射到用户空间,中断也通过poll机制暴露给用户态进程。这样DMA描述符的管理、缓冲区的处理全部可以用普通C代码完成,出了问题可以直接用gdb调试,不需要在驱动开发上耗太多时间。hs_dma_framework最终采用的是UIO方案,纯属从工程效率出发的选择——DMA逻辑本来就很复杂,把它放进用户态能省掉大量内核调试时间。
需要注意的是,UIO在Linux主线内的接口就是uio_pdrv_genirq,配合设备树属性就能工作,不需要自己写内核驱动。但如果你需要DMA缓冲区的缓存一致性管理,最好自己在UIO驱动里补充dma_alloc_coherent的分配逻辑,否则还得依赖用户态手动做cache flush,很容易把ARM64的Cache操作API搞错。
4.2 设备树里的配置要点
ARM64平台上Linux通过设备树描述硬件,FPGA在系统里的位置也要靠设备树交代清楚。设备树配错了,驱动大概率直接加载失败,而且报错信息不一定直观。
我常用的设备树节点长这样:
fpga_engine: fpga@a0000000 { compatible = "hs,dma-framework-v1"; reg = <0x0 0xa0000000 0x0 0x10000>; // 控制寄存器空间 reg-names = "control"; interrupts = <0 42 4>; // SPI中断,42号,高电平触发 interrupt-parent = <&gic>; dma-buf-size = <0x400000>; // 单个DMA缓冲区4MB dma-ring-size = <256>; // 描述符队列深度 };设备树里最容易出错的是reg和interrupts的格式。ARM64平台PCI和平台设备的reg属性格式不完全一样,平台设备通常需要两段地址来描述高32位和低32位,漏了可能会被裁剪到32位地址空间。另一个坑是interrupts属性里的中断号,填写前要确认中断控制器(GIC)的中断号分配,写错一个字驱动就收不到中断,DMA搬完数据无人知晓,采集线程卡死。
还有一点经验之谈:设备树节点里加一条status属性,默认设为"disabled",在需要启用的时候再通过bootargs或uboot环境变量改成"okay"。这样做的好处是批量部署时不用重新编译内核,改一条设备树就能控制不同板卡上FPGA引擎的开关。
4.3 mmap零拷贝与用户态环形缓冲
驱动把物理缓冲区映射到用户态之后,剩下的工作就是用户态数据的组织。最常用也是我推荐的结构是用户态环形缓冲,它和内核侧的DMA描述符队列形成两级流水:内核侧描述符环形队列负责“硬件到内核”的搬运,用户态环形缓冲负责“内核到应用”的交付。
mmap映射要注意对齐要求。Linux的mmap要求映射长度按页对齐,如果你dma_alloc_coherent分配的缓冲区是1MB,正好是256个页面的整数倍,没问题。但如果你图省事分配了一个370KB的非对齐大小,最后两个页面的映射就会出问题,用户态访问时会触发段错误。
用户态环形缓冲的容量通常做成DMA描述符队列容量的2到4倍,这样即便用户态应用偶尔掉链子处理不过来,缓冲也不会立刻溢出。缓冲区的读写指针采用生产者消费者模型,驱动每提交一块数据就更新写指针,应用消费完一帧更新读指针。这里有个性能优化点:两个指针的更新操作要加内存屏障,ARM64上用原子操作加RELEASE语义,避免编译器乱序优化导致读写指针错位。
5. ARM64平台适配与调试环境
5.1 交叉编译环境的搭建
ARM64的交叉编译是每个从x86主机过来的Linux开发者都要过的第一关。基本工具链是aarch64-linux-gnu-gcc,各家发行版都有打包好的交叉编译器,Ubuntu上直接装gcc-aarch64-linux-gnu就行。除此之外还得装交叉编译版的内核头文件和libc,不然编译驱动模块时连基本的printk、kmalloc声明都找不到。
驱动模块的编译用的是内核源码树,必须先用ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-配置好内核,再进入驱动目录执行内核模块的make命令。这一步经常有人卡住:宿主机上编译x86模块没问题,但交叉编译时忘记导出ARCH和CROSS_COMPILE环境变量,编译器还是主机工具链,编出来的.ko文件在板子上insmod直接报格式错误。
建议搭建环境时顺便把文件系统也准备好。ARM64的根文件系统直接用现成的发行版根文件系统也可以,但裁剪过的会更轻量。我习惯用debootstrap或buildroot生成最小根文件系统,然后通过NFS挂载到开发板上。这样每次改完驱动和应用,重新编译后直接拷贝到NFS目录就能运行,省去烧写SD卡和重启的漫长循环。
5.2 QEMU模拟ARM64的加速开发
板卡资源总是有限的,而且大量驱动迭代会很磨硬件的寿命。hs_dma_framework开发过程中,我在QEMU模拟的ARM64虚拟机上跑通了大部分用户态代码和部分内核模块的验证,效率提升非常明显。
QEMU的ARM64虚拟化支持已经很成熟,用qemu-system-aarch64配合virt平台,可以模拟出带GIC中断控制器、PCIe总线、网络和virtio磁盘的完整设备树环境。最关键的是它支持半虚拟化的串口和中断,Linux跑起来和真机几乎没有区别。设备树节点里的interrupts配置,在QEMU里就能验证语法是否正确,中断是否能够触发,这些做好了,上板之后大概率直接能用。
当然QEMU也有局限性,它模拟不了FPGA的实际时序,也模拟不了DMA引擎的AXI行为。所以我的使用方式是:QEMU主要验证驱动加载流程、设备树解析、用户态环形缓冲逻辑、内存映射是否正确;DMA硬件行为必须回到板子上用FPGA逻辑分析仪和Linux内核trace配合验证。两条腿走路,开发速度能快一倍以上。
5.3 启动流程与根文件系统
ARM64平台的启动流程要理解清楚,不然连“怎么把内核跑起来”都费劲。典型流程是:上电后ROM代码加载引导程序(U-Boot),U-Boot初始化DDR和基本外设,加载设备树二进制文件(dtb)和内核镜像(Image),然后跳转到内核入口。内核启动后会根据设备树初始化各个平台设备,挂载根文件系统,最后启动init进程。
调试启动阶段最常见的问题是串口没有输出。这个问题90%是设备树中串口节点的clock-frequency属性和实际串口时钟不一致导致的。很多人会怀疑内核镜像坏了或者U-Boot配置错了,实际上查一下设备树里uart节点的时钟频率,和板级原理图对比,问题立刻暴露。
根文件系统的选择上,我只推荐两条路。开发阶段用NFS挂载,方便快速迭代;发布阶段用SD卡或eMMC上的ext4根文件系统加uboot的initramfs支持。hs_dma_framework的采集应用要做成系统服务,通过systemd托管,开机自动加载驱动、启动采集进程。arm64v8相关的容器镜像如果项目要用到,也可以在ARM64平台直接跑,省去很多依赖编译的麻烦。
6. 仿真、实测与性能调优
6.1 FPGA侧Testbench怎么搭
FPGA逻辑的验证不能等到上板再发现问题。hs_dma_framework的FPGA部分,我习惯在Vivado/Quartus里建一套规范的Testbench,把AXI4-Stream的DMA写引擎、描述符队列维护逻辑、以及模拟ADC的数据源全部虚拟化。
Testbench的核心思路是模拟AXI总线时序。写一个简单的AXI4-Stream主设备模型,每隔几个时钟周期推出一拍数据,模拟真实ADC的采样节奏。同时用AXI4-Lite从设备模型去配置DMA引擎的寄存器。仿真时用VCD波形文件记录关键信号,重点关注write pointer、read pointer、FIFO水位这几个变量,确保描述符队列不会出现空转或重复执行。
带中断的仿真还有一个细节:ARM侧的Linux驱动在仿真环境里跑不通,所以Testbench要自己模拟中断的产生和清除流程。可以写一个自动检查描述符完成状态的逻辑,发现描述符完成标志置位后,自动拉高中断信号几个周期,模拟真实驱动读取并清除中断寄存器的行为。这样上板之后,驱动和FPGA配合的稳定性很大程度上已经被提前验证过了。
6.2 实测吞吐率与CPU占用
平台跑通之后一定要做量化测试,不能只满足于“数据不丢”。我的测试方法是构造三档负载:第一档用FPGA内部计数器生成固定格式的伪随机数据,不接真实ADC;第二档接信号发生器出正弦波,通过ADC量化后再采集;第三档接双通道真实传感器做长时间持续采集。
实测数据供参考:在四核ARM64主频1.5GHz的平台下,DMA描述符队列深度256,单块缓冲区1MB,持续采集中断频率约200次每秒,系统CPU占用率约三分之一核,内存带宽占用约150MB/s时没有丢帧。把缓冲区改小到64KB时中断频率骤升到3000次每秒,CPU占用率翻了差不多三倍。这说明DMA缓冲区大小对系统性能的影响是决定性的,一定要根据实际采集数据率和突发特性来权衡。
还要测长时间稳定性。我曾经碰到过连续跑两天后DMA突然停摆的现象,后来原因定位到描述符队列索引溢出——软件侧搞混了无符号整数的环绕逻辑。这种问题在短时间测试中根本暴露不了,所以稳定性测试至少要保持48小时以上。
6.3 性能瓶颈定位与参数调整
如果实测吞吐率上不去,不要急着改代码。用Linux的perf top先看热点在哪里,很多时候瓶颈根本不在DMA本身。
我遇到过的瓶颈主要有三类。第一类是中断处理路径太长,中断里做了太多内联处理,解决办法是把工作量移交到中断线程里。第二类是用户态轮的太频繁,应用为了尽快拿到数据用100%的CPU跑忙等,办法是把轮询间隔和DMA中断频率对齐,尽量让应用在数据就绪后才醒来。第三类最隐蔽,是用户态到内核态的缓冲区同步操作太频繁,ARM64上执行cache操作的代价其实很高,进入DMA前如果还要对整块缓冲区做invalid操作,数据率上去了开销就很可观。这种情况下宁可多留余量,让DMA只write并信任DMA_BIDIRECTIONAL标志的处理,也不要动不动全缓冲invalid。
调参我是这样做的:先固定描述符数量,扫一遍缓冲区大小从64KB到4MB的性能曲线;曲线平台期再反过来固定缓冲区大小,扫描述符数量。这样两个维度扫完之后,基本能找到当前平台的甜点参数。
7. 踩过的坑和应对方法
7.1 DMA地址对齐问题
这个坑几乎是所有自研DMA框架的梦魇。AXI总线的DMA要求源地址和目标地址按burst长度对齐,常见的是32字节或64字节对齐。如果你的描述符里填的目标地址没有对齐,DMA写传输要么直接报错,要么默默丢掉头部几个字节,数据错位还不知道哪里错的。
解决办法是驱动分配缓冲区时强制对齐。Linux的dma_alloc_coherent返回的地址本身就是对齐的,但如果你在用户态通过mmap再偏移使用,就要小心偏移量破坏了原始对齐。我后来索性用了页对齐的缓冲区池,每次从池子里取整块4KB或64KB的区域,确保任何分包组合都能保持对齐。
7.2 中断风暴与NAPI思路
DMA设计得越高效,中断频率反而可能越低,但如果遇到极端场景,比如短促高频的突发数据,中断频率就会瞬间飙升,把系统卡死。Linux网络子系统有NAPI机制专门解决这个问题——中断到来时先关闭中断,用轮询方式把一整批数据处理完,处理清了再重新开启中断。
hs_dma_framework的驱动也借鉴了这个思路。采集突发数据时,中断处理函数只把当前的描述符链表挂到待处理队列,然后立刻调度一个内核线程去批量处理。如果下一轮检查发现待处理队列还有数据,就继续处理,不需要再等新中断进来。效果非常明显,突发场景的中断次数下降了将近一个数量级。
7.3 多Die FPGA的约束问题
用到中大型FPGA时,跨Die布线会带来一系列让入时序收敛失败的问题。多Die芯片的每个Die都有独立的可编程资源和路由资源,Die之间通过有限的Super Logic Region互连,跨Die信号多了,布线资源紧张,时序自然崩。
处理办法是提前在引脚规划阶段就把数据通路限定在单一Die内。DMA引擎和它对应的AXI总线接口、FIFO务必放在同一个Die;跨Die的信号只保留低频率的控制信号和中断信号。这需要在工程的floorplanning阶段就做约束,后期再改是非常痛苦的返工。另一个技巧是给跨Die信号加上同步寄存器,打两拍再进逻辑,避免亚稳态问题。
7.4 板级调试的一点经验
最后说一个容易忽略的细节:电源质量对高速采集系统的影响。FPGA内部有大量高速翻转的逻辑,如果电源纹波超标,采样时钟的抖动会变大,采集数据的信噪比会肉眼可见地变差。做hs_dma_framework实测时,我试过把FPGA核心供电的DCDC开关频率调低,采样底噪立刻抬高了将近3dB。所以平台调试时不要光盯逻辑,模拟电源的余量也值得花时间检查。
另外一个习惯是给每块板卡预留JTAG和逻辑分析仪的测试点,FPGA侧的ILA核、ARM侧的trace工具都提前留好接口。项目进行到后期,80%的时间都花在定位问题上,不说瞎话,尽早把调试基础设施准备全,后面会感激现在的自己。