FPGA实现UDP协议栈:基于verilog-ethernet的千兆回环工程详解
2026/9/15 3:13:40 网站建设 项目流程

1. 为什么非要跟FPGA较劲UDP协议栈

1.1 什么场景下需要FPGA直接收发UDP

先交代一下背景。这个系列走到第10篇,前面的内容基本都在打地基:GPIO、UART、数码管、PLL、FIFO这些,说白了都是在芯片内部或者低速接口上折腾。到了网口这一块,很多人会觉得"FPGA搞以太网是不是有点过度设计?ARM上跑个lwIP不香吗?"我自己一开始也是这个想法,直到真的遇到了下面两类场景才改观。

第一类是高速数据采集。ADC采样率一旦上了几百兆,每秒产生的数据量就是几百MB甚至上GB,ARM的中断处理能力根本扛不住。而FPGA可以在采集端直接用状态机把数据打包成UDP报文,通过千兆网口实时送出去,CPU只需要在电脑端收包、存盘、做后处理。第二类是低延迟控制链路。比如一些仪器仪表、运动控制设备,要求从传感器到上位机的延迟控制在微秒级,跑协议栈的CPU因为操作系统调度和中断延迟,很难做到稳定低延迟。FPGA用纯硬件逻辑管线处理报文,延迟是可确定的,这在很多工控现场就是硬需求。

我自己做这个项目,其实是为了给一块高速ADC板卡配一个上位机接口,数据量大约是持续400Mbps左右。ARM方案算了一下,中断加上协议栈开销,跑到100Mbps就开始丢包了,所以才下决心在FPGA里把UDP这条链路打通。这篇文章就把我学习verilog-ethernet这个开源工程的整个过程、踩过的坑、以及最终跑通的最小回环工程完整记录下来,给后面想走同一条路的同学一个参考。

1.2 为什么选verilog-ethernet,而不是lwIP或自己写

选型的时候其实纠结过一阵子。lwIP是软件协议栈,跑在CPU上;verilog-ethernet是纯RTL实现的MAC和UDP/IP协议栈,直接在FPGA逻辑里完成链路层和网络层的处理。两者定位完全不同,选谁取决于你的场景。如果要跑TCP、要复杂的连接管理、要跑HTTP这类应用层协议,那老老实实用软核+lwIP;但如果是UDP简单收发、追求吞吐和低延迟,FPGA里直接做协议栈明显更合适。

verilog-ethernet这个工程在GitHub上由Alex Forencich维护,大家可以直接搜到。它并不是一个单一模块,而是一整套以太网组件库,从MAC层到IP、UDP、ARP、DDR3接口都有,而且是BSD风格的开源许可,商用也友好。我选它的理由有三个:一是接口用了标准的AXI-Stream,跟我前几篇学的FIFO、AXI总线知识能无缝衔接;二是参数化做得很好,可以只挑需要的模块出来用;三是有配套的testbench和文档,对新手比对着一堆时序图自己猜要友好得多。

当然也有人问,为什么不自己写一个UDP发送模块?我的建议是MAC层真的别自己写。以太网MAC涉及CRC32、前导码、冲突检测(半双工模式下)、帧间隙这些细节,自己从零写很容易写出一堆边界case的bug,而MAC层一旦出错,抓包看到的都是垃圾帧,排查起来非常痛苦。verilog-ethernet把MAC层封装好了,你的核心精力可以放在怎么组织数据、怎么对接自己的业务逻辑上。

2. 先搞懂UDP从网线到FPGA的一条路

2.1 帧格式:从MAC帧到UDP净荷

很多同学上来就把UDP协议栈当成一个黑盒子,直接按时序把数据塞进去就以为完事了。这样短时间可能能跑通,但一旦出了问题,你连抓包结果都看不懂。所以我建议先花半天时间把帧格式彻底搞清楚,后面所有调试都会顺畅很多。

一条完整的UDP报文从物理层到应用层依次是:前导码(8字节,用于时钟同步)、MAC目的地址(6字节)、MAC源地址(6字节)、可选VLAN标签(4字节)、以太网类型字段(2字节,IPv4是0x0800)、IP头部(标准20字节)、UDP头部(8字节)、UDP数据载荷、FCS校验(4字节CRC32,一般由MAC层硬件生成和校验)。

在verilog-ethernet里,MAC层处理的是从以太网类型字段之后到FCS之前的所有内容。如果是标准IPv4 UDP报文,MAC层把以太网类型识别为0x0800后,会把整帧IP数据包交给上层;反过来发送时,MAC层接收一个完整的IP数据包,自动加上前导码、MAC地址和CRC后从PHY发出去。

IP头部里面有几个字段要特别留意:源/目的IP地址、协议字段(UDP是17)、总长度、头校验和。IP头校验和不是可选项,路由器或PC网卡收到后都会做校验,错了直接丢弃。UDP头部则是源端口、目的端口、长度(UDP头和载荷的长度,8字节头+载荷,最小是8)、校验和。

我做loopback回环的时候,为了让PC端能正确识别数据,源IP、目的IP、源MAC、目的MAC这些字段都要认真填。最简单的做法是让源MAC和源IP设成FPGA板卡的固定值,目的MAC和目的IP设成PC网卡的值,端口自己约定一个比如8080。

2.2 校验和:唯一需要动手算的东西

verilog-ethernet这个工程里,MAC层和IP层的大部分工作都是自动完成的,唯一需要用户理解透的就是IP和UDP校验和。我当时在这里卡了差不多一整天,最后发现是UDP校验和的伪头部算错了。

IP头校验和的计算方法:把IP头按16位一组累加,如果累加过程中产生进位就把进位回卷到低位继续加,最终结果取反。算法本身很简单,但在Verilog里写的时候要防止溢出,代码写出来大概长这样:

// 简化的IP头校验和计算 function [15:0] ip_checksum; input [159:0] header; // 20字节IP头 integer i; reg [31:0] sum; begin sum = 0; for (i = 0; i < 10; i = i + 1) begin sum = sum + header[i*16 +: 16]; if (sum[31:16] != 0) begin sum = {16'b0, sum[15:0]} + {16'b0, sum[31:16]}; end end ip_checksum = ~sum[15:0]; end endfunction

UDP校验和比IP头校验和多一个伪头部(pseudo header),它不是UDP报文的一部分,只是在计算校验和时临时拼出来的12字节:源IP(4字节)、目的IP(4字节)、协议号(1字节,UDP为17,补1字节0)、UDP长度(2字节)。过程是把伪头部 + UDP头 + UDP数据一起按16位累加取反。IPv4下UDP校验和是可以省略的(置0即可),但不推荐,特别是在跨网段传输时,部分路由器会对UDP校验和做检查,丢了很冤。

我在实际工程里直接把UDP校验和也做了,因为verilog-ethernet里提供了eth_udp_tx模块的校验和选项,设置为自动计算就行。但理解原理仍然有必要——万一你拿到一个别人写的简化版代码,它把校验和跳过甚至算错了,你就要能一眼看出来问题在哪。

2.3 跨层看:verilog-ethernet的模块地图

verilog-ethernet整个工程工程目录很大,初学者进去容易懵。我建议不要从头到尾读代码,而是先找到自己路径上的几个关键模块。以最常用的1G RGMII接口为例,数据通路是这样的:

eth_mac_1g_rgmii_fifo(最外层,自带FIFO缓冲的完整MAC)→eth_mac_1g(纯MAC核心)→ RGMII接口信号连线到PHY芯片。

在MAC之上,用户数据进出发送/接收通路分别是eth_udp_txeth_udp_rxeth_udp_tx负责把你要发的UDP数据从AXI-Stream接口收进来,自动封装UDP头、IP头,然后通过m_axis_ip_tx接口把完整的IP包发给MAC层;eth_udp_rx则是从MAC层收到IP包,解析出UDP层数据,通过s_axis_ip_rx接口收进来,然后通过AXI-Stream接口把纯载荷数据吐出去。

我最初以为要先控制MAC层再自己处理IP和UDP,结果发现有了eth_udp_tx/rx这两个模块之后,IP和UDP层的组包拆包都被封装好了,我只需要关心三件事:一是AXI-Stream接口的时序(tvalid/tready/tlast/tdata怎么配合),二是配置端口参数和IP地址参数,三是怎么把RX通路和TX通路接到自己的业务逻辑上。

模块关系我用一个简表列出来,方便大家对照源码理解:

模块名作用用户需要关注的接口
eth_udp_tx封装UDP/IP头并发送s_axis_udp_tx (AXI-Stream输入)
eth_udp_rx解析UDP/IP头并接收m_axis_udp_rx (AXI-Stream输出)
eth_ip_tx / eth_ip_rx封装/解析IP头由udp模块自动连接
eth_mac_1g_rgmii_fifoMAC层+RGMII+内部FIFOgtx_clk、rgmii接口
axis_fifoAXI-Stream FIFO缓存数据缓冲、跨时钟域

3. 把verilog-ethernet跑起来:最小回环工程

3.1 工程结构准备与文件清单

我第一次用这个工程的时候犯了一个错误:直接把整个GitHub仓库全部Add到Vivado工程里,结果编译报了几十个错,查了半天发现是有些模块面向其他平台或者依赖特定IP核。正确做法是先看example目录,找到最接近你板卡的示例工程,然后只添加需要的文件。

以我用的Xilinx Artix-7 35T板卡、RGMII接口为例,需要添加的核心文件大概是这些:eth_mac_1g_rgmii_fifo.veth_mac_1g.veth_mac_1g_rgmii.veth_udp_tx.veth_udp_rx.veth_ip_tx.veth_ip_rx.veth_axis_tx.veth_axis_rx.vaxis_fifo.veth_clock_generator.vlfsr.v,以及reset_sync.v这类公共模块。如果代码里用到MIIM(MDIO管理接口),还需要eth_mdio.v

确定文件依赖最快的方式是打开example工程里的.xpr.tcl脚本,看它包含了哪些源文件。我当时的做法是先把整个仓库同步下来,然后新建空白工程,用一个空的顶层模块,把所有verilog-ethernet的.v文件一股脑加进去,编译看报错,根据报错逐个排除不需要的模块。这个方法笨但有效,能让你对模块依赖有一个直观感受。

另外要特别说一下IP核的问题。eth_mac_1g_rgmii_fifo内部如果配置成使用Xilinx原生三速MAC软核,就会依赖Xilinx的tri_mode_eth_macIP,这是需要单独生成并添加到工程的。但如果直接用verilog-ethernet自带的纯RTL MAC实现,就不需要额外生成IP核。默认配置下一般用的是自带RTL实现,这个对新手更友好,不会突然冒出一堆IP生成窗口。

3.2 RGMII接口与时钟:最容易翻车的地方

RGMII接口是Reduced GMII的缩写,数据线从8位减到4位,时钟从单沿采样改成DDR双沿采样。在125MHz时钟下,DDR模式等效数据率是1000Mbps,也就是千兆网。RGMII的发送时钟由FPGA产生并送给PHY,接收时钟由PHY恢复后送给FPGA,两者都是125MHz。

在Vivado里做RGMII约束时,有两个容易踩的坑。第一,RGMII的接收时钟rgmii_rxc是要作为时钟信号进FPGA的,必须用create_clock约束,并且在他进入逻辑之前最好经过BUFG或BUFIO;第二,RGMII的数据线和控制线相对于时钟有大约2ns的延迟(PCB上一般有等长约束,但依然会有偏差),所以需要做input delay约束。很多板卡的参考设计里直接用了IDELAYE2对输入数据做可调延迟,这套东西配置起来很麻烦,但效果也确实最稳。

我自己的做法是先用板卡厂商提供的参考设计跑通一遍,确认PHY芯片的寄存器配置和时钟约束没问题,然后再替换成自己的逻辑。如果你手里的板卡没有现成参考设计,至少要把rgmii_rxc的时钟约束写对,例如:

create_clock -period 8.000 -name rgmii_rxc [get_ports rgmii_rxc] set_input_delay -clock rgmii_rxc -max 2.000 [get_ports {rgmii_rxd[*] rgmii_rx_ctl}] set_input_delay -clock rgmii_rxc -min 0.500 [get_ports {rgmii_rxd[*] rgmii_rx_ctl}]

这里period 8.000对应125MHz。具体延迟值要参考PHY芯片数据手册和PCB走线长度来定,我这边演示用的值存在一定经验成分,实际项目里需要通过scan测试确定最优延迟。

3.3 顶层代码:把RX和TX用FIFO串起来

最小回环工程的目标很朴素:PC发一个UDP包给FPGA,FPGA把包里的数据原封不动再发回给PC。这样可以验证整条链路通不通,而不涉及其它业务逻辑。

我用的顶层结构很简单:eth_udp_rx收到的AXI-Stream数据直接进一个axis_fifo,FIFO输出端接eth_udp_tx的输入端。这样做的原因是RX和TX两侧的tvalid/tready节奏不同,不能直接硬连,必须加缓冲。关键连线代码大概是这样:

// 简化示意,时钟复位和参数省略 axis_fifo #( .DATA_WIDTH(8), .ADDR_WIDTH(10) ) udp_loopback_fifo ( .clk(clk_125m), .rst(rst), .s_axis_tdata(rx_udp_tdata), .s_axis_tvalid(rx_udp_tvalid), .s_axis_tlast(rx_udp_tlast), .s_axis_tready(rx_udp_tready), .m_axis_tdata(tx_udp_tdata), .m_axis_tvalid(tx_udp_tvalid), .m_axis_tlast(tx_udp_tlast), .m_axis_tready(tx_udp_tready) ); eth_udp_tx #( .TARGET_IP_ADDR({8'd192, 8'd168, 8'd1, 8'd10}), .TARGET_MAC_ADDR(48'h00_11_22_33_44_55), .SOURCE_IP_ADDR({8'd192, 8'd168, 8'd1, 8'd20}), .SOURCE_MAC_ADDR(48'hAA_BB_CC_DD_EE_FF), .UDP_SOURCE_PORT(16'd8080), .UDP_DESTINATION_PORT(16'd8080) ) u_udp_tx ( .clk(clk_125m), .rst(rst), .s_axis_udp_tdata(tx_udp_tdata), .s_axis_udp_tvalid(tx_udp_tvalid), .s_axis_udp_tlast(tx_udp_tlast), .s_axis_udp_tready(tx_udp_tready), // 其余信号连到MAC层 );

注意eth_udp_tx源地址和目的地址都做成了模块参数,编译后直接固化,不能在运行时更改。如果你要做动态修改,需要走它的AXI4-Lite配置接口,这属于后面进阶的玩法,回环demo阶段用参数就够了。还有一个细节:eth_udp_tx支持发送侧自动添加UDP长度、IP长度、校验和,但这些功能对应的参数要打开,否则发出去的包可能缺字段。

3.4 用网络调试助手和Wireshark验证回环

硬件工程编译烧录之后,就到了最兴奋也最容易失望的联调环节。我在Windows电脑上把IP地址设为192.168.1.10,子网掩码255.255.255.0,用网线直连FPGA板卡,然后打开两个工具:一个网络调试助手负责发UDP数据包,一个Wireshark负责抓包。

网络调试助手选UDP模式,设置目标IP为FPGA的IP(我设为192.168.1.20),目标端口8080,本地端口随便填一个。发送十六进制数据,比如01 02 03 04 05 06 07 08,然后立刻看Wireshark里有没有回包。如果回包出来了,说明RX链路、MAC、UDP解析、FIFO、TX链路全部正常,那一刻确实挺有成就感的。

Wireshark里有两个地方要重点看。一是帧格式对不对:源MAC应该变成FPGA板卡的MAC,目的MAC变成电脑的MAC,源IP、目的IP、端口都应该是我们在参数里设置的值。二是校验和:在Wireshark里展开IP头部,如果显示Header checksum: 0xXXXX (correct)说明IP校验和正确;展开UDP头部,UDP校验和如果是0x0000(IPv4允许)或者正确,就说明UDP层没问题。如果显示incorrect, should be 0x...,那问题基本都出在校验和计算逻辑上,后面会细说。

补一个当时困扰我的小细节:电脑很可能开机后自动配置了防火墙规则,会把非预期端口的入站UDP包挡掉。我用的网络调试助手发数据时没问题,但回包一直没反应,最后发现是Windows防火墙拦了8080端口的入站流量,在防火墙高级规则里放行对应端口就好了。另外要确认Wireshark抓的是正确的网卡,如果电脑上有虚拟网卡(比如装了虚拟机软件),抓错网卡会浪费很长时间。

4. 翻车现场:我在调试中踩过的坑

4.1 回环不通,先查哪个信号

回环不通用Wireshark抓不到任何包时,不要一上来就怀疑代码逻辑,先按物理层、MAC层、上层顺序排查。最有效的方法是让FPGA板卡发一个固定数据包,比如在顶层模块里用一个计数器定时触发eth_udp_tx发送"Hello FPGA"几个字节,然后去电脑上用Wireshark看能不能收到。如果这都收不到,问题大概率出在PHY配置或RGMII接口上,而不是UDP逻辑。

PHY芯片的问题常见有三种:复位时序不对、MDIO配置没写对、时钟没起来。RGMII PHY一般在硬件复位之后有100ms左右的稳定时间,FPGA逻辑里要用一个足够长的计数器或者复位管理模块来延迟启动。MDIO配置则主要是配置PHY的工作模式为千兆全双工、启用RGMII接口、关闭环回测试模式等。我当时用的PHY芯片默认是百兆模式,不通过MDIO写寄存器改成千兆全双工,就一直只能收到错误状态的包。这个排查起来比较枯燥,但一旦过了PHY这一关,后面的软件层问题反而好定位。

如果FPGA能自发包且电脑能收到,但电脑发给FPGA的包回不来,那问题就出在RX路径。最常用的定位思路是在eth_udp_rx的输出端拉一个信号去点亮LED,每当收到一个有效UDP包就翻转一次LED状态。配合网络调试助手从电脑端发包,看LED是否变化,就能快速判断RX链路是否工作。这种"硬件打点"的调试法比看仿真波形直观得多,强烈推荐在板级调试时多用。

4.2 校验和不对导致PC端收不到

我跑通自发包之后,一度以为已经大功告成,结果一测回环发现PC端只能收第一包,后面全部石沉大海。抓包一看,IP头的校验和标红为incorrect,UDP校验和也是incorrect。最开始我以为是verilog-ethernet的bug,后来仔细读代码才发现,它在eth_udp_tx里默认会把IP校验和和UDP校验和的计算作为可选项,如果顶层例化时没有对应参数开启,输出的是全0或者错误值。

开启方式很简单,参数里通常有ENABLE_IP_CHECKSUMENABLE_UDP_CHECKSUM这类开关,设置成1即可。但这里有一个非常隐蔽的坑:如果你同时开启了UDP校验和计算,它要求输入的IP地址等参数在数据从FIFO发出之前就已经稳定,而UDP长度字段在发送前必须正确写入。如果发送端tlast来得比预期晚,长度字段算出的值和实际发送字节数不一致,校验和也会跟着错。

排查校验和问题时,我建议在仿真阶段就把它验掉。verilog-ethernet仓库里自带的testbench很完善,把回环工程放到Vivado Simulator里仿真,JK能看到每个包的IP头字段、校验和、UDP长度是否正常。我一开始偷懒跳过仿真直接上板,结果一个校验和问题花了三个小时才定位,要是先跑一遍仿真,十分钟就能查出来。所以强烈建议大家上板前先仿真,这个习惯能帮你省下大量时间。

4.3 接口约束和跨时钟域的坑

再讲一个时序相关的坑。我最初写回环的时候,MAC层的时钟直接用了板载125MHz晶振提供的gtx_clk,然后把PHY的RX时钟也当成同一个时钟域来用,结果偶尔会出现包能通但长时间跑会丢包的诡异现象。后来细想明白了一个问题:gtx_clk是本地晶振时钟,而RG MII的接收数据是和rgmii_rxc(由PHY恢复出来)对齐的,这个时钟和本地晶振虽然都是125MHz,但频率和相位并不完全一致,是两个不同的时钟域,所以RX侧拿到的数据必须做异步处理。

verilog-ethernet里的eth_mac_1g_rgmii_fifo其实已经把接收数据用内部的异步FIFO做了跨时钟域处理,MAC层输出到上层的接口会被同步到gtx_clk域。但要注意,这个异步FIFO的深度有限,如果上层RX链路处理不够快,FIFO溢出就会丢包。我当时在回环代码里RX到TX之间只加了一个很浅的FIFO,遇到突发流量直接溢出丢包,后来把FIFO深度加大到2K字节才算稳下来。

跨时钟域还有一个隐含要求:复位同步器。不同时钟域的复位信号不能直接互相驱动,否则容易导致亚稳态。verilog-ethernet里自带的reset_sync模块就是干这个的,例化时要把MAC核心、RX侧业务逻辑、TX侧业务逻辑的复位分开处理,不能图省事共用一个异步复位。这块在仿真里看不出来,只有上板长期跑才会暴露。

4.4 抓包经验:Wireshark筛选和统计小技巧

调试UDP回环时,Wireshark的过滤表达式是效率神器。我当时PC上可能同时有ARP、ICMP、各种广播包,直接从一堆包里面找UDP回环报文非常痛苦。建议用这几条过滤规则配合查看:

  • udp.port == 8080:只看指定端口的UDP流量,最常用。
  • ip.addr == 192.168.1.10 && udp:只看跟FPGA通信的UDP包。
  • frame.time_delta_displayed:这是时间差字段,鼠标右键设置成列显示,可以看到相邻两帧的时间间隔,用于判断回环延迟。

如果回环通了,想验证吞吐和稳定性,可以用iperf3做UDP打流测试。注意iperf3的UDP模式需要指定带宽参数,例如iperf3 -u -c 192.168.1.10 -b 400M -t 10,然后在FPGA侧做回环,回到PC端观察Jitter和Lost值。我实测下来,一个最简单的回环工程能跑到300Mbps左右不掉包,瓶颈反而不是MAC或UDP逻辑,而是我的FIFO深度和PC网卡的中断处理能力。如果要做高吞吐,FIFO深度、M/AXI位宽、接收侧的处理节拍都要重新调优。

另外提一个Wireshark的细节:如果抓包发现UDP包大小和网络调试助手里设置的不一致,很可能是长度字段错了,eth_udp_tx的UDP长度字段必须等于8 + payload_bytes,IP总长度必须等于20 + 8 + payload_bytes。这些字段在verilog-ethernet里一般会根据tlast自动计算,但如果你自己改过发送逻辑,一定要检查长度字段有没有被正确刷新。

5. 从demo走向实战:还有哪些路要走

5.1 模块裁剪和位宽选择

回环跑通只是第一步,真正常用了才发现有很多优化空间。最典型的就是AXI-Stream数据位宽。我在demo里用了8位数据位宽,因为对初学者来说分组和校验都直观,但8位在125MHz时钟下理论吞吐只有1Gbps,实际因为控制开销只能跑到600-700Mbps,有点浪费。verilog-ethernet的eth_udp_tx/rx都支持配置成32位甚至64位数据位宽,配合MAC层的32位接口,可以在较低时钟下跑满千兆。

位宽改大之后,需要同步调整的是tkeep信号。8位数据时tkeep只有1位,32位时是4位,表示当前拍哪些字节有效。tlast出现在最后一个有效字节所在的那一拍,tkeep要保证尾拍字节数正确。很多第一次改32位的人会在最后一个短包上出错,因为尾拍不一定是4字节对齐。处理方法是看tkeep的高位有没有多余字节,如果有要置0丢弃。这个逻辑不复杂,但写错很隐蔽。

模块裁剪上也要注意:如果你的应用只需要发不需要收,完全可以只例化eth_udp_tx和MAC的发送通路,RX通路和相关的eth_udp_rx模块可以全部去掉,能省不少LUT。反过来只收不发也是同理。另外,ARP模块在纯UDP点对点场景下其实可以不用,但如果PC端要自动发现FPGA、或者FPGA要响应PING,ARP就是必须的。verilog-ethernet里的eth_arp模块在1G MAC链路中通常需要例化进去,否则Windows有时会找不到目标设备,通信会失败。

5.2 性能测试:超出回环的实战验证

回环验证的是正确性,但真正上项目之前,最好做一轮系统的性能测试。我用的工具是iperf3,这在前面提过,但这里再说一下测试方法:PC端作为客户端发送UDP流,FPGA收到后原样回传,PC端作为iperf3的服务端接收回包,这样就能同时测出上行是否有丢包、下行是否正确、吞吐能达到多少。

实测结果很有参考价值。最开始的8位宽、浅FIFO版本,UDP打流到150Mbps就开始出现间歇性丢包;把FIFO深度调到4K字节、数据位宽改成32位后,400Mbps持续打流能稳定不掉包,再高就开始受限于PC网卡的接收能力了。这个数字和你用的PHY芯片、板卡PCB质量、PC配置都有关,仅供参考,但方法论是通用的——用iperf3逼出瓶颈,再用Wireshark确认丢包发生在哪个方向。

另一边要测试的是延迟。FPGA回环的延迟理论上非常低,我的实测值大概在几个微秒量级(不含PC协议栈时间)。测试方法是用Wireshark抓包,看包从PC发出到被FPGA回传回来的frame.time_delta_displayed字段,通常远小于1毫秒,比任何软件协议栈都快很多。这也印证了FPGA做UDP低延迟链路的优势。

5.3 与采集链路结合:从"能收发"到"能做事"

跑通UDP回环之后,我自己最大的感受是:协议栈本身只是传输管道,真正的价值在于你把什么数据放进管道里。我的高速ADC项目是把ADC采样的数据通过DMA或FIFO先缓存下来,然后按一定的帧格式封装成UDP包发送。这里有两个关键设计要点:

第一是分包策略:网络传输单位是MTU,对标准以太网是1500字节,扣掉IP头20字节和UDP头8字节,实际UDP载荷最大是1472字节。如果你的业务数据超过这个值,就要在FPGA里做拆分,一个大包拆成多个UDP包,并为每个包增加序列号,这样接收端可以检查顺序、判断丢包。verilog-ethernet本身不负责拆包组包,这需要在你的数据通路里自己写状态机。

第二是背压机制:数据采集端的速率通常不稳定,而网口的发送速率是相对稳定的。如果你的采集速率瞬间超过发送能力,就必须有FIFO或者暂停采集的逻辑,否则只能丢数据。我在工程中用了axis_fifo加水位信号,当FIFO快满时给采集端发一个暂停信号,优先保证已缓存的数据能完整发出去。这个机制在回环demo里用不上,但做真实数据链路时是必须的。

再往后可以扩展的方向包括:加入ARP自动应答,让上位机开机后能自动发现FPGA设备;加入DDR3缓冲,实现大数据量的先缓存后发送;或者把协议栈从UDP升级到TCP——不过verilog-ethernet本身主要支持UDP,官方还有一个verilog-tcp仓库,按同样思路可以去啃,但TCP的状态机和重传逻辑明显更复杂,建议先把UDP链路做稳再做TCP。

我个人在实际操作中的体会是:学协议栈这类东西,最重要的是把"帧结构"和"时序接口"两块基石打牢。帧结构决定你发的包能被对端识别,时序接口决定你的数据能在系统里流动起来。verilog-ethernet把这两块的实现细节封装好了,让你能站在别人的肩膀上快速跑通全链路,但千万不要因为有了它就跳过抓包分析这个基本功——Wireshark里的每一个字段,将来都可能是在现场救你命的线索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询