ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FPGA+Verilog实现UDP千兆传输:从协议解析到上板联调

FPGA+Verilog实现UDP千兆传输:从协议解析到上板联调 简介面向FPGA开发者的UDP协议硬件实现工程包基于Verilog在Quartus环境下完成以太网UDP数据发送设计适用于高速实时数据传输、网络测试设备及嵌入式系统等场景的原型开发。工程按照UDP包处理、IP接口、MAC接口、CRC校验和控制逻辑等模块划分源码结构清晰便于移植和二次开发同时涉及时钟同步、乒乓缓冲、错误检测等工程化考虑具有一定参考价值。压缩包共113个文件大小约4MB以.v源文件、.qsf/.qpf工程配置和编译生成的.sof配置文件为主其余多为Quartus编译过程产生的.cdb/.ddb/.rdb等中间文件可帮助整体理解工程构建流程。目前已有964人学习下载。通过阅读源码可对照学习UDP头部字段构造、IP层封装、MAC帧收发、校验和计算等关键环节并可直接打开Quartus工程进行综合、布局布线与上板验证帮助快速上手UDP硬件实现。 UDP、Verilog、FPGA这三个词放在一起基本就是一个高速网络通信项目的核心拼图。我最初做FPGA网络传输是被图像采集项目逼出来的——之前用UART传一帧640x480的RAW图要好几秒换成UDP之后同样一帧数据不到一毫秒就出去了这个差距直接决定了方案选型。这个项目表面上是“用Verilog在FPGA上实现UDP协议收发”实际上走完一遍你会发现状态机设计、CRC校验、跨时钟域处理、时序约束这些FPGA基本功全部被串了起来。如果你正准备做数据采集回传、图像传输或者想从UART/SPI这类低速接口往千兆以太网迈一步这篇文章差不多能把从RTL设计到上板联调的完整路径捋清楚。1. 方案选型与整体数据通路为什么UDP在FPGA里最吃香1.1 从TCP复杂度对比看UDP优势在FPGA上做网络通信第一个需要想清楚的问题不是怎么写代码而是选UDP还是TCP。TCP的可靠传输依赖连接管理、序号确认、超时重传、滑动窗口这些机制在CPU上跑本身就不轻放到FPGA里意味着要维护一大堆状态和定时器逻辑资源和RAM开销都很大。UDP则完全是另一回事无连接不用握手不用应答把头部填好、CRC算对、按帧发出去就算完成。对大部分实时数据采集和图像传输场景丢几帧可以接受但延迟必须低UDP天然贴合这种需求。实测下来千兆以太网用UDP可以稳定跑到900Mbps以上而同样硬件资源做TCP不仅要为重传缓存消耗大量BRAM吞吐还容易受RTT影响。很多FPGA网卡开源项目比如Corundum这类也都是以UDP为主做数据传输面TCP只留给CPU去做控制面。所以结论很直接除非业务明确要求可靠有序传输否则FPGA侧首选UDP。做这个项目之前先把这个选型逻辑确认下来后面所有设计都是为了服务低延迟、高吞吐这两个目标。1.2 从业务逻辑到PC网卡的完整数据通路选定了UDP接下来要清楚数据到底怎么从FPGA业务逻辑走到PC网卡。整体链路大概是这样的用户业务逻辑先把要发送的数据写入发送FIFOUDP发送模块按固定时序把FIFO里的数据读出来依次封装上UDP头、IP头、MAC头同时算好IP校验和与UDP校验和再经过MAC层的CRC32校验模块生成FCS字段最后通过RGMII接口把数据送到板载PHY芯片PHY完成编码和电平转换后经网络变压器和RJ45出去PC端网卡收包后由上位机软件接收。接收方向正好反过来RGMII进来的字节流先被MAC接收模块解析剥离MAC头后判断EtherType再进IP层解析、校验最后在UDP层做端口匹配把payload写入接收FIFO交给业务逻辑。两个方向都有几个躲不开的关键点一是用户业务时钟和以太网时钟往往不是一个频率必须用异步FIFO跨时钟域二是RGMII是DDR采样125MHz时钟下数据率就是1000Mbps采样沿和时钟偏斜处理不好会直接导致数据错位三是CRC32的覆盖范围必须算对否则PC端抓包全是坏帧。这一整条通路在设计阶段就要在脑子里跑通后面写RTL才有章法。2. 帧格式拆解与校验计算避开字节序和CRC的坑2.1 UDP over IPv4报文格式速查写UDP协议栈之前必须把帧格式背到滚瓜烂熟很多人调不通问题就出在字段错位或长度算错上。一个完整的UDP over IPv4以太网帧从PHY角度看依次是前导码和帧起始定界符8字节大多数PHY芯片会自动生成FPGA侧不用管、目的MAC6字节、源MAC6字节、EtherType2字节IPv4写0x0800、IPv4头20字节、UDP头8字节、用户payload、FCS4字节CRC32。各字段的核心信息整理如下表字段长度关键内容FPGA侧注意点前导码SFD8B0x55...0x55 0xD5一般由PHY自动添加目的MAC6B接收方网卡MAC发到PC必须填PC网卡MAC或广播地址源MAC6BFPGA板卡MAC自定义即可别和局域网内设备冲突EtherType2BIPv4为0x0800网络序大端字节不能写反IPv4头20B版本/IHL、总长度、TTL、协议号、头校验和、源/目的IP协议号TCP是6、UDP是17UDP头8B源端口、目的端口、长度、校验和长度是8payload不是只算数据payload0~1472B用户数据受MTU限制千兆以太网标准MTU是1500FCS4BCRC32校验值计算范围从目的MAC到payload结束这里有个特别容易踩的坑以太网最小帧长是64字节从目的MAC开始算也就是说当UDP payload不足18字节时必须在payload后面补0填充到满足最小帧长但UDP长度字段和IP总长度字段里不能把这部分填充算进去否则接收端解析出的长度和实际帧长度对不上包会被直接丢掉。另一个坑是帧间隙IFG相邻两帧之间至少要留96bit12字节的空闲时间很多PHY芯片对IFG过短的帧会丢弃或者合并处理FPGA发帧太急反而会丢包。2.2 IP校验和、UDP校验和的FPGA实现思路IP头校验和在IPv4里是必算的PC端网卡收到IP头校验失败的包会直接丢弃。计算方法是把IP头按16位一组做反码求和再把结果取反填入校验和字段。FPGA实现时注意两点一是发送时校验和字段先填0再计算二是校验和覆盖范围只有20字节IP头不包含payload所以计算量很小用组合逻辑或者几个周期的时序逻辑就能完成。UDP校验和实现上有两种选择。IPv4协议允许UDP校验和为0表示不校验很多简单实现就直接填0PC端默认也不会报错。但严谨一点的做法还是计算因为某些防火墙或抓包工具会把校验和为0的UDP包标记为异常而且如果你后面要跑iperf3这类工具它默认会对校验和敏感。UDP校验和的计算范围比较特殊除了UDP头和数据还要加上一个12字节的伪首部源IP、目的IP、保留0、协议号17、UDP长度。这个伪首部只参与校验和计算不实际发送。如果payload是连续数据流每个包都要完整扫描一遍所有16位字做反码求和会占用不少发送带宽。实际工程里可以用增量更新incremental update来优化当只改了某个字段比如序号或时间戳时不需要重算整个校验和只在旧校验和基础上减去旧值、加上新值就可以了。公式是new_sum old_sum - old_word new_word按16位反码运算在FPGA里用几个加法和取反就能完成能省下大量组合逻辑和流水级数。我第一次做的时候偷懒没做增量更新结果发送速率一高校验和计算成了关键路径时钟频率上不去后来改成增量更新才解决。3. RTL实现要点发送状态机、接收解析与FIFO缓冲3.1 发送侧状态机与最小帧填充发送侧的核心是一个字节级的状态机控制帧的每个阶段。我用的是这种状态划分IDLE空闲、MAC_HDR发MAC头和EtherType、IP_HDR发20字节IP头、UDP_HDR发8字节UDP头、DATA发payload、PAD补零到最小帧长、FCS发4字节CRC、IFG帧间隙。每个状态用计数器控制字节数状态转移条件是“当前段计数结束”。简化后的Verilog状态机结构如下localparam S_IDLE 3d0; localparam S_MAC 3d1; localparam S_IP 3d2; localparam S_UDP 3d3; localparam S_DATA 3d4; localparam S_PAD 3d5; localparam S_FCS 3d6; localparam S_IFG 3d7; always (posedge clk_tx or posedge rst) begin if (rst) begin state S_IDLE; end else begin case (state) S_IDLE: if (tx_fifo_empty 1b0) state S_MAC; S_MAC: if (cnt_mac MAC_HDR_LEN) state S_IP; // 其他状态类似计数器到终点后跳转 endcase end end发送状态机里最需要小心的是时序配合。CRC32模块要跟数据并行工作数据发送的同时把当前帧的所有字节送入CRC计算DATA或者PAD阶段结束后立刻把CRC结果按高字节在前输出到RGMII。如果CRC模块比数据慢一拍FCS就会晚一个周期可能吞掉下一个帧的第一个字节。我习惯把CRC模块做成全并行的组合逻辑输出端打一拍寄存状态机进入FCS状态前提前一拍启动保证数据流不中断。另外一个容易忽略的点是发送启动时机。发送FIFO的读使能不能在IDLE就拉高因为FIFO非空不代表已经攒够一整包数据如果业务逻辑按“包”写入而不是“字节流”写入一包数据还没写完状态机就开始发帧头发到DATA阶段发现FIFO空了就会产生欠载underrun输出中间断了PC端收到一个残帧直接丢弃。稳妥做法是FIFO里增加“包结束标记”或者至少预留一个“包长度寄存器”状态机等到确认完整数据包就绪后再启动。3.2 接收侧解析与帧过滤接收侧比发送侧更考验细节因为进来的是一串连续的字节流你得自己找帧边界、判断帧类型、提取有效数据。我的做法是把接收通路拆成两级流水第一级是RGMII字节对齐和帧起始检测第二级是MAC/IP/UDP逐层解析。帧起始检测关注的是前导码。如果PHY不带前导码剥离功能你会先收到0x55的前导码和0xD5的SFD检测到SFD之后再开始按14字节数MAC头。很多FPGA的RGMII实现里PHY默认会自动剥离前导码这时候直接按目的MAC开始解析就行具体行为要看PHY芯片的寄存器配置。解析过程中要做的过滤逻辑按照从外到内的顺序来目的MAC不匹配本机MAC且不是广播地址就丢弃EtherType不是0x0800就丢弃如果是ARP请求可能要交给ARP模块响应但精简UDP实现可以先不理IPv4头的协议号不是17就丢弃目的IP不匹配本机IP就丢弃UDP目的端口不匹配业务端口就丢弃。这些过滤条件看着多实际每个都比较简单用一组比较器加上状态计数就能实现。比较关键的是payload长度的提取。UDP头里有UDP长度字段等于8加payload长度但实际帧里可能有填充字节所以不能直接靠UDP长度去精确切分帧应该用IP头里的“总长度”字段减去IP头长度得到UDP数据报长度再减去8得到真正payload长度。接收状态机按这个长度精确计数多出来的填充字节直接忽略这样解析出来的payload才不会带着一堆0x00尾巴。接收侧的CRC校验也不要省虽然很多简单实现不看FCS但一旦出现CRC错误帧你无法判断是链路噪声还是自己的逻辑问题上板调试时会非常痛苦。3.3 异步FIFO与跨时钟域处理跨时钟域处理是整个工程的隐形难点。用户业务逻辑的时钟可能是100MHz、75MHz或者图像传感器的Pixel Clock而1000M以太网的GTX时钟是125MHzRGMII模式两个时钟完全不同步直接连寄存器会采到亚稳态。标准做法是中间插入异步FIFO发送方向业务逻辑写、UDP发送模块读接收方向UDP接收模块写、业务逻辑读。FIFO深度怎么选我的经验是按“最大突发包长×时钟差比例余量”来估算。举个例子一包最大1400字节写入时钟100MHz、读时钟125MHz如果业务侧一次性突发写入一整包读侧要以更高的速率把它读走问题不大倒是反过来写入速率高于读出速率时FIFO必须能装下连续多包的差值。一般保守选择至少能缓存2~4个最大包比如512或1024深度宽度按数据位宽来RGMII常用8位内部可以用32位打平。实际工程中因为FIFO几乎不会到满状态更多是为了兜住突发深度选大了没坏处选小了就是疑难杂症。异步FIFO的位宽转换也要提前规划好。RGMII接口上数据是8位DDR而用户数据可能是32位总线如果直接按8位存、32位读读侧要自己拼字节序很容易出错。我建议在MAC层内部统一使用32位数据通路RGMII收发两端做8/32位转换这样到FIFO时已经是规整的32位业务侧处理起来省心很多。字节序问题要在同一个地方集中处理千万别今天在MAC层转一下、明天在UDP层又转一下调试的时候会疯。4. 上板联调从Wireshark抓包到iperf3打流4.1 先用固定报文验证帧格式代码写完上板第一件事不是测吞吐而是先验证帧格式对不对。最简单的方法FPGA端做一个小模块循环发送一个固定内容的UDP包比如源IP 192.168.1.10、目的IP 192.168.1.100、目的端口5000、payload是递增数或者Hello FPGA然后PC端用Wireshark抓包。Wireshark打开后选择对应网卡在过滤栏输入udp.port 5000立刻就能看到FPGA发来的包。此时重点看几个信息帧头的MAC地址是否和预期一致IP头的总长度、源/目的IP是否正确UDP头的源/目的端口、长度对不对payload内容是否和FPGA里写的一样。如果Wireshark里帧显示红色或者标注bad checksum说明CRC32或者校验和字段有问题优先查发送状态机的字段填充逻辑和CRC覆盖范围。如果完全抓不到包就要分层次排查先看PHY芯片的link状态网线插上后PHY的Link灯亮不亮RGMII的时钟有没有正常产生再用ILA抓RGMII的tx_ctl和txd信号确认FPGA这边到底有没有波形输出。我遇到过一种情况逻辑仿真完全正常上板后Wireshark啥也抓不到最后发现是PHY芯片的复位时序不对PHY一直没完成初始化RGMII时钟根本没有输出。这种问题仿真里很难暴露必须先确认PHY寄存器可以正常读写。在Windows和WSL2之间联调也可以在这个阶段用起来。WSL2里的Ubuntu可以直接装iperf3和tcpdump和Windows宿主共享物理网卡需要开启WSL2的镜像网络模式或使用端口转发这样你可以一边在Windows里用Wireshark抓包一边在WSL2里跑工具发送和接收UDP一套环境把调试和验证全包了。4.2 用iperf3实测吞吐与丢包率固定包验证没问题后下一步就是性能测试。iperf3是最常用的打流工具PC端先起服务端iperf3 -u -sFPGA端如果作为发送源需要让FPGA持续以一定的包长和速率发送UDP数据。比如每包1400字节、以一定速率连续发然后PC端iperf3服务端会统计收到的带宽和丢包率。反过来也可以PC端用iperf3作为客户端向FPGA发送数据iperf3 -u -c 192.168.1.10 -b 500M -t 30 -l 1400这条命令表示向FPGA发送UDP流目标带宽500Mbps持续30秒每包1400字节。FPGA接收侧统计收到的包数和字节数就能知道实际接收带宽和丢包情况。实测中我的经验是在千兆RGMII下FPGA单方向跑到900Mbps以上是很正常的这时CPU端iperf3的统计会受到网卡中断和调度的影响丢包率在0.1%以内基本可以接受。如果丢包率明显偏高先不要怀疑网络拥塞大概率是FPGA侧问题发送侧FIFO欠载会导致帧中间断裂、接收侧FIFO溢出会导致来不及写入的包被丢掉、CRC错误帧被接收侧主动丢弃也会表现为丢包。所以性能有问题时FPGA内部的收发包计数器是你的第一手证据把“收到多少帧、CRC错多少帧、FIFO满多少次”这些计数通过UART或者ILA读出来定位就快了。需要注意的是iperf3的UDP模式默认会校验序列号丢包率会直接反映在结果里非常直观。如果你在WSL2的Ubuntu里跑iperf3观察到的带宽和丢包可能和Windows原生iperf3有细微差别这属于不同系统网络协议栈的差异对比测试时固定在同一端环境就好。4.3 常见问题速查与调试心得把调试过程中常见的现象和原因整理成一张表方便排查时快速对照现象可能原因排查方向PC完全收不到包PHY未初始化/RGMII时钟异常/目的MAC或IP不匹配查PHY寄存器、ILA抓RGMII信号、核对帧头字段Wireshark报CRC错误FCS覆盖范围不对/CRC字节序颠倒/PHY自动加FCS干扰确认CRC计算范围、检查FCS输出字节序、查PHY寄存器配置能收到包但payload是乱的RGMII采样沿不对/跨时钟域未同步/字节序转换错误检查input_delay约束、采样时钟沿、32位拼接顺序吞吐量明显偏低FIFO欠载/IFG过长/发送时钟未跑满查看FIFO空标志拉高频率、精简IFG等待周期丢包率高发送FIFO溢出/接收FIFO不足/CRC错帧被丢增大FIFO深度、检查帧间隔、读接收侧错误计数器ILA抓不到内部信号ILA时钟域选错/触发条件不满足使用RXCLK或TXCLK作为ILA时钟检查触发条件调试过程中积累的几个心得值得单独说一下。第一RGMII的input_delay约束一定要写。RGMII协议要求接收数据相对于时钟有大约1.5~2ns的偏斜如果不告诉工具这个延迟布局布线工具可能会按零偏斜去优化导致采样沿正好落在数据跳变点上。这个坑特别隐蔽表现为仿真全对、上板偶发错字节改成随机错最后加一条约束就稳定了。具体延迟值可以查PHY芯片手册很多PHY的RX delay也可以通过寄存器调整。第二ILA的采样时钟必须用RGMII对应的时钟域。发送侧用clk_tx125MHz接收侧务必用PHY恢复出来的clk_rx不要图省事统一用系统时钟去采否则采回来的数据在跨时钟域边缘本来就是不确定的你会看到一堆乱码误判成逻辑bug。第三板级调试一定要靠计数器说话。在发送侧和接收侧各放几个计数器发送帧数、发送字节数、FIFO欠载次数接收帧数、CRC错误帧数、FIFO溢出次数。所有现象最终都能落到某个计数器异常上比对着波形猜快得多。把计数器挂在ILA里或者通过UART打印出来整个联调从“盲人摸象”变成“定点打击”。这个项目做完之后还有一个很大的收获是整套UDP通路也可以作为更复杂网络栈的基础比如在此基础上加ARP响应、加DHCP、加更细粒度的发送调度或者把数据通路接到DMA引擎上做高性能数据采集。但要提醒的是如果想把协议栈做完整千万不要一头扎进去自己从头写所有模块先去看看Corundum这类开源FPGA网卡项目的实现思路它把PCIe、DMA、多队列、协议卸载都做了参考价值非常大。从一个小而精的UDP通路开始逐步往高性能方向走这条路我自己走下来是越走越顺的。本文还有配套的精品资源点击获取
返回列表