这阵子在调试一块采集板,主控端用的是Cypress(现在应该说Infineon)的EZ-USB FX3,接口模式选了SlaveFIFO,搭档是一颗中等规模FPGA。整套方案说白了就是FPGA采集ADC数据,通过FX3的USB 3.0口灌给PC端上位机。整个链路从硬件设计到固件、FPGA逻辑再到上位机读写,前前后后折腾了不少时间。过程中踩的坑、翻的车、总结出来的可用经验,我觉得比标准文档里写的东西更实在,这里整理出来,希望对正在做或者准备做FX3 SlaveFIFO调试的朋友有点帮助。
先说清楚这篇总结的适用范围。如果你手里是CYUSB3014或者其他FX3系列芯片,想把GPIF II接口配置成SlaveFIFO模式和FPGA/单片机/ARM对接,那这篇文章几乎每个章节都能对号入座。如果你只是想用FX3自带的USB启动、UART这类功能,那SlaveFIFO相关的部分可以跳过,但硬件设计和调试思路的部分也能有参考。下面这五千多字,我自己写完都觉得有点长,但每一节都是实际调试过程里积累出来的,不是从数据手册里搬出来的空话。
1. SlaveFIFO方案到底是怎么回事
1.1 为什么很多高速采集板都选FX3
先聊两句选型逻辑,不然你也不知道为什么要在FX3上死磕。USB 3.0的5Gbps理论带宽看着很宽,但实际可用吞吐率受限于协议开销、端点buffer、驱动栈和硬件实现,能稳定跑到350MB/s以上已经算不错了。FX3在这类应用里几乎是绕不开的选择,一方面它内置USB 3.0 PHY和协议栈,另一方面它那颗ARM9核跑着固件,可以灵活配置GPIF II接口去对接各种外部总线。相比用FPGA直接实现USB 3.0协议栈的方案,FX3加FPGA的组合开发难度和成本都要低不少。
SlaveFIFO这个词听起来抽象,拆开就很好理解。Slave代表FX3在总线上是“从”的角色,FIFO代表外部主机看到的是两个深度可控的FIFO,一个用于上行(外设到USB),一个用于下行(USB到外设)。外部主机——也就是FPGA——通过一组并行信号线往里写数据或者从里面读数据,FX3负责把FIFO里的数据搬运到USB端点上。外部主机完全不用关心USB协议细节,只需要遵守一套类FIFO的读写时序就行。
这种架构的最大好处是解耦。FPGA侧不涉及USB协议,USB侧不关心数据是怎么生成的,两边各自干各自的事。调试时也可以分头定位,FPGA这边抓总线波形,PC那边看端点数,问题出在哪一层很快就能圈定。
1.2 数据流里到底有哪些角色
整套系统的数据流大概是这样的:传感器或者ADC输出的数据进入FPGA,FPGA对数据做预处理、缓存,然后通过GPIF接口写入FX3的DMA buffer,FX3再把buffer里的数据打包成USB bulk传输送上PC。反过来,PC下发控制命令或者配置数据时,数据会通过USB OUT端点进入FX3,FX3写到下行FIFO,FPGA再从总线上把数据读走。
这里最容易让人晕的是FX3内部的DMA架构。FX3内部有多个DMA通道,每个通道连接一个外设接口(比如GPIF II)和一个USB端点。数据从GPIF II接口进入后,会通过DMA自动搬运到USB端点的buffer里。这个过程虽然在固件里只是几个API的事情,但它决定了整个链路的带宽上限,也决定了调试时你会遇到什么样的问题。我在第四节会详细展开固件配置的细节,这里先记住一句话:slaveFIFO模式下,FPGA看到的FIFO深度和流控状态,本质上是由DMA通道的buffer大小和固件里设置的flag阈值决定的。
另外,FX3的GPIF II接口在设计上支持16位或32位总线宽度,常见配置是32位总线配合5位地址线(A0~A3加上一些控制位)。具体用什么宽度,取决于你对吞吐率的要求,也取决于FPGA那边的资源开销。32位模式单次读写能传4字节,理论上在100MHz时钟下就能跑到400MB/s的峰值,实际打八折也有320MB/s,对于绝大多数高速采集场景够用了。
1.3 什么场景适合、什么场景不适合
别把SlaveFIFO当成万金油,它也有明显的适用边界。最适合它的场景是“PC和FPGA之间有大量连续数据要双向交换”,典型代表就是软件无线电、高速数据采集卡、逻辑分析仪的前端、工业相机采集。这类应用里数据一旦开始传输就自然地持续流动,FIFO机制和流控标志能很好匹配。
不太适合的场景有两个特征。一是数据量很小但要求极低延迟,比如控制类命令,走了SlaveFIFO反而被USB bulk传输的调度延迟拖累,这种应该用FX3的另外一套机制或者直接换USB HID这类低延迟方式。二是要求FPGA主动发起传输的场景。SlaveFIFO是外部主机主动读写的模式,FX3自己不会向外部总线“推”数据,虽然有GPIF事件和中断可以通知外部,但本质还是靠外部不断轮询flag状态。如果应用链路要求FX3主动上报数据,你可能得考虑FX3的ProgMode或者把主从关系反过来。
选型阶段把这些边界想清楚,比后期在固件和FPGA逻辑里拆东墙补西墙要省力得多。
2. 硬件连接与信号完整性设计
2.1 关键引脚分配与含义
FX3的GPIF II口是一组复用引脚,硬件上把它接到FPGA的通用IO上就行。实际项目中我用的信号大致有这些:
| 信号名 | 方向(FPGA视角) | 作用 |
|---|---|---|
| IFCLK | 输入 | GPIF接口时钟,可选择内部产生或外部输入 |
| SLCS# | 输出 | 片选,拉低表示本设备被选中 |
| SLWR# | 输出 | 写使能,上升沿写入数据 |
| SLRD# | 输出 | 读使能,上升沿读取数据 |
| SLOE# | 输出 | 输出使能,拉低时数据总线由FX3驱动 |
| PKTEND# | 输出 | 包结束信号,用于提交短包 |
| FLAGA/B/C/D | 输入 | FIFO状态标志,可配置为可编程水位标志 |
| A[1:0] | 输出 | 地址线,选择FIFO或命令/状态寄存器 |
| DQ[31:0] | 双向 | 数据总线 |
这里要特别注意FLAG的极性问题。FX3的FLAG在默认配置下通常是低有效,但这不是绝对的——固件里可以通过寄存器配置翻转极性。FPGA侧写逻辑时如果不知道配的是高有效还是低有效,调试时就会出现“flag明明说没数据,但读出来全是有效数据”或者反过来“flag说有数据,读出来全是0”的诡异现象。我建议在项目一开始就约定好统一的极性和时序定义,写成一个硬件接口文档,两边对照着来,能少掉很多无效沟通。
还有一个容易忽略的点是PKTEND信号。Bulk传输里PC端read通常按请求长度读取,如果FPGA这一侧要传的数据量不是USB bulk包大小(比如512字节)的整数倍,最后那个不满一包的数据就必须靠PKTEND来“封口”。不拉PKTEND的话,PC端会一直等数据直到超时,表现出来就是上位机读卡死。这个问题几乎每个做SlaveFIFO的人都会遇到一次,遇到之后就再也不会忘了。
2.2 电平、电源与时钟处理
FX3 GPIO的IO电压是可配置的,GPIF II接口建议工作在1.8V到3.3V之间。如果FPGA侧的bank电压和FX3不一致,就必须加电平转换。不要图省事把两个不同电平的bank直接连在一起,轻则信号不稳定,重则烧芯片。我调试时FPGA正好有3.3V的bank,就干脆配成3.3V电平均,省了转换芯片。如果你的FPGA用1.8V bank,记得在固件里把GPIO对应的电压配置也改成1.8V,否则电平判断会出问题。
说到供电就得多啰嗦两句。FX3对电源敏感,核心电压1.2V通常由内部LDO产生,但如果输入电源纹波大,高速USB传输会间歇性出现设备掉线、枚举失败、批量传输返回错误等怪毛病。实测中我发现劣质USB线带来的压降比想象中大得多,线一长,插上后芯片工作电压可能已经不在规格范围内了。如果调试中遇到“拔插一下又好一阵”的情况,先怀疑电源,再怀疑固件。
时钟方面,FX3内部PLL可以产生GPIF接口时钟,也可以由外部输入时钟。SlaveFIFO模式下我习惯用FX3内部产生时钟,然后在固件里把GPIF时钟频率设好,FPGA通过IFCLK引脚直接采样。这样少一个外部时钟源,两边只对上频率就行。默认100MHz是常见选择,FPGA侧逻辑比较好做时序收敛,也不容易因为过高的接口频率导致布局布线困难。
有一点很多人忽视:如果选择内部时钟模式,IFCLK引脚在FPGA侧看到的实际上是一个由FX3输出驱动的时钟,FPGA应该把它当成输入时钟,用它来同步SLWR/SLRD以及数据总线。不要搞出一个外部晶振再往IFCLK上灌,那是另一个时钟模式的做法。
2.3 硬件设计里的几个容易翻车的细节
第一个是数据总线的方向控制。DQ[31:0]是双向总线,FPGA侧如果没有用IOBUF原语正确管理三态,就会出现总线争用,具体表现是数据时而正确时而错误,而且错误模式没什么规律。一定要保证SLOE有效期间FPGA把数据总线释放成高阻,SLOE无效时由FPGA驱动,这个切换时机要严格对照时序图来写状态机。
第二个是引脚分配不冲突。FX3的GPIF II引脚和UART、I2C、SPI、USB启动配置引脚是有部分复用的。如果固件里同时启用了UART打印日志,而UART引脚恰好和GPIF的数据线复用,那么每次串口打印都会把总线状态搞乱,调试时你会看到FLAG状态正常但数据全是垃圾。建议硬件设计阶段就把GPIF专用引脚隔离出来,不要和其他外设共用。
第三个是信号完整性的粗浅处理。GPIF总线如果走线过长或者经过连接器,建议在FPGA侧串联22到33欧姆的电阻,抑制过冲。FX3和FPGA之间属于同步并行总线,信号边沿太差会直接影响时序裕量。实测中在数据线上串了33欧姆电阻后,原来偶发的数据位错误基本消失了。这个属于低成本高收益的操作,值得做。
3. 固件侧的关键配置
3.1 理解GPIF状态机和DMA通道的关系
FX3固件里最容易让人困惑的部分就是GPIF配置和DMA通道的关系。GPIF II是一个可编程状态机,通过一组称为“GPIF State Machine”的寄存器描述符来定义外部总线上的读写时序。FX3 SDK里带了一个叫做GPIF II Designer的可视化工具,你可以在里面画状态跳转图、定义每个状态下信号的电平,然后生成C代码。
SlaveFIFO模式下,固件的工作是把两个层面的东西打通:GPIF状态机负责在外部总线上搬运数据,DMA通道负责把GPIF拿到的数据送到USB。两者之间通过socket连接,GPIF侧是P端口(生产端),USB侧是U端口(消费端)。固件里设置DMA通道的buffer大小、数量、传输模式(自动还是手动),就决定了FIFO的深度和flag跳变的时机。
一旦理解了这层关系,你就能自己推导很多行为。比如为什么FLAGA(可编程标志,通常配置成“FIFO可写入”信号)在某个时间段内一直无效?可能是因为DMA通道里所有buffer都被USB占用了,生产者没有空闲buffer可用。这时候你去查USB端点的状态,大概率是PC端read请求处理不及时或者根本没发。
3.2 固件初始化顺序和关键配置项
FX3固件开发基于SDK,我用的版本是1.3.3,IDE是Eclipse基础上的那个开发环境。初始化的大致顺序是:
- 初始化硬件:CyU3PDeviceInit,配置时钟、IO电压。
- 初始化缓存:CyU3PMemInit,分配DMA用的buffer。
- 使能USB:CyU3PUsbStart,注册USB事件回调。
- 加载GPIF配置:CyU3PGpifLoadApi,加载GPIF II Designer生成的配置。
- 设置GPIF接口:CyU3PGpifStart,启动状态机,设置flag极性等参数。
- 创建DMA通道:CyU3PDmaChannelCreate,配置buffer数量和大小。
- 启动DMA通道:CyU3PDmaChannelSetXfer。
- 连接USB端点:保存USB事件回调,等在DeviceConnected事件里调用CyU3PDmaChannelSetupBuffers之类的API把DMA通道和端点绑定。
这里最关键的参数是DMA buffer的大小和数量。FX3内部有256KB的SRAM可用作buffer。如果每个buffer设成16KB,那最多能分配16个。buffer越小,flag翻转越频繁,FPGA侧等待空位的概率越高;buffer越大,占用的SRAM越多,但连续传输的稳定性更好。实际项目中我用了32个8KB的buffer,对于100MB/s级别的持续传输已经很充裕。如果你追求极高吞吐,建议buffer数量适当多些,每个buffer大小不要小于4KB,否则DMA中断和端点切换的开销会吃掉不少有效带宽。
固件里另一个容易出问题的点是传输模式。SlaveFIFO模式下通常使用MANUAL模式,也就是外部主机每提交一个buffer的数据后,FX3才会触发事件把数据送USB。AUTO模式虽然省事,但FIFO水位控制和flag信号的行为会和MANUAL不太一样。我调试时曾经为了省事开了AUTO,结果发现FLAGA的有效时机和预期完全不同,FPGA侧状态机经常等到超时。后来换回MANUAL,一切恢复正常。
固件代码片段(简化):
CyU3PReturnStatus_t status; CyU3PGpifConfig_t gpifConfig; gpifConfig.interface0 = CY_U3P_GPIF_SYNC_MASTER; gpifConfig.clock = 100000000; // 100MHz内部时钟 status = CyU3PGpifLoadApi(&gpifConfig); CyU3PDmaChannelConfig_t dmaConfig; dmaConfig.size = 8192; // 8KB per buffer dmaConfig.count = 32; // 32 buffers dmaConfig.prodSckId = CY_U3P_PIB_SOCKET_0; dmaConfig.consSckId = CY_U3P_UIB_SOCKET_0; dmaConfig.prodHeader = 0; dmaConfig.prodFooter = 0; dmaConfig.consHeader = 0; dmaConfig.dmaMode = CY_U3P_DMA_MODE_MANUAL; status = CyU3PDmaChannelCreate(&dmaHandle, CY_U3P_DMA_TYPE_MANUAL, &dmaConfig);这段代码里的prodSckId和consSckId是经常配错的地方。上行数据链路(FPGA写、USB读)的生产端是GPIF接口(PIB),消费端是USB接口(UIB)。下行链路则反过来。如果这两个sockeet配反了,数据根本就不会往USB端点流,但固件还不会报错,调试起来相当头疼。
3.3 用串口日志辅助固件状态检查
固件里加串口打印是非常有效的调试手段。FX3的UART在启动阶段就能用,你可以把初始化过程的每一步状态都打印出来,看是哪一步返回了失败。固件运行过程中,也可以在DMA事件回调里打印一些统计信息,比如每秒钟产生了多少个buffer、USB端收到多少byte。串口调试助手这边就能直接看到这些实时日志,配合上位机的读写测试结果,可以快速定位问题是在固件层、FPGA层还是PC端应用层。
我习惯在固件里维护几个全局计数器,比如:
- g_upBufferCount:上行DMA通道累计提交的buffer数。
- g_usbBytesSent:USB IN端点累计发送的字节数。
- g_dmaEventCount:DMA事件回调触发的次数。
然后在某个定时器里每隔一秒往UART上打印一次。如果FPGA侧一直在写,但g_upBufferCount不动,说明GPIF接口压根没收到数据,问题在FPGA和FX3总线那一层。如果g_upBufferCount在涨但g_usbBytesSent不动,说明问题在USB端点和DMA通道的连接上。用串口打印做这类二分定位,比瞎猜高效得多。
串口调试助手的波特率一般配成115200或者921600都行,注意和固件初始化的CyU3PUartSetBaudRate保持一致就行。
4. FPGA侧读写状态机设计与实现
4.1 写数据通路设计(FPGA到PC)
FPGA侧写状态机的核心任务就是监控FLAGA,一旦发现FIFO可写,就把数据放到总线上,拉一个有效的SLWR脉冲。说起来简单,实际设计里有几个细节决定了写通路能不能稳定干活。
先说等待机制。SlaveFIFO接口要求SLWR高电平有效期间数据要稳定,也就是说数据必须在SLWR变高之前就放到总线上。FPGA里最简单的做法是:先等待FLAGA有效,然后把数据打到DQ总线上,经过一拍延迟后拉高SLWR,保持一拍后拉低,同时准备下一笔数据。这个过程用Verilog写的话就是典型的有限状态机:
localparam IDLE = 2'd0; localparam ASSERT_DATA = 2'd1; localparam ASSERT_WR = 2'd2; localparam RELEASE_WR = 2'd3; always @(posedge ifclk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; slwr_n <= 1'b1; dq_out <= 32'b0; end else begin case (state) IDLE: begin if (flaga == 1'b0) begin // flag低有效表示可写 dq_out <= data_from_internal_fifo; state <= ASSERT_WR; end end ASSERT_WR: begin slwr_n <= 1'b0; // 拉低写使能,上升沿写入 state <= RELEASE_WR; end RELEASE_WR: begin slwr_n <= 1'b1; state <= IDLE; end default: state <= IDLE; endcase end end注意上面的代码用了一个假设:FLAGA低有效。如果你在固件里把flag配成了高有效,这个判断就要反过来。实际调试中,我建议把flag的极性约束条件写在一个宏定义里,万一后面改配置,FPGA侧只改一个宏就行。
第二个关键点是连续写和burst操作。如果你每一拍数据之间都插入IDLE等待FLAGA的判断,那么有效带宽会损失不少。更好的做法是检测到FLAGA有效后,连续写若干个数据,比如4个或者8个,然后再回IDLE重新检查flag。这个连续长度要跟DMA buffer的大小匹配,别超过水线太多,否则一次写太多直接把FIFO写穿。实测中我设了每次burst写16拍,也就是64字节,然后再查一次flag,吞吐率比一拍一查高了30%以上。
第三个关键点是短包提交。FPGA内部的数据通常按帧或者按包组织,最后一包数据经常不是512的整数倍。这种情况下,写完最后一个数据之后,需要拉低PKTEND一拍,告诉FX3“这个数据包结束”。不这么做,PC端的read操作会一直等待直到超时,如果你的上位机没有设计超时重试,整个采集流程就会卡死。对付这个问题,我的习惯是数据包逻辑层里专门加一个“包结束”控制位,FPGA写状态机一看到这位置1,就在写完本次数据后自动产生一个PKTEND脉冲。
4.2 读数据通路设计(PC到FPGA)
读通路是PC下发数据到FPGA的方向,过程跟写通路镜像对称,但有个额外的信号要处理好:SLOE。SLOE拉低时,FX3的数据总线被驱动出来,FPGA才能读到有效数据。SLOE拉高时,总线是高阻,FPGA必须把总线释放掉,否则会有总线冲突的风险。
读状态机的主要流程是等待FLAGB(配置成“FIFO非空”或者“可读标志”),一旦有效,拉低SLOE和SLRD,在一个时钟边沿把数据采进来,然后释放SLRD,再判断下一笔是否继续。SLOE在burst读的过程中可以一直保持低,直到本次burst结束再拉高,这样省去每次读都重新开SLOE的时间。
这里有一个经常踩的坑:SLOE和SLRD的时序配合。FX3数据手册上有明确要求,SLOE拉低到数据稳定需要一定的建立时间,SLRD上升沿采样数据时要保证数据已经有效。FPGA逻辑上,如果你用同一个always块同时控制SLOE和SLRD,很容易出现SLOE和SLRD同时变化,导致采样到的数据是总线还没稳定的状态。我的解决办法是让SLOE比SLRD提前一拍动作,也就是在进入READ状态时先拉低SLOE,等到下一拍再拉低SLRD去采样。这样虽然损失了一点吞吐率,但数据稳定性大幅改善。
读方向的数据缓冲同样重要。FPGA从GPIF总线上拿到的数据是连续流,但内部模块(比如配置寄存器组)往往需要按字节使能、按命令字解析。我一般在读通路后面挂一个小FIFO,先把GPIF总线数据缓存下来,然后由内部逻辑按自己节奏处理。这个小FIFO同时起到跨时钟域隔离的作用,因为GPIF时钟和FPGA内部逻辑时钟不一定同源,贸然直接处理会引入亚稳态和时序收敛问题。
4.3 跨时钟域与缓冲设计要点
如果你FPGA内部逻辑跑的是另一个时钟(比如200MHz),而GPIF接口工作在100MHz,那两边数据交互就涉及跨时钟域。最稳妥的方式就是FIFO。上行方向,内部模块把数据写入异步FIFO,GPIF写状态机从FIFO读数据再搬到FX3。下行方向则是GPIF读状态机把数据写入另一个FIFO,内部模块自己读走。
异步FIFO设计在FPGA工程里是基本功,很多IP核可以直接生成,不要自己手搓,除非你想在高深的时序问题上多花几个通宵。选FIFO深度时,至少要大于GPIF最多连续读写的burst长度,我一般设成1KB到4KB,具体看应用。如果FIFO深度太小,GPIF状态机会频繁被“空”或“满”信号卡住,吞吐率上不去;太大又浪费FPGA内部的block RAM资源,所以要平衡。
跨时钟域还有一个容易被忽略的点:状态标志本身的同步。FLAG信号从FX3过来,相对于IFCLK到底是同源还是不同源,不同项目不一样。如果FLAG不是由IFCLK直接产生的,最好在FPGA里用两级触发器同步后再进状态机。“打两拍”虽然简单,但确实能避免很多随机性的逻辑错误——省掉它会出现的故障往往特别难排查,因为错误出现频率低、时机无规律。
5. 调试过程中的问题速查与避坑思路
5.1 枚举正常但数据不流动怎么办
这个现象是SlaveFIFO调试中出现频率最高的:设备能够在PC端正常枚举,设备管理器里能看得到,或者Cypress的USB Control Center能识别芯片,但上位机read或write的时候数据完全不动,或者返回超时。
按照排查优先级,我会依次做下面几件事:
看一下固件串口日志,确认DMA通道是否真的创建并启动了。如果CyU3PDmaChannelCreate返回成功但CyU3PDmaChannelSetXfer没被调用,那DMA通道处于未启动状态,flag信号根本不会正常翻转。Socket号和端点号是否匹配,也在这里一并确认。
检查flag极性和FPGA测的状态机判断是否一致。用逻辑分析仪直接抓FLAGA和FLAGB的波形,如果在启动DMA通道之后flag根本没变化,那大概率是固件配置问题;如果flag在变化但跟预期方向相反,那就是极性配反了。
把DMA数据通路打一个回环测试。最简单的方法是让固件不依赖GPIF,而是用一个CyU3PDmaChannelWrite之类的API向DMA通道里塞测试数据,然后PC端去read。如果能read到数据,说明USB端点和DMA通道没问题,问题锁定在GPIF接口和FPGA逻辑。如果read不到,那就要回头查DMA通道配置和端点连接。
检查PC端读写API的缓冲区和请求长度是不是合理。有些上位机程序里用了一个很小的buffer去read大量数据,比如每次只读4KB,然后在上位机循环里拼命调用read,这种用法的表现是数据传输极慢但没完全卡死,跟完全无数据还是有区别的。如果连read返回值都是0或者错误,那再往下查协议栈层。
5.2 数据错位、丢数与时序问题
数据出现错位或者偶发丢数,是最让人头疼的问题,因为复现不稳定、定位困难。这类问题通常有两种根源。
第一种是时序边沿采样问题。GPIF总线上数据、控制信号、时钟之间的关系非常严格,比如SLWR上升沿采样数据,数据必须在建立时间之前就有效。如果你的FPGA状态机把数据信号和SLWR同一拍给出,那么实际时序上可能就差了一点点建立时间,综合布线后在这个频率下可能刚好能满足要求,但温度、电压一变就出问题。解决方法是把数据比控制信号早半拍到一拍给出,给时序多留裕量。我在4.1的代码里就是先打数据再拉写使能,就是这个道理。
第二种是总线三态控制问题。FPGA代码里如果没有正确管理双向IO的高阻态,在SLOE有效期间FPGA还在驱动数据总线,就可能和FX3的输出打架,造成数据位错误。这个问题最典型的特征是:直接读FLAG正常、单独看某个信号正常、一旦真正跑burst传输数据就开始花。解决办法是检查IOBUF的使用,确保SLOE有效期间FPGA数据总线是高阻,只有SLOE无效时FPGA才驱动数据。
第三种是DMA buffer大小和burst长度不匹配。FPGA侧burst了一次很大的数据量,超出了单个DMA buffer容量,而FX3在MANUAL模式下可能会等buffer满了才送USB,导致FPGA侧认为FIFO已满而停止写入,而PC端迟迟等不到数据。这种表现既有丢数又有卡顿。解决思路是把FPGA侧burst长度控制在一个buffer容量之内,比如buffer 8KB就每次burst不超过8KB的数据量,或者干脆把burst长度减到256字节,配合flag可编程水线,反而更稳定。
5.3 吞吐率上不去的调优手段
如果数据能通、看起来也能读写,但速度就是达不到预期,比如理论能跑350MB/s实测只有150MB/s,那就要逐个排查瓶颈。
先说FPGA侧。写状态机如果每次写一拍都要回到IDLE重新检测flag,那等待flag变化的周期会占用大量时钟,吞吐率自然上不去。解决办法就是改成burst写,每次连续写8到16拍,减少flag轮询的频率。另外,GPIF接口时钟能不能提高也很关键,如果固件配置里时钟只有50MHz,那无论FPGA多努力都只能在50MHz下传输,可以考虑改到100MHz。
再看固件侧。DMA buffer小会导致中断频繁,CPU忙于处理DMA事件而无法及时提交新的buffer。在MANUAL模式,每个buffer满了之后CPU需要处理事件,处理时间没优化好的话,吞吐率会被严重拉低。我试过把buffer从4KB增到16KB后,上行吞吐率从180MB/s提升到了280MB/s,效果立竿见影。如果你不想频繁在CPU事件回调里做太多事情,可以考虑适当增大buffer数量并减少回调里的日志操作。
最后说PC端。Cypress提供了CyUSB.NET驱动库,上位机read请求的buffer如果太小,比如每次只申请4KB,协议栈里每处理一次都需要切换内核态和用户态,开销非常大。我建议每次read至少申请1MB,然后用一个循环里多次读取的方式填充应用层缓冲。同样的逻辑也适用于write。还有一个容易被忽略的点是:Bulk传输模式下,USB控制器下发请求后必须等端点有数据或者超时才返回,如果你用同步read去读一个大包,而数据源只是每秒才发几个小包,那大量的时间会浪费在等待超时上。这种情况适合用异步或者多线程方式做读操作。
5.4 调试工具组合拳:逻辑分析仪、串口、上位机三管齐下
最后说说调试工具的使用思路,这部分的经验对“快速定位问题”帮助最大,比盲目改代码高效太多了。
逻辑分析仪建议买一个采样率至少200MHz、通道数16路以上的,不用太贵,但一定得支持长时间深存储采集。调试时把IFCLK、SLWR、SLRD、SLOE、FLAGA、FLAGB、PKTEND、数据总线低位几根信号都挂上,然后触发条件设成SLWR下降沿或者FLAG变化。一帧波形拉出来,你就能看到FPGA到底有没有按照时序去读写,也能看到FLAG和实际读写之间差了多长时间。很多固定时序错误靠波形一眼就能发现,比如SLWR脉冲宽度不够、数据建立时间太紧、burst中间有不该有的空闲状态等。
串口日志这块前面讲了,固件里加计数器、打印状态,是整个系统里最灵活的观测点。因为FPGA波形只能看到GPIF总线层面,USB端点的状态它看不到,这时候就得靠固件日志来补充。我习惯把固件的UART日志做成开关式的,平时关闭,需要调试时打开,避免日志打印本身干扰DMA传输性能。
上位机端建议自己写一个简单的吞吐率测试工具,用CyUSB.NET的CyUSBDevice类枚举设备,找到对应的bulk端点,循环收发,每秒钟打印一次MB/s。别只用Cypress自带的USB Control Center,那个工具适合做控制传输和简单读写测试,但它的吞吐率统计粒度太粗,而且不支持灵活的buffer大小配置。自己写一个小工具,几十行C#代码就够了,但对抗这种性能问题的帮助非常大。
另外,调试时手边放一个串口调试助手特别有用,尤其当你的PC上位机还没开发完,或者你想快速验证固件某个模块逻辑时,直接用串口助手发命令、收统计数据,完全不依赖主上位机程序。很多时候就是靠这个“土办法”先把固件到DMA到USB这一整段验证通了,再回头完善应用层。
最后再说一点个人感受
这套方案调试下来,最大的体会是:SlaveFIFO本身不复杂,复杂的是它横跨FPGA、固件、USB驱动、上位机四个层面,每一个层面都可能出问题,而且问题表象经常互相掩盖。调试时如果只是盯着某一层反复折腾,效率会很低。正确心态是把整条链路拆成几个可独立验证的段,逐段打通。先用固件回环测试排除USB段,再用逻辑分析仪确认GPIF时序,最后才把FPGA内部逻辑挂进来全链路联调。每一步都确认清楚了再做下一步,就不会出现那种“改了一堆东西但问题还在”的绝望局面。
我早期犯过的最大错误是FPGA侧burst长度和FX3的buffer大小没对齐,导致数据在低速时正常、高速时偶发丢包,整整排了两天才通过波形加固件日志的组合锁到原因。所以这个项目做完之后,我特意把这种跨层参数匹配的检查列成了checklist,每次新项目启动时先过一遍,省了非常多的时间。希望这篇总结也能帮你少走一段弯路。
最后再分享一个可以扩展的方向:如果你后续需要支持多通道并行采集或者数据率超过GPIF接口带宽,可以考虑把FX3配置成两个DMA通道对接到USB的多个Bulk端点,配合FPGA侧的多路FIFO调度。吞吐率能再上一个台阶,不过那就是另一个debug故事了。先把SlaveFIFO这条基本功练好,很多高级玩法都是在这个基础上叠加的。