从零开始用Verilog复现UDP协议栈:FPGA以太网回环实战笔记
2026/9/15 3:15:04 网站建设 项目流程

第一次在调试板上成功把网线的另一端连起来,看到上位机收到FPGA回过来的UDP数据包,那种感觉和“把灯点亮”完全不一样。FPGA在你手里不再只是一块不断翻转电平的算数板,而是一台能跟电脑、路由器、交换机对话的设备。这篇笔记写的是我从接近0基础开始复现verilog-ethernet开源UDP协议栈工程的过程,重点讲清楚架构、RTL逻辑、仿真验证和上板调试。文章写得比较啰嗦,但都是实际操作过的内容,适合刚学完Verilog语法、想碰完整以太网工程又不知道该从哪下手的读者。

为什么选这个工程?因为FPGA网络通信看起来高大上,实际上拆开以后每一步都有标准答案。verilog-ethernet把以太网MAC、ARP、IP、UDP这些层都做成了可独立复用的模块,而且大量使用了AXI4-Stream接口,你不需要从零开始写MAC帧组装,也不需要死磕校验和算法,只需要理解“数据是怎么在一层一层模块之间传递的”。这对学习者来说,是极其珍贵的活教材。

1. 先看整体:为什么要用开源UDP协议栈入门

1.1 “接近0基础”的门槛到底在哪里

很多朋友问学FPGA是不是得先精通计算机网络,才能碰网口。我自己的感受是:并不需要。你需要的基础只有三样:看得懂Verilog的always块和状态机、知道同步时序里时钟和复位怎么处理、愿意打开仿真波形逐拍去看数据。

“接近0基础”这个说法,在我这里指的是:会写加法器、会写简单状态机、能自己点亮数码管,但从来没把FPGA和外部高速接口连起来。对网络协议的认识停留在“IP地址是192.168开头的东西”这种程度。如果你也是这个状态,那网络协议栈其实就是一套非常好的进阶练习:它的复杂点不在语法,而在数据流控制,以及多个模块之间时序配合。这个过程训练的是你“怎么把一个复杂的协议拆成硬件状态机”,而不是让你背TCP三次握手。

我当时踩的第一个坑,就是总觉得“要用FPGA实现UDP协议栈”一定得自己从PHY芯片的寄存器配置写起。实际上工程里PHY控制、MDIO配置、MAC帧封装这些已经被开源库处理得相当完整,我们需要做的是选对模块、接对接口、理解数据流向,然后再去改它、扩展它。

1.2 为什么选择verilog-ethernet而不是自己造轮子

自己写一个最小UDP栈,听起来很酷,但实际工作量远比想象中高。你要处理CRC32校验、MAC帧填充、IP头校验和、ARP缓存维护、跨时钟域以及各种位宽转换。就算全写完,debug时间也足够劝退。业界成熟做法是“站在开源模块上做工程”,verilog-ethernet就是其中一个常用选择。

这个库在GitHub上可以找到,alexforencich/verilog-ethernet,里面包含了从10G以太网MAC到1G MAC、ARP、IP、UDP、VLAN、RBM、DMA等大量RTL代码。它提供的不是一个只有hello world级别的demo,而是一整套结构化设计的范例。我最初只是想做UDP回环,结果看着看着,把AXI4-Stream的规范也顺便学明白了。

这里也要说句公道话:用开源代码不是让你复制粘贴完事,而是让你学习“真正工程里的代码长什么样”。verilog-ethernet的模块划分非常清晰,每个文件顶部都有详细端口说明,仿真测试平台也相对完整。你把它当成教材来精读,比啃半本协议栈设计书更高效。

2. 把UDP协议栈拆开看:每个模块到底在干什么

2.1 从外到内:PHY、MAC、ARP、IP、UDP的分工

先理清一次UDP数据包从电脑网卡发送到FPGA,硬件上到底发生了什么事。网线里的信号是差分电平,PHY芯片负责把这些电信号转成数字的比特流;MAC模块负责按以太网帧格式切分数据,识别前导码、目的MAC、源MAC、类型字段以及帧校验CRC32;再往上是ARP,它解决“知道对方IP,但不知道对方网卡MAC地址”的问题;然后IP层处理IP头以及源目IP地址;最后UDP层处理端口号,把数据交给你的用户逻辑。

这个分层很像快递寄包裹:PHY是运输卡车,MAC是快递单,ARP是查电话号码本,IP是邮政编码,UDP是收件人姓名。每一层只关心自己负责的字段,不关心别的层。所以你在上层看到的,就是干净的数据,不用自己去拼一整个以太网帧。

verilog-ethernet特别适合学习分层,因为它没有把逻辑揉成一个巨型模块,而是分成一组文件,每个文件的名字基本对应一个协议层。例如eth_mac_1g_rgmii是千兆RGMII MAC,arp.v负责ARP请求响应和缓存,ip_tx/ip_rx处理IPv4发送和接收,udp.v处理UDP头,udp_complete.v则把这些模块组合成一个对用户透明的UDP收发通道。

2.2 顶层模块到底连接了什么

我复现时重点研究了udp_complete.v这个顶层封装。它内部例化了MAC、ARP、IP、UDP以及一些FIFO和寄存器,对外暴露的接口主要分三类。

第一类是物理接口,如果是RGMII方案,就会有rgmii_td、rgmii_tx_ctl、rgmii_txc等信号;第二类是配置参数,比如local_mac、local_ip、local_port,也就是FPGA这一端的MAC地址、IP地址和UDP端口号;第三类是用户数据接口,也就是基于AXI4-Stream的rx_axis和tx_axis端口,你的用户逻辑在这里接收和发送数据。

我整理了一张简化表,方便对照端口含义:

端口方向信号名含义
输入local_mac[47:0]本端MAC地址,可以写成48’h00_11_22_33_44_55
输入local_ip[31:0]本端IP地址,比如192.168.1.10写为32’hc0a8010a
输入local_port[15:0]本端UDP端口号
输入tx_axis_tdata / tvalid / tlast用户要发送的UDP数据流
输出tx_axis_tready下游模块准备好接收时的握手信号
输出rx_axis_tdata / tvalid / tlast接收到的UDP数据流
输入rx_axis_tready用户逻辑准备好接收时的握手信号

实际仿真和上板前,要特别注意把local_ip里的每一个字节换算成十六进制。很多初学者会把IP写成十进制、十六进制混在一起的串,导致ARP永远应答不对,我以为自己板子坏了,查了两天发现是地址常量写反了字节序。

2.3 AXI4-Stream握手与tlast、tuser的意义

理解AXI4-Stream是使用这个库的关键。AXI4-Stream的核心就一句话:发送方拉高tvalid表示“我有数据”,接收方拉高tready表示“我可以接收”,当tvalid和tready同时为高时,一个数据节拍传输成功。

很多第一次接触的人会问:为什么不能一直拉高tvalid?因为接收方的处理速度是有限的。如果它内部FIFO满了,tready必须拉低,发送方就要暂停发送。这种机制叫背压,是整个流水线不出错的前提。你写回环逻辑时如果不管tready,直接把收到的数据直接丢给发送端口,大概率会出现丢帧,原因就在这。

字段名tlast表示“这是当前帧的最后一个数据”,tuser则表示错误或者附加信息,在UDP接收链路里通常表示帧是否被丢弃。只要tuser为高,这一帧基本可以认为无效。所以回环逻辑里不能只把tdata接回去,tlast要跟着走,tuser也要做处理,否则发送端口可能把异常帧当正常帧发出去。

3. 在Vivado里复现最小UDP回环工程

3.1 建立工程与添加RTL源文件

我用的开发环境是Vivado,目标板卡是常见的Artix-7系列小板子,板载PHY芯片支持千兆RGMII。先把verilog-ethernet仓库克隆或者下载到本地,然后新建一个空工程,把rtl目录和lib目录下的相关文件加入工程,文件比较多,但不建议一次性全部加入,最好按依赖关系手动添加,避免残留无用的底层模块干扰查错。

我的做法是先建一个rtl顶层文件,比如udp_loopback_top.v,然后在这个文件里实例化udp_complete。打开的datasheet式端口注释是老外写的英语,但读起来不复杂,端口命名基本一致,比如以tx_axis_开头的和以rx_axis_开头的一一对应。把不需要的物理接口引脚先悬空,只保留时钟、复位、RGMII接口、UDP用户接口这四组信号,工程一下子就清爽了。

工程里需要根据PHY类型选择正确的MAC模块。我的板载PHY是RGMII接口,所以例化的MAC模块是eth_mac_1g_rgmii,如果PHY是GMII或者MII,对应模块也不同。这个选择做错了,后面时序和引脚约束必然出问题。

3.2 最简回环逻辑的代码实现

下面这段是我在顶层里写的回环逻辑,核心是例化一个axis_fifo,把接收到的UDP数据存入FIFO,再将FIFO读侧接到发送端口。之所以中间垫一个FIFO,而不是直接用assign把接收数据接到发送端口,是为了解决背压问题:如果MAC在某个时刻不能输出数据,而用户回环逻辑又没有缓存,数据只能被丢掉。FIFO就是个蓄水池,让两端速率不完全匹配时也能平滑传输。

代码示意如下:

wire [7:0] rx_tdata; wire rx_tvalid; wire rx_tready; wire rx_tlast; wire rx_tuser; wire [7:0] tx_tdata; wire tx_tvalid; wire tx_tready; wire tx_tlast; wire tx_tuser; axis_fifo #( .DATA_WIDTH(8), .ADDR_WIDTH(10) ) u_loopback_fifo ( .clk (clk), .rst (rst), .s_axis_tdata (rx_tdata), .s_axis_tvalid (rx_tvalid), .s_axis_tready (rx_tready), .s_axis_tlast (rx_tlast), .s_axis_tuser (rx_tuser), .m_axis_tdata (tx_tdata), .m_axis_tvalid (tx_tvalid), .m_axis_tready (tx_tready), .m_axis_tlast (tx_tlast), .m_axis_tuser (tx_tuser) );

对应地在udp_complete例化端口里,把rx_axis_tdata接到这里的rx_tdata,把rx_axis_tvalid接给s_axis_tvalid,把s_axis_tready连回udp_complete的rx_axis_tready,发送侧同理。这样用户逻辑不感知完整的以太网帧,只感知UDP数据内容,非常干净。

如果只想验证最简单的通断,也可以把接收端口和发送端口用寄存器打一拍再直连,但丢帧概率较高,不适合传输超过一个FIFO深度的数据。FIFO深度的选择也有门道,1000字节的UDP负载至少要选10位地址宽度,不然一帧还没发完前面就溢出了。

3.3 仿真验证与上板基本操作

先把工程仿真通过再说上板。verilog-ethernet仓库自带一些testbench,但针对我的顶层逻辑,还是写一个自己的仿真脚本更直观。仿真阶段可以只例化udp_complete而不接PHY,用模拟的AXI数据产生器喂入UDP包,观察rx_axis到tx_axis的数据是否原样返回。

仿真里常见的问题是数据虽然回来了,但tlast没对齐。这时候去看波形,一帧结束的位置是否与tlast高电平重合即可。如果tuser在返回的数据上被拉高,说明接收端认为这一帧出现了校验和或长度错误,回环出去也会被当坏帧丢弃。

上板前还要处理物理约束。RGMII接口需要管脚约束和时钟约束,尽量参考板卡官方例程里的XDC。我记得第一次上板的时候,时钟没加约束,Vivado按默认频率布线,时序违例一堆,PHY偶尔能link上但数据始终不通。后来把时钟约束成125MHz,同时给RGMII的输入时钟加上合适的约束,问题才稳定下来。

上板后用网线把FPGA板卡直接连到电脑,不要先接交换机。手动给电脑网卡设置一个静态IP,比如192.168.1.100,FPGA端local_ip设为192.168.1.10,子网掩码255.255.255.0,端口设为某个固定值。然后在电脑上打开网络调试助手或者用Python脚本向192.168.1.10:5000发送UDP数据,大概率能看到FPGA回给你同样的内容。

4. 踩坑实录与排查技巧

4.1 链路通但ping不通怎么办

这是我复现工程时遇到的第一类问题。网线插上后PHY灯亮,电脑端也显示网络已连接,但ping FPGA的IP始终不通。用网线直连时,如果ARP解析不了对方MAC地址,ping就会卡住。排查思路是先确认FPGA端local_mac和local_ip是不是真的写对了,最好用ILA抓一下ARP请求。如果ILA里能看到ARP请求包,说明PHY链路和MAC已经工作,问题大概率出在IP层或ARP模块没有正确回包。

另一个容易忽略的坑是:电脑端防火墙。它往往会拦截来自未知“网络位置”的ICMP和UDP包。简单粗暴的办法是先关闭防火墙或者把网络位置改成专用网络,能大大减少排查干扰。这不是FPGA的问题,但你查起来会怀疑人生。

还有一种是开了交换机的端口隔离或者VLAN配置不当,导致广播ARP被隔离。所以建议一开始直连,别通过交换机。

4.2 UDP数据乱序、字节序翻转怎么处理

UDP收发正常之后,你可能会发现收到的内容每个字节的顺序是反的。这是字节序的问题。以太网采用大端字节序,而FPGA内部常见的是小端处理。比如发送端数据0x12345678,在有些上位机看来会变成0x78563412。

解决方法是统一约定字节序处理规则。如果你用FPGA发一个固定魔数比如AA55,就能快速判断有没有翻转。遇到翻转后,要么在FPGA端把数据按字节倒序,要么在上位机解析时交换字节顺序。两种做法都可以,但一定在项目一开始就约定清楚,不然团队协作时特别容易互相甩锅。

4.3 调试工具与长期学习方法

调试FPGA网口,我用得最多的工具还是ILA和Wireshark组合。ILA用来抓FPGA内部的ARP请求、UDP数据帧和tvalid/tready时序,Wireshark用来确认上位机实际看到的网络包结构和内容。两边对照,经常一眼就看出问题出在“软件端没发对”还是“硬件端没解对”。

还有个小技巧,把本端MAC地址设为某种可见的固定值,比如00:11:22:33:44:55,然后在Wireshark里过滤这个MAC,这样一眼就能从嘈杂的网络流量里定位到FPGA发来的包。我每次调试都会先发一串递增的字节,这样在Wireshark里很容易看出来有没有丢包、重复或者乱序。

如果未来想更进一步,不只是回环,还可以在这个工程基础上扩展多端口UDP,或者把UDP数据直接接到卷积、FFT之类的算法模块,这时候你会真正感受到AXI4-Stream接口给你带来的便利。只要数据流接口对齐,算法模块和通信模块几乎可以无缝拼接。

写在最后的一点个人体会

从“点灯”到“能跑一个UDP回环”,中间最难的并不是Verilog语法,也不是UDP协议本身,而是适应“硬件世界里一切都有握手信号”这件事。软件里你可以随便发,发错了系统不会崩;硬件里如果不管tvalid和tready,数据就是会丢,帧就是会碎。verilog-ethernet这个工程教给我的,不只是怎么用UDP,更是怎么搭建一个有节奏、有握手、能背压的数据通路。

如果你正要开始学习这条路,我的建议是:不要急着改代码,先对着波形把每个模块的数据走一遍;不要急着上板,先把仿真跑通;不要急着扩展功能,先让一帧数据老老实实从网线进来,再原样出去。这个过程跑通一次,你对FPGA的信心会完全不一样。后面再去碰PCIe、DDR、更高速的以太网,你就会发现,很多设计思路其实都是相通的。

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

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

立即咨询