ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA USB2.0 Slave FIFO实践:用CH346C给Crosslink-NX打造高速上行通道

FPGA USB2.0 Slave FIFO实践:用CH346C给Crosslink-NX打造高速上行通道 上一篇文章我们聊到Crosslink-NX把MIPI图像数据采集进来之后在FPGA内部完成了格式转换和缓存评论区马上就有朋友问数据攒在内存里不算完怎么才能稳定地搬到电脑上这篇我就把这条链路彻底打通用CH346C这颗USB2.0 Slave FIFO桥接芯片给Crosslink-NX接一条上行通道让FPGA侧的数据能以FIFO接口的方式直接交到USB主机手里。整篇文章基于我实际调试这个项目的记录思路、代码、坑位都放出来给你做个参考。1. 先交代选型背景Crosslink-NX的上行数据通路为什么是USB2.0 Slave FIFO1.1 这个连载做到第14篇时的前置状态前面13篇的进度大致是这么个情况Crosslink-NX通过MIPI D-PHY接收图像传感器的数据流在FPGA内部做了处理、缓存需要往外部传输。Crosslink-NX这颗芯片的定位本来就是低功耗桥接、视觉处理适合在摄像头和上位机之间做一个“中间人”。但数据在上位机侧要能被识别、被读取必须走一条和PC能对接的总线。经常看到有朋友问为什么不用UART为什么不用千兆网为什么不用PCIe原因很简单。UART过顶天几Mbps传个字体、传个温度没问题传图像完全不现实。千兆网麻烦在MAC和PHYCrosslink-NX没有现成的RGMII方案自己要塞一个MAC核占用资源不说数据搬运逻辑也复杂。PCIe对Crosslink-NX来说能挂但是整体开销太重为了一个工业相机或者数据采集盒专门堆PCIe链路板卡成本、layout成本都上去了。于是USB2.0就成了最务实的选择——每台电脑都有开发工具链成熟480Mbps的峰值速率跑图像预览级别的数据流绰绰有余。1.2 上行通路的四种方案我为什么最终选了CH346C市面上FPGA接USB这条路大致有四种走法我先对比一下再解释结论。方案典型代表FPGA侧接口复杂度实际能达到的带宽适合场景串口转USB方案CH340, CP2102, FT232极低UART接口3Mbps以下低速调试、传感器数据回传SPI/I2C转USB方案FT232H, FT2232H低SPI从机接口10MB/s左右受SPI时钟限制低速采集、小流量传输FPGA直接外接USB PHY 自己实现协议栈无内置USB PHY的FPGA USB3300等高需要自己实现ULPI、描述符、控制传输、批量传输状态机理论可达40MB/s但开发量极大极少数必须定制USB功能的场景USB2.0 Slave FIFO桥接芯片CH346C, CY7C68013AFX2LP低FPGA侧就是异步/同步FIFO接口40MB/s以上受批量传输有效带宽限制高速图像、数据连续传输、需要快速上手的项目CH346C和CY7C68013A的Slave FIFO模式很相似都适合做FPGA和USB之间的桥。但CH346C有一个明显优势芯片内部把USB协议引擎、收发器、以及Slave FIFO控制逻辑都整合好了FPGA侧不需要去碰USB协议栈只需要对着FIFO信号读写。FX2LP本身要写8051固件去配置工作模式虽然网上基础工程很多但毕竟多一个MCU固件需要维护。CH346C这类芯片更加“桥”化很多工作模式直接用硬件引脚或者配置寄存器固化省掉了8051的维护量。另外CH346C支持批量传输端点别小看这一点。批量传输能占满整个USB带宽的80%以上并且自带CRC校验和重传机制对数据可靠性要求高的工业场景非常合适。相比之下用中断传输虽然延迟固定但带宽上不去等时传输带宽高却不保证可靠交付。我们这次传输的是图像数据流错一个字节都会花屏花线批量传输是权衡之后最稳的选型。1.3 这块板子上CH346C具体承担什么角色CH346C在整条数据链路里扮演的角色就是“把FPGA的并行FIFO接到USB总线上”。数据从Crosslink-NX的IO引脚出来以16位总线的形式进入CH346C的FIFO芯片自动把它打包成USB批量传输包提交给上位机。FPGA侧看到的CH346C周边逻辑就是一个可以往里面“倒水”的FIFO。你要做的就是管理好FIFO的空满标志在合适的时候把数据放到总线上再给一个写选通信号。USB协议里那些令牌、握手、ACK/NACK、CRC全是芯片内部处理掉的。这也是我强烈推荐Slave FIFO方案的原因把复杂的东西封装进芯片让FPGA开发者把精力集中在自己真正该管的数据搬运上。2. CH346C Slave FIFO接口信号与读写时序先吃透手册再写RTL2.1 Slave FIFO接口的本质USB端点变成一块“可读写的内存”理解Slave FIFO用生活里的例子最方便。PC和CH346C之间USB总线上是批量传输管道CH346C和FPGA之间就是一组并行的FIFO引脚。PC端发一个“读端点”请求CH346C就把自己FIFO里的数据“推”到USB总线上。FPGA端要发数据就往CH346C的FIFO里“推”芯片检测到FIFO非空就会把数据“拉”到USB总线上。开发者的体验就好比PC软件在读取一块虚拟硬盘而FPGA在往同一块硬盘里写数据。中间不需要关心USB的调度细节。Slave FIFO接口一般在芯片手册里有两种模式同步和异步。同步模式需要提供一个时钟数据在时钟沿被采样异步模式完全靠读/写选通的跳变来触发不依赖时钟但时序设计上更考究。CH346C的Slave FIFO接口可以选择同步模式我会推荐你优先用同步模式——时钟沿统一采样时序分析简单FPGA里的逻辑也能直接和它对齐。2.2 关键信号一览我以16位数据总线为例把CH346C Slave FIFO模式涉及到的核心信号列成一张表方便你对着手册逐项核对信号名方向相对FPGA作用CLKI / IFCLK输入同步模式时钟由FPGA提供一般30MHz48MHzDATA[15:0]双向数据总线写入和读出共用SLCS_N输入片选信号拉低表示选中该FIFO通道SLWR_N输入写选通低有效与时钟配合将数据写入FIFOSLRD_N输入读选通低有效从FIFO读出数据到数据总线SLOE_N输入输出使能控制数据总线输出方向FIFOADR[1:0]输入选择端点FIFO一般可以区分上传/下发通道FLAGA_N/FLAGB_N/FLAGC_N输出FIFO状态标志常见的是空/满/可编程阈值PKTEND_N输入包结束信号强制把当前FIFO内容作为一个短包提交给USBRST_N输入复位信号低有效注意不同芯片厂家的Flag定义会有差异有的芯片把FLAGA定义为“可写”有的定义为“空”甚至有的是高有效。这些细节必须以CH346C的数据手册为准我们项目里专门花了两个晚上核对这一组信号的有效电平光在这个上面吃亏就很不值得。2.3 同步写时序的关键点同步Slave FIFO写数据的约定通常是这样将FIFOADR设置到要写的端点保持稳定。将数据放到DATA[15:0]总线上。等待FIFO的“可写”标志有效表示还有空间。在时钟上升沿到来时让SLWR_N拉低芯片采样数据完成写入。一个常见的错误是数据总线的变化和SLWR_N的变化发生在同一个时钟沿导致芯片采样时数据还没稳定。规范做法是让数据总线在SLWR_N拉低之前就稳定最好用一个寄存器输出在时钟下降沿更新数据、在上升沿让芯片采样这样两边能错开半个周期时序裕量会大很多。我项目里设定的总线位宽是16bit时钟跑30MHz理论写速率就是16bit × 30MHz 60MB/s。USB2.0批量传输的实测有效带宽通常在3545MB/s之间所以30MHz的写时钟不仅够用还能留下一点余量处理FIFO满标志带来的等待周期。2.4 硬件连接里的几个细节电平匹配Crosslink-NX的IO Bank如果配置成3.3V LVCMOS可以直接和CH346C的3.3V逻辑相连。不过Crosslink-NX有的Bank只支持1.8V这种情况要么换Bank要么加电平转换芯片。建议在原理图阶段就把Bank电压和代码里的IO标准规划好省得后期飞线。引脚分配DATA[15:0]尽量放在同一个Bank并做等长约束。FIFO接口虽然是并行总线跑30MHz不算高频但16根线的skew如果太大上升沿采样依然有风险。差分对和普通IO保持安全间距。复位信号RST_N上电后至少保持几十微秒低电平FPGA里可以用一个计数器产生上电复位再释放给CH346C。别直接连FPGA的全局复位否则FPGA还没配置完成芯片就处于不稳定状态。串阻预留每个数据线串联一个22Ω33Ω电阻。USB2.0接口本身速度不高但并联电阻能改善信号边沿这在排查误码的时候会很有用。3. Crosslink-NX侧RTL实现状态机、FIFO缓存与端点管理3.1 整条数据流串起来是什么样从Crosslink-NX内部到CH346C数据流的路径是这样传感器数据 → MIPI接收 → Crosslink-NX内部处理 → 异步FIFO跨时钟域缓冲 → Slave FIFO写状态机 → CH346C内部的FIFO → USB批量端点 → PC上位机这里最关键的组件是异步FIFO。Crosslink-NX内部处理模块的时钟和Slave FIFO写状态机工作的时钟往往是不同频率。传感器侧时钟可能是24MHzUSB桥侧时钟是30MHz直接跨时钟域往下传数据必然出错。所以必须在中间放一个异步FIFO左侧用一个时钟写右侧用另一个时钟读把数据安全地从一个时钟域搬到另一个时钟域。Lattice的Crosslink-NX系列里可以用Lattice Primitive里的异步FIFO也可以用通用FIFO IP配置成不同读写时钟。重点是要把读侧的可编程满阈值prog_full引出来用于控制上游停止写入防止FIFO溢出。3.2 写状态机的核心逻辑写状态机是整个FPGA侧驱动CH346C的关键。它的任务就是从异步FIFO读数据然后按照Slave FIFO的时序要求把数据搬运到CH346C的数据总线上。我用Verilog实现了一个比较简洁的写状态机状态划分为IDLE、WRITE、WAIT_FULL三个状态思路如下localparam IDLE 3d0, WRITE 3d1, WAIT_FULL 3d2; reg [2:0] state_wr; reg [15:0] data_out_r; reg slwr_n_r; reg pktend_n_r; always (posedge clk_usb or negedge rst_n) begin if (!rst_n) begin state_wr IDLE; slwr_n_r 1b1; pktend_n_r 1b1; data_out_r 16d0; end else begin case (state_wr) IDLE: begin // 当上游FIFO非空、CH346C FIFO未满且当前包没有结束后进入写状态 if (fifo_in_empty_n !ch346c_full_n) begin slwr_n_r 1b0; data_out_r fifo_in_data; state_wr WRITE; end end WRITE: begin // 数据在时钟上升沿被CH346C采样同时拉高写选通回到IDLE slwr_n_r 1b1; fifo_in_rd 1b0; // 在WRITE期间给出读请求 state_wr IDLE; end WAIT_FULL: begin // 当FLAG显示FIFO满时等待满标志释放 if (!ch346c_full_n) begin state_wr IDLE; end end default: state_wr IDLE; endcase end end这里有个很重要的地方写选通和数据的时序关系。我设计的是在IDLE状态先把数据更新到数据总线上然后在下一个时钟沿到来时保持SLWR_N低电平让芯片采样。由于数据在上升沿之前已经稳定芯片采到的就是正确数值。如果你在同一个进程里同时翻转数据总线和SLWR_N可能就违反建立时间要求了。另外fifo_in_rd的时序要特别小心。异步FIFO的读请求不能和读出的数据在同一时刻判断否则会漏读。建议把读请求做成组合逻辑在进入WRITE状态时同时给出然后下一个状态拉掉这样能保证每拍读一个数据不丢节拍。3.3 为什么要在状态机里加入WAIT_FULL状态CH346C内部的FIFO不会无限大当USB总线上设备繁忙或者PC端应用程序读数据不及时FIFO就会满。这时候如果FPGA继续往里面写数据就会被覆盖产生不可恢复的丢失。WAIT_FULL状态的作用是发现满标志有效后挂起写操作。我见过不少初版逻辑根本没加这个状态满标志来了还硬写最后图像数据断断续续、画面撕裂定位问题花了一天。加一个WAIT_FULL状态并不会浪费带宽因为USB批量传输本来就有间断性满标志释放后继续写即可实时性影响很小。3.4 分包与PKTEND的处理USB批量传输每次传输的最大包长度是512字节。如果数据总线上连续写入512字节芯片会自动把这些数据组成一个USB包发送出去。但最后一帧的数据往往不足512字节这时就需要PKTEND信号告诉芯片当前数据就这么多了直接组成一个短包提交。我的做法是每传输一定长度后拉一个时钟周期的PKTEND_N低电平。具体长度选择可以结合应用场景比如图像的一行正好是2048字节那我就每2048字节提交一次PC端按行解析非常方便。如果PKTEND处理不正确最后一个短包不会被发送PC端会一直等下一条数据导致卡死或超时。3.5 多端点方向的考虑有些项目既需要上行传图像也需要下行接收控制命令。CH346C一般有多个FIFO端点通过FIFOADR选择。我的做法是FIFOADR[0]固定处理上行数据FIFOADR[1]处理下行命令。这样一个芯片同时完成双向通信结构很清晰。下行的读操作其实和写操作对称也就是FPGA侧要实现对Slave FIFO的读时序。读操作的核心是SLRD_N和SLOE_N的配合SLOE_N低电平让芯片输出数据到总线SLRD_N低电平时钟沿把数据锁存到FPGA侧。实测下行带宽要求不高用几个字节的控制命令完全够。4. 实测与踩坑记录带宽、误码、满标志这几个坑我逐个排掉的4.1 第一个坑满标志有延迟不能等它“变满”才停板子调出来之后第一版逻辑在连续传输几十秒钟后偶发数据丢失。逻辑分析仪抓FLAGC_N信号发现一个规律CH346C的满标志相对实际FIFO状态存在固定延迟大约有12个时钟周期。也就是说FIFO内部实际已经满了但满标志还没拉低等你看见满标志再停最后那两拍数据已经写爆了。这就好比接水的时候水杯已经满了但眼睛看到“满了”的信息传到手上中间还有半秒的延迟手还是继续开着水龙头水就溢出来了。解决办法是给标志信号加一个“提前量”。我当时是做了一个两级同步器把FLAG信号同步到FPGA时钟域之后再额外延迟两拍和一个比较阈值逻辑配合模拟“软满”信号。当FIFO剩余空间少于预设阈值时就认为FIFO即将满提前停止写入。即使FLAG还有延迟也留出了足够的反应时间。4.2 第二个坑带宽上不去实测只有20MB/s左右按30MHz、16bit位宽计算理论写入速率有60MB/sUSB2.0一般极限也有40MB/s左右。但第一版实测只跑到20MB/s差了一半。排查下来有两个致命问题。第一个问题是状态机里插入了一拍多余的等待。我当时为了图省事在IDLE到WRITE之间加了一个过渡态导致每写一个16bit数据就要消耗两个时钟周期。带宽直接砍半。优化办法是把状态机压缩让“数据就绪”和“写信号有效”尽可能在同一个状态内完成连续写时没有气泡。第二个问题是FT232的对比实验启发了我。USB批量传输是有调度间隙的不是每毫秒都在传输每个包的间隔芯片会有协议开销。如果每个周期都在频繁发送反而不如一次连续发送多个512字节包来的高效。这个其实不是FPGA侧能控制的但如果你发现带宽卡在某个值上试着在PC端用更大的读取缓冲区比如64KB一次性读一堆数据减少USB请求的调度开销实测有效。4.3 第三个坑数据错位图像全是斜条纹带宽跑到40MB/s以后新的问题出现了PC端收到的数据顺序偶尔错位整帧图像出现一条一条的斜线。我用回环测试的方法定位——FPGA侧发送一个递增计数器0、1、2……一直到65535循环PC端接收后解析发现大部分区间是连续的但每隔一段会出现连续几个数错位像是丢失了一个字节。这个问题的根子还是在FIFO读时序上。异步FIFO的读请求和数据输出之间有时序约束如果读请求信号在WRITE状态里同时拉高或拉低导致数据实际上比预期晚了一拍就相当于每N个数据里丢掉一个。解决办法是把读请求固定在WRITE状态的前半段给出并保证数据到达数据总线的时间早于CH346C的采样沿。我修改之后错位消失了。另外我还做了一层保险在数据流里插入了帧头。每一帧数据的开头都写0x5A5A和一个32位递增帧号PC端只要解析到0x5A5A就重新对齐数据流。这样就算偶发丢包PC端也能自动恢复同步不会一直错位下去。4.4 第四个坑USB端点STALLPC端直接报设备错误还有一种情况上位机读数据读到一半返回一个USB错误设备直接STALL。排查下来是端点的配置描述符和FPGA这边FIFO选择不一致。我在FPGA里用FIFOADR[1:0]选择端点但在CH346C配置里这个端点被配置成了输出方向PC到设备FPGA这边却当成上行数据在写芯片端到端的方向错了就STALL。这提醒我拿到一块CH346C板子第一件事就是确认它的默认配置是什么哪个FIFO是可写、哪个可读千万别靠猜。我当时对照寄存器配置逐项改把端点改成IN方向、批量传输问题立刻消失。4.5 一套可复用的排查链路如果你也遇到类似问题按这个顺序排查效率会高很多先确认PC端能不能枚举到设备确认端点方向和类型。用逻辑分析仪抓FPGA侧写CH346C的时序看SLWR_N和DATA配合是否满足手册要求。在FPGA内部加一个计数器统计写入CH346C的数据量PC端统计收到的数据量两边对比就能定位是FPGA没写进去还是USB传输丢包。数据流里插帧头帧尾用递增序列做回环测试验证数据的连续性和顺序性。最后别忘检查地线。USB连接器和FPGA板之间如果参考地不干净高速传输的时候误码率会莫名其妙上升。5. PC端识别与端到端验证让Windows和Linux都能顺畅读取CH346C5.1 Windows下的设备识别CH346C在Windows系统下如果是用厂商自己的驱动或者WinUSB驱动设备管理器里会显示一个USB设备。如果是批量传输的自定义设备Windows默认不一定有可用驱动这时可以用libusb或WinUSB来代替。常见做法是使用Zadig工具把设备驱动切换成WinUSB然后上位机用libusb进行读写。注意Zadig切换驱动只对开发调试阶段好用产品化的时候建议签一个正式的驱动或者使用微软的WinUSB免签方式部署。开发阶段图省事直接上Zadig没有毛病踩坑少、上手快。5.2 Linux下的免驱访问Linux下USB设备可以用usbfs也可以直接用libusb库。绝大多数情况下内核自带的usbfs驱动就会接管设备你甚至不需要安装任何额外的驱动只要确认设备有读权限就行。一般需要写一个udev规则把设备节点的权限改成0666或者把当前用户加入plugdev组。我用pyusb做过一个简单的Python读取Demo思路是找到VID/PID打开设备然后启动一个批量读线程import usb.core import usb.util DEV_VID 0x1A86 # 替换成CH346C的实际VID DEV_PID 0x???? # 替换成CH346C的实际PID dev usb.core.find(idVendorDEV_VID, idProductDEV_PID) if dev is None: raise ValueError(Device not found) # 如果系统已经有内核驱动占用先释放 if dev.is_kernel_driver_active(0): dev.detach_kernel_driver(0) # 设置配置 dev.set_configuration() cfg dev.get_active_configuration() intf cfg[(0, 0)] # 假设EP1是批量输入端点 ep_in usb.util.find_descriptor( intf, custom_matchlambda e: usb.util.endpoint_direction(e.bEndpointAddress) usb.util.ENDPOINT_IN ) while True: data ep_in.read(4096, timeout1000) # 在这里解析数据 print(freceived {len(data)} bytes: {data[:16].tobytes().hex()})这个脚本看起来简单但已经把设备的打开、内核驱动释放、端点查找、批量读全部串起来了。实测在树莓派和Ubuntu主机上都能稳定跑到35MB/s以上CPU占用也不高。5.3 端到端数据一致性测试硬件调通之后一定要做一次端到端的数据一致性测试。测试方案很简单FPGA内部用LFSR生成一段伪随机序列打包成固定帧格式每帧带帧号和数据长度发送给PC。PC端解析每一帧验证帧号递增、数据长度正确、LFSR校验值匹配。保留这个测试代码后续每次改逻辑都可以回归。矩阵如下测试项预期结果实际结果结论枚举测试设备管理器/设备树中出现设备正常通过小包回环测试1000包0x5A5A无丢包无错位通过通过满负荷连续传输1小时计数器无丢码PC端数据递增连续偶发两次丢包加WAIT_FULL后消失通过比特错误测试LFSR校验正确通过通过5.4 带宽为什么稳定在40MB/s左右上不去了明白人都知道USB2.0的理论速率480Mbps折算下来是60MB/s但实际批量传输一般只能跑到3545MB/s。这中间的损耗来自USB令牌包、握手包、帧间隔、调度间隙协议开销。就算你FPGA侧写得再快PC侧读得再快也突破不了物理上限。所以带宽设计的时候不要只盯着USB2.0理论数值做预算。如果你的摄像头数据率就超过40MB/s老老实实用USB3.0桥接芯片或者走千兆网。CH346C这种USB2.0方案适合数据量在40MB/s以内的应用。我们这次图像流大约是30MB/s留了余量稳。6. 这个方案最后再扩展点什么我会怎么走CH346C这个方案目前跑通之后整条链路从Crosslink-NX到CH346C再到PC上位机带宽稳定在3540MB/s之间图像数据连续传输一小时没有丢包。实际项目里这套方案最大的价值就是开发周期短、稳定可靠不用陷进USB协议栈的泥潭里。后续如果要做更大数据量的产品我准备把桥接芯片换成USB3.0的Slave FIFO方案FPGA侧状态机结构基本不变主要是把数据总线位宽从16bit扩展到更高的位宽时钟频率也可以往上提。同样的状态机框架、同样的PKTEND处理思路迁移成本很低。这个改造思路我后面可能会单独写一篇到时候再展开聊聊。再分享一个小技巧CH346C这类芯片做批量传输的时候PC端读取缓冲区的长度尽量大于1MB而且要用异步方式读取不要阻塞在等待上实测能再挤出两三MB/s的余量。这些细节通常数据手册不会告诉你都是调试时一点一点试出来的。
返回列表