FPGA和PC之间的高速数据交互,一直是嵌入式开发里绕不开的话题。我这个项目标题是“基于FPGA的cy7c68013a双向通信实验”,说白了就是用一块FPGA做主控,通过CY7C68013A这个USB 2.0控制器芯片,和电脑之间实现既能发、又能收的双向数据传输。很多人一听到USB就头疼,觉得协议栈复杂,但CY7C68013A这颗芯片的巧妙之处在于,它在硬件上帮你把USB协议几乎全部消化掉了,FPGA这边只需要面对一组通用的FIFO接口,读和写都变成了简单的时序操作。这篇文章我会把整套方案的选型逻辑、硬件连接、FPGA代码结构、固件配置、上位机对接全部讲透,适合正在做数据采集、图像传输、仪器控制这类项目的朋友参考,也适合想系统搞懂FPGA与USB之间怎么配合的初学者。
我最初接触这颗芯片是在一个高速数据采集的项目里,当时的方案是用FPGA采集ADC数据,通过USB上传到电脑做波形显示,同时还要接收上位机下发的参数配置。最开始考虑过直接用FPGA上的软核跑USB协议栈,也考虑过用USB PHY加外部协议芯片,但最终都因为复杂度太高被否掉了。CY7C68013A走Slave FIFO模式是当时性价比最高、开发周期最短的路线,实际跑下来也确实非常稳。下面我就一步一步拆解,把这个项目从设计到调试的完整路径整理出来。
1. 项目整体设计与思路拆解
1.1 为什么选CY7C68013A打通数据链路
先说CY7C68013A这颗芯片到底是个什么东西。它是Cypress(现在归英飞凌)的FX2LP系列USB 2.0控制器,内部集成了一颗增强型8051内核、一个USB 2.0的SIE(串行接口引擎),还有一组可以灵活配置的端点FIFO。它的最大价值在于,当工作在Slave FIFO模式下时,8051内核只负责USB枚举和设备配置,一旦枚举完成,数据通路完全由外部主控(也就是我们的FPGA)直接操作端点FIFO,8051不再介入数据搬运。
这意味着什么呢?意味着你在FPGA侧看到的CY7C68013A,不再是一个复杂的USB设备,而是一个简单的、可以读写的外部FIFO。你要做的,就是根据FIFO的状态信号(空、满、可编程阈值)产生读或写的时序,仅此而已。USB协议层的令牌处理、握手、CRC校验,全部由芯片内部的SIE处理了,这在USB开发里是非常大的简化。
实际开发中我们也对比过FT245/FT2232H这类FTDI芯片。FTDI芯片胜在驱动稳定、上手极快,但它的问题在于灵活性差,端点缓冲和FIFO深度都是固定的,想做一些定制化的流控策略很费劲。CY7C68013A的端点FIFO可以配置成2/4倍缓冲,深度最大可以到4KB,而且读写可以同时进行,双向吞吐能力比FTDI方案强很多。再加上它内部有8051,你可以在固件里做很多个性化操作,比如自定义厂商请求、动态切换端点配置等,这是FTDI做不到的。
1.2 双向通信的架构选型:Slave FIFO模式为什么省心
CY7C68013A支持多种接口模式,包括Ports模式、GPIF主控模式和Slave FIFO模式。项目标题里说的“双向通信”,最合适的就是Slave FIFO模式。在这个模式下,CY7C68013A作为从设备,FPGA作为主控,两者之间通过一组并行的数据总线和控制信号交互。
为什么选Slave FIFO而不是GPIF?GPIF模式是CY7C68013A自己作为主控去读写外部设备,这个时候CY7C68013A是发起者,FPGA是被动的一方。但在我们的场景里,数据流的主控权应该在FPGA这边,因为FPGA知道什么时候该发数据、什么时候该接收数据。比如数据采集应用里,FPGA的ADC采样率是固定的,采样数据产生后需要尽快搬走,这时候FPGA主动去写FIFO是最自然的思路。反向也一样,上位机下发的配置数据到了USB端点FIFO里,FPGA需要主动去读。所以主从关系决定了我们必须选Slave FIFO。
这部分的另一个关键考量是时钟方案。Slave FIFO模式支持同步和异步两种时序:同步模式下读写操作跟随时钟沿(IFCLK),最高可以跑到48MHz;异步模式下则通过独立的读写脉冲控制,速度会慢不少。我做这个项目时毫不犹豫选了同步模式,因为在FPGA里同步时序非常好约束,而且能跑满USB 2.0的高速带宽。顺便说一句,IFCLK既可以用CY7C68013A内部产生的30MHz/48MHz时钟,也可以由外部输入,实际使用时我推荐直接使用芯片内部时钟,省掉一个时钟源,还能保证和USB SIE的时钟同源。
1.3 数据通路规划:上传和下载是两条独立的管道
双向通信必须在设计之初就把两条数据通路分开规划,而不是混在一起。上传方向(FPGA到PC)和下载方向(PC到FPGA)看起来都是读写FIFO,但它们在端点配置、FIFO标志位和状态控制上是完全独立的。我在这个项目里用了四个端点中的两个:EP2配置为批量传输IN端点,负责上传;EP6配置为批量传输OUT端点,负责下载。EP4和EP8留作备用。
为什么要分开?因为USB是主从轮询机制,主机(PC)会周期性发送IN或OUT令牌。如果上传和下载混在一个端点里,就意味着FPGA必须时刻判断当前令牌是要求它读还是写,状态机复杂度会成倍上升。分开之后,EP2的FIFO永远只做写操作,EP6的FIFO永远只做读操作,FPGA的状态机变得非常清晰——写通道判断EP2的FIFO是否满,不满就写;读通道判断EP6的FIFO是否空,不空就读。这种“各管各的”思路,在实际调试时能省掉大量时间。
2. 硬件搭建与关键信号解析
2.1 最小系统:电源、时钟与外围电路
CY7C68013A的硬件外围比想象中简单,这也是它受欢迎的原因之一。核心电源是3.3V,但要注意它内部有1.8V的稳压器(VCC),需要在外围接一个去耦电容。USB的D+和D-直接连到USB连接器,需要在D+上接一个1.5kΩ的上拉电阻,这个电阻是告诉主机“这是一个高速设备”的关键,如果漏接或者接错,设备就只能跑全速模式,速度直接掉到12Mbps。
时钟方面,我使用的是24MHz无源晶振,配合两个12pF左右的负载电容。有人图省事用有源晶振,但其实没必要,FX2LP内部有时钟振荡电路,无源晶振加两个电容就是标准接法。IFCLK这里我没有额外接外部时钟源,而是使用了芯片内部产生的48MHz时钟,挡位配置在IFCONFIG寄存器里。
另外强烈建议把WAKEUP引脚通过10kΩ电阻上拉,把RESERVED引脚接地。这两个是芯片手册里明确要求的外围处理,很多人不看datasheet直接按照最小电路画板,结果出现间歇性枚举失败。我在第一版PCB上就没有把RESERVED引脚处理好,导致设备偶尔能被识别偶尔不行,后来对照手册逐脚排查才找到原因。
2.2 FPGA与CY7C68013A的信号连接要点
Slave FIFO模式下,FPGA和CY7C68013A的信号连接看起来不复杂,但每一个信号都有讲究。数据线是FD[15:0],因为芯片的FIFO数据总线可以做8位也可以做16位,我这个项目里为了带宽选了16位模式。地址线FIFOADR[1:0]用来选择当前操作哪个端点FIFO,00对应EP2、01对应EP4、10对应EP6、11对应EP8。注意这里选择的是端点缓冲区的编址,不是端点号本身,对应关系在配置寄存器里确定。
控制信号方面,SLCS是片选信号,低有效。SLWR是写使能,SLRD是读使能,SLOE是输出使能,这三个信号在同步模式下都要求与IFCLK保持时序关系。PKTEND是强制提交短数据包的关键信号,这个后面会详细讲。状态输出方面,FLAGA到FLAGD四个标志位可以通过寄存器配置成不同含义,常用的配置是把FLAGA映射为当前FIFO的“可编程阈值标志”,把FLAGB映射为“满”标志,把FLAGC映射为“空”标志。我在实际项目里,上传通道主要关注“满”标志(满就不写),下载通道主要关注“空”标志(空就不读)。
这里要特别提醒一下:SLOE这个信号很多人容易忽略。在读取FIFO数据时,SLOE必须有效(拉低),数据总线才会输出FIFO当前指针指向的数据。如果你想预读下一个数据,在SLOE无效的情况下读操作是不会正确输出数据的。我见过不少人的代码里把SLOE直接接地,这在某些情况下可以工作,但如果在多端点复用数据总线时,不控制SLOE会导致总线冲突。
2.3 一块实用PCB布局的建议
如果你不是用现成开发板,而是自己画板子,布局上有几条建议是我踩过坑后总结的。第一,CY7C68013A的USB差分对D+和D-要等长走线,阻抗控制在90Ω差分,长度尽量短,别打太多过孔。第二,FPGA与CY7C68013A之间的数据线和控制线虽然频率不算特别高(48MHz),但仍然建议做等长处理,至少保证一组信号线长度差异控制在5mm以内,否则在高速模式下可能因为信号偏斜导致数据传输错位。
第三,模拟地和数字地、USB座子的外壳地要分开处理,外壳地通过一个1MΩ电阻加100nF电容并联到地,可以显著降低静电干扰。我之前的板子没有做这个处理,冬天插拔USB的时候偶尔会把设备搞脱机,加了之后这个问题再也没出现过。电源部分,3.3V通过LDO供电,在每个电源引脚旁边放一个100nF去耦电容,靠近引脚放置,这是最基本的做法,但也是很多人偷懒没做好的地方。
3. FPGA侧代码实现详解
3.1 上传方向整体状态机设计
FPGA侧写通道的核心任务,是把数据按照Slave FIFO时序写入CY7C68013A的端点FIFO。这里最关键的一点是:写操作必须严格与IFCLK同步,且要持续监视FIFO满标志。我设计了一个简单的四状态状态机——IDLE、WAIT_FOR_NOT_FULL、WRITE_DATA、WAIT_FOR_COMMIT。
IDLE状态下,如果FIFOADR选择的是EP2,并且检测到FIFO不满,就进入WRITE_DATA。WRITE_DATA状态下,每个IFCLK上升沿写入一个16位数据。在这里要特别注意“半满”变化的问题:CY7C68013A的FIFO满标志是组合逻辑输出的,在同步Slave FIFO模式下,满信号的变化会有一个时钟的延迟,所以FPGA在写最后几个数据时,需要预留足够的余量,不要等到满标志出现才停止。一个稳妥的做法是:设置一个内部计数器,记录已经写入的数据量,当剩余空间小于某个阈值时,提前停写。这个阈值一般是4到8个深度,这样就算满标志还没来得及更新,也不至于覆盖已有数据。
当一组数据写完(比如512字节),需要提交给USB端点。对于刚好等于端点缓冲区深度的数据量,不需要额外操作,因为缓冲区满了之后芯片会自动打包提交。但如果是最后一包数据不足一个整个缓冲区,就必须通过拉低PKTEND信号一个时钟周期来强制提交短包。忘记了这一步,上位机就会一直等不到数据,表现为“数据卡住不刷新”。
// 上传通道同步Slave FIFO写状态机核心代码 module fx2_slave_fifo_write ( input wire clk, // IFCLK 48MHz input wire rst_n, input wire [15:0] tx_data, // 待发送数据 input wire tx_valid, output reg tx_ready, // CY7C68013A接口 output reg [1:0] fifoadr, output reg slwr, output reg pktend, output reg slcs, input wire flag_full // EP2满标志 ); localparam IDLE = 2'd0; localparam CHECK_SPACE = 2'd1; localparam WRITING = 2'd2; localparam FORCE_PKTEND = 2'd3; reg [1:0] state; reg [9:0] word_cnt; // 已写入字数统计 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; slwr <= 1'b1; pktend <= 1'b1; slcs <= 1'b1; fifoadr <= 2'b00; tx_ready <= 1'b0; word_cnt <= 10'd0; end else begin case (state) IDLE: begin slcs <= 1'b1; slwr <= 1'b1; pktend <= 1'b1; fifoadr <= 2'b00; // 选择EP2 if (tx_valid) begin state <= CHECK_SPACE; end end CHECK_SPACE: begin slcs <= 1'b0; // 拉低片选 if (!flag_full) begin state <= WRITING; tx_ready <= 1'b1; word_cnt <= 10'd0; end end WRITING: begin if (!flag_full && tx_valid && word_cnt < 10'd512) begin slwr <= 1'b0; // 有效写信号 word_cnt <= word_cnt + 1'b1; if (word_cnt == 10'd511) begin // 最后一个数据,检查是否需要强制提交 state <= FORCE_PKTEND; slwr <= 1'b1; tx_ready <= 1'b0; end end end FORCE_PKTEND: begin pktend <= 1'b0; // 拉低PKTEND一个时钟 state <= IDLE; end default: state <= IDLE; endcase end end endmodule上面的代码演示了一个简化版本,实际工程里还需要增加超时保护、数据源反压等逻辑。我在实测时发现,如果数据源长时间有效而FIFO一直满,CHECK_SPACE状态里就死等,占用了状态机时间。后来我加了一个“超时丢弃”机制,一旦等待超过一定时钟周期,就向上游报告BUSY,宁可丢数据也不要卡死主循环,这在工程上非常重要。
3.2 下载方向读通道的实现要点
下载方向是PC向FPGA发数据,FPGA需要从EP6的FIFO中读取数据。这里有两个容易踩的坑,第一个是没有正确处理FIFO空标志,第二个是没有理解同步读时序的预读特性。
同步Slave FIFO模式下,读操作其实是一个“预读”的过程。当你拉低SLRD时,数据总线上出现的不是当前地址的数据,而是下一个地址的数据。也就是说,第一次读操作时数据总线上的值是无效的。正确的做法是:第一次拉低SLRD后,虽然看到了数据,但不能直接使用,要再触发一次读操作,这时得到的数据才是第一个有效数据。用Verilog实现时,我一般用一个额外的寄存器把数据打一拍,形成一个两拍延迟的读取流水线。
空标志的判断也需要特别注意。FLAGC(空标志)为低时表示FIFO非空,可以读。但由于标志信号的更新相对于FIFO内部指针存在延迟,在连续快速读取时,停在“最后一个有效数据”时空标志可能还没来得及拉高,如果此时继续读,就会读出垃圾数据。所以稳妥的做法是在判断“非空且还有剩余数据”时多等几个时钟周期。我实测下来,在48MHz时钟下,多等2到3个周期可以彻底规避这个问题,代价只是吞吐量稍微降低一点点。
还要记得处理SLOE信号。在读取时,SLOE必须为低,否则数据总线不会输出有效数据。有一个小技巧是,对于只使用一个端点做下载的情况,可以把SLOE和SLRD绑定,做一个简单的“读使能”输出,而不是单独控制。但如果你在多个端点间切换,就必须独立控制每个信号,防止数据总线冲突。
3.3 双向同时工作的时钟域处理
FPGA内部逻辑的时钟可能与IFCLK是异步的。比如我的FPGA主逻辑运行在100MHz,而USB接口时钟是48MHz。双向同时工作时,两个方向的数据都要跨时钟域,这时候就需要异步FIFO来隔离。CY7C68013A本身有端点FIFO,但这个FIFO是用于USB接口和外部接口之间的缓冲,不能指望它同时解决FPGA内部跨时钟域的问题。
我的做法是在FPGA内部为上传方向设计一个异步FIFO,写时钟是FPGA内部100MHz,读时钟是IFCLK 48MHz;下载方向则反过来,写时钟是IFCLK,读时钟是FPGA内部时钟。这样USB接口逻辑和内部逻辑就完全解耦了。异步FIFO的深度要根据突发数据量来定,我做图像传输时用的是2KB深度,实测下来在没有长时延反压的情况下足够用。如果上位机处理较慢,导致长时间反压,就需要把深度加大或者增加流控机制。
这里有一个实用的工程经验:下载方向的异步FIFO写满后,USB的端点FIFO会自动反压到主机,但这个过程不会通知到PC应用层,PC应用层的表现是“发送请求一直pending”。所以你在上位机软件里一定要做超时处理,不然用户会以为电脑死机了。
4. 固件配置与上位机对接
4.1 固件里必须改的寄存器配置
CY7C68013A的固件开发使用Keil C51,官方提供了USB FX2LP的开发框架,包括TD_Init、TD_Poll、DR_VendorCmnd等回调函数。我们只需要在TD_Init里完成寄存器初始化即可。最关键的是以下几个配置:
IFCONFIG寄存器需要配置为0xE3。这个值的含义是:接口使用内部48MHz时钟,Slave FIFO模式,同步时序。这里要注意,0xE3里的高两位(IFCLKSRC和xMHz)决定了时钟来源和频率,中间两位(IFCLKOE和IFCLKPOL)控制时钟输出和极性,低两位(IFCFG[1:0])选择工作模式,值为11时即为Slave FIFO模式。很多人照着网上抄一个0xE3就能工作,但如果用的外部时钟或者改过极性,就要重新计算这个值。
EP2CFG配置为0xA0,表示EP2为批量传输IN端点,16位数据总线,4倍缓冲。EP6CFG配置为0x80,表示EP6为批量传输OUT端点,16位数据总线,4倍缓冲。这里有一个比较容易忽略的点:EP2CFG最高位是VALID位,必须写1,否则端点不生效。而且修改端点配置前需要先FIFORESET,复位对应的FIFO,否则可能出现状态错乱。
FIFORESET操作顺序是有讲究的:向FIFORESET寄存器写0x80(命名NAK所有端点),再写0x02复位EP2,0x06复位EP6,最后写0x00释放。这个过程要连续执行,中间不能有其他USB处理,否则有的端点会复位不成功。
// TD_Init关键配置代码 void TD_Init(void) { // 设为内部48MHz时钟,Slave FIFO模式,同步 IFCONFIG = 0xE3; // EP2: 批量IN,16位,4倍缓冲 EP2CFG = 0xA0; // EP6: 批量OUT,16位,4倍缓冲 EP6CFG = 0x80; // FIFO复位序列 FIFORESET = 0x80; FIFORESET = 0x02; FIFORESET = 0x06; FIFORESET = 0x00; // 配置FLAGA为可编程阈值标志,FLAGB为满标志,FLAGC为空标志 PINFLAGSAB = 0xE8; // FLAGA -> PF, FLAGB -> FF PINFLAGSCD = 0x38; // FLAGC -> EF, FLAGD -> 保留 // 设置EP2可编程阈值为4KB(即4倍缓冲的一半) EP2FIFOPFH = 0x10; EP2FIFOPFL = 0x00; // 自动提交数据包(AUTOIN),不需要PKTEND手动提交(这里选择手动) EP2AUTOIN = 0x00; // 0表示手动提交,1表示自动提交 EP6AUTOOUT = 0x00; }关于AUTOIN和手动PKTEND的选择,我这里多说两句。如果在固件里把EP2AUTOIN置1,FPGA写入的数据达到缓冲区大小后会自动打包提交,短包则需要通过PKTEND触发。如果置0,所有数据都必须通过PKTEND提交,包括整包数据。我的经验是关闭自动提交,统一由FPGA侧控制PKTEND,这样在调试时能明确知道每一包数据是什么时候提交的,定位问题方便得多。
4.2 上位机软件用CyUSB.dll收发数据
上位机对接,最省心的是使用Cypress官方提供的CyUSB.NET库,在C#里调用即可。这个库封装了底层驱动,直接操作端点对象,代码非常简洁。使用前需要安装CyUSB驱动,在设备管理器里为CY7C68013A加载官方驱动(cyusb.sys),之后就可以通过CyUSB.dll进行数据传输了。
上位机的核心逻辑是:找到USB设备,打开双向端点BulkInEndpoint和BulkOutEndpoint,然后分别做读和写操作。读操作时,上位机发一个异步读请求,等待FPGA上传数据;写操作时,上位机把字节数组写到OUT端点。这里要注意的是,数据传输的单位是“包”,每次读操作返回的数据长度不一定是请求的长度,所以要用循环接收来处理半包和粘包的问题。我写了一个简单的数据接收循环,用BeginRead异步接口持续读取,收到数据后丢到队列里,由处理线程消费,这样界面不会卡顿。
另一个容易忽略的点是USB连续传输时的带宽争抢。如果你的应用是双向同时大流量传输,USB总线的总带宽是480Mbps(高速模式),但实际有效带宽大约只有40MB/s左右。上传和下载要平分这部分带宽。如果两侧都试图跑满,就会出现互相拖慢的现象。解决方案是给上传通道设置优先级——在FPGA侧可以做一个简单的调度器,优先保证上传带宽,下载只利用剩余带宽。我在数据采集项目里就是这么做的,效果非常好。
4.3 上位机与FPGA的联调验证方法
不要一上来就跑全速,联调要分步走。第一步先做“回环测试”:FPGA把收到的上位机数据原样返回。这个测试能验证USB链路、固件配置、FPGA读写状态机的正确性。第二步做“块传输测试”:上位机发送固定模式(比如0x55AA)的数据,FPGA接收后校验,再发送固定模式数据给上位机,上位机校验。第三步才做真正的业务数据测试。
我在做回环测试时发现一个问题:如果上位机用同步API发送大量数据,界面会卡死,原因是CyUSB的同步XferData内部是阻塞式的,在接受数据时会占满UI线程。后来我改用异步API,并且把接收循环放在单独的线程里,才彻底解决。这块代码经验强烈建议你提前做,不要等到UI卡死了再改。
// 上位机异步接收数据核心代码 public void StartReceiveLoop() { receiveThread = new Thread(() => { byte[] buffer = new byte[512 * 16]; while (isRunning) { int bytesRead = bulkInEndpoint.BeginRead(buffer); if (bytesRead > 0) { // 把收到的数据放入队列 receivedDataQueue.Enqueue(buffer.Take(bytesRead).ToArray()); } } }); receiveThread.IsBackground = true; receiveThread.Start(); }5. 常见问题与排查技巧实录
5.1 设备枚举失败,电脑不识别
这个问题的原因比较多,但排查顺序是有规律可循的。先看供电:CY7C68013A要求3.3V稳定供电,电压跌落会造成枚举不稳定,用万用表测一下VCC引脚的电压,再看USB座的VBUS是否正常。接着查晶振是否起振:用示波器测24MHz晶振引脚,如果波形幅度不够或者完全没波形,枚举必然失败。再查D+的上拉电阻:这个1.5kΩ上拉电阻如果焊接不良,USB主机无法检测到设备连接。最后查USB线的质量,这个看起来不起眼但很常见,我试过一根劣质USB线插上去设备时好时坏,换一根线就好了。
如果以上硬件检查都正常,就要考虑固件是否进入正常的枚举流程。可以在固件里加一个LED翻转的测试代码,放在TD_Init开头。如果上电后LED没反应,说明8051根本没跑起来,这时候检查WAKEUP引脚是否被拉低,这个引脚如果低电平会让芯片进入挂起模式,8051停在那里,设备自然无法正常枚举。
5.2 能枚举但FPGA读写FIFO没反应
设备能被识别,说明USB链路正常,问题大概率出在FPGA与芯片的接口配置上。先检查IFCONFIG寄存器是否真的配置成了Slave FIFO模式,可以用Cypress的USB Control Center读寄存器值,确认0xE3无误。然后检查FPGA这边的片选信号SLCS有没有拉低,很多人的状态机里忘记拉低这个信号,导致所有读写操作都被芯片忽略了。
还有一个常见问题是FIFOADR选择错误。我在调试时就出现过一次,EP2在固件里配置的是00地址,但我代码里写了01,结果片选有效、时序正确,但数据和EP4缓冲区发生交互,现象是上位机能收到数据,但数据内容完全不对。这种错误用示波器抓FIFOADR和SLWR的对应关系就能发现。另外,如果你用ILA或者逻辑分析仪抓FPGA内部信号,注意FLAG信号的极性,空标志和满标志都是低有效,很多人把它们当成高有效来判断,逻辑就完全反了。
5.3 数据传输错位和丢包
数据错位,最常见的原因就是我在3.2节提到的同步读“预读”特性没处理。第一次拉低SLRD时读到的数据是无效的,如果直接用于业务逻辑,整个数据流都会错位一个周期。解决方法是增加一拍延迟,确保数据在第二次读操作时才被采样。数据错位的另一个来源是16位FIFO模式下的字节序问题:FPGA侧把高8位和低8位哪个先发送,上位机必须保持一致,否则会出现字节交叉错乱。
丢包问题则要关注上位机的接收策略。如果用同步API且接收线程不够快,上位机缓存的USB令牌响应会超过缓冲上限,导致数据包丢失。我的建议是使用异步API或专门的接收线程,并且接收缓冲设置得足够大。FPGA侧也要注意,如果写入速度偶尔超过上位机读取速度,上传FIFO会满,这时没有别的办法,只能靠流控。我用的策略是:FPGA内部缓存少量数据后,等待FIFO满恢复再继续写,上位机侧则把超时时间设置到几百毫秒,避免因为瞬时拥堵直接报错。
5.4 速度上不去的瓶颈分析
如果你的应用对传输速度有要求,但要检查几个关键点。第一,确认USB设备工作在高速模式(480Mbps)而不是全速模式(12Mbps)。在Windows设备管理器里可以看到设备连接速度。如果显示的是全速,检查D+上拉电阻的值,应该用1.5kΩ而不是10kΩ,后者会让设备只运行在全速模式。第二,确认IFCLK频率,同步Slave FIFO模式下48MHz对应16位数据总线,理论峰值是96MB/s,但实际USB批量传输一般只能跑到40~43MB/s左右,这是USB 2.0高速模式的协议开销决定的。
第三,检查端点的缓冲配置。EP2和EP6都配置成4倍缓冲了吗?如果只用单倍缓冲,传输间隙会明显增大,速度上不去。第四,上位机单次请求的字节数要大,如果每次只请求64字节,USB的轮询间隔会成为瓶颈。建议单次请求至少2KB以上,我实测单次请求512字节和4KB相比,总吞吐量能差到30%。最后,检查FPGA侧的写时序是否每个周期都有效,如果状态机里有一拍空转,48MHz同步时吞吐量直接下降一半,这种损耗是很可惜的。
最后分享一个我在实际项目里总结的体会:FPGA与CY7C68013A的双向通信,难点从来不在协议本身,而在于你如何把“USB链接”这件事抽象成一个只管读写FIFO的简单接口。一旦完成了这个思维转换,整个项目的大部分工作其实就集中在了状态机的时序设计和异常处理上。我在第一版调试时,花了整整两天排查一个“上位机收到数据全0”的问题,最后发现是FPGA侧在上电时没有把数据总线释放,导致芯片的输出驱动被外部信号干扰。从那以后,我在编写FPGA代码时都会刻意注意总线方向的控制逻辑——在非传输阶段,所有输出信号要么置为高阻,要么输出稳定电平,绝不悬空。这个小习惯,后来帮我避免了很多莫名其妙的怪问题。希望这篇文章能帮你少走一些弯路,早日跑通自己的双向通信链路。