拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA以太网通信实战:verilog-ethernet开源工程UDP协议栈拆解与避坑指南

以太网通信在FPGA开发里属于那种看起来不难真动手全是坑的典型场景。我刚开始接触这块的时候觉得UDP协议栈嘛不就是把数据打包发出去、收回来能有多复杂结果从PHY芯片的RGMII时序对不齐到AXI-Stream的握手信号死活拉不通再到ARP表项老化导致ping不通前后折腾了快两周才把链路跑稳。这次拿verilog-ethernet这个开源工程做一次完整的拆解把我在这个过程中踩过的坑、想明白的原理、以及最终跑通的配置都摊开来聊。如果你也是从近似零基础开始学FPGA正在找一条能真正跑通UDP通信的路径这篇内容应该能帮你省掉不少试错时间。verilog-ethernet是GitHub上一个维护得比较活跃的开源项目作者Alex Forencich把MAC层、ARP、UDP、IP这些协议用纯Verilog实现了出来而且接口设计得比较规整支持AXI-Stream作为用户侧数据通道。它最大的价值在于你不需要从零去写一个MAC控制器也不用去啃IEEE 802.3的完整规范直接拿这个工程做二次开发把精力放在自己的业务逻辑上就行。当然前提是你得先理解它的架构和接口约定否则连仿真都跑不起来。1. 为什么选verilog-ethernet而不是自己从头写MAC1.1 自研MAC控制器的隐性成本很多人一开始的想法是我自己写一个MAC不就行了我当初也是这么想的。RGMII接口就那几根线TXC、TXD[3:0]、TX_CTL、RXC、RXD[3:0]、RX_CTL看起来简单得很。但真正动手之后你会发现事情远没有表面那么简单。首先是RGMII的DDR时序问题。RGMII在1000M模式下数据是在时钟的上升沿和下降沿都采样的也就是说TXD[3:0]在TXC的每个边沿都要变化。这就意味着你的FPGA内部逻辑必须跑在125MHz以上而且要对输出数据做精确的相位控制。Xilinx的FPGA通常需要用ODDR原语来输出RGMII信号IDDR来接收中间还涉及IDELAY或者MMCM的相位调整。这些细节如果自己摸索光时序约束就能让你调好几天。其次是协议层的完整性。一个能用的MAC不只是收发原始数据帧还要处理前导码、SFD、CRC校验、帧间隙、冲突检测、退避算法等等。你如果只是想让数据能发出去可能忽略CRC也能凑合但一旦对端设备严格校验CRC你的帧就会被直接丢弃。而CRC32的计算虽然原理不复杂但要在一个时钟周期内完成32位数据的CRC更新需要用到组合逻辑的异或树写起来容易出错。再往上走ARP协议、IP头校验和、UDP伪头校验和这些每一层都有细节。ARP请求和应答的格式、IP头的版本号和头部长度字段、UDP长度字段的计算方式任何一个地方搞错了数据包就是发不通。自己从头写等于要把这些坑全部踩一遍。1.2 verilog-ethernet的架构优势verilog-ethernet把这些底层细节都封装好了。它的核心模块划分很清晰eth_mac_1g负责RGMII接口和MAC层eth_axis_rx和eth_axis_tx负责在MAC层和AXI-Stream之间做转换arp模块处理ARP请求和应答ip模块处理IPv4的收发和校验udp模块处理UDP层的封装和解封装。每个模块之间的接口都是AXI-Stream这意味着你可以很方便地在任意一层插入自己的逻辑。这个工程还有一个很大的好处是它自带了完整的仿真测试平台。你不需要先有硬件才能验证逻辑是否正确直接用ModelSim或者Vivado自带的仿真器就能跑通整个数据通路。对于新手来说这一点极其重要——先仿真跑通再上板调试能省掉大量的抓瞎时间。另外这个工程对Xilinx和Intel的FPGA都有对应的例化模板RGMII的ODDR/IDDR原语、时钟管理这些都帮你处理好了。你只需要根据自己的板子改一下引脚约束和时钟频率就行。1.3 适合什么样的学习阶段这个工程最适合已经掌握了Verilog基本语法、了解状态机和时序逻辑、但对网络协议和高速接口还不太熟悉的开发者。如果你连always块和assign语句都还没写明白建议先把基础打牢再来看这个。但如果你已经能写简单的SPI、UART通信想往上走一步接触以太网那这个工程是非常好的切入点。我自己的路径是先跑通了UART回环然后做了个简单的SPI Master接着就开始啃这个以太网工程。中间确实有卡壳的地方但整体来说比从零写一个MAC要高效太多了。2. RGMII接口的时序约束与ODDR/IDDR原语实操2.1 RGMII的时序模型到底在约束什么RGMII接口的时序问题是整个链路里最容易出问题的地方。先搞清楚它的基本模型在1000M模式下TXC是125MHz的时钟数据在TXC的上升沿和下降沿都有效。发送端FPGA需要在TXC的边沿附近保持TXD稳定接收端PHY在TXC的边沿采样数据。这里有个关键概念叫时钟-数据偏斜。RGMII规范里定义了两种模式一种是数据在时钟边沿对齐即数据和时钟同时变化另一种是数据相对时钟有延迟。大多数PHY芯片支持通过寄存器配置来选择模式但FPGA这边必须配合调整。在Xilinx 7系列FPGA上标准的做法是用ODDR原语来输出TXC和TXD。ODDR可以把一个时钟信号变成DDR输出同时保证TXC和TXD之间的相位关系是确定的。具体来说TXC用ODDR输出一个恒定的1和0交替相当于把时钟转发出去TXD用ODDR输出实际数据。这样TXC和TXD都经过相同的ODDR路径延迟基本一致。接收方向用IDDR原语把DDR输入拆成两个SDR信号。但IDDR采样需要时钟而RXC是从PHY过来的它的相位和FPGA内部时钟不一定对齐。所以通常需要用IDELAY或者MMCM来调整RXC的采样相位。2.2 ODDR和IDDR的Verilog例化模板下面是我在实际项目中用的ODDR例化代码针对Xilinx 7系列// RGMII TX clock output using ODDR ODDR #( .DDR_CLK_EDGE(OPPOSITE_EDGE), .INIT(1b0), .SRTYPE(SYNC) ) rgmii_txc_oddr ( .Q(rgmii_txc), .C(tx_clk_125m), .CE(1b1), .D1(1b1), .D2(1b0), .R(1b0), .S(1b0) ); // RGMII TX data output using ODDR genvar i; generate for (i 0; i 4; i i 1) begin : rgmii_txd_oddr ODDR #( .DDR_CLK_EDGE(OPPOSITE_EDGE), .INIT(1b0), .SRTYPE(SYNC) ) rgmii_txd_oddr_inst ( .Q(rgmii_txd[i]), .C(tx_clk_125m), .CE(1b1), .D1(txd_rise[i]), .D2(txd_fall[i]), .R(1b0), .S(1b0) ); end endgenerate接收方向的IDDR// RGMII RX data input using IDDR genvar j; generate for (j 0; j 4; j j 1) begin : rgmii_rxd_iddr IDDR #( .DDR_CLK_EDGE(OPPOSITE_EDGE), .INIT_Q1(1b0), .INIT_Q2(1b0), .SRTYPE(SYNC) ) rgmii_rxd_iddr_inst ( .Q1(rxd_rise[j]), .Q2(rxd_fall[j]), .C(rx_clk_125m), .CE(1b1), .D(rgmii_rxd[j]), .R(1b0), .S(1b0) ); end endgenerate这里有个细节需要注意DDR_CLK_EDGE参数选择OPPOSITE_EDGE还是SAME_EDGE会影响D1和D2的采样时刻。对于RGMII来说通常用OPPOSITE_EDGE因为数据和时钟是边沿对齐的。但具体还要看PHY芯片的要求有些PHY需要数据相对时钟有延迟这时候可能需要在TXC路径上加额外的延迟。2.3 时序约束文件的写法光有ODDR/IDDR还不够你还需要在XDC文件里写正确的时序约束。RGMII的约束主要包含两部分输出延迟和输入延迟。# RGMII TX output delay set_output_delay -clock [get_clocks tx_clk_125m] -max 1.0 [get_ports rgmii_txd*] set_output_delay -clock [get_clocks tx_clk_125m] -min -1.0 [get_ports rgmii_txd*] set_output_delay -clock [get_clocks tx_clk_125m] -max 1.0 [get_ports rgmii_tx_ctl] # RGMII RX input delay set_input_delay -clock [get_clocks rx_clk_125m] -max 1.5 [get_ports rgmii_rxd*] set_input_delay -clock [get_clocks rx_clk_125m] -min 0.5 [get_ports rgmii_rxd*] set_input_delay -clock [get_clocks rx_clk_125m] -max 1.5 [get_ports rgmii_rx_ctl]这些数值不是随便写的需要根据PHY芯片的数据手册来确定。通常PHY的数据手册会给出TXD相对于TXC的建立时间和保持时间要求你根据这些参数和PCB走线延迟来反推约束值。如果约束写错了Vivado的时序报告会报violation但有时候即使报了violation链路也能勉强工作这就很具有迷惑性——你可能觉得能通就行但实际上链路处于亚稳态边缘温度一变或者电压一波动就挂了。注意RGMII的时序约束一定要以PHY数据手册为准不要照搬别人的数值。不同厂家的PHY芯片比如Realtek、Marvell、TI对时序的要求可能差别很大。2.4 上板调试时怎么确认RGMII时序是否正常仿真阶段你只能验证逻辑功能时序问题必须上板才能发现。我通常的做法是先用一个简单的测试模式让FPGA持续发送固定的数据包然后在PHY侧用示波器或者逻辑分析仪抓TXC和TXD的波形看数据和时钟的相位关系是否满足PHY的要求。如果没有示波器也可以用Vivado的ILAIntegrated Logic Analyzer来抓内部信号。把IDDR输出的rxd_rise和rxd_fall接到ILA上观察接收到的数据是否正确。如果发现接收数据偶尔出错大概率是采样相位不对需要调整IDELAY的值。IDELAY的调整可以通过Vivado的idelayctrl原语来动态控制也可以在约束里固定一个值。我一般会先扫描一遍IDELAY的tap值0到31找到一个误码率最低的区间然后取中间值作为最终配置。3. AXI-Stream握手协议在UDP数据通路中的实际表现3.1 AXI-Stream的基本握手规则AXI-Stream的握手协议看起来很简单发送方拉高TVALID接收方拉高TREADY当两者同时为高时数据在时钟上升沿完成传输。但实际用起来坑比想象的多。首先TVALID一旦拉高就不能随便撤销必须等到TREADY拉高完成握手之后才能拉低。这是AXI-Stream协议的基本要求但很多人在写状态机的时候容易忽略这一点导致仿真时出现数据丢失或者重复传输。其次TLAST信号标记一帧数据的结束。在以太网场景下一个完整的以太网帧对应一个AXI-Stream事务TLAST在最后一个数据beat时拉高。如果你的逻辑没有正确处理TLAST接收端就不知道帧边界在哪里数据就会串在一起。还有一个容易忽略的是TKEEP信号它标记哪些字节是有效的。在32位数据宽度下TKEEP有4位每一位对应一个字节。如果一帧数据的长度不是4的整数倍最后一个beat的TKEEP就会有不全为1的情况。verilog-ethernet的MAC模块会根据TKEEP来决定实际发送多少字节如果你忽略了这个信号可能会多发或少发数据。3.2 verilog-ethernet中AXI-Stream的位宽转换verilog-ethernet的MAC层通常使用8位或者32位的AXI-Stream接口而UDP模块可能使用不同位宽。工程里提供了axis_adapter模块来做位宽转换比如从8位转32位或者从32位转64位。位宽转换的逻辑看起来简单但有几个细节需要注意。第一是TLAST的位置当从窄位宽转宽位宽时如果原始数据的最后一个beat不满一个宽beat需要在转换后的TLAST beat里正确设置TKEEP。第二是背压处理如果下游TREADY长时间为低上游的数据需要缓存否则会丢数据。axis_adapter内部有FIFO来缓冲但FIFO深度有限如果下游长时间不readyFIFO满了之后上游就必须等待。我在实际使用中遇到过一个问题UDP发送模块的数据源是一个FIFOFIFO的输出直接接到AXI-Stream。当FIFO为空时TVALID应该拉低但我的逻辑没有正确处理这个情况导致TVALID一直为高但数据无效接收端收到了垃圾数据。后来在FIFO和AXI-Stream之间加了一个简单的状态机只有当FIFO非空时才拉高TVALID问题才解决。3.3 数据通路中的反压与FIFO深度计算反压是AXI-Stream里最让人头疼的问题之一。当接收端处理不过来时TREADY会拉低发送端必须暂停发送。如果发送端的数据源是连续不断的比如ADC采样数据就必须有足够的缓冲来吸收这段时间的数据。FIFO深度的计算需要考虑几个因素接收端最长的处理延迟、发送端的数据速率、以及允许丢失的数据量。举个例子假设你的ADC以100MSPS的速率产生数据每个采样点16位那么数据速率是200MB/s。如果接收端比如DDR控制器的最长响应延迟是1微秒那么在这1微秒内产生的数据量是200字节。考虑到突发性和余量FIFO深度至少应该设置为512字节或者1KB。在verilog-ethernet的UDP发送路径上我通常会在用户逻辑和UDP模块之间加一个至少2KB的FIFO。这样即使网络暂时拥塞也不会立刻丢数据。当然如果网络长时间拥塞FIFO最终还是会满这时候就需要上层协议来做流控或者丢包处理。3.4 仿真中如何验证AXI-Stream握手是否正确验证AXI-Stream握手最直接的方法是在仿真中加断言assertion。比如检查TVALID拉高后是否在TREADY拉高之前保持不变// AXI-Stream protocol assertion property p_valid_stable; (posedge clk) disable iff (reset) (tvalid !tready) | tvalid; endproperty assert property (p_valid_stable) else $error(TVALID deasserted before TREADY);另外可以在仿真波形里观察TLAST和TKEEP的时序确认每一帧数据的边界是否正确。我习惯在测试平台里加一个简单的记分板scoreboard把发送的数据和接收的数据做比对如果不一致就报错。这样能在仿真阶段就发现大部分数据通路的问题。4. ARP、IP、UDP三层协议在工程中的协作方式4.1 ARP表项的建立与老化机制ARP协议的作用是把IP地址解析成MAC地址。在verilog-ethernet中arp模块负责处理ARP请求和应答并维护一个ARP缓存表。当你需要发送一个UDP包时如果目标IP的MAC地址还不在ARP表里arp模块会先发一个ARP请求等收到应答后再更新表项然后才能发送UDP包。这个过程中最容易出问题的是ARP表项的老化。ARP缓存是有生命周期的通常几分钟到几十分钟不等。如果表项过期了但没有及时更新后续的UDP包就会因为找不到MAC地址而发送失败。verilog-ethernet的ARP模块有一个老化计数器但默认值可能不适合你的场景。如果你的设备需要长时间稳定运行建议把老化时间设置得长一些或者在每次发送前都检查一下表项是否有效。还有一个坑是ARP请求的广播。当FPGA发送ARP请求时目标MAC地址是广播地址FF:FF:FF:FF:FF:FF这个帧会被网络里所有设备收到。如果网络里设备很多ARP请求可能会引起广播风暴。虽然FPGA本身不会主动制造风暴但如果你的设备频繁发送ARP请求比如表项老化时间设得太短就会增加网络负担。4.2 IP头的校验和计算IP头的校验和是一个16位的反码和计算方法是把IP头里的所有16位字相加如果有进位就回卷最后取反。这个计算看起来简单但实现起来有几个细节。第一IP头的校验和字段在计算时应该先置零计算完后再填入。第二IP头的长度是可变的因为有选项字段但大多数情况下是20字节没有选项。verilog-ethernet的ip模块默认处理20字节的IP头如果你需要支持选项字段需要修改代码。第三校验和的计算需要在一个时钟周期内完成因为IP头是在发送过程中实时组装的。这意味着你需要用组合逻辑来实现加法树。对于20字节的IP头有10个16位字需要相加加法树的深度是4级10 - 5 - 3 - 2 - 1。这个逻辑的时序可能会成为关键路径特别是在高频时钟下。如果时序不满足可以考虑用流水线的方式分多个周期计算。4.3 UDP伪头校验和的实现细节UDP的校验和计算需要包含一个伪头伪头里包含源IP、目标IP、协议类型和UDP长度。这个伪头不是实际发送的数据只是用于校验和计算。很多新手在这里容易搞错忘记加伪头或者伪头的字段填错了导致校验和计算错误接收端直接丢包。verilog-ethernet的udp模块已经处理了伪头的计算但你需要确保传入的源IP和目标IP是正确的。如果你在IP层做了NAT或者地址转换UDP层的伪头也需要相应更新否则校验和会出错。另外UDP校验和是可选的。在IPv4中如果UDP校验和字段为0表示发送端没有计算校验和接收端可以选择忽略。但在实际应用中大多数接收端都会校验UDP校验和所以建议还是老老实实算上。4.4 三层协议协作的完整数据流把ARP、IP、UDP串起来看一个完整的UDP发送流程是这样的用户逻辑把数据写入发送FIFO并指定目标IP和目标端口。UDP模块从FIFO读取数据组装UDP头源端口、目标端口、长度、校验和。IP模块在UDP数据前加上IP头版本、头部长度、总长度、标识、标志、片偏移、TTL、协议、校验和、源IP、目标IP。ARP模块查询目标IP对应的MAC地址如果不在表里就先发ARP请求。MAC模块在IP数据前加上前导码、SFD、目标MAC、源MAC、类型字段计算CRC然后通过RGMII发送出去。接收流程反过来MAC模块收到帧后检查CRC去掉前导码和SFD把数据传给IP模块IP模块检查版本和校验和去掉IP头把数据传给UDP模块UDP模块检查校验和去掉UDP头把数据写入接收FIFO用户逻辑从FIFO读取数据。这个流程里每一步都有可能出现问题。我在调试的时候习惯在每一层的输入输出都加ILA探针这样一旦数据不通可以快速定位是哪一层出了问题。5. 从仿真到上板的完整调试链路5.1 仿真环境的搭建与测试用例设计verilog-ethernet自带了仿真测试平台但默认的测试用例比较简单只是发几个包看看能不能通。我建议自己扩展一下测试用例覆盖更多的场景。比如可以设计一个测试用例模拟ARP表项不存在的情况先发送一个UDP包触发ARP请求然后模拟ARP应答再发送UDP包验证数据是否正确。还可以设计一个测试用例模拟网络拥塞让接收端的TREADY随机拉低验证发送端是否能正确处理反压。仿真的时候我通常会用Vivado自带的仿真器或者ModelSim。verilog-ethernet的代码风格比较规范仿真速度还可以。如果仿真跑得太慢可以考虑只仿真关键模块比如单独仿真UDP模块把IP和ARP的行为用简单的模型代替。5.2 上板前的检查清单在把比特流下载到FPGA之前有几项检查是必须做的引脚约束是否正确RGMII的引脚、时钟引脚、复位引脚都要和原理图对上。时钟频率是否正确RGMII需要125MHz的时钟这个时钟通常由MMCM或者PLL从板载晶振倍频得到。确认MMCM的配置参数是否正确。PHY芯片的配置是否正确有些PHY芯片需要在上电后通过MDIO接口配置寄存器比如设置RGMII模式、使能自动协商等。verilog-ethernet工程里通常包含MDIO控制器但你需要根据PHY的数据手册来配置。复位逻辑是否正确PHY芯片和FPGA的复位时序要匹配通常PHY需要先复位完成FPGA才能开始发送数据。我踩过的一个坑是PHY的复位时间不够。有一次上板后链路死活不通查了半天发现是PHY的复位脉冲太短PHY还没有完成内部初始化FPGA就开始发数据了。后来把复位脉冲从1ms延长到10ms问题就解决了。5.3 用ping和iperf做基本连通性测试上板之后第一步是测试基本的连通性。把FPGA板卡和PC用网线直连给PC配置一个同网段的IP地址然后从PC ping FPGA的IP。如果ping通了说明ARP、IP、ICMPverilog-ethernet通常也支持ICMP应答都工作正常。如果ping不通先检查PC的ARP表里有没有FPGA的MAC地址。在Windows上可以用arp -a命令查看在Linux上可以用ip neigh。如果没有表项说明ARP请求或应答有问题。如果有表项但ping不通可能是IP层或ICMP层的问题。ping通了之后可以用iperf来测试UDP的吞吐量。iperf3支持UDP打流命令是iperf3 -c FPGA_IP -u -b 100M表示以100Mbps的速率发送UDP数据。在FPGA侧你需要实现一个简单的UDP回环或者接收统计逻辑来验证数据是否正确接收。5.4 常见问题排查表下面是我在实际调试中遇到的一些常见问题和对应的排查方法现象可能原因排查方法链路不通PHY指示灯不亮引脚约束错误、PHY未复位、时钟未起振检查引脚约束、测量PHY复位引脚、用示波器测时钟ping不通但ARP表有表项IP校验和错误、ICMP未响应用ILA抓IP层数据检查校验和字段ping通但丢包严重RGMII时序margin不足、FIFO溢出调整IDELAY、增大FIFO深度UDP数据能发不能收接收路径的AXI-Stream握手问题用ILA抓TREADY和TVALID检查握手时序长时间运行后通信中断ARP表项老化、PHY温度漂移延长ARP老化时间、检查PHY散热这张表里的每一个问题我都实际遇到过排查过程往往比想象中耗时。特别是RGMII时序问题有时候链路能通但误码率很高ping看起来正常但大流量传输就丢包。这种问题最难查因为你需要用误码率测试仪或者长时间的打流测试才能发现。6. 几个让我卡了很久的坑和最终解法6.1 ARP表项老化导致的间歇性断连这个问题困扰了我最久。现象是FPGA和PC之间的UDP通信一开始正常但运行十几分钟后就断了重启FPGA又能恢复。一开始我以为是FPGA逻辑有bug查了很久没找到问题。后来用Wireshark抓包才发现断连的时候FPGA在发ARP请求但PC没有回应。原因是PC的ARP表项也老化了而FPGA发的ARP请求PC没有正确处理可能是PC的防火墙拦截了也可能是PC的ARP缓存策略问题。最终的解法是双管齐下一方面把FPGA的ARP老化时间延长到30分钟另一方面在PC上设置静态ARP表项把FPGA的IP和MAC绑定。这样即使ARP表项老化也不会影响通信。6.2 AXI-Stream的TLAST丢失导致帧粘连有一次调试UDP接收发现收到的数据偶尔会多出几个字节或者两帧数据粘在一起。用ILA抓波形后发现TLAST信号在某些情况下没有正确拉高。原因是我的接收状态机在最后一个beat时TREADY和TVALID的握手条件判断有误导致TLAST被跳过了。修复方法是在状态机里明确区分最后一个beat和中间beat确保在最后一个beat时TLAST一定拉高。具体来说我加了一个计数器来跟踪已接收的beat数当计数器达到帧长度时强制拉高TLAST。6.3 RGMII接收时钟相位偏差导致的误码这个问题是在大流量测试时发现的。ping小包没问题但用iperf打流到500Mbps以上时误码率急剧上升。用示波器测量RXC和RXD的相位关系发现RXC的边沿几乎和RXD的变化沿重合采样窗口非常窄。解决方法是调整IDELAY的tap值把RXC的采样点移到数据稳定的中心位置。具体操作是在Vivado里例化IDELAYCTRL和IDELAYE2通过动态配置接口扫描tap值找到一个误码率最低的点。最终我把tap值设为12总共32个tap误码率降到了可接受的范围。6.4 仿真通过但上板失败的原因分析仿真通过但上板失败这是FPGA开发中最常见也最让人沮丧的情况。原因通常有几类一是时序问题仿真不检查时序上板后时序violation导致逻辑错误二是复位问题仿真的复位行为是理想的上板的复位可能有毛刺或者释放时刻不对三是外部器件的行为差异比如PHY芯片的上电初始化时间、时钟的抖动等。我的经验是仿真通过后不要急着上板先做一次完整的时序分析确保所有路径都满足时序要求。然后检查复位逻辑确保复位释放是同步的并且有足够的复位脉冲宽度。最后如果条件允许先用低速模式比如100M模式测试确认基本功能正常后再切到1000M模式。7. 工程二次开发时值得注意的扩展点7.1 多端口UDP通信的实现思路verilog-ethernet默认支持单个UDP端口但实际应用中经常需要同时处理多个端口的数据。比如一个端口用于控制命令另一个端口用于数据传输。实现多端口的方式有几种一是例化多个UDP模块每个模块处理一个端口二是在一个UDP模块里维护多个端口的状态根据目标端口号分发数据。第一种方式资源消耗大但逻辑简单适合端口数量少的情况。第二种方式资源利用率高但状态机复杂度增加。我通常会在端口数量少于4个时用第一种方式超过4个时考虑第二种。7.2 添加ICMP协议支持verilog-ethernet默认可能不包含ICMP模块但ping测试需要ICMP应答。添加ICMP支持的方法是在IP层收到ICMP包时解析ICMP头如果是Echo Request就生成一个Echo Reply把源IP和目标IP互换重新计算校验和然后发送回去。ICMP的校验和计算和IP头类似也是16位反码和。需要注意的是ICMP Reply的IP头里TTL通常设置为64标识字段可以复用Request的值。7.3 与DDR配合实现大数据量缓存在需要缓存大量数据的场景下比如视频流传输FPGA内部的BRAM不够用需要外挂DDR。verilog-ethernet的UDP发送模块可以从FIFO读取数据你可以把FIFO换成DDR控制器从DDR里读取数据发送。这里的关键是DDR的带宽要足够。千兆以太网的理论带宽是125MB/sDDR3的带宽通常在1GB/s以上所以带宽不是瓶颈。但DDR的访问延迟比较大需要用足够深的FIFO来缓冲避免UDP发送时出现空档。7.4 时间戳与精确同步的扩展在一些对时间敏感的应用中需要在UDP包里加入时间戳。verilog-ethernet的UDP模块允许你在数据前添加自定义的头部你可以利用这个特性加入64位的时间戳。时间戳的来源可以是FPGA内部的计数器也可以是从外部时钟芯片读取的PTP时间。如果需要更高精度的同步可以考虑实现PTP协议IEEE 1588。verilog-ethernet工程里可能包含PTP相关的模块但配置起来比较复杂需要对PTP协议有深入理解。7.5 资源占用与时序收敛的平衡verilog-ethernet的完整工程会占用不少FPGA资源特别是在低端FPGA上比如Artix-7的入门型号可能会遇到资源不够或者时序收敛困难的问题。我的经验是如果资源紧张可以裁剪掉不需要的模块比如只保留UDP发送去掉接收路径或者降低AXI-Stream的位宽从64位降到32位甚至8位。时序收敛方面RGMII的125MHz时钟域是关键。如果时序不满足可以尝试优化组合逻辑把长的组合路径拆成多级流水线。另外AXI-Stream的握手逻辑也容易成为关键路径特别是当TREADY和TVALID的组合逻辑比较复杂时可以考虑加寄存器打拍。我在一个Artix-7 35T的板子上跑过完整的verilog-ethernet工程资源占用大概在60%左右时序在125MHz下能收敛但余量不大。如果再加一些用户逻辑可能就需要换更大规模的FPGA了。7.6 跨时钟域处理的实际经验verilog-ethernet涉及多个时钟域RGMII的125MHz收发时钟、AXI-Stream的用户时钟、配置接口的时钟等。跨时钟域处理是必须的否则会出现亚稳态问题。工程里通常用异步FIFO来做跨时钟域但异步FIFO的深度和宽度需要根据数据速率来设计。我的经验是跨时钟域的FIFO深度至少要是突发数据量的两倍并且要确保FIFO的读写指针同步逻辑是正确的通常用格雷码。另外复位信号的跨时钟域处理也很重要。复位释放必须同步到目标时钟域否则可能导致部分逻辑提前退出复位部分逻辑还在复位状态出现不可预期的行为。我通常用两级触发器来做复位同步reg [1:0] reset_sync; always (posedge clk or negedge reset_n) begin if (!reset_n) reset_sync 2b00; else reset_sync {reset_sync[0], 1b1}; end wire reset_synced_n reset_sync[1];这个简单的同步逻辑能避免大部分复位相关的亚稳态问题。7.7 实际项目中的性能调优记录最后分享一下我在实际项目中的性能调优记录。目标是在千兆链路上实现稳定的900Mbps UDP吞吐。经过以下优化步骤最终达到了920Mbps优化步骤调整内容效果初始状态默认配置650Mbps偶有丢包调整IDELAYtap值从8改为12780Mbps丢包减少增大FIFO从2KB增加到8KB850Mbps基本不丢包优化AXI-Stream位宽从32位改为64位900Mbps流水线优化IP校验和计算加一级流水920Mbps稳定这个过程中最关键的优化是AXI-Stream位宽的调整。32位位宽在125MHz下理论带宽是500MB/s但实际因为握手开销和反压有效带宽只有300MB/s左右。改成64位后有效带宽提升到了600MB/s以上满足了千兆线的需求。另外IP校验和的流水线优化也很重要。原本的组合逻辑路径太长限制了时钟频率。加了一级流水后时序余量从0.2ns提升到了1.5ns时钟可以稳定跑在125MHz。这些优化不是一蹴而就的每一步都需要用ILA和性能计数器来验证效果。我建议在做性能调优时先建立一个基准测试环境每次只改一个变量观察效果这样才能准确判断哪个优化真正起了作用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门