news 2026/9/18 9:29:05

FPGA以太网UDP协议栈实战:从零跑通verilog-ethernet

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA以太网UDP协议栈实战:从零跑通verilog-ethernet

学 FPGA 学到某个阶段,一定会想搞一块网口。理由很朴素:串口太慢、SPI 又只能点对点,而网口是"一根线插上,全世界都能连"的东西。可真正动手才发现,从 PHY 芯片到 MAC、从 IP 到 UDP,中间那套协议栈是一道很难跨的坎。自己从零写一个以太网收发,前前后后能把人折磨到怀疑人生:前导码、CRC、帧间隔、ARP 请求应答、IP 头校验和、UDP 校验和、字节序……每一项单看都不复杂,串在一起就是一个"看不到尽头"的深坑。所以当我决定认真搞 FPGA 网口通信之后,很快就放弃了造轮子,转去啃 Alex Forencich 的 verilog-ethernet 开源工程。这套代码在圈里口碑非常好,几乎成了 FPGA 以太网协议栈的事实标准参考实现。这篇就聊聊我一个近似零基础的人,是怎么一步步把这套工程看懂、跑通、最后搬进自己项目里的。核心关键词是 FPGA、verilog-ethernet、UDP 协议栈,如果你也在为"FPGA 怎么发一个 UDP 包"发愁,这篇大概率能帮你省掉不少弯路。

1. 为什么近零基础的我选择先啃 verilog-ethernet 而不是自己写协议栈

很多刚入门的朋友有个执念:既然要学协议,那不如自己从零撸一遍,撸完就彻底懂了。我一开始也是这么想的,还真的写了几个模块。结果折腾了一个多礼拜,卡在 UDP 校验和和 ARP 应答上,最后连一个能稳定收发的回环都跑不起来。冷静下来复盘,我才发现"自己写"这件事在教学价值和工程价值之间有个巨大的错位。

1.1 自己从零写协议栈的三个现实代价

第一个代价是时间。硬件协议栈不像软件,软件里你 printf 一下就能定位问题,FPGA 里你只有波形。写一个以太网收发,涉及 GMII/RGMII 接口时序、8b/10b 或 4b/5b 编码的认知、CRC32 校验、帧间隙IFG、前导码PRE、SFD定界符……这些东西在教科书里可能各占一小节,但真到板子上,它们是同时对你发难的。调试一个时序问题可能就要一整天。

第二个代价是正确性无参照。你自己写的栈,跑不通的时候,你根本不知道是自己的逻辑错了,还是对协议的理解错了,还是 PHY 配置错了。没有参照物的时候,排错是盲人摸象。verilog-ethernet 是经过大量工程验证的,至少你能确定"这套代码本身是对的",问题只可能出在接口和配置上,这就把排查范围缩小了一大半。

第三个代价是可维护性。手撸出来的代码往往是"能跑就行",结构一团乱,后面想加个 ICMP 或者加个自定义应用层通道,基本等于重写。开源工程的模块划分是清晰的,每一层职责明确,你改哪一层、加哪一层,心里有数。

提示:学习协议本身和实现协议栈,是两件应该分开做的事。协议细节你可以对着 RFC 慢慢读,但实现层面,站在巨人肩膀上才是理性选择。

1.2 verilog-ethernet 到底帮你解决了哪些"脏活"

这套工程最值钱的地方,不是它写了 UDP,而是它把那些"又脏又细又容易错"的活全干了。具体来说,它封装了这些内容:以太网帧的组装与解析(含前导码、SFD、CRC 的对接)、GMII/RGMII 接口的收发适配、IP 层的报文封装与头部处理、ARP 请求与应答、ARP 缓存表、UDP 报文的封装与校验和生成(可选转发到硬件 offload)、以及一整套 AXI-Stream 风格的流式接口。

对使用者来说,最直观的感受是:你只需要面对 AXI-Stream 的 payload。你想发一个 UDP 包,就把目标 MAC、源 MAC、源 IP、目标 IP、源端口、目标端口填好,然后把数据以 AXI-Stream 的形式喂进去,剩下的它全帮你搞定。接收侧反过来,你在 m_udp_payload_axis 上把数据取出来就行。这种"只管载荷、不管封装"的抽象,对于零基础的人来说友好太多了。

1.3 正确的学习姿势:先当黑盒用,再拆开看

我踩过的一个认知坑,是一开始就钻进源码里逐行读。那么大的工程,你从 eth_mac_1g 的某个 always 块开始读,读两天就会失去方向。后来我调整了策略,分三步走:

  • 第一步,当黑盒。只用最顶层的 udp_complete(或者 ip_complete)模块,按官方例程例化,先把收发跑通,建立"我能控制它"的信心。
  • 第二步,看数据通路。从顶层模块的端口往里追,搞清楚一个 UDP 包进来后,一次握手是怎么在层与层之间传递的,重点看 AXI-Stream 的 valid/ready 是怎么级联的。
  • 第三步,按需深入。只有当你要改某一层的细节(比如换 PHY 接口、改 ARP 行为),再深入那一层读。不要在不需要的时候去理解 UDP 校验和算法的每一位。

这个顺序很重要。先建立"能用"的感觉,再建立"看懂"的框架,最后才是"改得动"的细节。

2. 撕开模块全景:从 PHY 引脚一路到 UDP 载荷的那条数据通路

把这套工程当黑盒用没问题,但如果完全不知道里面怎么分层,一旦出问题就会抓瞎。所以第二步我强迫自己画了一张从硬件引脚到应用载荷的完整链路图(在脑子里画的,不是画在图上),把每个模块的职责记下来。这一节就把这条链路讲清楚。

2.1 仓库的核心分层结构

verilog-ethernet 的分层,基本上就是按照 TCP/IP 的层次来的,只是实现上用的是流水线和 AXI-Stream。从下往上看:

  • PHY 接口层:比如 eth_mac_1g / eth_mac_1g_fifo,负责把 GMII 或 RGMII 的收发信号,转成内部用的 8 位数据流,同时处理帧定界、CRC 校验。RGMII 有 DDR 的时序要求,这一层还会做 IDDR/ODDR 的拆合。
  • AXIS 适配层:像 axis_gmii_rx / axis_gmii_tx 这种,把 MAC 的字节流包装成带 tvalid/tready/tlast 的 AXI-Stream,方便上层用握手的方式收发。
  • IP/ARP 层:ip_complete 系列,包含 IP 报文收发、ARP 请求应答、ARP 缓存。它对外又套了一层 AXI-Stream 加一个"以太网头"握手接口。
  • UDP 层:udp_complete 系列,在 IP 层之上做 UDP 头的封装/解析,并可选地生成或校验 UDP 校验和。

这里有个关键设计叫"分发器/汇聚器"(通常叫 demux / deparse 之类),它把不同协议类型的以太网帧分流到对应的处理模块(ARP 走 ARP,IP 走 IP),发送时再汇聚回一条流。理解这个分流机制,你就理解了为什么这套栈能同时处理 ARP 和 UDP 而不打架。

2.2 一个 UDP 包从 AXI-Stream 到线上的完整生命周期

假设你现在要发一个 UDP 包,数据从应用侧喂进来。整个流程大致是这样:

  1. 应用侧发起:你在 s_udp_payload_axis 上送数据,同时在 s_udp_hdr 接口上给出目标/源端口、长度、校验和(或者让硬件帮你算)。valid/ready 完成一次握手。
  2. UDP 封装:UDP 层把 UDP 头拼上,生成 UDP 报文,交给 IP 层。
  3. IP 封装:IP 层加上 IP 头,填好源/目标 IP、协议号(UDP 是 17)、总长度、TTL 等,交给 ARP/以太网层。
  4. 查 ARP 缓存:这一层要看目标 IP 对应的 MAC 地址。如果缓存里有,直接用;如果没有,它会先发一个 ARP 请求,把你的包暂存起来(或丢弃,取决于配置),等 ARP 应答回来再发。这是很多人第一次调不通的坑点。
  5. 以太网成帧:加上目标 MAC、源 MAC、以太网类型(0x0800 表示 IP),交给 MAC 层。
  6. MAC 发送:加上前导码、SFD,计算 FCS,通过 GMII 输出到 PHY,最终上线。

接收方向正好相反,ARP/IP 分发器看到以太网类型字段决定把帧往哪送,IP 层查协议号,UDP 层剥头,最后你在 m_udp_payload_axis 上拿到纯数据。

2.3 关键模块的接口参数速查

下面这张表把几个最常用的顶层模块和它们的主要配置参数列一下。不同版本的端口名可能略有差异,但思路是一致的,具体以你下载的那版源码为准。

模块主要职责关键配置/接口典型用途
eth_mac_1g_fifo单路 GMII/RGMII 收发 + FIFO选择 GMII 或 RGMII,PHY 时钟接一颗千兆 PHY
axis_gmii_rx / tx字节流与 AXI-Stream 互转tdata 8bit, tkeep, tuserMAC 与上层桥接
ip_completeIP + ARP + 分发local_mac, local_ip, gateway_ip, subnet_mask需要 IP/ARP 功能
udp_complete在 ip_complete 基础上加 UDPlocal_udp_port 等直接收发 UDP
arp_cacheARP 表管理容量、超时独立使用或排查 ARP 问题

注意:接口里的 "hdr" 握手和 "payload" 的 AXI-Stream 是两套独立的握手。先 hdr 有效、再送 payload,这个顺序别搞反,否则包会发不出去。

3. 先在仿真里跑通:AXI-Stream 握手和包收发的时序

上板之前一定要仿真。很多人图快,直接综合上板,结果 ping 不通,然后只能对着板子干瞪眼。仿真的价值在于,你能在波形里看到每一个信号的跳变,把"协议栈在干什么"变成"我看得见的东西"。这是零基础的人理解 AXI-Stream 握手最有效的办法。

3.1 仿真环境准备与流程

我用的流程是:先跑工程自带的 testbench,确认环境没问题,然后自己改一个最小的发送用例。环境上,ModelSim/QuestaSim 或者 Vivado 自带的仿真器都能跑,关键是把这个工程的源码和它依赖的公共库(作者还有一套 verilog-axis、verilog-axi 的公共库)一起加进去。很多新手编译报错,八成是没有把那套 axis 公共库加进来。

大致步骤:

  1. 克隆 verilog-ethernet,同时把 verilog-axis 也克隆下来,放在同级目录。
  2. 新建一个仿真工程,把两边的 rtl 目录都加入编译。
  3. 加一个顶层 testbench,例化 udp_complete,给本地 MAC/IP/端口赋固定值。
  4. 加一个简单的激励:构造一个 UDP 头,然后送几个字节 payload。
  5. 仿真几百微秒,看波形里有没有 eth_tx 的数据出来。

3.2 用 cocotb 或者自带 testbench 发一包看波形

如果你习惯 Python,cocotb 是个很舒服的选择,能在 Python 里用 await 写时序。如果你更想贴近硬件,就写一个纯 Verilog 的 testbench。我个人建议零基础的同学先用纯 Verilog 写一个最简单的激励,把精力放在读波形上,而不是纠结 Python 环境。

一个发送侧激励的核心逻辑大概长这样(伪代码思路):

// 1. 给出以太网/IP/UDP 头握手 s_eth_hdr_valid <= 1'b1; s_eth_dest_mac <= 48'h112233445566; s_eth_src_mac <= 48'hAABBCCDDEEFF; s_eth_type <= 16'h0800; // IPv4 // 2. 等 ready wait (s_eth_hdr_ready); // 3. 送 UDP 头,再送 payload // 4. payload 用 AXI-Stream,tvalid/tready/tlast

真正调试的时候,你会发现大多数问题都出在握手没对齐:valid 拉起来了,但 ready 一直没回;或者 last 早了一拍;或者 hdr 握手还没完成就开始送 payload。这些在波形里一眼就能看出来,比你上板猜半天强太多。

3.3 波形里必须盯住的几个信号

仿真跑起来以后,别漫无目的地看。我总结了几组必看的信号:

  • hdr 握手对s_eth_hdr_valid/s_eth_hdr_ready,看 hdr 是否被成功接受。
  • payload 流控s_udp_payload_axis_tvalid/tready/tlast,确认数据被完整吃进去。
  • 发送输出eth_tx_*那一组,看最终有没有成帧数据出来,前导码和 FCS 是否正确。
  • ARP 相关:如果你发的是陌生 IP,看有没有 ARP 请求产生,以及 ARP 缓存有没有更新。

盯这几组信号,基本能覆盖 90% 的"发不出去"问题。

4. 上板实测:ARP、ICMP、UDP 回环与 iperf3 打流验证

仿真跑通只是过了第一关,真正的考验是上板。这一节讲我在真实硬件上把 UDP 收发跑起来的过程,包括怎么配、怎么测、怎么验证。

4.1 硬件侧配置:MAC、IP、时钟与 PHY 接口选择

上板前要把几个参数确定下来,它们直接影响能不能通:

  • 本地 MAC 地址:随便给一个不与局域网冲突的就行,比如02:00:00:00:00:01这种本地管理地址,别用厂商 OUI 开头的,避免撞车。
  • 本地 IP、子网掩码、网关:要和你 PC 的网卡在同一网段。比如 PC 是 192.168.1.10/24,那 FPGA 就设成 192.168.1.20/24,网关随意填同网段即可。
  • PHY 接口类型:确认你板子上的 PHY 是 GMII 还是 RGMII。RGMII 有延迟补偿的坑(PHY 内部延时 vs PCB 走线延时),如果 ping 不通,先怀疑这个。
  • 时钟:RGMII 用 125MHz,注意时钟来源和相位。

把这些在顶层参数里配好,例化 udp_complete,接上 MAC 层和 PHY 层,综合下板。

4.2 网络侧配置与 ping 不通的排查顺序

下板之后第一件事是 ping。ping 通说明 ARP、IP、ICMP 这一条链路至少是活的。如果 ping 不通,我习惯按这个顺序排查:

  1. 看链路指示灯:PHY 的 link 灯亮不亮?不亮先查 PHY 复位和时钟,别急着看逻辑。
  2. 看有没有收到包:在接收侧挂个计数器,看 PC 发来的 ARP 请求有没有被 FPGA 收到。收到了说明物理层和 MAC 层没问题,问题在上层。
  3. 看有没有回包:FPGA 收到 ARP 请求后有没有回 ARP 应答?如果没有,可能是 ARP 层的 local_ip/local_mac 配置不对。
  4. 看 IP 校验和:如果 ARP 能响应但 ping 不通,检查 IP 头校验和是否被硬件正确生成。有些配置需要你手动设置是否做 offload。

这个顺序的逻辑是"从下往上、从物理到逻辑",能避免你在一堆逻辑里瞎找。

4.3 用 iperf3 走 UDP 看吞吐和丢包

ping 通了之后,就可以上 iperf3 打流测吞吐了。测试命令大概是这样,PC 端当服务端:

# PC 端 iperf3 -s # 如果 PC 是客户端,向 FPGA 打 UDP 流(FPGA 端不做 iperf,需要你自己写回环逻辑) iperf3 -c 192.168.1.20 -u -b 100M -t 30

因为 FPGA 端通常不是完整的 iperf 服务端,所以更常见的做法是:FPGA 做一个UDP 回环,收到 PC 发来的 UDP 包后原样发回去,然后用 UDP 网络调试助手或者自己写的脚本统计收发。iperf3 的 UDP 模式(-u)可以帮你观察丢包和抖动,-b控制带宽,这对评估你的协议栈能跑多快很有用。

提示:千兆线路上跑到 900Mbps 以上其实很难,因为小包的开销大。用大包(比如 1400 字节 payload)测,更容易看到接近线速的结果。

5. 踩坑记录:那些让我盯了两个晚上的时序与位宽问题

这一节是全文最有价值的部分,因为前面讲的都是"应该怎样",这里讲"我实际是怎么翻车的"。每一个坑我都熬过夜,希望你能绕过去。

5.1 tready 拉低导致整个包被截断

这是我最开始遇到的最阴险的坑。现象是:包能发出去,但接收端显示长度不对,或者 UART 打出来少了几个字节。查了半天波形,发现是tready在发送过程中被拉低了,而我的发送逻辑没有正确响应 ready,直接把数据全推了出去,导致部分数据被丢弃。

AXI-Stream 的规矩是:tvalid 和 tready 同时为高才算一次有效传输,发送方必须在 tready 为低时保持 tvalid 和数据不变。我当时的逻辑是"我数据准备好了就啪一下全送出去",完全没管 ready。改法很简单,用状态机等 ready,ready 不回就 hold。这个错误新手极容易犯,如果你发现数据时有时无,先怀疑它。

5.2 校验和与字节序的连环坑

UDP 校验和和 IP 校验和是两个独立的坑。首先是要不要算的问题:这套栈支持硬件计算校验和,也支持你外部给。如果你选择了外部给但给了个错值,接收端会直接把包丢掉,现象是"包发出去了但对方收不到",非常迷惑。

其次是字节序。网络字节序是大端,而 Verilog 里你拼数据的时候很容易按小端拼。特别是 mac 地址、IP 地址这些多字节字段,拼反了会直接导致 ARP 应答里 MAC 对不上,或者 IP 头解析失败。我的建议是:所有多字节字段都用明确的拼接方式,别用位选去偷懒,写清楚哪一段是高字节。

5.3 时钟域与复位:跨时钟域最容易"偶尔抽风"

协议栈内部通常只有一个主时钟(比如 125MHz),但 PHY 侧的时钟和系统时钟可能不同源,这就涉及跨时钟域。如果你的系统逻辑跑在别的时钟上,和协议栈接口的时候,一定要做正确的跨时钟处理(异步 FIFO 或者至少打两拍同步)。

复位也有讲究。复位要异步断言、同步释放,而且复位期间不要急着送数据。我见过因为复位释放和第一个 valid 同时发生,导致首包丢失的情况。稳妥的做法是复位后等几十个时钟,让 ARP 缓存和内部状态机都初始化完再开始收发。

5.4 ARP 缓存容量和老化:别小看那张表

ARP 缓存表小到你可能忽略它。我第一次测的时候,目标是 PC,只涉及一个 IP,完全没问题。但当我把设备接到一个有多台机器的局域网里,就出现了"偶尔发不出去"的情况——因为 ARP 表满了或者条目被挤掉了,新的目标 IP 找不到 MAC,包就被暂存或丢弃了。

解决方法是:合理设置 ARP 缓存容量,根据你的应用场景决定。如果是对外通信只针对固定几个地址,容量小点没关系;如果要做广播发现或者面对动态对端,就要给足容量,并关注老化机制。verilog-ethernet 的 ARP 模块通常支持配置容量和超时,去顶层参数里找一下。

6. 把这套栈搬进自己的工程:裁剪、例化与二次开发

跑通官方的例程只是第一步,真正的目标是把它变成"我自己的网络模块"。这一节讲讲怎么裁剪、怎么例化、怎么在它上面加自己的应用层。

6.1 最小可用配置:只要 UDP 收发

如果你的需求就是"收 UDP 包 + 发 UDP 包",完全不需要把所有功能都打开。最小配置通常是:一个 PHY 接口模块、一个 ip_complete(含 ARP)、一个 udp_complete,再加上你的应用逻辑。ARP 是必须的,因为你要回应别人对你这台设备的 ARP 查询,否则别人根本找不到你的 MAC。

例化的时候,把本地 MAC/IP/端口设成常量,把 UDP 收发接口引到你自己的模块上。别一上来就想着支持多种协议、多路端口,先跑通一路再说。

6.2 加一条自定义应用层通道

这套栈的优雅之处在于,你可以在 UDP 之上随便加东西。比如你想传图像数据(结合 FPGA 图像处理场景),你只需要把图像数据打包成固定格式,通过 UDP payload 发出去;接收端收到 UDP payload 后解析。协议栈完全不用改,你的自定义帧格式是纯应用层的事。

我自己的做法是定义了一个简单的应用层帧头(几个字节的序号 + 类型 + 长度),放在 UDP payload 的前面。这样接收端能区分不同类型的消息,也能检测丢包重排。这个思路对做 FPGA 数据传输、远程控制都通用。

6.3 资源与时序收敛的取舍

最后说个现实的:这套栈不是免费的,它会吃资源。一个完整的千兆 UDP 栈,加上 ARP,在中小规模 FPGA 上可能会占掉不少 LUT 和 BRAM。如果你资源紧张,可以考虑:

  • 关掉不需要的校验和 offload,改成软件(上位机)负责校验。
  • 减小 ARP 缓存容量和 FIFO 深度。
  • 只保留单路端口,不做多路分发。

时序上,125MHz 的千兆收发本身不难,但如果你把系统逻辑也塞进同一个时钟域、逻辑又很复杂,就可能出现时序违例。我的经验是:把协议栈和你的应用逻辑尽量放在不同的时钟域,用 FIFO 跨过去,这样各自的时序压力都小,也更容易收敛。

我个人从零基础啃这套栈最大的体会是:不要试图一次看懂全部。先用起来,遇到问题再回头查,看波形永远比读代码快。第一次跑通 UDP 回环的那个晚上,我盯着屏幕看了很久——不是因为难,而是因为前面踩的坑实在太多了,多到你能从每个坑里学到一条以后不会再犯的规矩。这套开源工程值得反复啃,等你哪天能闭着眼睛画出从 PHY 到 UDP 载荷的数据通路,基本就说明你真的把 FPGA 网络通信这道坎迈过去了。如果你也在做类似的事,建议从最小的一路 UDP 回环开始,先让它动起来,剩下的细节会在调试中自然被你吃透。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 9:29:02

MySQL自增ID从0开始实战:NO_AUTO_VALUE_ON_ZERO与sql_mode全解析

先说个我自己踩过的坑。前阵子接了一个老系统数据迁移的活&#xff0c;对方核心表的主键 ID 是从 0 开始用的&#xff0c;业务代码里到处是id 0表示系统内置账号的判断。迁到我们这边 MySQL 之后&#xff0c;默认自增 ID 从 1 开始&#xff0c;两边数据语义直接对不上&#xf…

作者头像 李华
网站建设 2026/9/18 9:28:29

AI生成内容检测工具测评与教育应用指南

1. 项目背景与核心需求作为一名长期关注AI生成内容检测的教育从业者&#xff0c;我注意到越来越多的高校开始面临学生作业中AI生成内容的识别难题。特别是在文科类课程中&#xff0c;论文、报告等文本作业的AI生成比例显著上升。根据2023年高等教育学术诚信报告显示&#xff0c…

作者头像 李华
网站建设 2026/9/18 9:24:41

AI编程:从80%代码生成到可维护工程体系

看到一份提交记录里八成以上的代码都带着AI补全的痕迹&#xff0c;不少人的第一反应是"效率真高"&#xff0c;第二反应是"那还要我干嘛"。更有意思的是&#xff0c;喊出"代码80%由AI产出"的&#xff0c;恰恰是做AI的那家公司自己&#xff0c;转头…

作者头像 李华