做高速数据采集,最难办的不在采集本身,而在数据怎么不丢、不重、不卡地送进电脑。去年我接到一个项目,前端是两片125MSPS的高速ADC,满速跑起来数据率稳稳超过300MB/s,后端要求通过USB3.0实时上传到PC。一开始我想得挺简单——FPGA直接接USB3.0 PHY不就完了?结果越做越发现,USB协议栈的复杂度根本不是FPGA裸逻辑那点活儿能扛住的。折腾一圈,最终老老实实回到FPGA+CYUSB3014(FX3)这套组合方案,也就是本篇文章的标题来源:实测338MB/s,整条链路端到端稳定跑通。这篇文章把从方案选型、硬件设计、固件开发、FPGA逻辑到性能调优的完整过程全部整理出来,给正在做类似高速采集系统的同行当个参考。
这套系统最后能跑出338MB/s,说起来容易,实际里面每一级都需要精细设计。我刚开始搭的时候,从原理图到固件用了将近三周,第一版跑起来只有123MB/s,后来逐个环节拆开查,才把吞吐拉上去。下面的内容基本就是我当时的设计笔记加踩坑记录,尽量把文档里没写明白的坑都替你踩一遍。
1. 为什么是FPGA+FX3:三种USB3.0采集方案的取舍
选型这件事值得先说清楚。很多人拿到需求第一反应是“FPGA直接上USB”,但我实际试了一圈之后,结论很明确:除非你想做USB协议分析仪或者特殊设备,否则老老实实用一颗成熟的USB控制器,把FPGA的精力留给采集和预处理。
1.1 直接上USB PHY的路线
FPGA通过ULPI接口外挂USB3.0 PHY,用逻辑自己实现协议栈,这条路在理论上是可行的,实际做起来却非常酸爽。USB3.0协议栈包含链路训练、流控、重传、包管理、电源管理等等,这些逻辑全部塞进FPGA里,不仅开发周期爆炸,而且时序收敛困难。再加上USB主机端的兼容性问题,一堆主板芯片组对协议细节要求很高,FPGA逻辑实现稍有偏差就枚举失败或者传输报错。这套方案只适合真的需要自己掌控协议的极少数场景,普通数据采集项目完全不建议碰。
1.2 直接用FT601这类FIFO桥接芯片
FTDI的FT601也是一颗USB3.0转FIFO的芯片,接口相对简单,开发难度确实低。但问题在于它的FIFO接口是固定的,时序和信号都不能按你的系统去定制。数据采集中FPGA往往有自己严格时序的数据源,FT601这种“固定FIFO”方案灵活性就不太够,而且它在连续大流量下的稳定性、缓冲区管理能力,跟FX3的DMA架构比还是弱了一截。想要压到300MB/s级别并长时间稳定运行,FT601不是最优解。
1.3 CYUSB3014 FX3的核心优势
FX3是我最后锁定的方案。它内部是一颗ARM926EJ-S处理器加上专门为数据传输设计的GPIF II可编程接口(通用可编程接口,可以配置成同步从FIFO、异步从FIFO、主模式等),数据走硬件的DMA通道绕开CPU,这是它能跑高速的关键。GPIF II最高100MHz时钟、32位数据总线,理论上能给到400MB/s的接口带宽,和USB3.0实际的链路带宽是匹配的。再加上Cypress官方SDK和GPIF II Designer工具,固件大部分都是现成的模板改一改,开发效率高很多。
三种路线对比下来,我自己的结论写在表格里供你参考:
| 方案 | 接口方式 | 理论峰值 | 开发复杂度 | 灵活性 | 生态成熟度 |
|---|---|---|---|---|---|
| FPGA+USB PHY | 自定义ULPI | 高 | 极高 | 最高 | 低 |
| FT601 | 固定FIFO | 中等 | 低 | 低 | 一般 |
| FX3 | GPIF II | 400MB/s | 中等 | 高 | 很好 |
选FX3还有一个现实理由:数据采集项目的痛点往往不只是USB吞吐,还有采集速率抖动、突发流量、多通道同步这些问题。FX3的GPIF II能跟FPGA内部状态机自然配合,数据从FPGA到总线再到USB端点的路径都是硬件的,CPU只在配置和枚举阶段参与,这种方式对高速采集来说是最稳的。
2. 整条数据链路是怎么设计的:从采集端到PC的每个环节
系统整体结构决定了性能上限,布局设计时要把每一级缓冲都想清楚。我的链路是这样的:
ADC输出 → FPGA采集模块 → FPGA预处理 → FPGA内FIFO缓冲 → GPIF II写总线 → FX3 DMA描述符缓冲区 → USB3.0批量端点 → 主机驱动缓冲区 → 上位机应用层
每一级的存在都有明确目的。ADC过来的数据流是连续实时的,但USB传输是突发式的,必须以包为单位发送。FPGA内部如果不用FIFO把两边速率“隔离”开,要么丢数据,要么总线利用率极低。FX3内部也有DMA缓冲区,用于把GPIF II接口的数据搬运到USB端点,缓冲区大小和数量直接影响吞吐。主机侧同样需要足够的接收缓冲,否则一次长时间突发就溢出了。
2.1 数据率怎么算出来的
系统满速时数据率是:125MSPS × 16bit × 2片 = 5000Mbps,约等于625MB/s。这已经超出USB3.0实际链路带宽了,所以我把FPGA预处理里加了降采样和抽帧逻辑(项目本身允许),最终进入USB管道的是稳定的330MB/s左右。这里有个基本原则:实时采集系统的数据链路预算必须从源头算到最后,任何一级的理论带宽都不能低于前级实际输出,否则丢数据只是时间问题。
2.2 每一级缓冲的容量怎么定
FPGA内部FIFO我最终选了256KB,用两块BRAM拼出来的双时钟FIFO。为什么是256KB?因为USB一个突发传输周期内,主机轮询可能带来几十微秒的间隙,334MB/s在这段时间内大约会产生几十KB的数据堆积,256KB留了一半以上的余量。FX3侧DMA缓冲区配置了16个16KB的描述符缓冲,相当于256KB的环形队列。主机侧则用了4MB的用户态接收缓冲区。这三个数字互相匹配,整条链路像三个水箱串在一起,容量逐级放大,不会出现小口瓶卡水的情况。
2.3 数据传输格式也影响速率
很多人忽略的是:你要是往USB数据流里塞过多的协议头,开销会直接吃掉有效载荷。我设计的帧格式是64字节包头 + 用户数据 + 4字节CRC。包头里放了帧号、时间戳、有效数据长度、通道状态,这些信息上位机都要用,但把它们全部压缩进64字节而不是搞成几百字节的自定义协议。数据包大小整体定在16KB左右,这个尺寸兼顾了USB批量传输的效率和FPGA侧FIFO的粒度。
链路设计还有一个细节:FX3的USB端点要用批量传输(BULK)还是等时传输(ISO)?我选了BULK。等时传输虽然有保证带宽,但端点缓冲小,而且数据错了不重传,对数据采集来说一旦有错就完蛋。BULK协议上允许重传,对可靠性和通用性都好,代价是吞吐受主机端调度影响,但实测下来只要上位机处理够快,BULK完全可以跑到330MB/s以上。
3. 硬件上容易被忽略的细节:电源、时钟、阻抗和电平
这一章单独拿出来说,是因为我第一版板子有两处硬件问题直接导致枚举失败,最后查了好几天才发现是原理图阶段埋下的雷。硬件设计确实枯燥,但FX3这种高速器件,一个引脚接错就是整套系统跑不起来。
3.1 FX3的电源和上电时序
FX3核心供电是1.2V,IO供电3.3V或者1.8V(取决于你接口电平),还有一路模拟电源给USB PHY。注意USB PHY那块电源要单独加磁珠和去耦电容,不然插入主机时电压跌落会导致枚举失败。上电时序也要控制,FX3没有内部POR,必须外部给复位信号,建议用电源监控芯片如TPS3808或者LTC2955,确保1.2V起来至少几毫秒后再释放复位。我第一版就是图省事把复位接到了RC电路上,结果低温环境下偶尔枚举不到,后来换成监控芯片才稳定。
IO电平这一项,GPIF II接口电压完全由VIO引脚决定,如果FPGA那边是2.5V或者1.8V的bank,这边就不能直接接3.3V,两边必须一致。更稳妥的做法是FPGA和FX3之间加电平转换或者选兼容bank供电,但电平转换芯片本身有延迟,对100MHz总线来说要小心选型。我的板子是直接把两边bank都做成3.3V,省掉了转换,这也是能跑上100MHz的原因之一。
3.2 时钟方案:19.2MHz晶振还是外部时钟
FX3的时钟输入有两种方式:晶振或者是外部时钟源,我用了19.2MHz的无源晶振。这里有个细节:晶振要靠近FX3的XTALIN/XTALOUT引脚,负载电容要按晶振手册调整,别照抄开发板,因为PCB杂散电容不一样。时钟精度影响USB的位时钟恢复,如果晶体偏差超过几百ppm,高速枚举可能失败或者传输误码率上升。画PCB时时钟走线周围要包地,避免和USB差分线耦合。
3.3 USB3.0差分对的布线要求
USB3.0的TX/RX两对差分线,必须要90Ω差分阻抗。这个阻抗不是芯片自动给的,是PCB叠层和线宽线距算出来的。4层板的话,直接把差分线走在顶层参考第二层完整地平面是最容易满足阻抗的。两对差分线等长我控制在5mil以内,对内等长做得更严。连接器附近我加了一颗ESD保护芯片,位置紧贴USB座,防护走线别拉太长,否则ESD性能和阻抗都受影响。
3.4 启动配置:I2C EEPROM还是USB启动
FX3支持USB启动、I2C EEPROM启动、SPI Flash启动三种方式。开发阶段肯定是用USB启动最方便,固件每次通过Cypress的工具下载到片内RAM运行,改代码快。但交付用户时不可能每次开机都手工下载固件,所以我留了I2C EEPROM。FX3上电先找EEPROM,有有效固件就加载运行,没有就进入USB枚举等待主机下载。这个设计在开发调试和产品化之间两头兼顾,非常推荐。
EEPROM里烧录固件时有个坑:固件版本号如果没更新,刷了几次之后你会发现板子还是旧逻辑在跑。我用的办法是在固件里打印编译时间和版本号,每次下载前先习惯性看一眼版本信息,不然就是改了个寂寞。
4. FX3固件开发:GPIF II状态机和DMA通道的配置逻辑
FX3固件开发是整个项目里最核心也最容易出问题的部分。很多人拿到SDK直接用官方SlaveFIFO例程跑demo,一跑就通,换成自己的系统后就十几MB/s,问题大多出在GPIF II配置和DMA描述符数量上。
4.1 先搞清楚GPIF II的两个方向
GPIF II是一个状态机驱动的可编程接口,核心特点是它可以用图形化工具(GPIF II Designer)配置成各种总线协议。我的系统里用得是同步从FIFO模式,从名字理解就是FX3作为FIFO的“从设备”,FPGA是主设备。FPGA侧观察到的接口非常简单:拉低SLWR#同时把数据放到D[31:0]总线,FX3在下一个时钟上升沿把数据采样进DMA缓冲区。
GPIF II同步从FIFO涉及的关键信号有这些:
| 信号 | 方向 | 作用 |
|---|---|---|
| D[31:0] | FPGA→FX3 | 数据总线 |
| SLWR# | FPGA→FX3 | 写使能,低有效 |
| PKTEND# | FPGA→FX3 | 提交短包/结束包 |
| FLAGA/FLAGB | FX3→FPGA | FIFO状态标志 |
| PCLK | FX3→FPGA | 接口时钟 |
| A[1:0] | FPGA→FX3 | 内部DMA socket选择地址 |
这个模式的优点在于FPGA端逻辑极其简单,本质上就是“向FIFO写数据”。FX3内部会把GPIF II收到的数据自动搬运到USB端点,不需要ARM内核干预。FLAGA是“可编程阈值”标志,我把它配置成当某个DMA socket的缓冲剩余空间大于16KB时拉高,FPGA看到这个标志才允许发起写突发,这样就保证了FPGA不会向已满的FIFO里写溢出。
4.2 DMA通道和描述符数量是吞吐的关键
这里必须强调一个很多人忽略的概念:FX3的DMA通道工作在AUTO模式下,数据完全由硬件流转,CPU不参与。如果选了MANUAL模式,CPU就必须处理每个缓冲区的中断并搬运数据,性能直接腰斩。所以高速方案一律用AUTO。
GPIF II收到的数据要流到USB端点,必须创建一个DMA通道连接“生产者socket”(GPIF侧)和“消费者socket”(USB端点侧)。创建通道时要指定缓冲区大小和数量。我调试时试过用8个4KB缓冲区,结果吞吐只有180MB/s,后来改成16个16KB缓冲区,吞吐直接涨到330MB/s以上。原因很简单:描述符越多,USB控制器就越不容易出现等待空缓冲区的情况;而每个缓冲区越大,GPIF II突发时能连续写入的数据就越多。USB传输机制本质上是一个吞吞吐吐的管道,缓冲区数量多一点就像给水管加了蓄水罐,流量越稳定。
CyU3PDmaChannelConfig_t dmaConfig; dmaConfig.size = CY_FX_USB_DMA_BUF_SIZE; // 16 * 1024 dmaConfig.count = CY_FX_USB_DMA_BUF_COUNT; // 16 dmaConfig.prodSckId = CY_U3P_PIB_SOCKET_0; // GPIF II侧 dmaConfig.consSckId = CY_U3P_UIB_SOCKET_EP_1; // USB EP 1 BULK dmaConfig.dmaMode = CY_U3P_DMA_MODE_BYTE; ... CyU3PDmaChannelCreate(&gpifToUsbHandle, CyU3PDmaChannelAuto, &dmaConfig);4.3 固件初始化流程
固件流程其实就八步,但每一步顺序都不能乱。先跑设备初始化,再设置中断和缓存,然后加载GPIF II配置,创建DMA通道,配置USB端点,最后启动USB和设备。核心代码如下,注意最后一个CyU3PUsbStart调用之前,必须确保DMA通道已经创建好,否则端点起来了但数据流没通,USB会枚举成功但收不到任何数据。
CyU3PDeviceInit(NULL); // 初始化芯片 CyU3PKernelEntry(); // 启动内核调度 CyU3PToolChainInit(); // 辅助功能初始化 CyU3PGpifLoad(&cyFxGpifConfig); // 加载GPIF II Designer生成的配置 CyU3PDmaChannelCreate(...); // 创建DMA通道 CyU3PUsbSetEpConfig(0x01, CY_U3P_USB_EP_BULK, ...); // 配置BULK IN端点 CyU3PConnectStateCallback(ConnectCallback); // 注册连接事件回调 CyU3PUsbStart(); // 启动USB枚举4.4 上位机枚举和端点参数
USB端点我配置为512字节的BULK IN端点,如果PC主机不支持高带宽模式,这个配置是全兼容的。FX3在USB枚举阶段会向上报端点描述符,上位机通过VID/PID识别的设备。我用的VID是Cypress的0x04B4,PID自己编一个,在固件初始化里设定。Cypress SDK自带的Scsi示例里有现成的描述符定义,直接改PID和字符串就行。
5. FPGA侧写总线逻辑:状态机怎么搭,时序约束怎么加
FX3固件搞定之后,另一大块就是FPGA侧。FPGA内部模块看起来多,其实核心就两个任务:把采集数据正确地装进FIFO,再用一个状态机把数据按GPIF II的时序写出去。这个状态机写不好,数据会错位,帧结构会乱,甚至会丢数据。
5.1 双时钟FIFO隔离两个时钟域
FPGA内部有两个明显不同的时钟域:ADC采样时钟域和GPIF II接口时钟域。采样时钟由ADC提供,GPIF接口时钟由FX3提供(PCLK,我配置为100MHz)。两个时钟完全没有相位关系,所以中间必须用异步FIFO(双时钟FIFO)过渡,这是FPGA一切的基石。我在FIFO输入端写数据,输出端读数据,读写侧各自独立时钟,用XPM宏或者Quartus/ Vivado的FIFO IP核都可以实现。
FIFO深度256KB,写入侧水线和读出侧水线分别设定。写入侧采集数据只要FIFO不满就一直写;读侧只有当FIFO内数据量超过64KB时才发给USB,这样就保证了GPIF II总线每次突发都是至少64KB的连续传输,不会因为FIFO空了就频繁打断。
5.2 写状态机:核心就是“等标志、送数据、提交包”
GPIF II同步从FIFO模式的写状态机非常直接,状态数不超过6个。我按IDLE、WAIT_FLAG、WRITE、WAIT_EMPTY、SEND_PKTEND这五个状态来写。当FX3的FLAGA标志有效(即DMA缓冲区有空间),状态机从FIFO读数据放到DQ总线上,同时拉低SLWR#,在每个PCLK上升沿向FX3送一个32位数据。一包数据长度为16KB,最后发一个周期PKTEND#结束包提交。这个状态机写起来不复杂,但有一个性能细节:WAIT_FLAG状态的等待时间要尽量短,因为FLAGA拉低说明FX3内部DMA描述符用完了,FPGA在等待DMA缓冲释放,这个等待时间要是太长,总线利用率就往下掉。
module usb3_writer #( parameter DW = 32 )( input wire pclk, // 来自FX3的100MHz时钟 input wire rst_n, input wire fifo_empty, input wire [DW-1:0] fifo_dout, input wire flag_a_valid, // FX3的FLAGA output wire [DW-1:0] gpif_data, output wire gpif_slwr_n, output wire gpif_pktend_n, output wire fifo_rd_en ); reg [2:0] state; localparam IDLE=0, WAIT_FLAG=1, WRITE_BURST=2, SEND_PKTEND=3; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; gpif_slwr_n <= 1'b1; fifo_rd_en <= 1'b0; end else begin case (state) IDLE: if (!fifo_empty) state <= WAIT_FLAG; WAIT_FLAG: if (flag_a_valid) state <= WRITE_BURST; WRITE_BURST: begin if (burst_cnt == PACKET_WORDS-1) state <= SEND_PKTEND; end SEND_PKTEND: state <= IDLE; endcase end end endmodule5.3 时序约束和综合设置
FPGA侧的时序约束是整个项目最容易被忽视的点。GPIF II总线的数据信号和SLWR#、PCLK之间的setup/hold时间必须满足FX3芯片手册的要求。在Vivado里,我给GPIF接口创建了一个独立的时钟组约束,把PCLK定义为主时钟,所有输出到FX3的数据和使能信号都设set_output_delay,参考FX3手册给出最大最小值。这一步不做,综合出来的时序可能只是“碰巧能用”,温度一变或换个芯片批次就出问题。
跨时钟域的FIFO读侧数据,通过FIFO IP核本身已经做了异步处理,不需要额外约束。但采集模块内部如果有多位宽的跨时钟数据,一定要用异步FIFO或者格雷码同步器,别图省事用寄存器直接打拍。
6. 从123MB/s到338MB/s:一次完整的性能调优
硬件和软件都通了之后,我第一次测整条链路的吞吐:123MB/s。这个数字离需求还差得远。当时的操作是打开上位机软件,看着速度在那跳,心里凉了半截。但后来我总结出一个固定在头脑里的排查方法论:USB3.0高速传输的性能问题,必须分层定位,别上来就猜。
6.1 分层拆解:固件裸吞吐先测
第一步用Cypress官方的USB Streamer例程替代我的固件,用的是CyUsb3Kit上的USB Control Center直接发流,数据在板内自产自销(FX3内部循环到端点),不经过我FPGA和ADC。这一测,固件+硬件的裸吞吐接近395MB/s。这说明USB3.0物理链路、FX3 DMA配置、上位机基础驱动都是OK的,问题不出在USB端。
6.2 FPGA端到FX3的链路检查
接下来把FPGA逻辑加进来,用FPGA内部产生一个已知伪随机序列作为数据源,替代ADC输入,其他环节不变,再测吞吐。这时吞吐掉到了260MB/s。单独抓GPIF II波形发现,总线上频繁出现空闲间隔,每写完几百个周期就要停一会儿。原因是FLAGA标志经常拉低,说明FX3的DMA描述符还没释放出来。我一开始怀疑DMA描述符不够,但前面固件裸测明明很流畅,同样的DMA配置为什么接上FPGA就变慢?
最终定位出来的原因非常微妙:FPGA的FIFO读侧水线设置太低,导致FLAGA一拉低,FIFO里正好也没多少数据了,状态机无法立刻恢复写总线。也就是说,FLAGA恢复和FIFO水位恢复在时间上重合,形成了“等待加重”的节奏。解决方法是把FIFO读侧水线抬高到64KB,并让状态机在FLAGA无效期间依然保持从FIFO持续读数据、把数据在内部寄存器里准备好,这样FLAGA一回来就能立刻开始写GPIF总线。改完后FPGA到FX3这一段恢复到了约360MB/s。
6.3 上位机才是最后一道坎
链路还没完,FPGA侧360MB/s不代表整条链路能跑到360MB/s。上位机原始版本用单线程同步读USB端点,每次只读64KB,然后写文件,再继续读。读64KB、写文件、再读,这个循环带来大量系统调用,实际吞吐只有200MB/s出头。这里做了一个经典改造:开两个线程,一个线程专门读USB数据(读4MB的大缓冲),另一个线程负责存储到NVMe固态盘,两个线程之间用内存队列和事件通知传递数据。这样USB端点永远有主机缓冲等着接收数据,存储的磁盘耗时不再拖累接收。
6.4 最终实测结果
做完这三层优化,整条链路最后的稳定实测值是338MB/s,文件写入过程通过软件统计,持续传输时速度平稳,没有周期性掉速。下面是最后状态下的关键配置记录:
| 环节 | 关键配置 | 数值 |
|---|---|---|
| FX3固件 | DMA缓冲区大小/数量 | 16KB / 16个 |
| FX3固件 | GPIF II接口模式 | 同步从FIFO,32位,100MHz |
| FX3固件 | USB端点类型 | BULK IN,512字节 |
| FPGA逻辑 | 接口FIFO深度 | 256KB |
| FPGA逻辑 | 读侧水线 | 64KB |
| 上位机 | 接收缓冲区 | 4MB |
| 上位机 | 接收/存储线程 | 独立双线程 |
6.5 为什么338MB/s差不多就是这道菜的上限
看到这个数字,很多人会问还能不能更高。理论上GPIF II接口带宽是400MB/s,USB3.0实际有效载荷约400-450MB/s,但USB协议本身有Token、握手、包间隔、重传这些开销,8b/10b编码也会吃掉一部分链路带宽。实际长传输中,BULK传输能达到350-400MB/s已经是优秀水平。338MB/s相当于GPIF II接口理论值的84.5%,是USB3.0链路有效带宽的七成多。剩余的部分主要花在USB主机控制器的调度和FX3内部状态切换上,还有我上位机接收时不可避免的零拷贝开销。想再往上压,就要改成更大的突发长度、减少上位机系统调用、或者用多USB端点聚合,但那对于大多数采集场景来说边际收益已经不大。
7. 避坑清单:这系统跑起来之后我踩过的各种坑
最后这部分,把所有调试中踩过的坑按“症状—原因—解法”的方式列出来。很多问题你在文档里找不到,或者只有靠长时间的日志排查才能发现。
7.1 枚举失败/时而能看到设备时而看不到
症状是插上USB线后电脑无法识别,设备管理器里没有出现新硬件。先查电源和复位,再用示波器抓19.2MHz时钟波形,确认波形幅度在正常范围。我第一版板子就是复位信号被一个电容拖慢,导致偶尔上电时序不对。如果你用的是I2C EEPROM启动,还要确认EEPROM里固件确实是好的,因为FX3一旦从EEPROM读到一个非法镜像,它会直接停在错误状态,不会转到USB枚举。解决办法是先拔掉EEPROM,用USB启动模式把系统跑起来,再接EEPROM烧录,排除两块问题。
7.2 传输速度低而且波动大
能达到100MB/s上下但上不去,重点查DMA描述符数量、FLAGA水线、上位机缓冲。按我的经验这个症状大概率是FPGA侧在等待DMA缓冲区,或者是上位机接收缓冲区太小导致USB端点一直在等待主机给它腾地方。用逻辑分析仪或FPGA内部ILA抓一下FLAGA的波形,如果长时间处于无效状态,问题基本就锁定在FX3的DMA配置上;如果能长时间有效但还是慢,那就要看GPIF II总线有没有因为读侧FIFO空而插入空泡。
7.3 数据出错:帧头错乱、偶发丢包
高速传输中偶发数据错误,很多人直接就怀疑线缆或接口。我排查过一次发现是GPIF II接口的setup时间不够。FPGA输出的数据信号和SLWR#几乎同时翻转,FX3在PCLK的上升沿采样时数据还没有稳定。解决方式是把数据输出相对SLWR#提前半个周期,或者调整set_output_delay约束,让FPGA工具把数据输出寄存器有意往前压。这种问题往往不是每次必现,而是温度、电压、频率跑偏时闪现,特别坑。
7.4 长时间运行后掉速
系统刚启动很好,跑几分钟后吞吐下降,多数是主机侧或FPGA侧的缓冲碎片化。上位机我用的是预分配固定大缓冲区轮转复用,不反复new/delete,持续跑了一个多小时稳定。FX3侧的DMA描述符如果配置合理又会自动循环,不会积累。如果FPGA内FIFO用到了BRAM的74%以上,布线可能会变差,跑久了因为时序余量不够也会偶发卡顿,建议留出充足的BRAM余量。
7.5 调试期间开着串口打印
开发FX3固件时我习惯开一个调试串口打印状态,默认波特率115200。后来发现这个串口打印本身会把USB吞吐拉低一截,因为CPU被printf阻塞住,哪怕DMA通道是AUTO模式,CPU也需要处理USB事件和GPIF配置相关的工作。我的做法是产品模式固件里保留串口代码,但用一个编译宏开关,正式测试时直接关掉打印,否则调好的三三八会被自己的调试输出拖回三百一二十。
最后再说一个经验:如果条件允许,在FPGA里留一个字符流生成器,能在任意时刻向链路里灌入已知模式的数据,对验证固件、上位机、硬件都帮助巨大。我系统里这个模块一直保留到现在,遇到问题先让它跑一遍,链路哪一段有问题立刻能定位,比拿真实ADC数据盲猜高效得多。这套FPGA+FX3的方案做到现在,338MB/s不是极限,但已经足够应对绝大多数高速采集场景,希望这篇文章能帮后来者少走一些弯路。