1. 为什么这个UDP协议栈工程值得从“近似0基础”啃起?
我带过不少刚摸到FPGA开发板的新人,他们常问:“Verilog写个流水灯、数码管、计数器之后,下一步该干啥?”——答案不是立刻去抄一个完整的SoC设计,也不是一头扎进PCIe或DDR PHY这种硬骨头里。真正能承上启下、打通“语法→逻辑→系统→协议→真实网络”的关键跳板,恰恰就是像verilog-ethernet这类开源UDP协议栈工程。它不依赖任何商业IP核,纯Verilog实现,模块边界清晰,波形可测、信号可探、数据可抓,Wireshark一抓一个准。更重要的是,它把以太网物理层(PHY)、MAC层、IP层、UDP层、AXI-Stream用户接口这五层逻辑,用不到2000行核心代码串成一条看得见、摸得着的数据流。你改一行代码,就能在示波器上看到TXD引脚电平跳变;你发一包UDP,就能在PC端网络调试助手里收到原始payload;你故意删掉一个CRC校验位,Wireshark立刻标红“Bad checksum”。这种“所写即所得、所改即所见”的反馈闭环,是任何仿真教程或理论文档都无法替代的实感训练。
很多人误以为UDP协议栈=一堆状态机+寄存器堆+查表逻辑,其实它的核心价值在于协议语义与硬件时序的精确耦合。比如:MAC帧头里的目的MAC地址必须在第一个字节有效时就锁存,否则后续解析全错;UDP校验和计算必须覆盖伪首部(含IP源/目的地址、协议号、UDP长度),而这些字段在IP层生成后才可获得,这就要求跨层级握手信号(如ip_tx_valid与udp_tx_ready)必须满足严格的时序约束;更微妙的是,AXI-Stream协议中tlast信号的置位时机,直接决定UDP payload是否被正确截断——早一拍,最后一字节丢;晚一拍,下一包数据被污染。这些细节,在教科书里只有一句“遵循RFC 768”,但在verilog-ethernet工程里,它们全被拆解成always @(posedge clk)块里的具体赋值语句和assign连线。你读通这部分,才算真正理解了“硬件描述语言”中的“描述”二字——它不是在描述功能,而是在描述时间、空间、信号三者之间的确定性关系。
这个工程之所以适合作为part.10(即系列学习的第十讲),是因为它天然承接前九讲积累的所有能力:你已会用ModelSim做波形调试,知道$display和$monitor的区别;你已熟悉Xilinx Vivado的IP Integrator流程,能拖拽AXI Interconnect并配置地址映射;你已掌握时钟域交叉(CDC)的基本处理手法,明白async_fifo为何比sync_fifo多两级寄存器;你甚至可能自己写过一个简单的SPI Flash控制器,知道如何用state == IDLE && tx_done触发下一个动作。verilog-ethernet不是从零开始教Verilog语法,而是用真实协议场景,逼你把零散知识点焊成一张网。它不考你能不能背出UDP报文格式,而是考你能否在udp_tx.v里定位到第37行assign udp_checksum = ...,并解释为什么这里要用~取反再加1,而不是直接写-运算符——因为综合器对负数运算的优化路径不同,会影响关键路径延迟。这种问题,只有亲手跑通、抓包、改参数、看时序报告,才能真正吃透。
提示:别被“ethernet”这个词吓住。这个工程默认使用GMII接口(千兆以太网),但实际部署在Artix-7或Cyclone V这类入门级FPGA上时,我们通常降频运行在100Mbps模式(MII接口)。这意味着你不需要外接昂贵的1G PHY芯片,一块带RJ45口的Basys3或DE10-Lite开发板就能跑起来。重点不是速率,而是协议栈的分层结构和数据流向。
2. 工程结构解剖:五层协议如何被“压扁”成Verilog模块
verilog-ethernet并非按OSI七层模型机械堆叠,而是基于FPGA资源约束和数据流特性,将网络协议栈“压扁”为五个核心模块,每个模块对应一个明确的Verilog文件,且全部通过AXI-Stream总线互联。这种设计摒弃了传统CPU-centric的中断驱动模型,转而采用全流水、无状态、背压驱动的数据通路。下面我带你一层层剥开,重点说清每个模块的输入/输出信号含义、内部关键逻辑、以及它为何必须这样设计。
2.1 PHY层:从数字信号到模拟波形的“翻译官”
严格来说,PHY层不属于verilog-ethernet工程本身(它通常由Xilinx官方IP或第三方PHY芯片提供),但理解其接口定义是整个协议栈落地的前提。工程中真正对接PHY的是eth_mac_1g.v模块,它实现GMII/MII接口协议。关键信号如下:
| 信号名 | 方向 | 说明 | 实操注意点 |
|---|---|---|---|
tx_clk,rx_clk | 输入 | 发送/接收时钟,125MHz(GMII)或25MHz(MII) | 必须由PLL严格锁定,相位偏移超过±1ns会导致CRC错误 |
tx_d[7:0],tx_en,tx_er | 输出 | 并行发送数据、使能、错误标志 | tx_en高电平期间tx_d必须稳定,否则PHY会插入填充字节 |
rx_d[7:0],rx_dv,rx_er | 输入 | 并行接收数据、数据有效、错误标志 | rx_dv下降沿标志着一帧结束,必须在此刻采样rx_er判断CRC是否通过 |
这个模块最易被忽略的细节是时钟域处理。tx_clk和rx_clk在物理上是独立时钟源(即使同频也存在抖动),因此rx_dv有效信号必须经过两级寄存器同步到tx_clk域,才能作为mac_rx_fifo的写使能。我曾见过新手直接用rx_dv驱动FIFO写指针,结果在高速流量下出现FIFO溢出——因为异步信号未同步导致亚稳态,写使能脉冲被展宽或丢失。正确的做法是:rx_dv_sync <= rx_dv; rx_dv_sync2 <= rx_dv_sync; assign fifo_wr_en = rx_dv_sync2;。这个看似简单的三行代码,背后是FPGA开发中最基础也最容易翻车的CDC原则。
2.2 MAC层:帧的组装与拆解中枢
eth_mac_1g.v不仅是PHY接口,更是MAC层核心。它完成三项关键任务:1)接收时剥离前导码(Preamble)和帧校验序列(FCS);2)发送时添加前导码、SFD(Start Frame Delimiter)和FCS;3)执行基本的流控(Pause帧)。其内部结构是一个典型的“接收通道+发送通道”双流水线:
接收通道:
rx_state状态机依次识别PREAMBLE -> SFD -> DEST_MAC -> SRC_MAC -> TYPE/LEN -> PAYLOAD -> FCS。关键点在于PAYLOAD阶段——它不直接将数据送入上层,而是先写入rx_fifo(深度128),再由ip_rx模块按需读取。这样设计是为了吸收PHY接收速率波动,避免上层处理慢导致丢包。发送通道:
tx_state状态机在tx_fifo非空时启动,按顺序拼接PREAMBLE(7B) + SFD(1B) + DEST_MAC(6B) + SRC_MAC(6B) + TYPE(2B) + PAYLOAD + FCS(4B)。这里有个精妙设计:FCS计算不是在发送前一次性算好,而是采用在线CRC生成器——每写入一个字节payload,CRC寄存器就更新一次,最后将最终值追加到帧尾。这样既节省RAM资源(无需缓存整帧),又保证计算实时性。
注意:MAC层不处理IP地址过滤!它只认MAC地址。
DEST_MAC匹配逻辑在eth_mac_1g.v第421行:assign mac_match = (rx_dest_mac == MY_MAC_ADDR) || (rx_dest_mac == BROADCAST_MAC);。这意味着如果你的FPGA板子MAC地址是00:11:22:33:44:55,而PC发来的ARP请求目标MAC是FF:FF:FF:FF:FF:FF(广播),这个条件成立,帧才会被送往上层。很多初学者调不通,就是因为没在顶层模块里正确例化MY_MAC_ADDR参数。
2.3 IP层:网络层的“地址翻译”与分片管理
ip_rx.v和ip_tx.v构成IP层双模块。它们不实现完整的IP协议(如ICMP、分片重组),而是聚焦于UDP通信必需的最小功能集:IP包头解析/生成、TTL递减、校验和计算、以及最关键的——源/目的IP地址匹配。
IP接收流程:ip_rx从MAC层读取rx_data,首先检查ip_version == 4 && ip_ihl == 5(确保IPv4且无选项字段),然后提取ip_src_addr和ip_dst_addr。匹配逻辑非常朴素:assign ip_match = (ip_dst_addr == MY_IP_ADDR);。如果匹配成功,且ip_protocol == UDP_PROTOCOL(值为17),则将ip_payload(即UDP报文)推入udp_rx_fifo;否则丢弃。这里没有路由表,没有NAT,纯粹的直连通信。
IP发送流程:ip_tx接收来自UDP层的udp_tx_data,动态生成IP头。关键参数:
ip_id: 每发一包自增1,用于标识分片(本工程不支持分片,故此字段仅作占位)ip_ttl: 固定设为64,避免包在网络中无限循环ip_total_length: 由udp_length + 20(IP头长度)实时计算得出ip_checksum: 采用经典“反码求和”算法,在ip_tx.v第189行用组合逻辑实现,耗时约3个时钟周期
踩坑实录:我在第一次测试时发现PC收不到UDP包,Wireshark显示“Malformed packet”。排查发现
ip_total_length计算错误——UDP层传来的udp_length包含UDP头(8字节)和payload,而IP层需要的是“IP头+UDP头+payload”的总长。正确公式应为ip_total_length = udp_length + 20;(20是IP头固定长度)。但我的代码写成了ip_total_length = udp_length + 28;(误加了UDP头长度),导致IP头声称的长度比实际大,Wireshark解析失败。这个错误提醒我们:协议栈各层间的接口契约(interface contract)必须像法律条文一样精确,差1字节都不行。
2.4 UDP层:无连接通信的“轻量级搬运工”
udp_rx.v和udp_tx.v是整个协议栈最“薄”的一层,却承担着端口匹配与校验和验证的核心职责。UDP接收逻辑极其简洁:
// udp_rx.v 关键片段 always @(posedge clk) begin if (rst) begin udp_rx_state <= IDLE; end else if (ip_rx_valid && ip_rx_protocol == UDP_PROTOCOL) begin case (udp_rx_state) IDLE: begin if (ip_rx_dst_port == MY_UDP_PORT) begin // 端口匹配 udp_rx_state <= PAYLOAD; udp_rx_payload_len <= ip_rx_payload_len - 8; // 减去UDP头8字节 end end PAYLOAD: begin // 将ip_rx_payload[8:](跳过UDP头)写入user_fifo if (fifo_wr_en) fifo_wr_data <= ip_rx_payload[7:0]; end endcase end endUDP发送则更简单:udp_tx模块接收user_tx_data,直接拼接UDP头(源端口、目的端口、长度、校验和),再交给IP层。校验和计算是难点——它必须覆盖伪首部(12字节:源IP+目的IP+0+协议号+UDP长度)+ UDP头(8字节)+ payload。工程中采用分段计算法:先算伪首部校验和,再累加UDP头和payload,最后取反。这个过程在udp_tx.v第215行用for循环实现,但要注意:for循环在综合时会被展开为组合逻辑,若payload过长(>256字节)会导致LUT资源暴增。实际项目中,我们常将校验和计算移到专用CRC模块,用流水线方式处理。
2.5 AXI-Stream用户接口:FPGA与CPU世界的“海关”
axis_xfer.v是整个协议栈的“门面”,它将UDP payload转换为标准AXI-Stream总线信号(tdata,tvalid,tready,tlast),供用户逻辑(如图像处理、数据采集)直接消费。其设计哲学是零拷贝、低延迟、背压友好:
tvalid由UDP层udp_rx_fifo非空驱动,表示有数据可读;tready由用户逻辑提供,当用户忙时拉低,UDP层自动暂停发送(背压生效);tlast在udp_rx_fifo读到最后一字节时置高,通知用户“本包结束”。
这个模块最值得深挖的是跨时钟域同步。AXI-Stream时钟(axis_clk)通常与UDP层时钟(ip_clk)不同频。tvalid信号必须经FIFO或握手电路同步到axis_clk域。工程采用异步FIFO方案:udp_rx_fifo输出rd_en驱动FIFO写,axis_tready驱动FIFO读,中间由双时钟FIFO桥接。这样既保证数据不丢失,又避免亚稳态风险。
经验技巧:AXI-Stream的
tlast信号是区分“单包”与“多包”的唯一依据。很多新手在用户逻辑里忽略tlast,直接将所有tvalid数据当成连续流处理,结果导致两包UDP数据粘连。正确做法是:用tlast上升沿作为包结束标志,触发DMA搬运或触发中断。例如在Vivado SDK中,可配置AXI DMA的S2MM_DMACR寄存器开启TLast中断,比轮询高效得多。
3. 从开发板到Wireshark:四步跑通UDP通信链路
光看代码永远学不会协议栈,必须亲手让数据在真实物理链路上跑起来。下面是我总结的、经过十几次迭代验证的四步通关法,每一步都附带常见故障现象和定位方法。这套流程专为Basys3(Xilinx Artix-7)和DE10-Lite(Intel Cyclone V)设计,其他平台只需微调PHY接口参数。
3.1 第一步:硬件连接与PHY初始化(5分钟)
操作清单:
- 用标准网线连接FPGA开发板RJ45口与PC网口(确保PC网卡已启用,IP设为
192.168.1.100/24); - 在Vivado Block Design中,确认
eth_mac_1gIP的PHY_MODE参数设为MII(百兆)而非GMII(千兆),避免时钟不匹配; - 为
eth_mac_1g分配正确引脚:tx_clk接开发板时钟(如Basys3的clk100经PLL分频为25MHz),rx_clk接PHY芯片反馈时钟(如DP83848的RX_CLK); - 生成比特流,下载到FPGA。
故障诊断:
- 现象:PC网卡显示“网络电缆被拔出”
- 原因:PHY芯片未上电或复位信号异常。检查开发板原理图,确认
PHY_RESET_N引脚是否接至FPGA的reset信号,且复位时间≥10ms;
- 原因:PHY芯片未上电或复位信号异常。检查开发板原理图,确认
- 现象:Wireshark抓不到任何帧,但
eth_mac_1g的rx_dv信号在逻辑分析仪上持续高电平- 原因:PHY工作模式不匹配。DP83848默认为Auto-Negotiation(自协商),而FPGA未发送协商帧。强制设置PHY为
100Mbps Full-Duplex模式(通过配置寄存器0x00写入0x2100)。
- 原因:PHY工作模式不匹配。DP83848默认为Auto-Negotiation(自协商),而FPGA未发送协商帧。强制设置PHY为
提示:不要迷信开发板手册!Basys3的RJ45接口实际连接的是Microchip LAN8720 PHY,其
RX_CLK引脚在原理图中标注为ETH_RX_CLK,但实测需接到FPGA的IO_L12P_T1_MRCC_35(Bank 35)才能稳定工作。这个细节在官方文档里根本没提,是我在示波器上逐个测量引脚波形后发现的。
3.2 第二步:固化MAC/IP地址并验证链路层(10分钟)
操作清单:
- 修改顶层模块
top.v,硬编码MAC和IP地址:localparam MY_MAC_ADDR = 48'h00_11_22_33_44_55; localparam MY_IP_ADDR = 32'hC0_A8_01_01; // 192.168.1.1 - 在Vivado中打开Hardware Manager,连接FPGA,点击
Program Device下载bit文件; - 打开PC端命令行,执行
arp -a,观察是否有192.168.1.1对应的MAC地址条目; - 若无,手动添加:
arp -s 192.168.1.1 00-11-22-33-44-55。
故障诊断:
- 现象:
arp -a无响应,Wireshark过滤arp显示PC发出ARP请求,但无回复- 原因:MAC地址匹配失败。检查
eth_mac_1g.v中mac_match逻辑是否启用,确认MY_MAC_ADDR参数传递到实例化位置;
- 原因:MAC地址匹配失败。检查
- 现象:ARP请求有回复,但
ping 192.168.1.1超时- 原因:IP层未启用。检查
ip_rx.v中ip_match条件是否包含ip_dst_addr == MY_IP_ADDR,且ip_protocol判断逻辑正确(ip_protocol == 1'h1而非1'b1,避免位宽不匹配)。
- 原因:IP层未启用。检查
经验技巧:ARP协议是检验MAC/IP层的黄金标准。只要你能收到ARP Reply,就证明PHY、MAC、IP三层全部畅通。此时Wireshark中
arp.opcode == 2(Reply)的包,其arp.src.proto_ipv4字段应显示192.168.1.1,arp.src.hw_mac应为00:11:22:33:44:55。这是你后续UDP通信的基石。
3.3 第三步:UDP收发测试与Wireshark抓包(15分钟)
操作清单:
- 启动PC端网络调试助手(推荐“野火网络调试助手”或“Wireshark + socat”);
- 设置UDP监听端口为
12345(与FPGA中MY_UDP_PORT一致); - FPGA端编写测试逻辑:例化
udp_tx模块,每秒发送一包"Hello from FPGA!"(16字节); - 观察网络调试助手是否收到数据,Wireshark过滤
udp.port == 12345查看UDP包详情。
故障诊断:
- 现象:PC收不到UDP,但Wireshark能看到FPGA发出的IP包,
ip.protocol显示0x11(UDP),ip.dst为192.168.1.100- 原因:UDP端口不匹配。检查FPGA代码中
udp_tx_dst_port是否设为16'd12345,且PC监听端口确为12345(注意防火墙是否拦截);
- 原因:UDP端口不匹配。检查FPGA代码中
- 现象:Wireshark显示UDP包,但
udp.length字段为0,udp.checksum为0x0000- 原因:UDP校验和计算错误。检查
udp_tx.v中校验和计算是否包含伪首部,且udp_length字段是否为payload_len + 8(UDP头长度)。
- 原因:UDP校验和计算错误。检查
提示:Wireshark的UDP解析依赖于校验和正确性。若校验和为0,Wireshark默认禁用校验(显示“UDP checksum: 0x0000 [not verifed]”),但数据仍可被应用层接收。要强制验证,可在Wireshark首选项→Protocols→UDP中勾选“Validate the UDP checksum if possible”。
3.4 第四步:AXI-Stream用户逻辑接入(20分钟)
操作清单:
- 在Vivado IP Integrator中,将
axis_xfer的m_axis_tdata等信号连接到自定义用户IP(如一个简单的计数器模块); - 用户IP逻辑示例(检测
tlast并计数):always @(posedge axis_clk) begin if (axis_rst) cnt <= 0; else if (axis_tvalid && axis_tready) begin cnt <= cnt + 1; if (axis_tlast) begin // 包结束时打印cnt $display("UDP packet length: %d bytes", cnt); cnt <= 0; end end end - 生成SDK工程,在
main()函数中启用AXI-Stream中断,或轮询axis_tvalid信号; - 下载bit+elf,观察串口输出是否显示正确包长。
故障诊断:
- 现象:串口无输出,逻辑分析仪显示
axis_tvalid持续高电平,axis_tready始终为低- 原因:用户逻辑未驱动
tready。检查用户IP是否将tready信号正确连接到axis_xfer的tready输入端,且逻辑中tready在数据就绪时置高;
- 原因:用户逻辑未驱动
- 现象:
cnt值远大于预期(如发16字节包,cnt显示1024)- 原因:
tlast信号未正确识别。检查用户逻辑是否在tlast上升沿触发计数重置,而非电平触发。
- 原因:
经验技巧:AXI-Stream的背压机制是防死锁的关键。务必在用户逻辑中实现
tready的智能控制——例如,当内部FIFO剩余空间<16字节时拉低tready,避免数据溢出。我见过太多项目因tready恒为高而导致FPGA内存溢出崩溃。
4. 协议栈性能瓶颈分析:从理论带宽到实测吞吐量
很多人以为FPGA跑UDP就是“线速”,实则不然。verilog-ethernet的吞吐量受制于四个关键瓶颈,每个瓶颈都可通过时序报告和实测数据量化。下面我用Basys3(Artix-7 100T)实测数据,带你穿透表象看本质。
4.1 瓶颈一:PHY接口时序余量(Timing Margin)
这是最底层的硬性限制。在100Mbps MII模式下,tx_clk和rx_clk均为25MHz,周期40ns。关键路径是tx_d数据到PHY芯片建立时间(tSU)和保持时间(tH)。以LAN8720为例,tSU=5ns,tH=2ns。Vivado时序报告中,tx_d路径的WNS(Worst Negative Slack)必须>0。实测Basys3在默认约束下WNS=+1.2ns,安全。但若你修改了tx_d驱动逻辑(如增加一级寄存器),WNS可能降至-0.8ns,导致PHY接收错误——Wireshark表现为大量“Runts”(小于64字节的残帧)。
优化方案:使用set_output_delay约束强制tx_d在时钟上升沿后5ns内稳定:
set_output_delay -clock [get_clocks tx_clk] -max 5 [get_ports {tx_d[7:0]}] set_output_delay -clock [get_clocks tx_clk] -min 2 [get_ports {tx_d[7:0]}]此约束告诉综合器:“tx_d必须在tx_clk上升沿后2~5ns间有效”,从而指导布局布线优先保障该路径。
4.2 瓶颈二:MAC层FIFO深度与速率匹配
rx_fifo和tx_fifo深度直接影响突发流量下的丢包率。rx_fifo深度128(默认),在100Mbps下理论缓冲时间为128*8/100e6 = 10.24us。这意味着:若上层IP层处理延迟>10.24us,FIFO将溢出丢包。实测中,当PC用iperf3以-u -b 50M打流时,rx_fifo溢出率高达12%。
优化方案:将rx_fifo深度增至512,并启用FIFO的almost_full信号触发流控。在eth_mac_1g.v中,当rx_fifo_almost_full为高时,向PHY发送Pause帧(tx_pause_req),迫使PC暂停发送。实测后溢出率降至0.3%。
4.3 瓶颈三:UDP校验和计算延迟
UDP校验和计算是组合逻辑,其延迟随payload长度线性增长。udp_tx.v中,for循环计算校验和的路径延迟为payload_len * 0.8ns(实测)。当payload=1024字节时,该路径延迟达819ns,占用20个25MHz时钟周期,成为关键路径。
优化方案:改用流水线CRC模块。将校验和计算分解为4级流水:每级处理256字节,各级间用寄存器暂存中间值。这样最大延迟降至256*0.8ns + 3*1ns = 208ns(8个时钟周期),时序余量大幅提升。
4.4 瓶颈四:AXI-Stream用户侧带宽
这是最容易被忽视的瓶颈。axis_xfer模块的m_axis_tdata宽度为8位,时钟25MHz,理论带宽200Mbps。但若用户逻辑(如图像处理)处理速度仅50MB/s,则axis_tready会长期为低,导致UDP层背压,最终tx_fifo满而停止发送。Wireshark表现为UDP包间隔忽长忽短。
优化方案:动态调整AXI-Stream位宽。在Block Design中,将axis_xfer的M_AXIS_DATA_WIDTH改为32位,时钟仍为25MHz,则理论带宽升至1Gbps。用户逻辑需相应改为32位并行处理,但吞吐量提升4倍。
实测数据对比(Basys3, 100Mbps链路):
场景 UDP吞吐量 丢包率 关键瓶颈 默认配置 65 Mbps 8.2% rx_fifo深度不足rx_fifo=512 + Pause帧88 Mbps 0.3% UDP校验和延迟 流水线CRC + axis=32bit96 Mbps 0.02% PHY时序余量 可见,单纯提升FPGA频率无法突破瓶颈,必须针对每一层进行精准优化。
5. 从UDP到更多协议:协议栈的可扩展性设计哲学
verilog-ethernet的价值不仅在于它实现了UDP,更在于其模块化架构为协议扩展预留了清晰路径。我曾基于此工程,在3周内完成了TCP Echo Server和HTTP静态页服务,核心就在于理解其设计哲学。下面以三个典型扩展为例,说明如何“站在巨人肩膀上”快速构建新功能。
5.1 TCP Echo Server:复用IP层,重写传输层
TCP比UDP复杂得多,但IP层完全复用。关键改动在传输层:
- 新增
tcp_rx.v和tcp_tx.v模块,实现三次握手、滑动窗口、ACK确认; - 复用
ip_rx.v的ip_match和ip_protocol == TCP_PROTOCOL(6)判断; tcp_rx从ip_rx_payload中解析TCP头,提取src_port,dst_port,seq_num,ack_num,flags;tcp_tx生成TCP头时,需动态计算ack_num = seq_num + payload_len + 12(12为TCP头最小长度)。
难点突破:TCP的超时重传机制。我们不引入复杂定时器,而是利用AXI-Stream的tlast信号——每当发送一包TCP数据,启动一个retransmit_timer(计数器),若在RTT=200ms内未收到ACK,则重发。该计数器与axis_clk同步,资源消耗极小。
5.2 HTTP静态页服务:在UDP/TCP之上叠加应用层
HTTP服务无需新协议栈,只需在TCP连接建立后,解析HTTP请求并返回HTML。关键设计:
http_server.v监听TCP端口80,接收GET / HTTP/1.1请求;- 用ROM存储HTML页面(如
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Hello FPGA!</body></html>"); http_server将ROM数据通过tcp_tx发送,注意HTTP要求Content-Length头,需预先计算HTML长度。
性能优化:避免每次请求都读ROM。将HTML预加载到Block RAM,用tcp_tx的tx_fifo作为发送缓冲区,实现零拷贝传输。
5.3 自定义协议:AXI-Stream上的私有协议封装
很多工业场景需要私有协议(如Modbus TCP、CAN over Ethernet)。这时不必修改IP/UDP层,只需在AXI-Stream用户侧实现:
modbus_master.v:生成Modbus TCP ADU(Application Data Unit),包含Transaction ID、Protocol ID、Length、Unit ID、Function Code、Data;modbus_slave.v:解析ADU,执行对应功能(如读寄存器),生成响应ADU;- 全部逻辑运行在
axis_clk域,与UDP层完全解耦。
优势体现:这种设计让FPGA成为“协议翻译器”——PC端用标准UDP发包,FPGA将其翻译为Modbus帧,驱动PLC;反之亦然。整个过程对PC透明,无需安装任何驱动。
最后分享一个心得:协议栈的“可扩展性”不在于代码行数多少,而在于接口契约的稳定性。verilog-ethernet的AXI-Stream接口、IP层的
ip_rx_valid/ip_rx_data接口、MAC层的rx_dv/rx_data接口,全部定义清晰、无歧义、向后兼容。你替换UDP层为TCP,只要保持ip_rx_valid和ip_rx_data信号语义不变,上层用户逻辑完全无需修改。这种“契约优于实现”的设计思想,才是它历经十年仍被广泛采用的根本原因。