资讯动态

FPGA实现UDP回环:verilog-ethernet协议栈实战详解

发布时间:2026/9/18 19:42:10 来源:尧图企业网站定制
把一块FPGA开发板用网线连到电脑然后在电脑上发一个UDP包让板子收到后再原样弹回来——这个在很多人看来是“搞网络的人才会碰”的事其实用FPGA做也不难关键是要找对工程、理清协议栈的层次。这期笔记就是我以一个接近零基础的视角把verilog-ethernet这个开源UDP协议栈工程啃下来的完整过程。适合刚学完Verilog语法、想把FPGA和以太网串起来的新手也适合那些看开源代码总是“看懂了但不会用”的朋友。verilog-ethernet是Alex Forencich维护的一个开源项目里面包含了从10G到1G的MAC、ARP、IP、UDP等一堆以太网相关模块。我最开始打开仓库的时候其实有点慌文件一大堆名字又长根本不知道从哪看起。但真正花时间捋下来之后会发现它的分层非常清楚PHY在最底下往上是以太网MAC层再往上是ARP和UDP这类网络层协议模块最上面才是用户逻辑。只要理解了每一层的职责整个工程就像搭积木一样选自己需要的模块接上去就行。我这次的目标很明确用一块常见的千兆以太网开发板把verilog-ethernet里的UDP模块跑起来实现和PC之间的双向收发。下面就从环境选型、架构拆解、仿真验证、上板调试、常见坑这五个方向把我实际操作中记录下来的东西完整分享出来。1. 为什么选verilog-ethernet而不是自己写协议栈1.1 三种方案的现实对比在动手之前其实有三个选择摆在面前第一是自己从零写MAC和UDP第二是直接用厂商IP核第三就是基于verilog-ethernet这种开源工程来改。我最初还真试着写过一小段UDP发送逻辑结果发现一个很残酷的现实自己写MAC的CRC校验还好说但要做到千兆速率下不丢帧、时序收敛、跨时钟域处理好真不是新手两周能搞定的。我也对比过Xilinx和Altera官方的以太网IP核。厂商IP核好处是专业、稳定但坏处也很明显接口通常带着一堆AXI4-Lite配置寄存器状态机复杂得多而且要想在IP核外面接UDP逻辑还得自己搞定AXI-Stream的时序。对只想要一个“能收发UDP就行”的入门需求来说官方的全功能IP核反而显得笨重。verilog-ethernet这个开源工程则精准地卡在中间它的MAC层是完整的支持GMII、RGMII等主流接口数据通路统一用AXI-Stream对外这样上层协议模块ARP、IP、UDP就能像流水线一样串起来。我只需要关心自己这部分用户逻辑不用去碰PHY芯片的寄存器配置细节。1.2 工程结构里藏着设计思路把仓库克隆下来之后第一件事是看rtl目录下的文件列表。里面分了几个关键模块eth_mac_1g是千兆MACeth_mac_1g_fifo是带FIFO缓冲的MAC版本eth_udp、eth_arp、eth_ip是网络层模块还有eth_axis_rx、eth_axis_tx负责AXI-Stream接口格式转换。这个文件划分本身就说明了设计套路每个模块只干一件事模块之间用标准接口互相连接。比如eth_mac_1g只管把MAC帧转换成AXI-Stream流或者反过来把流打包成MAC帧eth_udp只关心UDP头收到数据就剥离头交给上层要发送就加上头往下层传。这种解耦方式让我这种新手读代码时非常舒服想查哪个问题就直奔对应文件不用在一大坨代码里找逻辑。我最开始以为要搞懂所有模块才能跑通后来才发现最简单的UDP回环工程只需要四个核心组件eth_mac_1g_fifo、eth_axis_rx、eth_axis_tx、eth_udp。如果还要让PC能发现FPGA的MAC地址还要加一个eth_arp。模块之间用标准的8位AXI-Stream接口连起来数据流方向清清楚楚。2. 吃透架构从RGMII引脚到UDP报文要穿过哪些层2.1 物理层接口的选型与区别verilog-ethernet里最常用的MAC接口有两种——GMII和RGMII。GMII是8位数据接口在125MHz时钟下满跑就能达到1Gbps但是占用的引脚太多了大概24根左右。RGMII则把数据线压缩到4位利用时钟的双沿采样这样可以用更少的引脚跑同样的带宽是绝大多数千兆PHY芯片和FPGA开发板的默认选择。我用的开发板上PHY芯片是常见的RTL8211系列和FPGA之间走的就是RGMII接口。引脚分配的时候要特别留意RGMII的TX_CLK、TX_CTL、TXD[3:0]是FPGA输出到PHY的RX_CLK、RX_CTL、RXD[3:0]是PHY输出给FPGA的。有个容易搞混的地方是RGMII的时钟沿是错开的接收数据在时钟的双沿都有效所以FPGA内部必须用IDDR原语或者库里的专用模块来捕获不能简单地在时钟上升沿打一拍。对新手来说这些引脚只要在约束文件里配对了基本不需要自己写底层时序逻辑因为eth_mac_1g内部已经把这些用原语封装好了。但你要理解整个过程的大致时序PHY发来的RXD和RX_CTL在RX_CLK的双沿到达MAC模块先在内部合成出“时钟使能8位数据”的格式再经过FIFO缓冲到用户时钟域。2.2 数据是怎么从AXI-Stream流变成UDP报文的AXI-Stream把数据打包成一个个“帧”核心信号有tvalid、tready、tdata、tlast、tkeep。tvalid和tready握手表示数据有效tlast标志这一帧的最后一个周期tkeep则告诉接收方当前周期里哪些字节是有效的。这个概念有点像快递打包tdata是车厢tkeep告诉你哪些位置装了货tlast是这辆车的尾灯。eth_axis_tx模块负责把用户给的AXI-Stream流转换成MAC层能处理的格式反过来eth_axis_rx负责把MAC层收到的帧还原成AXI-Stream流。这两个模块本质上是在做接口适配让用户逻辑不用关心MAC帧的前导码和FCS字段。而eth_udp模块则是在MAC帧和UDP报文之间做翻译。发送方向用户把UDP载荷和目的IP、目的端口交给eth_udp它自动在前面加上8字节UDP头、20字节IP头再交给MAC层加以太网头接收方向收到一帧数据后先由内部逻辑检查IP头、UDP头校验源IP、目的IP、源端口、目的端口是否匹配匹配上了才把载荷部分通过AXI-Stream输出给用户。2.3 ARP模块让电脑能找到FPGA这里有个非常关键的细节PC上发送UDP包之前操作系统会先想办法知道目标的MAC地址。如果目标IP不在本机ARP缓存表里系统会先发一个ARP广播请求询问“谁的IP是192.168.1.10请告诉我你的MAC地址”。verilog-ethernet里的eth_arp模块就是干这件事的。它会自动识别发给本机的ARP请求然后回一个ARP应答包告诉对方“本机的MAC是xx:xx:xx:xx:xx:xx”。同时它也会缓存学习到的对端MAC地址这样当上层UDP模块要发数据给某个IP时可以先去查ARP缓存如果缓存里没有还可以主动发ARP请求去询问。这个模块我一开始没接上结果实测的时候电脑发了一堆ARP请求开发板毫无反应。后来把eth_arp加进工程PC端的网络连接立刻就不一样了。所以如果你想用PC直接和FPGA通信ETH_ARP几乎是不能省的一环。3. 先把仿真跑通用Testbench验证UDP回环逻辑3.1 选对仿真工具别一上来就上板我在学习过程中踩过最大的一个坑就是太心急想上板结果调试效率极低。后来老老实实回到仿真把整个UDP收发流程在电脑上模拟一遍一切就顺畅多了。仿真工具方面用Vivado自带xsim或者ModelSim都可以verilog-ethernet仓库里本身就带了不少testbench直接打开跑就行。我这里的做法是单独建一个顶层tb文件例化一个极简的UDP回环用户逻辑收到eth_udp输出的AXI-Stream数据后原封不动地再通过eth_udp的发送通道发回去。Testbench中模拟一个PHY模型用来代替真实的PHY芯片产生时钟和数据。3.2 一个最简的用户回环逻辑用户逻辑其实很简单。核心就是下面这样一个双向通道的搬运module udp_loopback ( input wire clk, input wire rst, // 接收通道 input wire [7:0] rx_axis_tdata, input wire rx_axis_tvalid, output reg rx_axis_tready, input wire rx_axis_tlast, // 发送通道 output reg [7:0] tx_axis_tdata, output reg tx_axis_tvalid, input wire tx_axis_tready, output reg tx_axis_tlast ); reg [7:0] buf; reg have_data; always (posedge clk) begin if (rst) begin rx_axis_tready 1b0; tx_axis_tdata 8d0; tx_axis_tvalid 1b0; tx_axis_tlast 1b0; have_data 1b0; end else begin // 接收方向能收就收并把数据打进发送侧 if (rx_axis_tvalid rx_axis_tready) begin buf rx_axis_tdata; have_data 1b1; end else if (have_data tx_axis_tready) begin tx_axis_tdata buf; tx_axis_tvalid 1b1; tx_axis_tlast rx_axis_tlast; // 帧尾直接透传 have_data 1b0; end else begin tx_axis_tvalid 1b0; end end end endmodule这段代码的意思是只要接收通道上有数据且自己准备好接收就把这个字节暂存起来并在下一个周期由发送通道输出。严格来说这个“回环”不是无缝的中间多了一个节拍的延迟但作为验证完全够用。实际工程项目里通常用FIFO做缓冲避免数据来太快时丢掉。3.3 仿真中到底要观察哪些信号仿真不是跑完就算关键是要会看波形。我总结了自己重点盯的几个点eth_udp的rx_udp_valid和rx_udp_ready握手是否符合预期rx_udp_valid拉高的同时rx_udp_ready也拉高表示数据被有效接收。tx_udp_valid和tx_udp_ready的握手过程是否完整发送侧如果对方还没准备好就拉高tvalid可能会卡住。tlast信号每一帧末尾必须准确出现一次否则接收方的状态机可能认为帧不完整直接丢弃整包数据。对着工程里的仿真脚本把MAC帧的确定性数据比如固定源MAC、目的MAC解析出来确认帧内容符合预期。很多新手在仿真阶段遇到帧卡住不动多半就是握手信号写错了。你光看valid拉高没用必须确认ready也是拉高的否则数据传输就是不成立的。4. 上板实测把PC和FPGA用网线连起来4.1 硬件连线和基础配置仿真通过之后就可以上板实测了。准备一根网线一头插开发板上的RJ45口另一头直接插电脑的网口。然后在电脑上把以太网适配器的IP地址设置成静态IP例如192.168.1.100子网掩码255.255.255.0。FPGA工程的IP地址可以设成192.168.1.10端口号选一个不常用的比如8080。在综合之前必须检查引脚约束。不同的开发板PHY引脚完全不同这个不能照抄别人的工程一定要对着自己板的原理图确认。我因为引脚没配对上板以后发现link灯都不亮查了半天才发现有两根引脚反了。RGMII接口的引脚命名通常是ETH_RXC、ETH_RXCTL、ETH_RXD[3:0]、ETH_TXC、ETH_TXCTL、ETH_TXD[3:0]。另外PHY芯片的时钟通常需要外部晶振供给但有些开发板设计成交由FPGA输出一个125MHz时钟给PHY这个要看具体板子的设计。如果PHY没有时钟link状态会一直起不来。4.2 用网络调试助手验证UDP回环上板开机后先用Wireshark抓包看有没有ARP请求和应答。正常情况下电脑会先发送一个广播ARP请求询问192.168.1.10的MAC地址开发板收到后会立刻回一个ARP应答。这个流程跑通了说明PHY、MAC、ARP模块工作正常。接着打开网络调试助手协议选择UDP本地端口随意远程IP填192.168.1.10远程端口填工程里配置的端口号点击“打开”然后发送一串测试数据比如“hello_fpga”。如果一切正常调试助手的接收窗口里会原样弹回这串数据。我实测过程中遇到一次非常诡异的现象数据偶尔能收到但内容里多了一两个字节的乱码。后来发现是发送端数据长度处理有问题eth_udp发送通道在用户发送帧时必须用tlast精确标识帧结束并且tkeep在最后一个周期要正确表示有效的字节数。UDP是数据报协议每一帧的长度必须在发送时就确定好不像TCP有流的概念可以随便切分。4.3 用Wireshark验证协议细节网络调试助手能跑通已经说明核心链路是好的但如果想更深一层理解协议建议同时打开Wireshark抓包来看。抓包结果里能看到完整的以太网帧目的MAC、源MAC、类型字段0x0800表示IPv4、IP头、UDP头、载荷、FCS校验。我最初一直很好奇FPGA发出来的帧结构对不对但看Wireshark的解析结果会直接显示“Destination unreachable”之类信息所以其实不太需要自己去分析CRC对不对——硬件网卡已经帮你做了FCS校验。同时也要注意Wireshark看到的报文和物理层实际跑的字节序都是一致的如果帧解析出来IP头校验和不对那大概率是IP头里的某个字段写错了。5. 实操中遇到的坑和排查方法5.1 板子link灯亮了但PC永远ping不通FPGA这是新手最容易困惑的问题。注意verilog-ethernet里的eth_arp模块默认只会响应ARP请求不支持ICMP协议所以用ping这个工具是测不通的。ping走的是ICMPUDP协议栈并不解析ICMP报文。就算PC能通过ARP获知FPGA的MAC地址ping也永远无法得到回应这并不代表工程有问题。正确验证方式就是用UDP调试工具直接发包。如果UDP数据能收能发那整个链路就是通的。5.2 PC发UDP给FPGAFPGA没反应排查思路按顺序走先抓包确认PC端有没有收到ARP响应。如果没有响应说明问题大概率在PHY的配置或者引脚连接上。如果ARP响应正常但还是收不到UDP数据那就要看eth_udp模块里配置的IP地址和端口是否和PC设置一致。这里容易踩坑的是端口号冲突。有些开发板例程默认用某个端口如果你调试工具把本地端口和远程端口设成同一个可能没问题但一旦设错报文就会被eth_udp模块默默丢弃因为它没找到匹配的端口过滤规则。5.3 数据能通但偶尔会有乱码或多字节这个问题的根源通常在AXI-Stream的tkeep信号上。tkeep告诉接收方当前这个时钟周期里tdata的哪些字节是有效的。如果发送侧在最后一拍没有把tkeep按实际数据长度拉高/拉低接收方就可能把无效字节当成有效数据读进来。还有一种情况是跨时钟域问题。MAC层的时钟和用户逻辑时钟频率不同FIFO起到缓冲作用但FIFO的读、写使能时序如果没调好也可能产生半个字的错位。遇到这种问题先检查用户侧时钟是不是和eth_udp模块使用的时钟一致不一致就老老实实加异步FIFO别自己用寄存器硬扛。5.4 ARP缓存超时导致主机找不到目标机PC操作系统的ARP缓存是有老化时间的通常几十秒到几分钟就会消失。如果你把FPGA断电重启电脑可能还在用旧的ARP缓存而FPGA重启后状态已经清空此时PC会直接发UDP包但目的MAC是旧的FPGA会识别不出来。解决方式很简单在PC上执行arp -d清空ARP缓存或者在FPGA工程里给eth_arp模块加上主动发送ARP请求的逻辑保证每次发送UDP之前先确认对方MAC还存在。这也是很多网络设备设计时都会做的链路探测。5.5 跨时钟域带来的偶发丢包verilog-ethernet在MAC层内置了FIFO但用户逻辑和eth_udp模块之间的时钟域关系仍然要注意。很多开发板的用户逻辑时钟用的是125MHz而PHY时钟也是125MHz看起来同频但两者可能来自不同时钟源相位关系不确定。稳妥的做法是在设计里把用户逻辑的时钟也用同一个PLL产生并让MAC层的TX clock和RX clock都统一到同一相位域必要的时候用异步FIFO彻底隔离。我这次因为比较赶一开始直接沿用了开发板例程中提供的clk125和reset_n结果出现了偶发的丢包。后来把用户逻辑也切到和MAC RX侧同一个125MHz时钟下再经过一个小FIFO做缓冲丢包率就明显降下来了。6. 从代码到板的完整梳理一个最小可用的UDP工程该长什么样6.1 工程顶层模块怎么拼最小工程在顶层需要例化五个部分时钟管理模块PLL/MMCM、复位同步模块、eth_mac_1g_fifo、eth_axis_rx、eth_axis_tx、eth_udp和eth_arp。这里有个细节eth_mac_1g_fifo本身已经带了发送和接收FIFO用户可以不用再专门例化额外的跨时钟域FIFO但前提是PHY和用户逻辑共用同一个时钟域。下面是一个简化版顶层结构示意// 伪代码仅展示模块连接关系 pll u_pll (.clkin(sys_clk), .clk125(clk125), .clk125_90(clk125_90)); eth_mac_1g_fifo u_mac ( .gtx_clk(clk125), .rx_clk(rx_clk_from_phy), .rgmii_tx(rgmii_tx), .rgmii_rx(rgmii_rx), .axis_rx_tdata(axis_rx_tdata), .axis_rx_tvalid(axis_rx_tvalid), .axis_rx_tlast(axis_rx_tlast), .axis_rx_tready(axis_rx_tready), .axis_tx_tdata(axis_tx_tdata), .axis_tx_tvalid(axis_tx_tvalid), .axis_tx_tlast(axis_tx_tlast), .axis_tx_tready(axis_tx_tready) ); // eth_udp 和 eth_arp 再在此基础上对接关于时钟eth_mac_1g需要的gtx_clk是125MHz如果PHY工作模式是RGMII to GMII这个时钟在FPGA内部产生即可而接收侧的rx_clk是从PHY的RX_CLK引脚过来的。这两个时钟在本质上也可以同源但为了安全建议保持原样连接不要随便对调。6.2 状态机设计和发送流程除开POR上电复位用户侧UDP发送逻辑的核心是一个简单状态机IDLE状态等待上层触发发送组好UDP头拉高请求信号SEND_HEADER状态依次输出目的MAC、源MAC、类型字段、IP头、UDP头SEND_DATA状态按AXI-Stream协议把用户数据逐个字节输出SEND_LAST状态拉高tlast表示帧结束。看起来简单实际上帧头部分的每个字节都是有讲究的。以太网帧头目的MAC和源MAC各6字节类型0x0800IP头固定20字节其中版本是4首部长度是5协议号是17UDPUDP头固定8字节分别是源端口、目的端口、长度、校验和。这堆字段如果不熟直接抄Wireshark里抓到的正常帧改IP就行。但是有个坑IP头的校验和是16位反码求和UDP校验和如果为0表示不校验。在调试阶段可以先把UDP校验和填成0x0000让IP/UDP协议栈不去校验它方便快速跑通。等链路稳定了再补上正确的校验和计算逻辑。6.3 测试框架与时序收敛的复盘写完工程后综合和布局布线阶段还会遇到时序问题。千兆以太网的用户逻辑工作在125MHz这个频率不算高但如果用户在关键路径上写了太多组合逻辑比如把MAC帧头拼接写成一长串连续赋值吃Timing是很正常的。我的经验是把帧头的组包逻辑拆成多拍用寄存器分段处理宁可多几个周期延迟也别让组合逻辑链太长。布局布线后如果时序不过优先检查RGMII接口的output delay约束和PHY芯片的IO timing参数是否设置正确。这个约束很多开发板例程里已经给了如果全新工程需要自己写可以对着PHY datasheet里tSU/tH参数计算。7. 回看这次学习一点心得体会把verilog-ethernet从仿真跑到上板对我这个接近零基础的人来说最大的收获不是“UDP收发通了”这个结果而是终于理解了数据流在FPGA里到底是怎么一层层穿过去的。以前觉得网络协议栈是软件领域的东西离硬件很远但实际上在FPGA里用Verilog实现UDP反而能逼着你去理解每一帧、每一个头字段的真实含义。如果你也打算啃这个工程我的建议是三条第一不要一上来就看完整源码先从eth_udp模块的接口图入手搞清楚数据从哪进、从哪出第二仿真环境一定先搭好上板前至少把ARP交互和UDP回环在波形里跑通第三遇到“板子没反应”的问题先从物理层排查link灯、PHY配置、时钟引脚这些硬件层面的问题远比逻辑问题隐蔽。这个工程后续还有很多可以扩展的地方比如加一个简单的ICMP应答模块让板子能ping通或者把UDP收到的数据写进FIFO再转成串口输出这样就能把FPGA变成一个便宜的数据采集/网口透传工具。协议栈本身不神秘拆开来看就是帧头、校验、握手几个固定动作的组合。搞明白一遍之后以后再用厂商IP核甚至自己写简化版协议栈心里都有底了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价