ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FPGA实现USB通信:从Verilog控制器到全速HID设备完整实战

FPGA实现USB通信:从Verilog控制器到全速HID设备完整实战 简介一套基于 Verilog 的 USB 接口 FPGA 实现源码工程面向 FPGA 开发者和对 USB 协议感兴趣的嵌入式学习者完整演示如何在 CPLD/FPGA 上通过同步从机 FIFO 方式与 USB 芯片对接解决高速数据传输接口的硬件设计与仿真验证问题。压缩包共 75 个文件、649KB其中主体是 Quartus 工程文件包含 RTL 源码、仿真波形和编译/时序报告同时还附带 8051 固件源码、测试说明 PDF 及多种 EDA 辅助文件。工程具体覆盖同步 FIFO 控制器、读写指针与空满边界处理等关键逻辑并已通过仿真验证配套的同步自动输入测试工程说明AUTOIN有助于理解 USB 批量传输中的自动输入模式。除 FPGA 侧逻辑外固件部分还包含 USB 描述符、跳转表等源码可与 FPGA 端配合完成枚举与数据通路搭建从而提供一条完整的参考路径。整个压缩包目录区分了测试工程、固件工程与说明文档便于分层查阅。目前已有 1110 人学习下载适合需要参考完整工程结构、快速上手 Verilog USB 同步 FIFO 设计的开发者。 最近我折腾了一个算是比较硬核的小项目用FPGA写USB通信。起因很直接——手里的板子跟电脑通信一直依赖串口115200bps在调试指令级别的东西绰绰有余但一旦涉及波形回传、图像数据、高速采集结果串口这个传输速率就变成瓶颈了。想换成USB又不想直接塞一颗CH340、FT232H或者CY7C68013A这种现成芯片于是我决定换个思路用Verilog在FPGA里把USB设备控制器完整写出来。这个项目做下来我的结论是这条路完全走得通特别适合全速USB 1.1设备。本文想完整分享一下这次实现的思路、源码结构和踩坑记录内容包括USB协议关键知识点、物理层和链路层的实现细节、描述符和枚举流程的处理、以及用Icarus Verilog做仿真验证的实操方法。想彻底搞懂USB数据到底怎么流动、想摆脱串口速率限制、或者想验证自己Verilog状态机功底的读者这篇应该能给你不少参考价值。1. 项目概述与需求拆解1.1 为什么要在FPGA上实现USB先聊清楚动机。FPGA开发里最常见的板级通信手段就是UART串口便宜、简单、一根线搞定但它有个天然短板速率上不去。常规UART波特率做到921600bps已经算不错了但换到USB全速接口12Mbps的带宽大约是它的13倍USB 2.0高速更是到了480Mbps。如果你的FPGA项目要做数据采集、图像传输这类高吞吐应用串口根本扛不住。商业USB芯片当然是一条捷径。FT232H、CY7C68013A、CH372这些芯片把USB协议栈封装好了你只要对着芯片手册操作FIFO或者并行总线就能完成和PC的数据交换。但这个方案有个问题它把USB协议变成了一只“黑盒”你始终不知道PC和你的设备之间到底发生了什么。而用Verilog自己实现USB控制器意味着你要把物理层信号采集、NRZI解码、CRC校验、协议状态机、枚举响应等全部跑在FPGA内部逻辑里——这个东西做完之后你对USB协议的理解深度绝对不亚于读完三遍USB官方规范。另外还有一个现实考量商用USB控制器芯片的端点数量、传输类型都是固定的比如很多芯片只支持批量传输Bulk。但如果你的项目需要自定义vendor请求、需要USB实时中断传输、或者需要同时模拟多个端点自己写USB协议栈反而是更灵活的方案。FPGA的并行特性和可重配置能力在这里就是天然优势。1.2 需求边界全速、高速还是HID做这个项目之前第一件事是搞清楚你要做哪一种USB设备。USB 2.0规范里定义了三种速率低速Low Speed 1.5Mbps、全速Full Speed 12Mbps、高速High Speed 480Mbps。放到FPGA语境下这三者难度完全不在一个量级。低速和全速USB的信号可以用普通的单端IO配合外部上拉电阻来驱动不需要专门的模拟PHY芯片。全速设备在D线上接一个1.5kΩ上拉电阻主机就能识别到设备插入。FPGA只需要用内部逻辑直接采样D和D-两根线的电平变化即可。高速USB则完全不同它需要额外的物理层芯片比如USB3300这类ULPI接口PHY来处理480Mbps的高速串行信号FPGA负责链路层以上协议。USB 3.x就更不用碰了那是超高速差分信号普通FPGA开发者自己实现的ROI极低。所以我的建议很明确第一次在FPGA上实现USB就老老实实做全速USB 1.1设备。这个选择从硬件复杂度、代码工作量、调试难度三个维度来看都是最优解。至于设备类型我选的是HID人机交互设备。原因很简单HID设备在PC上免驱插上就能用非常适合验证协议栈的正确性而且HID协议本身不复杂报告描述符写好后通过读写报告就能实现双向通信。如果你想要更高吞吐量后续可以在这个基础上改成自定义vendor设备配合WinUSB或libusb驱动使用。2. USB协议要点与Verilog实现路线2.1 物理层NRZI编码与位填充USB物理层用的是NRZI不归零反相编码核心规则只有两条逻辑0导致电平翻转逻辑1保持电平不变。这个编码方式的好处是即使总线上没有时钟信号接收端也可以通过监测电平跳变来恢复数据时序。但NRZI有个致命弱点如果连续传输大量1电平会一直不变接收端就没法判断每个bit的边界了。USB协议为此加入了位填充机制当发送端检测到连续的6个逻辑1时会在第6个1后面强制插入一个逻辑0。这样总线上的电平最长也不会超过7个bit的稳定时间接收端可以据此恢复出原始数据。在全速模式下USB数据位宽为12Mbps一个bit的时间大约是83.3ns。FPGA内部如果跑一个48MHz的采样时钟那么每个bit可以被采样4次完全足够检测跳变沿。我的Verilog实现思路是这样的用D信号驱动一个移位寄存器检测电平跳变时产生一个“bit有效”脉冲在脉冲位置输出NRZI解码后的bit值同时做位填充移除。// 全速USB接收侧48MHz采样检测跳变沿恢复NRZI数据 reg rx_d_ff, rx_d_ff2; wire rx_edge rx_d_ff ^ rx_d_ff2; // 跳变沿脉冲 always (posedge clk_48m) begin rx_d_ff usb_dp; // D信号 rx_d_ff2 rx_d_ff; end // 当检测到跳变说明收到逻辑0无跳变则收到逻辑1 wire nrzi_bit ~rx_edge;当然这只是NRZI解码的最基础形态实际工程还要考虑同步头SYNC检测、EOPEnd of Packet包结束识别以及位填充移除。这些细节在后续链路层部分会具体展开。如果你准备参考现有开源代码Corundum项目的USB实现部分有很规范的物理层设计可以借鉴但建议还是先把原理吃透再去看代码。2.2 链路层包结构与时序约束USB的链路层通信以“包”为单位。全速USB包的标准结构是SYNC同步字段8bit、PID包标识8bit、数据字段可选、CRC校验字段最后是EOP结束信号。SYNC是一串固定的模式全速下是00000001用于接收端时钟同步PID字段低4位是包类型编码高4位是低4位的反码用于校验PID本身。PID类型分四大类令牌包IN、OUT、SETUP、SOF、数据包DATA0、DATA1、握手包ACK、NAK、STALL和特殊包PRE、ERR等。整个USB通信流程可以概括为主机先发令牌包然后根据令牌类型传输数据包最后设备回复握手包。这个过程有点像快递流程——令牌包告诉你是要收件还是发件数据包是包裹内容握手包就是签收确认。全速设备在协议层有一个非常严格的时间约束设备必须在收到令牌包后的4个bit时间约333ns内开始响应。这个时序是所有FPGA状态机设计中最容易翻车的地方。如果状态机在检测完PID后处理消耗了过多时钟周期就会造成响应超时枚举直接失败。所以发送路径上尽量用硬件FIFO预装载数据状态机只负责在正确的时间点启动发送不要在状态机跳转里做复杂的运算。2.3 枚举流程描述符与标准请求新设备接入USB总线后主机做的第一件事叫枚举Enumeration。你可以把枚举理解成PC在办理入住手续——它先给设备复位然后通过控制传输向设备索要一系列“身份资料”描述符拿到资料后给设备分配地址最后激活配置设备就正式进入工作状态了。枚举过程涉及的标准请求主要有GET_DESCRIPTOR获取描述符、SET_ADDRESS设置地址、SET_CONFIGURATION设置配置、GET_STATUS获取状态、SET_FEATURE/CLEAR_FEATURE设置/清除特性。Verilog实现时控制传输的请求解析逻辑集中在端点0EP0里收到SETUP包后解析8字节的setup事务数据根据bRequest字段分发到对应处理逻辑。描述符是枚举成功与否的关键。代码里我建议把设备描述符、配置描述符、接口描述符、端点描述符和HID描述符定义成常量ROM数组这样既方便维护也能在综合时优化成LUT-ROM不占用额外BRAM。描述符结构层层嵌套设备描述符指向配置描述符配置描述符里挂着接口描述符接口描述符下面才是端点描述符。有一个非常容易踩的坑配置描述符的长度必须把所有子描述符的长度都算进去写错哪怕一个字节Windows就会直接报“无法识别的USB设备”。3. 核心模块设计与源码实现3.1 顶层模块划分我习惯把USB控制器按数据流方向拆成几个独立模块这样仿真和调试都能分而治之模块职责对应USB层次usb_phy_rxD/D-信号采样、SYNC检测、NRZI解码、位填充移除、串并转换物理层usb_phy_tx并串转换、NRZI编码、位填充插入、EOP生成物理层usb_crcCRC5令牌包和CRC16数据包生成与校验链路层usb_link包级状态机识别PID、检查地址、触发对应端点处理链路层usb_ep0控制传输状态机解析标准请求返回描述符协议层usb_ep1应用端点例如HID IN/OUT端点协议层usb_fifo跨时钟域缓冲连接USB协议层和用户应用逻辑接口层顶层模块把以上子模块例化连接对外只暴露usb_dp、usb_dm引脚以及用户侧的数据总线接口。这个划分思路和官方参考设计基本一致好处是物理层和协议层完全隔离——后续如果你想升级到USB高速只需要替换phy层模块上层协议代码不用动。3.2 接收与发送状态机链路层的核心是状态机。接收方向状态机从IDLE开始等待SYNC检测到同步模式后转移到接收PID状态然后根据PID类型决定是继续接收数据字段还是直接等待EOP。数据接收完成后进行CRC校验校验通过则把数据写入端点FIFO并触发握手响应校验失败则丢弃数据或者发送STALL。需要注意的是USB是半双工总线同一个USB总线上设备不能同时发送和接收。所以接收和发送状态机虽然独立存在但必须通过一个简单的总线占用标志来互斥。我在代码里用一个tx_active信号标识发送状态机正在驱动总线接收状态机在这个信号有效期间停止采样避免把自己发出去的数据又收回来。发送方向状态机的时序更敏感。以IN事务为例设备收到主机的IN令牌包后必须在333ns内开始发送SYNC、PID和数据包。我的做法是收到IN令牌后立刻拉高tx_active把FIFO预装载的数据按字节依次发送最后补上CRC16和EOP然后等主机的ACK握手包。如果收到NAK说明主机没能接收数据端点状态要保留等下一次IN令牌到来时重发。3.3 FIFO缓冲与跨时钟域设计USB控制器一般工作在48MHz时钟域而用户应用逻辑可能跑在100MHz、150MHz甚至不同的时钟域。这种情况下必须在交接处加FIFO缓冲一方面解决跨时钟域CDC数据同步的亚稳态问题另一方面也可以平滑USB突发传输和应用逻辑处理速率不匹配的问题。端点数不多的应用我倾向于直接例化FPGA厂商提供的FIFO IP核。Xilinx平台的xpm_fifo是一个很好的选择它支持原生First-Word Fall-Through模式读写时钟独立逻辑资源消耗也小。过去我自己手写过同步FIFO遇到异步FIFO时很容易在指针跨时钟比较上出问题后来干脆改用原语IP核稳定性提升明显。如果你也想手写务必注意读写指针的格雷码转换和两级同步器处理这是异步FIFO的关键。FIFO深度建议根据端点类型来定。全速USB一个USB帧是1ms批量传输一次最多可以是64字节。所以端点FIFO做到64字节基本够用但如果应用逻辑处理数据不够快FIFO会频繁溢出这时候需要设计背压机制FIFO满时暂停接收或者直接NAK主机让主机稍后重试。这个NAK重试机制是USB协议自带的流控手段用起来非常方便。4. 仿真验证与调试实录4.1 Icarus Verilog仿真环境搭建FPGA开发最常见的仿真工具是Vivado自带的XSim或ModelSim但我个人更推荐用Icarus Verilogiverilog做前期功能验证。它是免费开源的命令行工具启动速度快适合自动化跑testbench而且不依赖GUI可以直接在Makefile里集成回归测试改完代码立刻跑一遍仿真效率比大型IDE高得多。安装Icarus Verilog非常简单Windows上可以直接下载安装包Linux下用包管理器一行命令就能装好。编译仿真指令核心就两条iverilog -o usb_tb.vvp usb_top.v usb_phy_rx.v usb_link.v usb_ep0.v usb_tb.v vvp usb_tb.vvp跑完后会生成VCD波形文件用GTKWave打开即可查看各信号时序。GTKWave虽然界面上比不上ModelSim但看协议时序绰绰有余。我的testbench里模拟了一个USB主机端的收发器可以按协议时序向被测DUT发送SYNC、令牌包、数据包然后检查DUT发出的握手包是否正确。USB协议交互不少testbench写起来比常规设计代码还要费心思。建议把包发送封装成task函数比如send_token_packetIN、send_data_packetDATA0这样写测试用例会非常直观。4.2 常见问题与排查技巧这个项目里我遇到过不少问题挑几个典型的分享出来按经验排序现象可能原因排查方法PC提示“无法识别的USB设备”D上拉电阻未接或接错线检查1.5kΩ上拉是否接到3.3V测量D电平枚举中途失败描述符读不出来描述符长度字段或CRC16计算错误用USB分析仪或仿真波形核对每个字节能识别设备但无法收发数据端点FIFO读写时序不对重点检查FIFO空满信号和读使能的时序关系仿真正常、上板不工作复位信号未做异步复位同步释放增加两级同步器统一复位时序偶尔能枚举成功经常失败链路层响应超时统计从令牌包到握手包之间的时钟周期碰到枚举类问题建议条件允许的话用USB抓包工具比如线材式的USBCapture或者PC上的Wireshark配合usbmon看总线信号。很多开发者习惯一上来调代码其实协议问题用抓包工具看一遍总线交互问题定位会快得多。如果没有抓包工具用仿真testbench在关键节点打日志同样有效——重点观察枚举阶段的第一次GET_DESCRIPTOR请求主机那次是只取8字节还是全量取回这是很多FPGA实现容易遗漏的地方。还有一个细节HID设备的报告描述符必须用USAGE_PAGE等条目正确描述用法否则Windows虽然能识别设备但HID API拿到的usage信息会不对导致上层应用收发不到数据。报告描述符我建议用HID Descriptor Tool生成然后嵌入Verilog ROM不要手工写字节。5. 上板验证与扩展方向仿真全部通过后上板验证最容易忽略的是管脚约束和电气特性。D和D-建议约束到FPGA的差分或普通IO同时要做串联电阻匹配。全速模式下USB规定的信号线特性阻抗是90Ω很多开发板会在D、D-上串联22Ω~33Ω电阻防止信号反射。如果完全不加匹配电阻信号眼图会很差接近线缆长度上限时甚至无法通信。还有一个很多国产开发板容易踩的坑FPGA GPIO的电压域。USB全速信号电平是3.3V但如果你把D引脚约束在1.8V bank上电平不匹配会导致主机识别不到设备。做之前务必确认FPGA bank的VCCO供电用错bank的话要么换引脚要么加电平转换芯片。做完这个全速HID设备之后项目还可以往几个方向扩展。一个是改成自定义vendor设备走批量传输端点把USB吞吐量跑满12Mbps这时候你会发现全速带宽其实也够用单次64字节批量传输外加双缓冲实测可以到1MB/s左右。另一个是升级到USB高速也就是把物理层换成外部ULPI PHY芯片链路层代码基本可以复用。再复杂一点可以在FPGA里实现USB Host控制器直接去读U盘文件系统或者驱动USB摄像头——这属于另一个量级的项目了。根据我这次实现的经验核心还是要把协议拆成可验证的模块物理层、链路层、协议层逐层搞定每一步都用仿真确认无误之后再往上叠。USB协议细节多但只要架构清晰Verilog写起来其实一路畅通并没有想象中那么难。如果你正在犹豫要不要动手我的建议是先从全速HID设备开始跑通枚举那一下的感觉真的很值。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进