1. 项目缘起与整体设计思路
1.1 为什么还要折腾USB 2.0高速接口
这几年做FPGA项目,动不动就是千兆网、PCIe、DDR4,好像不提这些就落伍了。但实际干活的时候你会发现,USB 2.0高速(High Speed,480Mbps)接口依然是绕不开的存在。原因很实在: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把数据给MCU,MCU再通过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/FLAGD:FIFO状态标志,比如“空”、“满”、“可编程阈值”。
写时序的核心:在IFCLK上升沿,如果SLCS和SLWR都为低,且FLAG指示FIFO未满,数据就被写入。读时序类似,SLOE和SLRD为低时,控制器驱动FD总线,FPGA在IFCLK上升沿采样。
这里有个容易踩的坑:FLAG信号的采样和SLWR的断言之间要有足够的建立时间。我一开始没注意,直接组合逻辑把FLAG接到SLWR的使能上,结果在高速下出现偶发写入失败。后来改成用IFCLK同步后的FLAG做判断,并且SLWR用寄存器输出,问题就解决了。
2.2 跨时钟域FIFO的深度计算
异步FIFO的深度不是拍脑袋定的,要根据数据速率和时钟频率算。假设FPGA内部数据产生速率是80MB/s(100MHz时钟,每周期写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,可以通过固件配置为不同的方向、大小和缓冲方式。我的配置是:
- EP2:OUT端点(PC到FPGA),512字节双缓冲,用于下发命令。
- EP6:IN端点(FPGA到PC),1024字节四缓冲,用于上传数据。
- EP4:IN端点,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.v:Slave FIFO写控制状态机。
- usb_read_ctrl.v:Slave FIFO读控制状态机。
- cmd_parser.v:命令解析模块。
- data_gen.v:数据源模拟模块(实际项目中替换为真实数据源)。
模块划分的原则是:接口逻辑和业务逻辑分离。usb_slave_fifo_if只负责时序和协议,不关心数据内容;cmd_parser和data_gen负责业务。这样调试的时候可以单独仿真接口模块,用Testbench模拟USB控制器的行为。
3. 实操过程与核心环节实现
3.1 硬件连接与引脚约束
先列一下FPGA与USB控制器的主要连接:
| FPGA引脚 | USB控制器引脚 | 方向 | 说明 |
|---|---|---|---|
| ifclk | IFCLK | 输入 | 48MHz接口时钟 |
| fd[15:0] | FD[15:0] | 双向 | 数据总线 |
| fifoadr[1:0] | FIFOADR[1:0] | 输出 | 端点选择 |
| slcs_n | SLCS | 输出 | 片选 |
| slwr_n | SLWR | 输出 | 写选通 |
| slrd_n | SLRD | 输出 | 读选通 |
| sloe_n | SLOE | 输出 | 输出使能 |
| flag_a | FLAGA | 输入 | 端点状态标志 |
| flag_b | FLAGB | 输入 | 端点状态标志 |
在FPGA的约束文件里,IFCLK要约束为48MHz,FD总线要设置合适的IO标准(通常是3.3V LVCMOS)。双向总线在Verilog里用inout声明,读的时候FPGA释放总线(高阻),写的时候FPGA驱动。
// 双向总线处理 assign fd = (sloe_n == 1'b0) ? 16'bz : fd_out; assign fd_in = fd;注意:双向总线的方向控制一定要严格跟SLOE和SLRD的时序配合,否则会出现总线冲突。我建议在顶层模块里用一个IOBUF原语,而不是纯Verilog的inout,这样综合工具能更好地优化。
3.2 写控制状态机的实现
写控制状态机是整个接口的核心。它的任务是在IFCLK域下,检测到FIFO未满时,把数据推上FD总线并断言SLWR。状态机设计如下:
localparam WR_IDLE = 2'd0; localparam WR_LOAD = 2'd1; localparam WR_ASSERT = 2'd2; localparam WR_HOLD = 2'd3; always @(posedge ifclk or negedge rst_n) begin if (!rst_n) begin state <= WR_IDLE; slwr_n <= 1'b1; fd_out <= 16'd0; end else begin case (state) WR_IDLE: begin slwr_n <= 1'b1; 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 <= 1'b0; state <= WR_HOLD; end WR_HOLD: begin slwr_n <= 1'b1; fifo_rdreq <= 1'b1; state <= WR_IDLE; end endcase end end这个状态机的关键点:SLWR只断言一个IFCLK周期,数据在SLWR为低期间必须保持稳定。我在WR_LOAD状态把数据放到fd_out,WR_ASSERT状态拉低SLWR,WR_HOLD状态拉高SLWR并弹出FIFO。实测在48MHz下非常稳定。
3.3 读控制状态机的实现
读控制用于接收上位机下发的命令。FX2LP的OUT端点FIFO有数据时,FLAG会指示非空。FPGA端的状态机检测到非空,就断言SLOE和SLRD,从FD总线上采样数据。
localparam RD_IDLE = 2'd0; localparam RD_ASSERT = 2'd1; localparam RD_SAMPLE = 2'd2; localparam RD_DONE = 2'd3; always @(posedge ifclk or negedge rst_n) begin if (!rst_n) begin rd_state <= RD_IDLE; slrd_n <= 1'b1; sloe_n <= 1'b1; end else begin case (rd_state) RD_IDLE: begin slrd_n <= 1'b1; sloe_n <= 1'b1; if (flag_not_empty) begin rd_state <= RD_ASSERT; end end RD_ASSERT: begin sloe_n <= 1'b0; slrd_n <= 1'b0; rd_state <= RD_SAMPLE; end RD_SAMPLE: begin cmd_data <= fd_in; rd_state <= RD_DONE; end RD_DONE: begin slrd_n <= 1'b1; sloe_n <= 1'b1; cmd_valid <= 1'b1; 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为OUT,EP6为IN,EP4为IN。
- 启动异步传输:EP6用批量传输(Bulk Transfer),提交多个URB(USB 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只有30MHz,16位总线的理论最大速率是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寄存器输出 |
| 带宽低 | 端点缓冲小,上位机读取慢 | 查看端点配置和上位机线程模型 | 增大缓冲,改用异步URB |
| FIFO溢出 | 流控阈值太高,数据源响应慢 | 监测FIFO使用量 | 降低阈值,加backpressure |
| 命令无响应 | EP2未使能,SLOE采样时机不对 | 检查固件配置,抓读时序波形 | 使能EP2,SLOE后等2周期再采样 |
| 枚举失败 | 固件未正确加载,VID/PID冲突 | 查看设备管理器,检查固件 | 重新加载固件,修改VID/PID |
5. 调试工具与实战技巧
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_DEBUG=4,能看到详细的传输日志,包括每次提交的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端出了问题。这个计数器帮我省了很多排查时间。