news 2026/9/18 18:24:06

verilog-ethernet:FPGA UDP以太网协议栈入门

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
verilog-ethernet:FPGA UDP以太网协议栈入门

从近似0基础开始啃 FPGA 开发,走到第 10 篇,终于摸到了网络通信这块硬骨头。前面几篇我们把 Verilog 语法、时序逻辑、状态机、FIFO、跨时钟域这些基础攒得差不多了,但真要在板子上跑通一条 UDP 链路,光靠手写是远远不够的——你要处理 PHY 接口时序、MAC 帧格式、IP 头校验和、ARP 地址解析、UDP 伪首部校验,任何一个环节错了,抓包工具里就是一片沉默。这时候把 verilog-ethernet 这个开源工程拿过来读,性价比是最高的。它是一个纯 Verilog 实现的以太网协议栈,支持 1G/2.5G/10G 速率,覆盖 GMII、RGMII、SGMII、XGMII 这些常见 PHY 接口,内部用 AXI-Stream 做统一的数据通路,把 ARP、ICMP、IPv4、UDP 拆成独立模块,哪一层出问题就单独盯哪一层。我写这篇的目的很直接:给同样处在入门阶段、手里有一块带网口的 FPGA 板子、想把 UDP 收发跑起来的人,一份能照着抄的工程学习笔记。不管你是刚学完 Verilog 语法,还是已经能点灯但没碰过网络,这篇里的选型思路、模块拆解、仿真流程和踩坑记录,应该都能省你几天时间。

1. 为什么最后选了 verilog-ethernet 这个工程

1.1 从零手写 UDP 协议栈会撞上哪几堵墙

刚开始我也想过自己从 MAC 层往上撸一遍,觉得不就是拼几个字节嘛。真正动手才发现,第一堵墙是 PHY 接口时序:RGMII 是 DDR 双边沿采样,4 位数据在时钟上下沿各传一次,你还得在 FPGA 侧做输出延迟和输入延迟对齐,不同厂商的 PHY 芯片要求还不一样,差个几百皮秒就开始丢包。第二堵墙是帧结构,以太网帧头 14 字节、前导码 7 字节、SFD 1 字节、最小帧 64 字节、CRC32 校验,你少填一个字段对面直接当垃圾帧扔掉。第三堵墙是校验和,IP 头的 checksum 是 16 位反码求和再取反,UDP 还要额外算伪首部,涉及到字节序和进位回卷,手写特别容易出错。第四堵墙是 ARP,你得先发广播问“谁是 192.168.1.100”,等对方回了 MAC 地址才能发数据,中间还有超时重试和缓存老化。这四堵墙叠在一起,没有一两个月根本理不顺,而且你调试的时候根本不知道是自己逻辑错了还是对面没收,全凭猜。

1.2 verilog-ethernet 的定位和分层设计

verilog-ethernet 这个工程最舒服的地方在于它的分层特别干净。最底下是 PHY 接口层,负责把 RGMII/GMII 的物理信号转换成 8 位或 32 位的内部数据流;往上是 MAC 层,处理前导码、帧间隔、CRC 生成和校验、最小帧填充;再往上是以太网帧解析层,把帧头剥掉,把 payload 交给上层;最上面才是 ARP、ICMP、IPv4、UDP 这几个协议模块。每一层之间统一用 AXI-Stream 连接,也就是validreadydatalast这四根线的握手机制。这个设计的好处是你可以只关心自己那一层:想验证 UDP 收发逻辑,就绕开 PHY,直接在仿真里往 UDP 模块灌 AXI-Stream 数据;想验证 PHY 时序,就发固定帧看波形。我第一次跑通 ping 的时候才意识到,这种“每层可独立测试”的结构,对一个初学者有多重要——它把排错空间从整个链路压缩到了单个模块。

1.3 为什么不直接用 lwIP 或软核方案

有人会问,直接上一个软核跑 lwIP 不香吗,何必在 Verilog 里遭这个罪。这里得说清楚场景差异。lwIP 是跑在 CPU 上的软件协议栈,适合那些需要 TCP、HTTP、DHCP 这类复杂协议的场合,代价是它要占处理器资源和内存,吞吐还受 CPU 主频限制。而纯 Verilog 协议栈是硬件并行跑,一个时钟周期处理一批数据,1Gbps 线速下几乎不占 CPU,延迟稳定在微秒级。如果你的板子上本来就没有软核,或者你的应用就是高速采集、实时控制这类对延迟敏感的场景,那硬件协议栈才是对的选择。verilog-ethernet 还有个优势是纯 RTL、无厂商 IP 依赖,Xilinx 和 Altera 都能综合,这也让它成了很多教学和开源项目的默认底座。

1.4 上手前要先具备哪些前置知识

虽然标题写的是“近似 0 基础”,但读这个工程还是需要一点铺垫的。我个人建议至少掌握这几样再进来:一是能看懂always @(posedge clk)和阻塞、非阻塞赋值的行为差异,二是理解同步复位和异步复位的区别,三是知道 FIFO 是干什么用的、读写指针为什么不能直接拿来当握手信号,四是最好在仿真里跑过一个带跨时钟域的小设计。缺了这些直接冲进来看 RGMII 的 DDR 原语,会很痛苦,因为那部分代码里混着厂商原语例化和时序约束。基础不牢的时候,读开源工程最容易犯的错是“看不懂就跳过”,结果跳过的全是最关键的握手逻辑,最后上板不通又回头补,时间反而花得更多。

2. 工程目录与核心模块逐个拆解

2.1 顶层封装怎么选:从 FIFO 版本切入最省事

打开工程源码目录,你会看到rtl下面按功能排了一长串文件,第一次看容易懵。我的建议是从顶层封装文件开始看,也就是那些名字里带_fifo或者_64bit后缀的封装。以 1G 速率、RGMII 接口为例,eth_mac_1g_rgmii_fifo这个顶层把 PHY 接口、MAC、FIFO 全包进去了,对外只暴露一个标准的 AXI-Stream 收发接口。这样你几乎不用管 RGMII 的时序细节,只要把tx_axisrx_axis接上自己的逻辑就行。FIFO 版本里有一堆参数值得注意:TARGET用来指定厂商原语,Xilinx 填"XILINX",Altera 填"ALTERA"DATA_WIDTH决定内部数据位宽,8 位是标配但可以升到 32 位来降时钟压力;ENABLE_PADDING控制小于 60 字节的帧是否自动填充到最小帧长;TX_FIFO_DEPTHRX_FIFO_DEPTH决定缓冲深度,跑满 1G 线速的时候这两个值别设太小,我一般起始给 4096,跑通了再往下调。

2.2 PHY 与 MAC 层:GMII 和 RGMII 到底差在哪

GMII 和 RGMII 是两块最常打交道的接口。GMII 简单粗暴,8 位数据配一路 125MHz 时钟,上升沿就是上升沿,采样逻辑一目了然,缺点是引脚多,8 根数据加控制信号占一片 IO。RGMII 为了省引脚,把数据砍到 4 位,同时在时钟的上升沿和下降沿各传一次数据,还额外引入了 TXC 和 RXC 两根时钟线。代价是 FPGA 侧必须用 DDR 原语来收发,Xilinx 上对应ODDRIDDR,而且要在建立保持时间上做延迟补偿,通常通过约束或 PHY 内部延迟来调。工程里对这两种接口都有现成实现,我的经验是:初学阶段如果你的板子两种都支持,优先用 GMII 调试,出问题时波形好读;等逻辑跑通了再切 RGMII 去省引脚,切过去之后只需要重点盯 DDR 原语和延迟约束这两块。理解这一层的关键是搞清楚数据从 PHY 进来时是什么节拍,进来之后被拼成了几个字节、什么时候开始有效,这个信息直接决定你后面协议层模块的数据流怎么接。

2.3 AXI-Stream 握手:整个工程的通用语言

AXI-Stream 这个接口协议是这个工程的骨架,所有模块之间都靠它说话。它其实就四根关键线:valid表示发送方数据有效,ready表示接收方可以收,data是数据总线,last表示这是一帧的最后一个数据。握手规则是:只有当validready在同一个时钟上升沿同时为高时,数据才算真正传输成功。这个规则看起来简单,但初学最容易犯的错是把它当普通使能信号用,比如只看valid就开始处理数据,或者lastvalid不同步拉高。正确的心智模型是“一次握手一笔交易”:ready没来之前validdata都要保持不动,不能因为时钟走了一拍就把数据换掉。我刚开始写接收逻辑时,last位置判断错了一位,结果整帧数据永远差一个字节,抓包看到长度不对又找不到原因,最后对着波形一拍一拍数才定位到。读懂 AXI-Stream,这个工程就通了一半。

2.4 ARP、IP、UDP、ICMP 四块积木怎么拼

协议层这块,工程拆得特别清楚,最大的一个封装叫udp_complete,名字就直接告诉你它把 UDP 相关的东西全装好了。里面主要包含四块:arp负责地址解析,收到广播请求就回自己的 MAC,同时把别人的 IP-MAC 对应关系缓存下来,ARP_CACHE_SIZE默认给 8 条,够小网络用;ip负责 IPv4 头解析和生成,包括版本号、头长度、TTL、协议号这些字段,ip_tx负责把用户数据加上 IP 头再算校验和;udp是核心业务层,udp_rx剥掉 UDP 头把 payload 往外送,udp_tx加上源端口、目的端口、长度和校验和;icmp单独拿出来,专门处理 ping 请求,回了 ping 包才算网络层通了。这四块拼装的顺序是:以太网帧进ipip看协议号分流到udpicmp。我第一次看的时候不知道从哪下手,后来发现只要记住“数据从哪进、往哪出、不匹配的分流到哪”这三件事,整个数据流就理顺了。

2.5 参数化设计里的几个关键配置

这个工程全程参数化,很多行为靠参数开关。我挑几个实际会改的说说。UDP_CHECKSUM_ENABLE控制 UDP 校验和是否生成,如果你的上位机不校验,关掉能省点逻辑,但网络里混有校验的设备时最好开着,不然对面可能拒收。IPV4_ADDRIPV4_MAC_ADDR是本机地址,必须和上位机配成同一网段,我踩过把板子 IP 设成 192.168.1.x 而电脑是 192.168.0.x 的坑,ping 了半天才发现网段不通。ARP_REQUEST_RETRY_INTERVAL决定多久重发一次 ARP 请求,网络稍微慢点就把它调大。还有MII_*一堆参数跟 PHY 管理接口有关,用于配置 PHY 的速率和双工模式,如果你的板子用 MDIO 配置 PHY,这组参数得对着手册填。

3. 实操过程:从仿真到上板的完整链路

3.1 仿真环境搭建:先让数据流在波形里跑起来

我强烈建议第一步不要上板,先在仿真里把协议层跑通。工程自带的 testbench 通常都放在tb或者sim目录下,里面会例化 UDP 顶层,然后造一帧数据从tx灌进去,再从rx读出来对比。可惜大部分开源工程的 testbench 不完整,需要你自己补一点。我的做法是写一个最简单的激励:给udp_tx的 AXI-Stream 灌一帧固定内容,比如0x11 0x22 0x33 ...,同时在udp_rx侧监听输出,用$display或者写文件的方式把收到的 payload 打印出来。仿真跑通之后再往上层加 ARP 和 IP。这一步的核心目的是验证你的握手时序理解对不对,因为仿真是唯一能看到每一拍信号的地方,上板抓波形成本太高。这里有个小技巧:用$dumpfile$dumpvars存 VCD,再用 GTKWave 打开,把validreadylast加进波形组,这样握手有没有问题是肉眼可见的。

3.2 引脚、时钟、复位三件套怎么接

上板前的准备工作里,引脚约束、时钟、复位是最容易出问题的三样。先说时钟,RGMII 方案通常需要一个 125MHz 的参考时钟给 PHY,有的板子由 PHY 芯片回传 RXC,有的需要 FPGA 自己产生。如果工程配置成 FPGA 输出时钟,那你要在顶层例化一个 MMCM 或 PLL,把晶振频率倍频到 125MHz 并且注意相位。引脚约束要严格对着板子原理图来,我就因为把 RGMII 的 TXD 和 TXC 顺序写反,折腾了一晚上。复位这块,工程用的是同步复位,所以你不能把外部按键直接接到rst上,中间要加两级同步器消抖和打拍,否则按键抖动会造成状态机误复位。另外注意上电顺序:PHY 芯片需要时间完成内部初始化,主逻辑复位释放得太早,第一批数据会丢,我一般让复位信号保持几百毫秒再释放,或者干脆等 PHY 链路上的 RXC 稳定后再放。

3.3 与 PC 联调:先把 ping 跑通再谈 UDP

上板后第一件事不是发 UDP 数据,而是 ping。为什么先 ping?因为 ping 走的是 ICMP,它不需要 ARP 之外的其他解析,路径最短,能通就说明物理层、MAC 层、IP 层、ARP 全部没问题,后面 UDP 出问题就可以只在 UDP 模块里找。联调步骤是这样:先把板子和电脑用网线直连,电脑端把 IP 设成和板子同网段,比如板子是 192.168.1.10,电脑设 192.168.1.100,掩码 255.255.255.0。然后在电脑上ping 192.168.1.10,同时用抓包工具看有没有 ARP 请求发出去。如果 ARP 有请求但板子没回,检查IPV4_MAC_ADDR参数是不是填错了;如果 ARP 回了但 ping 不通,检查 ICMP 模块的校验和计算。我第一次 ping 通的那一刻真的挺激动,因为从那之后所有的问题都变成了局部问题。

3.4 用 UDP 打流验证数据通路

ping 通之后开始测 UDP。最土但最有效的办法是写一个简单的上位机脚本,用 Python 的 socket 往板子发数据,同时板子把收到的数据原样回发,看内容对不对。这里要注意端口号,板子的udp_rx侧只认自己配置的目的端口,上位机发的目标端口要对上。发送频率也从慢到快,先一秒钟一包,跑通了再加到线速。如果一开始就满速发,缓冲来不及处理会丢包,你反而以为是逻辑错了。我实测下来,1G 链路上用 32 位数据位宽、FIFO 深度给够,丢包率可以做到 0。另外建议在板子侧加一个计数器,统计正确收到的包数和校验失败的包数,用 LED 或者串口打出来,这样你在不看抓包的情况下也能判断链路健康度。

3.5 关键的时序约束和跨时钟域处理

上板跑通之后如果想跑稳,时序约束是绕不过去的。RGMII 的收发路径上,PHY 时钟和内部逻辑时钟是不同源的,属于典型的跨时钟域场景。工程里通常用异步 FIFO 来做隔离,你要做的是给这两个时钟域分别建时钟约束,并且给跨域的路径加set_false_path或者set_clock_groups把它们分开,否则工具会去优化那些本来不该优化的路径,反而引入问题。我第一次没加约束,综合报了上百个时序违例,后来加了时钟组就好了。另外,如果时钟是自己用 MMCM 生成的,别忘了给生成的时钟建约束,还要检查 MMCM 的锁定信号是否参与了复位释放逻辑。

4. 常见问题与排查技巧实录

4.1 问题速查表

现象可能原因排查方向
完全 ping 不通IP 不同网段、MAC 参数错、ARP 没工作抓包看是否有 ARP 请求和回复
ARP 通但 ping 不通ICMP 校验和错、TTL 为 0看 ICMP 模块生成的回包字段
ping 通但 UDP 收不到目的端口不匹配、UDP 校验和错抓包看 UDP 头是否符合预期
偶尔丢包缓冲太浅、时钟约束缺失加大 FIFO 深度、补时序约束
收到的数据偏移一位last判断位置错、握手采错拍波形逐拍核对valid/ready/last
上电后无反应复位释放时机早、PHY 未初始化检查复位时长和 MDIO 配置

这张表是我自己踩坑攒下来的,遇到问题先对照着缩小范围,比盲目改代码快得多。特别是“收到的数据偏移一位”这类问题,八成是握手逻辑写错了,先在波形里确认last拉高的那一拍对应的是不是最后一个字节,再回头改代码。

4.2 波形定位三步法

怎么用波形快速定位问题,我总结了三步。第一步看顶层的validready,如果有一方一直不拉高,说明卡在握手上了,往下找是哪个模块没准备好。第二步看last,一帧数据里last只应该出现一次,如果出现多次或者根本没出现,说明长度信息有问题。第三步看数据内容的前几个字节,正常以太网帧开头应该是目的 MAC 地址,如果开头全是 0 或者乱的,说明数据流接错了源头。这三步走下来,八成的问题都能定位到具体模块。我个人的习惯是把关键信号提前在仿真里打标记,上板时再用集成逻辑分析工具抓同样的信号,两边对比着看,效率会高很多。

4.3 几个让我印象深刻的坑

第一个坑是字节序。Verilog 里数据总线通常按大端排列,但我在拼 IP 头的时候按小端思维把地址填反了,结果包发出去了但对面不认。后来改成严格按照字段定义从高字节往低字节摆,问题就没了。第二个坑是 UDP 长度字段,它指的是“UDP 头 + 数据”的总长度,不包括以太网头和 IP 头,我一开始把整个帧长填进去了,被抓包工具标红。第三个坑是 ARP 缓存的老化时间,默认几分钟就过期,长时间不发数据之后突然要发,会先卡在 ARP 请求上,表现出来就是“隔一会儿第一包丢”。解决方式是让应用层在发送前检查 ARP 状态,或者把缓存超时调长。这些坑网上文档基本不讲,都是自己撞出来的,写出来给后面的人省点时间。

4.4 关于仿真授权和工具链的小提醒

另外提一句工具链的问题。有些人跑仿真时会遇到授权报错,比如拿不到仿真 license,这种情况一般是环境变量或者工具配置问题,不是代码本身的问题。我的建议是仿真优先用开源工具链,配合iverilog或者 Verilator 做功能验证,综合和上板再用厂商工具,这样能避开一堆授权和版本兼容的坑。verilog-ethernet 本身是纯 RTL,跨工具综合能力不错,但你要注意它里面用到的厂商原语在仿真时可能需要对应的库支持,遇到找不到模块的报错,先确认是不是原语库没加到仿真路径里。

5. 跑通之后还能往哪扩展

把基础 UDP 收发跑通之后,这个工程其实是个很好的起点。往性能方向走,可以试着把DATA_WIDTH从 8 位提到 32 位甚至 64 位,看看吞吐能不能翻倍,同时观察 FIFO 深度和时钟频率的配合;往协议方向走,工程里已经有不少上层接口的预留,你可以在 UDP 之上自己定义一个简单的应用层协议,加个序号和校验字段,做成可靠传输;往应用方向走,把数据采集逻辑挂到udp_tx前面,就变成了一个网络化的采集节点,比如把 ADC 数据或者图像数据打 UDP 包发出去,实时性比软核方案好很多。我自己下一步打算做的是把 UDP 接收和一个小型状态机结合,根据收到的命令字去控制板子上的外设,相当于一个最简的网络控制节点。整个过程里最值钱的其实不是代码本身,而是排错那套方法:先分层、再仿真、再抓波形、最后对照速查表,这套流程走到哪都通用。

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

Innovus CTS中clock_gen skew group自动分组的陷阱与处理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 18:22:47

立即上手gh-stack的7个理由:GitHub官方Stacked PRs工具深度解析

立即上手gh-stack的7个理由:GitHub官方Stacked PRs工具深度解析 【免费下载链接】gh-stack GitHub Stacked PRs 项目地址: https://gitcode.com/GitHub_Trending/ghst/gh-stack gh-stack 是 GitHub 官方推出的 Stacked PRs(堆叠 PR)命…

作者头像 李华
网站建设 2026/9/18 18:21:48

ChatGPT 外链偷渡 AI 智能体?TaoToken 这样改 Codex 的 config.toml

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 18:21:30

基于 Jev 的决策服务,TaoToken 只提供 Key 入口

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 18:20:39

Windows宽窄字符串转换全解析:从编码原理到实战避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华