FPGA实现UDP协议栈:从ARP到ICMP的纯逻辑设计指南

发布时间:2026/9/8 6:39:05
FPGA实现UDP协议栈:从ARP到ICMP的纯逻辑设计指南 1. UDP模块整体设计思路FPGA开发走到UDP协议栈这一块本身就是个分水岭。很多人在前面点亮LED、跑通UART、甚至驱动了DDR3之后都会面临同一个困惑怎么才能让我的板子和电脑真正对话串口速度太慢PCIe又过于复杂UDP恰好站在了一个尴尬又巧妙的位置上——协议本身足够简单但要在FPGA里用逻辑门去实现它又需要你对状态机、CRC、FIFO、跨时钟域这些基本功有相当扎实的掌握。第二节我们在讲MAC层的时候完成了RGMII接口的收发和以太网帧的组装那是整个协议栈的地基。现在到了第八节我们在这块地基上盖房子设计一个完整的UDP模块让FPGA能独立完成ARP请求、ARP应答、ICMP ping应答和UDP数据收发这一整套网络层和传输层的活儿。这个模块做完你的开发板就是一个真正意义上的网络节点了插上网线就能被电脑发现、能ping通、能互相收发数据那种成就感是串口和JTAG完全给不了的。1.1 方案选型为什么选择纯逻辑实现UDP在设计这个模块之前我认真想过三种技术路线第一种是用现成的第三方IP核比如Xilinx的lwIP或Altera的Triple Speed Ethernet加上软核处理器来跑协议栈第二种是自己用状态机硬撸一个精简UDP协议栈第三种是干脆绕开UDP直接操作MAC层收发裸以太网帧。第一种方案的优点是协议栈成熟稳定、支持TCP这种复杂协议缺点是资源开销大、开发周期长而且对于只想传个图像或者采集数据的场景来说lwIP的处理延迟远超纯逻辑实现。最关键的是用软核跑协议栈就没法真正理解底层协议是怎么工作的整个学习过程会变得非常黑盒。第三种方案在调试早期确实能用但没法用标准的网络调试工具去交互电脑端的测试软件不会认你自定义的帧格式工作量和维护成本其实更高。所以我选择了第二种——用纯状态机实现一个精简但完整的UDP协议栈只支持IPv4、UDP、ARP和ICMP echo功能。这里解释一下为什么需要ARP和ICMPARP是为了让电脑能找到你的FPGA通过IP地址解析MAC地址ICMP是为了能用ping命令验证网络链路是否通畅。这两个是UDP通信能跑起来的前置条件缺一不可。很多教程只写UDP收发结果用户插上网线怎么都ping不通抓包一看ARP请求根本没人理这就是典型的半吊子协议栈。1.2 功能需求拆解与模块划分在动笔写代码之前我先花了一整天把需求列清楚。这个UDP模块至少要完成以下几件事解析电脑发来的ARP请求如果是询问本机IP的MAC地址自动回一个ARP应答帧解析电脑发来的IPv4帧提取UDP数据报判断目的端口和目的IP是否匹配根据上层FIFO的写请求把用户数据封装成UDP数据报发送出去响应ICMP echo请求返回对应的echo reply让ping命令能通对接收到的数据做CRC32校验错误帧直接丢弃同时给出错误指示信号模块划分上我没有把所有逻辑塞进一个大文件里而是拆成了六个子模块每个模块职责单一、接口清晰。这样做的好处是第一单个文件体量小综合时间和仿真调试效率都高第二某一层出问题可以单独拉出来测不用一上来就整套仿真第三后续如果想把UDP换成TCP或者其他协议只需要替换网络层以上的部分就行。顶层模块叫udp_stack内部例化六个子模块。模块之间的通信全部用AXI4-Stream接口这个接口在Xilinx的IP核里用得非常多大家都熟悉而且做跨时钟域也方便——用异步FIFO把数据从一个时钟域搬到另一个时钟域tvalid/tready/tlast这三个信号一握手数据传输就完成了。2. 核心细节解析与关键代码实现有了整体框架接下来就是实打实的代码设计了。这一节我会按模块逐个拆解每个模块都会讲清楚接口定义、设计思路、关键代码和要注意的坑。代码是简化过的教学版本剔除了大量边界条件处理但核心逻辑是完整的可以直接抄到你的工程里跑。2.1 ARP模块链路层邻居发现ARP模块是整个协议栈里最先工作的。当FPGA上电并完成PHY初始化后如果电脑端ping 192.168.1.10假设这是FPGA的IP电脑会先发一个ARP请求广播帧问谁是192.168.1.10请告诉192.168.1.1电脑的IP。FPGA的ARP模块收到这个帧后需要比对帧头里的目标IP是否等于自己配置的IP如果匹配就回一个ARP应答单播帧。ARP请求帧的格式是以太网帧头14字节 ARP报文28字节。以太网帧头里目的MAC是全FF源MAC是电脑的MAC类型字段是0x0806。ARP报文里硬件类型是0x0001以太网协议类型是0x0800IPv4硬件地址长度是6协议地址长度是4操作码是0x0001表示请求、0x0002表示应答然后是发送方MAC、发送方IP、目标MAC、目标IP。这个模块的状态机和UDP收发相比算是简单的就两个状态WAIT和SEND_REPLY。收到请求后在下一个周期判断是否匹配匹配的话就进入发送状态把应答帧按字节顺序一个一个打出去。需要注意的是ARP应答帧里的源MAC要填FPGA自己的MAC源IP填FPGA自己的IP目标MAC填请求帧里的发送方MAC目标IP填请求帧里的发送方IP。实际调试中很多人在这个模块上踩坑。最常见的问题是MAC地址字节序搞反了。FPGA内部存储MAC地址时我建议直接用48位的reg类型高位在前。但以太网传输时是先发高字节还是低字节呢答案是低字节在前也就是我们常说的小端序和人们书写MAC地址时AA:BB:CC:DD:EE:FF从左到右的顺序刚好相反。举个例子假设FPGA的MAC地址是00:11:22:33:44:55在帧里发送时第一个字节发送0x00第二个字节发送0x11依次到最后0x55。这个顺序其实和我们平时书写是一致的并没有反转。真正容易错的是在RGMII接口那边因为它是4-bit DDR传输byte和nibble之间的顺序搞错了导致整个MAC地址在抓包软件里看起来像被切片了一样。如果遇到这个问题先用一个简单的回环测试让FPGA收到什么就发什么确认RGMII的数据通道没有问题再排查协议层。2.2 接收路径CRC校验、数据缓存与解析接收路径是整个模块里最容易出问题的地方因为数据进来的时候没有任何同步信号告诉你这一帧开始了、这一帧结束了全靠MAC层的rx_dv信号来划分边界。我的设计思路是MAC层解析出一个完整的以太网帧后直接把帧数据包括帧头写入一个异步FIFO然后UDP接收模块从FIFO里读数据并解析。为什么要把完整的帧先存下来再解析因为CRC校验需要从头到尾算一遍如果不存下来校验失败时数据都已经往外发了没法撤销。而且解析IP头里的总长度字段之前你也并不知道这一帧到底有多长必须先把整帧接收完毕才能确定边界。所以先缓存、再解析、解析完再决定是否丢弃是处理网络数据最稳妥的思路。CRC32校验我用了一个查表法实现时钟是125MHz一个时钟周期可以处理一个字节。多项式用的是标准CRC320x04C11DB7初值是0xFFFFFFFF结果要异或0xFFFFFFFF再和帧里的FCS字段比较。这个查表法有一个好处不占用太多逻辑资源一个256x32的ROM就能放下查询表。数据包缓存的FIFO深度我设为2048宽度8bit。这是因为以太网最大帧长是1518字节加上前导码和CRC也要不了1600字节2048深度的FIFO留了足够余量不会丢帧也不至于浪费太多BRAM。FIFO用的异步时钟写时钟是MAC层的125MHz读时钟是UDP解析模块的125MHz但两个125MHz来自不同的MMCM时钟树不能保证相位完全一致所以用异步FIFO是最稳妥的。解析逻辑里最核心的代码是检查IP头校验和。IP头长度是20字节按16-bit为单位逐字相加进位循环回加最后取反。这个校验和如果不正确说明帧在传输过程中出错或者被篡改了直接丢帧。另外还要检查IP总长度字段是否和实际接收到的字节数一致防止短帧或者超长帧混进来。在接收路径的代码实现上有一个效率优化技巧值得分享解析过程中只保存我们关心的字段比如源MAC地址、源IP地址、源端口、目的端口、UDP数据长度而不是保存整个IP头。这样在做数据分发时就不用一遍遍地回头去FIFO里翻数据了。2.3 UDP发送状态机与组帧逻辑发送方向相对简单但细节同样不能马虎。UDP发送模块的核心是一个状态机状态包括IDLE、WAIT_DATA、BUILD_UDP_HEADER、SEND_DATA、SEND_CRC、DONE。整个流程是这样的当上层应用比如图像采集模块把待发送的数据写入发送FIFO后发送模块检测到FIFO非空就进入BUILD_UDP_HEADER状态依次把目标MAC、源MAC、以太网类型、IP头、UDP头这些固定字段填好发出去然后进入SEND_DATA状态把FIFO里缓存的数据一个字节一个字节地读出来发送最后补上CRC32。这里有个关键参数需要设计和计算IP头里的Total Length字段它等于IP头长度20字节 UDP头长度8字节 UDP数据长度。UDP头里的Length字段它等于UDP头长度8字节 UDP数据长度。这两个值是在组装数据头时就算好的然后原样填入头部。CRC32则是在发送完所有数据之后计算出来的结果最后追加到帧尾。发送状态机的时钟是125MHz也就是千兆网的一个周期8ns。整个发送流程从IDLE到DONE只用了大概几百个周期关键时刻一定要保证状态跳转不被意外打断。比如TX FIFO在发送过程中突然变空了怎么办我在设计里做了保护如果SEND_DATA状态下FIFO读出tvalid拉低说明上层数据没跟上这时候不是跳回IDLE而是进入ABORT状态把当前这一帧作废等下一个完整的数据帧再来发送。这样做的原因是对于一个半截帧接收端会等待超时或者直接因为长度不对而丢帧还不如主动放弃。UDP数据报长度需要根据MTU来限制。以太网MTU是1500字节UDP头占8字节IP头占20字节所以UDP数据部分最大长度是1472字节。如果上层传来的数据超过这个长度发送模块应该截断或者报错否则会产生IP分片。IP分片这个事在FPGA里实现起来很麻烦所以干脆在设计时避免它。我的建议是上层应用把UDP单帧数据限制在1400字节以内留出余量给可能存在的VLAN tag或者PPPoE头这样在各种网络环境下传输都不会分片。下面给一个简化版的UDP发送模块核心代码module udp_tx #( parameter SRC_MAC 48h00_11_22_33_44_55, parameter SRC_IP 32hC0_A8_01_0A, // 192.168.1.10 parameter SRC_PORT 16h1234, parameter DST_MAC 48hFF_FF_FF_FF_FF_FF, parameter DST_IP 32hC0_A8_01_01, // 192.168.1.1 parameter DST_PORT 16h1234 )( input wire clk, input wire rst_n, input wire tx_fifo_empty, input wire [7:0] tx_fifo_dout, input wire tx_fifo_valid, output reg tx_fifo_rd_en, output reg [7:0] mac_tx_data, output reg mac_tx_valid, input wire mac_tx_ready, output reg tx_done ); localparam IDLE 4d0; localparam BUILD_ETH_HEADER 4d1; localparam BUILD_IP_HEADER 4d2; localparam BUILD_UDP_HEADER 4d3; localparam SEND_DATA 4d4; localparam SEND_CRC 4d5; localparam WAIT_LAST 4d6; localparam DONE 4d7; reg [3:0] state; reg [10:0] byte_cnt; reg [31:0] crc; // 以太网帧头的构建依次输出各字节 // 帧结构: DMAC(6) SMAC(6) Type(2) IP头(20) UDP头(8) Data FCS(4) always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; byte_cnt 0; tx_fifo_rd_en 0; mac_tx_valid 0; tx_done 0; end else begin case (state) IDLE: begin tx_done 0; if (!tx_fifo_empty) begin byte_cnt 0; state BUILD_ETH_HEADER; end end BUILD_ETH_HEADER: begin if (mac_tx_ready) begin mac_tx_valid 1; case (byte_cnt) 0: mac_tx_data DST_MAC[47:40]; 1: mac_tx_data DST_MAC[39:32]; 2: mac_tx_data DST_MAC[31:24]; 3: mac_tx_data DST_MAC[23:16]; 4: mac_tx_data DST_MAC[15:8]; 5: mac_tx_data DST_MAC[7:0]; 6: mac_tx_data SRC_MAC[47:40]; 7: mac_tx_data SRC_MAC[39:32]; 8: mac_tx_data SRC_MAC[31:24]; 9: mac_tx_data SRC_MAC[23:16]; 10: mac_tx_data SRC_MAC[15:8]; 11: mac_tx_data SRC_MAC[7:0]; 12: mac_tx_data 8h08; // 以太网类型: IPv4 13: mac_tx_data 8h00; default: mac_tx_data 8h00; endcase if (byte_cnt 13) begin byte_cnt 0; state BUILD_IP_HEADER; end else begin byte_cnt byte_cnt 1; end end end // 这里省略了IP头、UDP头和数据发送的具体实现 // 完整代码见工程仓库 default: state IDLE; endcase end end endmodule这段代码只写了以太网帧头的拼接逻辑IP头、UDP头和数据发送部分的代码较长这里不全部展开。核心思想就是状态机控制字节计数逐字节填字段。IP头里有个checksum字段这个值不能提前算好因为它依赖IP头里的源IP、目的IP、总长度这些字段的值而这些值在参数化设计里是固定不变的所以理论上可以综合时就算好。但为了可读性和可移植性我建议还是在运行时用组合逻辑算出来再填入。2.4 CRC32查表法实现与并行化改造CRC32在FPGA里有两种常见的实现方式串行LSFR和查表法。串行实现每个时钟周期只能处理1bit125MHz下处理一个字节要8个cycle效率太低了。查表法一次处理1字节效率刚好。但如果你的系统时钟是200MHz或者更高想在一个周期里处理2字节甚至4字节就需要对查表法做并行化改造。我先给出标准的查表法Verilog实现module crc32_byte ( input wire clk, input wire rst_n, input wire crc_en, input wire [7:0] data_in, input wire [31:0] crc_init, output reg [31:0] crc_out ); // CRC32查表经标准CRC32多项式0x04C11DB7预计算 reg [31:0] crc_reg; reg [31:0] crc_next; always * begin crc_next crc_reg ^ { {24{data_in[7]}}, data_in, 24h0 }; // 此处为展开的8次迭代或查表索引 end always (posedge clk or negedge rst_n) begin if (!rst_n) crc_reg 32hFFFFFFFF; else if (crc_en) crc_reg crc_next; end assign crc_out crc_reg ^ 32hFFFFFFFF; endmodule实际项目里我不会手写这个而是用一个小脚本预先算出256项查表生成一个Verilog的ROM文件或者直接用case语句。这样综合工具会把表优化成ROM或者组合逻辑效率很高。关键是校验时机发送路径里MAC层在发完所有数据后要紧接着发4字节的CRC。接收路径里MAC层在收完数据后也要继续收4字节的CRC把这4个字节和本地计算的结果比较。CRC出错的情况在调试中很常见尤其是RGMII的时序没调好的时候随机比特翻转导致CRC错误率极高。如果你发现抓包软件里全是bad checksum先别怀疑协议栈先检查PHY的配置和RGMII的时钟约束。在Vivado里打开report_timing_summary看看rx_ctl和rxd[3:0]的路径有没有时序违例。3. 顶层集成与仿真验证子模块单独工作正常不代表整个系统能跑通集成阶段才是真正考验耐心的时候。这一节讲顶层模块怎么组装、怎么设计testbench、怎么用仿真工具验证、以及上板调试的完整流程。3.1 顶层模块的接口规划与时钟树设计顶层模块udp_stack对外暴露的接口设计得很精简因为我希望使用者不用看懂协议栈内部就能完成数据收发。对外接口分三组收发接口、控制接口、状态接口。收发接口包括user_tx_data、user_tx_valid、user_tx_ready和user_rx_data、user_rx_valid、user_rx_ready构成一对标准的AXI4-Stream通道。控制接口包括mac_addr、ip_addr、udp_port配置信号这些信号在系统启动后由外部按下拉开关或者寄存器配置写入。状态接口包括link_up、tx_done、rx_error等。时钟树设计上我就用了两个时钟域eth_clk125MHz和user_clk可以由PLL分频产生比如100MHz。跨时钟域通过异步FIFO完成FIFO两侧分别接各自的时钟域。这样上层用户逻辑不需要关心以太网侧的时序只要保证自己的数据速率不超过千兆以太网的吞吐上限就行。这里有个性能计算的例子千兆以太网理论带宽是125MB/s去除帧间隔96ns和前导码8字节实际最大有效载荷带宽大约是119MB/s。如果UDP每帧携带1400字节数据每秒最多能发送约85000帧。对于图像传输场景1920x108060fps的RGB888图像每帧数据量约6.2MB需要62ms的传输时间也就是约16fps。如果只传YUV422可以到32fps。这些数字在设计初期就应该估算清楚避免做完了发现带宽不够又推倒重来。3.2 Testbench设计点对点回环测试仿真验证我用的方式是构造一个Testbench实例化顶层UDP模块再写一个简化的虚拟PC模型它会模拟电脑的行为向FPGA发送ARP请求、ICMP echo请求和UDP数据包然后检查FPGA返回的帧内容和期望值是否一致。Testbench的关键代码片段如下module tb_udp_stack; reg clk; reg rst_n; // 顶层接口信号省略 initial begin clk 0; rst_n 0; #100; rst_n 1; // 模拟电脑发送ARP请求 send_arp_request(); // 等待ARP应答 wait (mac_tx_valid); check_arp_reply(); // 模拟电脑Ping FPGA的IP地址 send_icmp_echo(); wait (mac_tx_valid); check_icmp_reply(); // 模拟电脑发送UDP数据到FPGA端口 send_udp_packet(8h01, 8h02, 8h03, 8h04); wait (user_rx_valid); check_udp_data(); // 模拟FPGA发送UDP数据到电脑 user_tx_valid 1; user_tx_data 8hAA; wait (mac_tx_valid); // 检查发出的UDP帧格式 $display(ALL TESTS PASSED); $finish; end // 任务定义生成一个UDP数据包的帧 task send_udp_packet; input [7:0] d0, d1, d2, d3; begin // 前导码 // 以太网头 // IP头 // UDP头 // 数据 // FCS end endtask endmoduleTestbench里有一个非常关键的设置发送完一帧数据后仿真要等足够多的时钟周期再发送下一帧。以太网标准要求帧间隔至少是96ns也就是12个时钟周期。如果连续背靠背发帧接收端的FIFO可能来不及处理导致丢帧。虽然实际PHY芯片会自动加帧间隔但在仿真里如果不模拟这个行为某些时序缺陷会被掩盖上板后才会暴露。仿真跑起来后重点观察几个波形mac_tx_valid和mac_tx_ready的握手时序、tx_fifo_rd_en是否在正确的时间拉高、CRC生成是否在最后4字节正确输出。如果发现波形不对不要急着改代码先用$display打印关键信号的值配合$finish定位具体是哪个状态出了问题。3.3 上板调试从Wireshark到iperf3测吞吐仿真通过后就是激动人心的上板调试了。我把调试流程总结为三步每一步都有对应的验证工具和判断标准。第一步是ARP连通性测试。插上网线后在电脑的命令行里执行arp -d清空ARP缓存然后ping FPGA的IP地址。用Wireshark抓包正常情况下应该能看到电脑发出ARP请求FPGA返回ARP应答然后电脑发出ICMP echo请求FPGA返回ICMP echo reply。如果只看到请求没看到应答说明ARP模块有问题抓包看FPGA是否真的发出了帧以及帧内容是否正确。第二步是UDP回环测试。我用电脑端的网络调试助手比如NetAssist或者SocketTool向FPGA的UDP端口发送一串数据FPGA收到后把数据原样返回或者加上一个递增的序号后再返回。调试助手能直观地看到返回的数据是否和发送的一致。这个测试能验证整条链路电脑 - UDP模块接收路径 - 顶层逻辑 - UDP模块发送路径 - 电脑。第三步是吞吐量测试。用iperf3工具电脑运行iperf3 -s开启服务端FPGA端或者配合上位机软件端持续发送UDP数据观察实际吞吐量。这里有一个经验参数UDP单帧数据长度设成1400字节左右时吞吐量可以达到900Mbps以上如果设成几十字节的小包吞吐量会断崖式下降因为帧间隔和帧头的开销占比太大了。上板调试时第一个要查的永远是PHY芯片的link状态。很多FPGA不工作的问题其实是PHY没起来或者RGMII的时钟方向搞反了RGMII的RX时钟由PHY提供TX时钟由FPGA提供千万别把这两个搞混。4. 常见问题与排查技巧实录这个模块开发和调试过程中我踩了不少坑也积累了一些实战经验。这一节我按照问题症状 - 可能原因 - 排查方法的方式整理出来供大家参考。4.1 Wireshark抓包看不到FPGA发出的帧这是最常见的硬件问题。症状是电脑能正常收发网络数据但抓包软件里完全看不到FPGA发出来的帧好像FPGA根本没有往网络里发数据。排查顺序可以按以下思路进行先用示波器或者逻辑分析仪看PHY芯片的TXD、TX_CTL、GTXCLK引脚是否有信号翻转。如果完全没有信号问题在FPGA侧查RGMII接口是单向写错还是整个发送路径都没跑起来如果引脚有信号问题很可能在PHY芯片或者变压器电路。这里有个细节RGMII的TX_CTL信号同时承载TX_EN和TX_ERR两个信号DDR模式下上升沿采样TX_EN、下降沿采样TX_ERR。如果TX_CTL的电平逻辑反了PHY会把帧当作错误帧丢弃但FPGA侧看起来数据已经送出去了。如果是Vivado工程检查综合后的I/O Ports是不是所有引脚都正确分配了。FPGA开发里一个常见的低级错误——引脚约束里把TXD[0]和TXD[1]的位置弄反了导致整个数据通道错位。4.2 ping通但UDP收发失败这个症状说明链路层和网络层是通的ARP、IP、ICMP都正常问题出在传输层。可能的原因有三种第一UDP目的端口不匹配电脑发到8000端口FPGA监听的是9000端口那肯定收不到第二UDP校验和计算错了接收端直接丢弃第三数据长度字段错误导致接收端等待更多的数据超时后丢弃。排查方法在FPGA代码里加上一个简单的led指示收到UDP数据包时点亮LED。然后电脑端发一帧测试数据看看LED亮不亮。如果不亮在Modelsim里加仿真信号观察udp_rx_parsing模块有没有进入UDP解析状态。很多网卡默认开启了UDP校验和分段卸载Checksum Offload导致Wireshark抓包看到的校验和字段是错误的但这不代表发送端真的错了。遇到所有UDP包checksum都是错的情况先到网卡设置里关掉Checksum Offload再抓包排除这个干扰项。4.3 吞吐量上不去或者数据丢包严重带宽计算和FIFO深度都要在系统设计阶段做好规划但如果上板测试时发现丢包率高得离谱八成是以下三个原因之一。第一个原因是上层数据产生速率超过了UDP模块能处理的速率。千兆以太网每个时钟周期最多发1字节如果你的用户逻辑在一个时钟周期里往发送FIFO里塞了不止1字节的数据而FIFO的读速率是固定的1字节/周期多余的字节就会堆积FIFO满了就丢包。解决办法是加一个反压信号当发送FIFO快满时让上层数据源暂停发送。第二个原因是接收路径的FIFO读时序不匹配。具体来说如果上层逻辑读FIFO的速率低于FIFO写速率FIFO就会满后续的数据帧没地方缓存就被MAC层丢弃了。在调试中我习惯在FIFO的prog_full信号上接一个计数器统计丢帧次数通过这个数值判断丢包是间歇性还是持续性的。第三个原因是DDR或者外部存储器的带宽瓶颈。如果你的UDP模块最终要把数据写入DDR3那么DDR的读写带宽必须大于网络带宽否则数据就会在DDR控制器那里堵住。常见的DDR3-1066的带宽理论值是8.5GB/s实际有效带宽大约6-7GB/s一般来说不会是瓶颈但如果DDR同时被两个以上的模块访问总线仲裁的延迟就可能引入随机丢包。下表是我整理的常用排查速查表症状可能原因排查方法ARP请求无响应ARP模块状态机异常仿真单步跟踪检查目标IP比对ARP能通但ping不通ICMP echo ID或序号字段处理错误对比Wireshark里的ID和序号ping通但UDP收不到端口过滤条件写错检查目的端口比较逻辑UDP能收到但回复错误源IP/源MAC填错Wireshark里比较帧头字段吞吐量低于500Mbps上层数据接口位宽不足改为32bit输入提高逻辑效率随机丢包跨时钟域FIFO深度不足增大FIFO深度加丢包计数过一段时间后死机状态机进入未知状态加default分支恢复IDLE4.4 FPGA资源与性能分析这个设计到底占用多少资源综合完成之后我特意看了下资源报告。在Xilinx Artix-7 XC7A35T上整个UDP协议栈包括ARP、ICMP、UDP收发、CRC、FIFO的逻辑资源占用大约是LUT 1200个、FF 900个、BRAM 4个。这个资源占用量很小即使是最入门的FPGA芯片也能轻松放下剩下的资源足够跑一个简单的图像处理或者信号采集逻辑。时序方面关键路径是CRC32查表ROM的读取路径在-2速度等级的Artix-7上可以跑到250MHz以上远超千兆以太网需要的125MHz。所以这个设计不会成为系统的瓶颈。如果想进一步优化性能可以考虑把CRC32改成2字节或4字节并行处理这在万兆以太网设计中几乎是必须的但千兆场景下完全没有必要。对比一下用Xilinx官方IP核比如Ethernet MAC IP lwIP软核的资源占用光一个Triple Speed Ethernet核就要占用大概800个LUT和2个BRAM再加上MicroBlaze软核和lwIP协议栈的内存占用整体资源消耗大约是纯逻辑实现的5到10倍而延迟高了两个数量级。这就是为什么很多高性能数据采集和图像传输项目宁愿自己写精简UDP协议栈的原因。4.5 协议栈扩展从UDP到TCP的路径写完了UDP模块很多人会问下一步是不是可以上TCP。这个问题要分情况回答如果你的应用是视频传输、数据采集、命令控制这种对实时性要求高、丢一两个包可以容忍的场景UDP完全够用。但如果你的应用是配置文件下发、固件升级、要保证数据完整性的场景就必须考虑TCP。在现有UDP模块的基础上做TCP扩展工作量主要在三个方面第一发送方向需要实现拥塞控制不能无脑往里灌数据要根据ACK调整发送窗口第二接收方向需要实现乱序重排和重传机制FIFO要改成按序存储第三状态管理方面需要实现TIME_WAIT、CLOSE_WAIT这些连接状态状态机的复杂度会上一个台阶。老实说自己写一个健壮的TCP协议栈工作量大概是UDP的5倍以上普通项目不如直接上lwIP软核方案。4.6 实际测试结果与带宽分析最后分享一组实测数据。板子是Artix-7 RTL8211E的千兆PHY电脑端是Intel I219-V网卡直连网线Windows 10系统。iperf3 UDP测试结果单线程、1400字节载荷实测带宽925Mbps丢包率0%。ping延迟稳定在0.2ms以内。如果你要往板卡里传输图像数据我强烈建议在UDP数据里加上帧序号和帧计数。帧序号用来检测丢包帧计数用来还原帧的完整性。实测下来即便在满负荷传输时网络偶尔丢一个包接收端通过帧序号补发机制也能把画面恢复得基本完整视频流不会出现花屏或者卡死。还有个细节Windows系统默认的UDP接收缓冲区只有64KB如果电脑端的接收程序处理不及时很容易因为缓冲区溢出而丢包。用iperf3测吞吐时记得加-w 1M参数把接收缓冲区调大到1MB不然你测出来的结果会严重偏低于真实带宽。这个坑我在很多项目里都遇到过尤其在做大流量UDP接收时系统缓冲区的配置直接决定了你能接收多少数据。