2026/10/13 5:23:32

FPGA与USB 2.0高速接口实战:Verilog实现Slave FIFO与流控

FPGA与USB 2.0高速接口实战:Verilog实现Slave FIFO与流控 1. 项目缘起与整体设计思路1.1 为什么还要折腾USB 2.0高速接口这几年做FPGA项目动不动就是千兆网、PCIe、DDR4好像不提这些就落伍了。但实际干活的时候你会发现USB 2.0高速High Speed480Mbps接口依然是绕不开的存在。原因很实在PC端兼容性无敌、线缆便宜、供电方便、协议栈成熟而且对于大多数数据采集、图像传输、工业控制场景480Mbps的带宽完全够用。我最近接手的一个项目需要把FPGA采集到的一路视频数据实时传到上位机做显示和存储。评估了一圈方案千兆网需要额外的PHY芯片和TCP/IP协议栈开发周期长PCIe对PC端软件要求高部署麻烦USB 3.0虽然快但FPGA端实现复杂度陡增调试成本不划算。最后选了USB 2.0高速用一颗成熟的USB控制器芯片配合Verilog写的接口逻辑两周就把数据通路跑通了。这个项目的核心就是用Verilog在FPGA内部实现一个与USB 2.0控制器芯片对接的高速并行接口把FPGA产生的数据稳定地推给上位机同时能接收上位机下发的控制命令。听起来简单但实际做起来时序约束、跨时钟域处理、流控机制、端点缓冲区管理每一个都是坑。1.2 整体架构怎么定先说说整体方案选型。FPGA与USB 2.0的对接常见的有三种路子FPGA直接实现USB协议栈理论上可行但USB协议本身复杂尤其是高速模式下的握手、分包、CRC校验用纯Verilog写工作量巨大而且调试极其痛苦。除非你是做USB IP核的否则不建议。用带USB接口的MCU做桥接比如STM32系列FPGA通过SPI/UART把数据给MCUMCU再通过USB传给PC。这个方案简单但带宽受限于SPI/UART通常跑不到高速USB的满速而且多了一层转发延迟增加。用专用USB控制器芯片比如经典的FX2LP系列CY7C68013A或者更新的FX3系列。FPGA通过并行总线FIFO接口与控制器芯片对接控制器芯片负责USB协议处理FPGA只需要管好FIFO的读写时序。这个方案带宽高、开发难度适中、资料丰富是我最终选择的路线。我选的是FX2LP系列的某款芯片它内部有一个增强型8051内核和可编程的端点FIFO。FPGA端只需要实现一个Slave FIFO接口就能以最高480Mbps的速率与PC通信。FPGA这边用Verilog实现的核心模块包括FIFO写控制、FIFO读控制、跨时钟域同步、流控与背压处理、命令解析。1.3 时钟域划分与数据流规划这个项目里有两个主要时钟域一个是USB控制器提供的IFCLK通常48MHz或30MHz另一个是FPGA内部的数据处理时钟比如100MHz。数据从FPGA内部产生要经过异步FIFO跨到IFCLK域再写入USB控制器的端点FIFO。数据流是这样的FPGA内部数据源产生数据写入一个异步FIFO写时钟100MHz读时钟IFCLK然后由FIFO读控制逻辑在IFCLK域下把数据推到USB控制器的FD总线同时控制SLWR、SLRD、SLOE、FIFOADR等信号。上位机通过USB控制端点下发命令FPGA端通过读FIFO接收命令并解析。注意跨时钟域处理是这个项目的核心难点之一。异步FIFO的深度要算好太浅会导致溢出丢数太深会浪费Block RAM资源。我后面会详细讲怎么算。2. 核心细节解析与实操要点2.1 Slave FIFO接口的时序要点FX2LP的Slave FIFO接口有两种模式同步和异步。同步模式下所有信号都跟IFCLK同步时序更干净推荐使用。关键信号包括IFCLK接口时钟由控制器输出或外部输入我用的48MHz。FD[15:0]16位双向数据总线。FIFOADR[1:0]选择哪个端点FIFO。SLCS片选低有效。SLWR写选通低有效。SLRD读选通低有效。SLOE输出使能低有效时FD总线由控制器驱动。FLAGA/FLAGB/FLAGC/FLAGDFIFO状态标志比如“空”、“满”、“可编程阈值”。写时序的核心在IFCLK上升沿如果SLCS和SLWR都为低且FLAG指示FIFO未满数据就被写入。读时序类似SLOE和SLRD为低时控制器驱动FD总线FPGA在IFCLK上升沿采样。这里有个容易踩的坑FLAG信号的采样和SLWR的断言之间要有足够的建立时间。我一开始没注意直接组合逻辑把FLAG接到SLWR的使能上结果在高速下出现偶发写入失败。后来改成用IFCLK同步后的FLAG做判断并且SLWR用寄存器输出问题就解决了。2.2 跨时钟域FIFO的深度计算异步FIFO的深度不是拍脑袋定的要根据数据速率和时钟频率算。假设FPGA内部数据产生速率是80MB/s100MHz时钟每周期写0.8个16位数据IFCLK是48MHz每个IFCLK周期最多写一个16位数据理论最大写入速率是96MB/s。看起来IFCLK域更快但实际中IFCLK域可能因为USB总线忙、上位机读取慢等原因出现背压导致FIFO读端暂停。最坏情况USB端暂停了1ms上位机调度延迟这1ms内FPGA端持续写入80MB/s数据量是80KB。所以FIFO深度至少要能缓存80KB。但Block RAM资源有限我实际用了16KB的FIFO配合流控机制——当FIFO快满时通过一个反馈信号通知数据源降速或暂停。这样既省资源又不会丢数。实操心得FIFO深度计算要留至少30%余量。我一开始算出来需要8KB实际用了16KB后来发现上位机偶尔会有10ms级别的卡顿16KB刚好扛住。2.3 端点FIFO的配置与带宽分配FX2LP内部有多个端点FIFO可以通过固件配置为不同的方向、大小和缓冲方式。我的配置是EP2OUT端点PC到FPGA512字节双缓冲用于下发命令。EP6IN端点FPGA到PC1024字节四缓冲用于上传数据。EP4IN端点512字节双缓冲用于上传状态信息。四缓冲的意思是控制器内部有4个1024字节的缓冲区FPGA写满一个控制器就自动打包发送同时FPGA可以继续写下一个。这样能有效隐藏USB总线传输的延迟提高吞吐量。带宽方面480Mbps是理论物理层速率实际应用层能跑到300Mbps以上就很不错了。我的实测结果是持续传输时EP6能稳定在280-320Mbps之间取决于上位机的读取速度和USB主控制器的调度。2.4 Verilog代码的模块划分整个FPGA端的设计我分了这几个模块usb_slave_fifo_if.v顶层接口模块例化所有子模块连接物理引脚。async_fifo.v异步FIFO用于跨时钟域。usb_write_ctrl.vSlave FIFO写控制状态机。usb_read_ctrl.vSlave FIFO读控制状态机。cmd_parser.v命令解析模块。data_gen.v数据源模拟模块实际项目中替换为真实数据源。模块划分的原则是接口逻辑和业务逻辑分离。usb_slave_fifo_if只负责时序和协议不关心数据内容cmd_parser和data_gen负责业务。这样调试的时候可以单独仿真接口模块用Testbench模拟USB控制器的行为。3. 实操过程与核心环节实现3.1 硬件连接与引脚约束先列一下FPGA与USB控制器的主要连接FPGA引脚USB控制器引脚方向说明ifclkIFCLK输入48MHz接口时钟fd[15:0]FD[15:0]双向数据总线fifoadr[1:0]FIFOADR[1:0]输出端点选择slcs_nSLCS输出片选slwr_nSLWR输出写选通slrd_nSLRD输出读选通sloe_nSLOE输出输出使能flag_aFLAGA输入端点状态标志flag_bFLAGB输入端点状态标志在FPGA的约束文件里IFCLK要约束为48MHzFD总线要设置合适的IO标准通常是3.3V LVCMOS。双向总线在Verilog里用inout声明读的时候FPGA释放总线高阻写的时候FPGA驱动。// 双向总线处理 assign fd (sloe_n 1b0) ? 16bz : fd_out; assign fd_in fd;注意双向总线的方向控制一定要严格跟SLOE和SLRD的时序配合否则会出现总线冲突。我建议在顶层模块里用一个IOBUF原语而不是纯Verilog的inout这样综合工具能更好地优化。3.2 写控制状态机的实现写控制状态机是整个接口的核心。它的任务是在IFCLK域下检测到FIFO未满时把数据推上FD总线并断言SLWR。状态机设计如下localparam WR_IDLE 2d0; localparam WR_LOAD 2d1; localparam WR_ASSERT 2d2; localparam WR_HOLD 2d3; always (posedge ifclk or negedge rst_n) begin if (!rst_n) begin state WR_IDLE; slwr_n 1b1; fd_out 16d0; end else begin case (state) WR_IDLE: begin slwr_n 1b1; if (fifo_rdusedw 0 flag_not_full) begin state WR_LOAD; end end WR_LOAD: begin fd_out fifo_dout; state WR_ASSERT; end WR_ASSERT: begin slwr_n 1b0; state WR_HOLD; end WR_HOLD: begin slwr_n 1b1; fifo_rdreq 1b1; state WR_IDLE; end endcase end end这个状态机的关键点SLWR只断言一个IFCLK周期数据在SLWR为低期间必须保持稳定。我在WR_LOAD状态把数据放到fd_outWR_ASSERT状态拉低SLWRWR_HOLD状态拉高SLWR并弹出FIFO。实测在48MHz下非常稳定。3.3 读控制状态机的实现读控制用于接收上位机下发的命令。FX2LP的OUT端点FIFO有数据时FLAG会指示非空。FPGA端的状态机检测到非空就断言SLOE和SLRD从FD总线上采样数据。localparam RD_IDLE 2d0; localparam RD_ASSERT 2d1; localparam RD_SAMPLE 2d2; localparam RD_DONE 2d3; always (posedge ifclk or negedge rst_n) begin if (!rst_n) begin rd_state RD_IDLE; slrd_n 1b1; sloe_n 1b1; end else begin case (rd_state) RD_IDLE: begin slrd_n 1b1; sloe_n 1b1; if (flag_not_empty) begin rd_state RD_ASSERT; end end RD_ASSERT: begin sloe_n 1b0; slrd_n 1b0; rd_state RD_SAMPLE; end RD_SAMPLE: begin cmd_data fd_in; rd_state RD_DONE; end RD_DONE: begin slrd_n 1b1; sloe_n 1b1; cmd_valid 1b1; rd_state RD_IDLE; end endcase end end读时序比写时序稍微复杂一点因为SLOE拉低后控制器需要几个时钟周期才能驱动FD总线。我实测下来SLOE拉低后至少等2个IFCLK周期再采样FD数据才稳定。所以状态机里RD_ASSERT到RD_SAMPLE之间可以插入等待周期。3.4 命令解析与响应机制上位机下发的命令我定义了一个简单的协议每包4字节第一个字节是命令码后面三个字节是参数。命令码包括0x01启动采集、0x02停止采集、0x03设置采样率、0x04读取状态。cmd_parser模块在IFCLK域下接收cmd_data和cmd_valid解析后通过一个握手信号把命令传到FPGA内部时钟域。这里又用了一个异步FIFO做跨时钟域因为命令解析和响应逻辑跑在100MHz域下。响应机制FPGA收到命令后通过EP4上传一个状态包告诉上位机命令执行结果。状态包也是4字节格式和命令包类似。实操心得命令协议一定要加校验我一开始没加偶尔会因为USB传输误码导致FPGA执行错误命令。后来在命令包最后加了一个字节的异或校验问题就没了。3.5 上位机端的配合FPGA端调好了上位机也得配合。我用的是libusb库在Linux和Windows下都能跑。关键操作是找到设备打开声明接口。配置端点EP2为OUTEP6为INEP4为IN。启动异步传输EP6用批量传输Bulk Transfer提交多个URBUSB Request Block保持管道满载。读取线程持续从EP6读数据写入文件或显示。上位机读取速度直接影响FPGA端的流控。如果上位机读得慢EP6的缓冲区会满FLAG会指示满FPGA端就会暂停写入。所以上位机端一定要用多线程异步IO避免阻塞。// libusb异步传输示例 struct libusb_transfer *transfer libusb_alloc_transfer(0); libusb_fill_bulk_transfer(transfer, dev_handle, EP6_IN, buffer, BUFFER_SIZE, transfer_callback, NULL, 0); libusb_submit_transfer(transfer);4. 常见问题与排查技巧实录4.1 数据传输不稳定偶发丢数这是最常见的问题。我遇到的时候先排查了硬件连接确认焊接没问题然后用示波器抓了IFCLK、SLWR、FD总线的波形发现SLWR的断言时机和FLAG的变化有竞争。根本原因FLAG信号是控制器内部组合逻辑输出的直接用来使能SLWR在高速下会出现毛刺。解决方法把FLAG用IFCLK打两拍同步后再用SLWR也用寄存器输出。改完之后连续跑24小时没丢过一包数据。4.2 带宽上不去只有100Mbps左右带宽上不去的原因通常有三个端点缓冲区太小如果EP6只配了双缓冲512字节控制器发完一个包就要等FPGA写下一个中间有空隙。改成四缓冲1024字节后带宽直接翻倍。上位机读取太慢用单线程同步读取每次读512字节读完了才提交下一次USB总线大量时间在空闲。改成异步提交多个URB后带宽提升明显。IFCLK频率太低如果IFCLK只有30MHz16位总线的理论最大速率是60MB/s实际能到40MB/s就不错了。换成48MHz后理论上限提到96MB/s。4.3 FPGA端FIFO溢出FIFO溢出通常是流控没做好。我的做法是在异步FIFO的写端加一个“几乎满”阈值比如深度用到80%当达到阈值时产生一个backpressure信号通知数据源暂停或降速。同时在USB端如果FLAG指示FIFO满写控制状态机就停在WR_IDLE不弹出FIFO数据。注意backpressure信号的响应延迟要算好。如果数据源响应慢FIFO阈值要设低一点留更多余量。4.4 命令下发无响应命令下发无响应先检查EP2的配置是否正确。FX2LP的OUT端点默认可能是关闭的需要在固件里使能。另外SLOE和SLRD的时序要确认SLOE拉低后要等至少2个IFCLK周期再采样FD。我一开始没等采到的全是0xFF。还有一个容易忽略的点上位机下发命令后要确保数据真正到达了FPGA。libusb的同步传输函数返回成功只代表数据写入了USB控制器的缓冲区不代表FPGA已经读走。所以命令协议里要有应答机制FPGA收到命令后通过EP4回一个ACK上位机收到ACK才认为命令执行成功。4.5 常见问题速查表现象可能原因排查方法解决方案偶发丢数FLAG未同步SLWR有毛刺示波器抓SLWR和FLAG波形FLAG打两拍SLWR寄存器输出带宽低端点缓冲小上位机读取慢查看端点配置和上位机线程模型增大缓冲改用异步URBFIFO溢出流控阈值太高数据源响应慢监测FIFO使用量降低阈值加backpressure命令无响应EP2未使能SLOE采样时机不对检查固件配置抓读时序波形使能EP2SLOE后等2周期再采样枚举失败固件未正确加载VID/PID冲突查看设备管理器检查固件重新加载固件修改VID/PID5. 调试工具与实战技巧5.1 仿真环境的搭建在硬件调试之前我强烈建议先做仿真。用Verilog写一个USB控制器的行为模型模拟FLAG信号、FD总线的响应、IFCLK的生成。Testbench里例化FPGA端的接口模块跑一个完整的写-读循环用波形查看器确认时序。仿真模型不需要太精确能模拟FLAG的变化和FD总线的读写就行。我写了一个简单的模型FLAG在FIFO空时拉低满时拉高FD总线在SLOE和SLRD为低时输出数据。这样能在不接硬件的情况下验证状态机的正确性。5.2 在线调试工具的使用FPGA厂商的在线调试工具比如Xilinx的ILA、Intel的SignalTap是排查时序问题的利器。我把IFCLK、SLWR、SLRD、SLOE、FLAG、FD总线都接入了ILA触发条件设为SLWR下降沿。这样每次写入都能抓到波形一眼就能看出时序对不对。实操心得ILA的采样深度要设够至少能抓1000个时钟周期。我一开始设了256结果触发后只能看到触发点前后的少量数据不够分析。5.3 上位机端的调试技巧上位机端用Wireshark加USB抓包插件能看到USB总线上的所有传输。如果发现某个端点一直没有数据或者传输频繁NAK就能定位到是FPGA端没写数据还是控制器端没发出去。另外libusb的调试输出也很有用。设置环境变量LIBUSB_DEBUG4能看到详细的传输日志包括每次提交的URB、完成状态、实际传输字节数。5.4 性能优化的几个关键点增大端点缓冲区四缓冲比双缓冲带宽高30%以上。提高IFCLK频率48MHz比30MHz带宽高60%。上位机异步IO同时提交8-16个URB保持管道满载。减少命令交互命令包尽量小避免占用数据带宽。FPGA端流水线写控制状态机的WR_LOAD和WR_ASSERT可以合并减少一个周期。6. 项目扩展与后续优化方向6.1 从USB 2.0升级到USB 3.0如果项目后期需要更高带宽可以考虑升级到USB 3.0。FPGA端需要换成支持USB 3.0的控制器芯片接口从Slave FIFO变成更复杂的同步FIFO或DMA接口。Verilog端的改动主要是时钟域和位宽的变化核心的流控和命令解析逻辑可以复用。6.2 增加数据压缩功能如果数据源产生的数据有冗余可以在FPGA端加一个简单的压缩模块比如RLE游程编码或差分编码减少传输数据量。这样在同样的USB带宽下能传更多有效数据。6.3 多端点并行传输FX2LP支持多个端点同时工作。可以把数据流分成多路通过不同的端点并行传输上位机端合并。这样能突破单端点的带宽瓶颈但会增加FPGA端和上位机端的复杂度。6.4 固件在线升级FX2LP的固件可以通过USB下载不需要专用的编程器。可以在FPGA端加一个固件存储区上位机通过命令触发固件更新。这个功能在产品化阶段很有用方便现场升级。7. 个人实操体会与建议这个项目做下来最大的体会是USB 2.0高速接口的难点不在协议本身而在时序细节和流控。协议有现成的控制器芯片帮你处理但FPGA与控制器之间的并行接口每一个信号的时序都要抠清楚。我踩过的坑包括FLAG信号没同步导致偶发丢数、SLOE采样时机不对导致命令读错、FIFO深度不够导致溢出、上位机同步读取导致带宽上不去。这些问题在文档里往往一笔带过但实际调试时能卡你好几天。给后来者的建议先仿真再上板。仿真能解决80%的时序问题剩下的20%用ILA和示波器搞定。另外流控机制一定要做不要指望USB总线永远不忙。FIFO深度算好backpressure信号加好系统才能稳定跑长时间。最后分享一个小技巧在FPGA端加一个计数器统计每秒写入USB的数据量通过EP4上传给上位机显示。这样能实时监控带宽一旦发现带宽下降就知道是USB端还是FPGA端出了问题。这个计数器帮我省了很多排查时间。