FPGA实现UDP协议栈:verilog-ethernet开源工程深度解析
2026/9/15 3:05:11 网站建设 项目流程

很多FPGA学习者到了网络通信这一步都会卡一阵子:点灯、按键、数码管这些基础实验都能玩明白,仿真也会写了,但一碰到以太网就头大。我今天要拆的这个开源项目,是进阶路上非常值得花时间啃的一块硬骨头——verilog-ethernet 开源 UDP 协议栈工程。它把从 MAC、ARP、ICMP、IP 到 UDP 的整套网络协议,用可综合的 Verilog 代码完整实现,你不需要一颗运行 Linux 的处理器,只需要一片 FPGA 加一颗 PHY 芯片,就能跟 PC 上的网络调试助手直接收发 UDP 数据,延迟还低得感人。

这套代码特别适合两类人:一类是想搞懂“FPGA 怎么上网”的学生和初学者,另一类是项目里需要高速数据传输、又不愿意砸钱买商业 IP 核的工程师。它的学习曲线确实不低,但只要按照“先跑仿真、再读接口、最后上板”的节奏走,你会发现网络协议栈这件事,远没有想象中那么玄乎。今天这篇文章,我就把整个工程从目录结构、模块拆解、接口时序,到回环实验和常见坑,完整过一遍。

1. 为什么选 verilog-ethernet 来学 UDP 协议栈

1.1 先弄明白 FPGA 上跑 UDP 协议栈到底解决什么问题

很多人第一次听到“FPGA 实现 UDP 协议栈”会觉得不太理解:协议栈这东西不是应该跑在 CPU 上吗?Linux 内核里不是现成的吗?确实,软件协议栈成熟稳定,但在高速数据采集、图像传输、雷达信号处理这类场景里,软件有天然的瓶颈。

举个最直观的例子:一块 200MSPS 的 ADC,16bit 采样,一个采样点就是 2 字节,每秒产生的数据量是 400MB。如果走 CPU 加软件协议栈,CPU 要一边搬运数据、一边拼包、一边处理中断,好不容易把包发出去,万兆网卡可能还能跑满,但千兆网卡基本就到极限了,而且数据路径上多了好几层拷贝,延迟很难压到微秒级。FPGA 做这件事就不一样:数据从 ADC 进来,经过简单的 FIFO 缓冲,直接进入硬件 UDP 协议栈,以太网帧、IP 头、UDP 头全是组合逻辑和状态机拼出来的,没有任何操作系统调度的不确定性,延迟可以做到几微秒以内。

UDP 协议本身又特别适合硬件实现。它无连接、无重传、无拥塞控制,发送方只需要把用户数据按照固定格式封装成包扔出去就行,接收方拆包再把数据交给用户逻辑。相比 TCP 那一堆状态机、序列号、重传定时器、拥塞窗口,UDP 的硬件开销小一个数量级。这也是为什么绝大多数科研仪器、图像采集卡、高速数据记录设备,都优先选择 UDP 而不是 TCP。

1.2 verilog-ethernet 到底包含哪些能力

verilog-ethernet 是 GitHub 上一个很有名的开源项目,作者是 Alex Forencich。它不是一个简单的“UDP 收发模块”,而是一整套以太网相关 IP 的集合,覆盖了从千兆到百兆再到万兆、甚至更高速率的以太网 MAC,以及建立在 MAC 之上的 ARP、ICMP、IP、UDP 协议处理逻辑。

具体来说,这个项目能给你这些东西:

  • 完整的以太网 MAC 实现,支持 1G、10G、25G、40G、100G 等多个速率档位;
  • 内置 UDP 协议栈,包括 UDP 收发、IP 头封装与解析、校验和计算;
  • 内置 ARP 协议处理,自动缓存对端 MAC 地址,不用你手动填写;
  • 内置 ICMP 协议回显,PC 端可以直接 ping FPGA,这对调试链路非常有用;
  • 对外接口统一采用 AXI-Stream 标准,和 Xilinx 自家的 IP 核接口风格一致;
  • 提供完整的 testbench,配套 Makefile,支持 iverilog、Verilator、Vivado xsim 等多种仿真方式。

另外,这个项目的代码风格非常规范,每个模块都有清晰的参数列表和信号注释,很适合当学习资料来读。它的依赖库也做得比较干净,核心依赖是同一个作者的 verilog-axis、verilog-axi、verilog-common 这三个仓库,通过相对路径 include 进来。

1.3 从“零基础”到“能上手”的学习路径设计

如果你是真正意义上的“近似 0 基础”,我建议不要一上来就打开 udp_10g.v 这个顶层文件,否则很容易被那一大堆端口吓住。合理的学习路径应该是这样:

第一步,先把数电和 Verilog 语法的基础打牢,至少能独立写一个状态机和一段简单的 AXI-Stream 逻辑;第二步,弄懂以太网帧、IP 头、UDP 头的格式,知道每个字段占几个 bit、谁负责填什么;第三步,把 verilog-ethernet 仓库拉下来,先跑它自带的仿真,观察 testbench 是怎么例化、怎么激励、怎么检查结果的;第四步,找一小块用户逻辑接上去,做成一个最简单的 UDP 回环;最后再考虑上板,用 Wireshark 抓包验证整个链路。

这个顺序是我自己踩了好几次坑才总结出来的。很多人一上来就想着上板调试,结果被 ARP 不发、CRC 不对、时序不收敛、PHY 芯片没初始化等问题搞到崩溃。实际上,仿真阶段能把协议栈内部的行为看明白,上板之后就只剩环境问题,而不是逻辑问题了。

2. 工程开箱:把 verilog-ethernet 跑起来

2.1 获取工程与依赖库

第一步自然是把代码拿到手。verilog-ethernet 依赖另外三个基础库,最好把它们都 clone 到同一个目录下,因为源码里的 include 路径是相对路径。我自己习惯这样组织目录:

my_fpga_work/ ├── verilog-ethernet/ ├── verilog-axis/ ├── verilog-axi/ └── verilog-common/

然后进入 verilog-ethernet 目录,先别急着改代码,花半小时把 rtl 目录下的文件名扫一遍。你会发现模块命名很有规律,eth_mac_*是 MAC 层,eth_arp_*arp_*是 ARP 相关,ip_*是 IP 层,udp_10gudp_64这类是集成度更高的 UDP 协议栈封装。

这里提醒一句:verilog-ethernet 的版本迭代比较快,不同版本的接口和模块名可能有差异。我自己最早接触的是老版本,里面直接用eth_udp_rxeth_udp_tx两个模块,现在新版本则更推荐用udp_10gudp_64这种封装好的模块。所以看网上教程时如果发现端口对不上,不用慌,先去仓库里确认一下当前版本的源码,一切以代码为准。

2.2 目录结构和核心文件定位

新手面对一个开源工程最容易犯的错,就是想一口气看懂所有文件。正确的做法是先把核心文件挑出来。以我用的版本为例,下面这几个文件是学习的重点:

  • rtl/udp_10g.v:10G 速率的 UDP 协议栈顶层,接口完整,建议从这里开始读;
  • rtl/eth_mac_10g.v:10G 以太网 MAC,负责成帧、CRC、XGMII 接口;
  • rtl/arp_cache.v:ARP 缓存模块,保存 IP 到 MAC 的映射关系;
  • rtl/ip_eth_rx.vrtl/ip_eth_tx.v:IP 层接收与发送,处理 IP 头、校验和;
  • rtl/udp_checksum_gen.v:UDP 校验和生成,里面是加法树和进位回卷逻辑;
  • sim/udp_10g_tb.v:对应的 testbench,强烈建议认真读一遍。

如果你用 1G 的 PHY 和 RGMII 接口,可能还需要看rtl/eth_mac_rgmii.v或对应的 PHY 适配模块。总之,先定位一个“最高层模块”作为入口,再按数据通路一层一层往下追,比从头到尾按文件名读要高效得多。

2.3 用 iverilog 快速跑通自带的 testbench

这个工程自带的 testbench 写得很完整,直接用 iverilog 就能跑。我自己用的命令一般是:

cd verilog-ethernet make -f Makefile.sim iverilog TEST=udp_10g_tb ./sim/udp_10g_tb

如果环境中没有安装 iverilog,可以用系统的包管理器装一个,这是个开源的轻量级仿真器,速度很快,对学习阶段来说完全够用。跑的时候如果提示找不到axis_fifo.v这类模块,说明依赖库路径没配对,检查一下 Makefile 里的-I参数是不是指向了 verilog-axis 和 verilog-common 的 rtl 目录。

仿真跑完后,你会看到一串类似PASS或者Test complete的打印信息。testbench 里模拟了 PC 端发包、协议栈收包、再发回给 PC 的完整过程,还会自动检查数据内容是否一致。这一步通了,说明环境没问题,源码基本可读,可以继续下一步了。

2.4 在 Vivado 里建工程并完成综合

如果你最终要在 Xilinx FPGA 上跑,建议用 Vivado 建一个工程,把多个仓库的 rtl 目录都加入 Design Sources。添加文件时要注意,Vivado 默认会自动识别 include 相对路径,但为了保险,我一般会在 project settings 里把verilog-ethernet\rtlverilog-axis\rtlverilog-axi\rtlverilog-common\rtl都加到 Verilog include 路径里。

综合之前,先把 testbench 加到 Simulation Sources,跑一次行为仿真。这一步的意义很大,因为综合环境里如果出现语法错误或者缺少模块,仿真阶段就能暴露出来。等仿真通过,再跑综合,观察资源占用和时序报告。第一次综合如果只跑udp_10g,资源占用一般不会太高,LUT 大概几千,BRAM 若干,具体数量取决于参数配置。

重点检查两件事:一是时钟频率是否满足约束,二是有没有跨时钟域警告。verilog-ethernet 内部用了不少异步 FIFO,跨时钟域警告只要不影响功能可以暂时忽略,但如果出现高扇出或者时序违例的关键路径,就要认真对待了。

3. UDP 协议栈核心模块拆解

3.1 整体分层架构

很多初学者一听到“协议栈”就以为里面有很复杂的软件层次,其实在 FPGA 里,它本质就是一条数据通路加上若干处理状态机。verilog-ethernet 的分层结构大概可以表达成下面这样:

用户逻辑(AXI-Stream 接口) ↓ 发送方向 UDP 封装:加上 UDP 头(源端口、目的端口、长度、校验和) ↓ IP 封装:加上 IP 头(版本、头部长度、协议号、源 IP、目的 IP、校验和) ↓ ARP 处理:查询对端 MAC,必要时发起 ARP 请求 ↓ MAC 封装:加上以太网头部(目的 MAC、源 MAC、EtherType),生成 FCS ↓ PHY 接口:XGMII / RGMII / GMII,交给外部 PHY 芯片

接收方向则是完全相反的过程:PHY 先把码流变成并行数据,MAC 校验 FCS 后剥掉以太网头,IP 层剥掉 IP 头,UDP 层剥掉 UDP 头,最后把纯用户数据通过 AXI-Stream 交给用户逻辑。

这个分层思路,和软件协议栈虽然有相似之处,但实现方式完全不同。软件协议栈是“函数调用 + 内存拷贝”,FPGA 协议栈是“模块级联 + 流水线处理”。在 verilog-ethernet 里,UDP 模块内部其实例化了 IP 层和 ARP 缓存,IP 层又依赖 MAC 层,所以对外看起来是一个黑盒子,端口不多,但内部结构挺丰富。

3.2 UDP 模块对外接口逐个理解

udp_10g为例,它对外的主要接口可以分成四组:时钟复位、用户收发、网络接口、管理配置。

用户发送侧的信号一般是s_axis_udp_*,接收侧是m_axis_udp_*,这一组是标准 AXI-Stream。发送方向,你的用户逻辑往s_axis_udp_tdata上放数据,然后拉高s_axis_udp_tvalid,等s_axis_udp_tready为高时数据就算成功交接;接收方向,协议栈把收到的数据放在m_axis_udp_tdata上,拉高m_axis_udp_tvalid,你作为接收方把m_axis_udp_tready拉高,数据就被消费掉了。

网络接口侧通常是xgmii_txdxgmii_txcxgmii_rxdxgmii_rxc。XGMII 是 10G 以太网的 MAC 与 PHY 之间的接口,数据位宽 64bit,时钟频率 156.25MHz。如果你的板卡是 1G RGMII 接口,对应模块则可能是rgmii_*信号,或者通过eth_mac_rgmii桥接。管理配置接口是 AXI-Lite,通过它写入本机 MAC、本机 IP、本机端口、目标 IP、目标端口等信息。

下面是一个简化版的udp_10g例化模板,帮助你先在头脑里建立端口映射:

udp_10g #( .TARGET ("XILINX"), .UDP_DATA_WIDTH (64) ) u_udp_10g ( .clk (clk), .rst (rst), // 用户发送侧 .s_axis_udp_tdata (user_tx_tdata), .s_axis_udp_tkeep (user_tx_tkeep), .s_axis_udp_tvalid (user_tx_tvalid), .s_axis_udp_tready (user_tx_tready), .s_axis_udp_tlast (user_tx_tlast), .s_axis_udp_tuser (user_tx_tuser), // 用户接收侧 .m_axis_udp_tdata (user_rx_tdata), .m_axis_udp_tkeep (user_rx_tkeep), .m_axis_udp_tvalid (user_rx_tvalid), .m_axis_udp_tready (user_rx_tready), .m_axis_udp_tlast (user_rx_tlast), .m_axis_udp_tuser (user_rx_tuser), // 网络接口侧,这里以 XGMII 为例 .xgmii_txd (xgmii_txd), .xgmii_txc (xgmii_txc), .xgmii_rxd (xgmii_rxd), .xgmii_rxc (xgmii_rxc) );

不同版本信号的名称可能略有出入,但核心的 AXI-Stream 握手信号是通用的,理解规则之后换版本也很快。

3.3 AXI-Stream 握手协议:贯穿全栈的主线

AXI-Stream 是 Xilinx IP 核常用的接口标准,verilog-ethernet 从 MAC 层到 UDP 层,几乎所有模块之间都靠它连接。理解这个握手协议,是读懂整个工程的关键。

规则其实就一条:当tvalidtready在同一个时钟上升沿都为高时,一个数据传输发生。tdata是数据,tkeep表示哪些字节有效,tlast表示这是这一帧的最后一拍,tuser通常携带错误标志或用户自定义信息。

初学者最容易犯的错误,是在tvalid还没有准备好的时候就等tready,或者反过来在tready未拉高的时候提前拉低了tvalid。正确做法是,作为发送方,tvalid一旦拉高,就必须维持到数据被接收方取走,也就是直到tready拉高的那个时钟;作为接收方,tready可以随时拉高,也可以随时拉低,但如果tvalid已经拉高,拉低tready就是告诉对方“先等一下,我上一拍还没处理完”。

打个生活中的比方:你去快递站寄包裹,tvalid就是你告诉工作人员“我这有个包裹要寄”,tready就是工作人员说“我现在有空,递给我吧”。只有你说“有包裹”同时工作人员说“有空”的那一瞬间,包裹才算真正交到对方手上。如果工作人员说“你等一下”,你就得一直举着包裹,不能放下。

在 verilog-ethernet 里,UDP 模块发送侧通常会内部处理 AXI-Stream 到 MAC 帧的转换,用户不需要自己处理以太网头,只需要在s_axis_udp_tvalid上按帧送入数据,并在最后一拍拉高tlast即可。

3.4 ARP 与 ICMP 在协议栈里的作用

很多人在 PC 和 FPGA 之间调 UDP,遇到的第一只拦路虎不是 UDP 本身,而是 ARP。ARP 的作用是解决“知道对端 IP 地址,但不知道对端 MAC 地址”的问题。以太网帧在二层传输时,目的 MAC 地址是必须的,而 PC 端一般只配置 IP,MAC 地址需要通过 ARP 查询获得。

verilog-ethernet 内部实现了完整的 ARP 客户端和缓存。当协议栈想给某个 IP 发 UDP 包,但缓存里没有对应的 MAC 地址时,它会自动发一个 ARP 请求,收到应答之后再发送真正的 UDP 数据。这个过程中,用户逻辑完全不需要干预,但如果配置的目标 IP 不可达,或者对端防火墙拦截了 ARP,UDP 数据就会一直发不出去。

ICMP 是另一个容易被忽略但非常有用的功能。verilog-ethernet 内置了 ICMP 回显支持,也就是说,只要板卡网络链路正常,你在 PC 上 ping 一下 FPGA 的 IP 地址,FPGA 能自动回一个 ICMP Reply。这简直是链路调试的“探针”。我自己的习惯是:上板测试 UDP 之前,先 ping 一下板卡的 IP,通了再测 UDP。如果 ping 不通,基本不用指望 UDP 能通。

3.5 头部校验和的计算与硬件实现

UDP 和 IP 头部都有校验和保护,verilog-ethernet 内部用纯组合逻辑实现了校验和计算。IP 头校验和覆盖整个 IP 头部,UDP 校验和则覆盖“伪头部 + UDP 头 + 数据”。伪头部由源 IP、目的 IP、协议号、UDP 长度组成,这些字段在发送端封装时已经确定,所以硬件实现并不复杂——本质就是按 16bit 为单位,把所有相关的字相加,累加过程中如果产生进位就回卷,最后取反。

硬件实现校验和有两个常见坑。第一个是宽度:直接用 32bit 加法器累加 16bit 数据时,最后要把高 16bit 的进位再加回低 16bit,否则结果会差 1 到 2 个进位;第二个是更新时机:如果你的应用在发送过程中动态改变数据内容或者源 IP,校验和必须重新计算,否则抓包会看到 checksum wrong。

verilog-ethernet 处理得比较规范,UDP 模块内部有独立的校验和生成模块,用户只要在 AXI-Stream 上送入数据,它会自动完成校验和计算和写入,不需要用户干预。但理解原理仍然重要,因为抓包时如果看到 checksum 错误,至少你能判断是哪里出了问题,而不是一头雾水。

4. 动手实践:实现一个 UDP 回环小系统

4.1 需求与顶层设计

学习协议栈最好的练手项目,就是做一个 UDP 回环:PC 上通过网络调试助手发送任意长度的 UDP 数据包给 FPGA,FPGA 原封不动地把数据再发回给 PC。这个需求看起来简单,但它覆盖了“数据接收、缓存、发送”的完整路径,做完这个实验,对整个协议栈的用法就有底了。

硬件方面,如果你手里是常见的 FPGA 开发板,一般带 RTL8211、88E1512 这类千兆 PHY 芯片,接口是 RGMII。verilog-ethernet 对 RGMII 的支持比较成熟,直接用eth_mac_rgmii或对应 PHY 适配模块即可。如果你用的是带 SFP+ 光口的板卡,那可以考虑 10G 链路,但调试难度会大不少,建议先把千兆回环跑通。

顶层设计不需要很复杂,FPGA 内部就三块:UDP 协议栈、接收 FIFO、发送控制逻辑。协议栈收到的数据直接写入 FIFO,同时周期性地从 FIFO 读出并送到发送通道。这样即使 PC 发的数据速率和 FPGA 发送速率不匹配,FIFO 也能起到缓冲作用。

4.2 用户逻辑侧代码编写

最简单的实现方式是把接收通道直接接到发送通道上,代码大概是这样:

assign user_tx_tdata = user_rx_tdata; assign user_tx_tkeep = user_rx_tkeep; assign user_tx_tvalid = user_rx_tvalid; assign user_rx_tready = user_tx_tready; assign user_tx_tlast = user_rx_tlast;

这种直通写法在仿真里往往能跑通,但上板后可能会遇到背压问题。因为user_rx_tvaliduser_rx_tdata来自协议栈输出,而user_tx_tready来自协议栈输入,这两条通路如果组合逻辑拉得太长,时序容易紧张,而且协议栈接收和发送通道实际上是独立的,数据不一定会同时准备好。

更稳妥的办法,是例化一个 AXI-Stream FIFO 做中间缓冲。verilog-axis 库里就有现成的axis_fifo,直接例化即可。比如:

axis_fifo #( .DEPTH (2048), .DATA_WIDTH (64), .KEEP_ENABLE (1), .KEEP_WIDTH (8) ) u_axis_fifo ( .clk (clk), .rst (rst), .s_axis_tdata (user_rx_tdata), .s_axis_tkeep (user_rx_tkeep), .s_axis_tvalid (user_rx_tvalid), .s_axis_tready (user_rx_tready), .s_axis_tlast (user_rx_tlast), .m_axis_tdata (user_tx_tdata), .m_axis_tkeep (user_tx_tkeep), .m_axis_tvalid (user_tx_tvalid), .m_axis_tready (user_tx_tready), .m_axis_tlast (user_tx_tlast) );

FIFO 的好处是解耦了接收和发送速率,即使协议栈接收通道和发送通道时钟不同步,也不会互相拖累。当然你也可以在 FIFO 之前加一个简单的解析模块,只转发特定端口的数据,或者修改数据内容,这些都可以在熟悉回环之后慢慢加。

4.3 PC 端与硬件联调

上板之前,先把 PC 的 IP 地址和 FPGA 的 IP 地址规划好。假设 PC 是 192.168.1.100,FPGA 是 192.168.1.10,子网掩码都是 255.255.255.0。然后通过 AXI-Lite 把本机 MAC、本机 IP、本机端口、目标 IP、目标端口写进协议栈的寄存器。寄存器具体偏移以源码为准,不同版本可能不同,但一般都能在udp_10g_regs.v或者模块顶部的注释里找到。

PC 端推荐用 Wireshark 和网络调试助手配合。先打开 Wireshark 抓包,然后向 FPGA 的 IP 和端口发送一段固定的数据,比如 64 字节的0x5A重复填充。正常情况下,Wireshark 里能看到 FPGA 回过来的 UDP 包,内容一模一样。

如果没收到回包,步骤是:第一步,看 ARP 有没有成功,Wireshark 里如果只有 FPGA 发来的 ARP 请求而 PC 不应答,大概率是 PC 防火墙拦了;第二步,看 ICMP,在 CMD 里 ping 一下 FPGA 的 IP,能通说明链路层和网络层没问题;第三步,看 UDP 包本身,检查源端口、目的端口、校验和是不是都正常。按这个顺序查,问题很快就能定位。

4.4 用抓包和波形确认问题

很多时候,网口调试比普通逻辑调试更容易“盲人摸象”,因为你不知道数据到底在哪一层丢了。这时候抓包和片上逻辑分析仪结合起来用,效率最高。

Wireshark 侧重于观察外部行为,ILA(Integrated Logic Analyzer)则能观察 FPGA 内部信号。我习惯在 AXI-Stream 通路上拉几个关键信号,比如user_rx_tvaliduser_rx_tlastuser_tx_tvaliduser_tx_tready。如果 ILA 里能看到接收数据进来了,但发送侧一直没有tvalid拉高,说明用户逻辑或者 FIFO 那里卡住了;如果发送侧有tvalid,但tready一直为低,说明协议栈发送通道被占满或者配置有问题。

另外,抓包时注意看校验和字段。Wireshark 里如果显示incorrect checksum,先别怀疑是 FPGA 算错,也可能是网卡的 checksum offload 功能导致 PC 发出的报文本身校验和没更新。这种情况在通达万兆网卡上尤其常见,抓包显示校验和错误,但实际设备收到的包是正常的,因为校验和在硬件层被重新计算了。

5. 常见坑与排查实录

5.1 敲黑板:时序收敛问题

verilog-ethernet 本身设计得比较干净,但拿到自己板子上综合,难免会遇到时序问题。最典型的是 10G 链路,XGMII 接口需要 156.25MHz 的高频时钟,如果你只做了行为仿真,没有在约束文件里正确声明时钟,综合结果大概率是时序违例。

我的建议是,初学者先从 1G RGMII 上手。RGMII 接口对时序的要求相对宽松,PHY 芯片一般自带时钟管理,顶层只需要约束好时钟和引脚位置,时序收敛容易得多。跑 10G 的话,你还需要处理 GT 高速收发器、参考时钟、复位序列,这些都是单独的知识点。

另外,复位信号也很关键。verilog-ethernet 的rst是高有效复位,并且内部逻辑大多用同步复位。如果你在顶层做的是异步复位,记得要做“异步复位、同步释放”处理,否则系统上电后容易进入不确定状态。这个坑我踩过一次,现象是板卡偶尔能通、偶尔不能通,重启一下可能就好了,最后发现是复位释放的毛刺把协议栈内部状态机打乱了。

5.2 ARP 缓存和 MAC 地址的迷思

很多时候 UDP 不通,根源根本不在 UDP,而在 ARP。这里有几个我见过无数人栽进去的细节。

一是 MAC 地址配置错误。以太网头里的 MAC 地址是 6 字节,写成 12 个十六进制字符,比如00:11:22:33:44:55。配置到寄存器时,要注意字节序是低字节在前还是高字节在前,不同版本可能有差异。如果你发现 FPGA 发出的帧里源 MAC 明显不对,Wireshark 抓包一看就是乱码,那基本就是写入顺序搞反了。

二是 ARP 缓存过期。协议栈内部有 ARP 缓存,默认保存一段时间,超时后会自动重新发起 ARP 查询。这本来是正常机制,但在调试时容易造成困惑:第一次发包能通,隔了十几秒再发包就卡住,抓包一看才发现 FPGA 在重新走 ARP 流程。遇到这种情况不用慌,说明链路是好的,只是 ARP 缓存重新建立需要一点时间。

三是 PC 端防火墙。Windows 自带的防火墙在默认设置下,会拦截很多 ping 和 UDP 探测包,导致 ARP 正常但 ICMP 和 UDP 都不通。调试时最简单的方法是临时关闭防火墙,或者给某个端口加放行规则,否则你会在一个根本不属于 FPGA 的问题上浪费大量时间。

5.3 跨时钟域、位宽转换和 FIFO

如果你的用户逻辑时钟和协议栈时钟不是同一个,比如协议栈跑 156.25MHz,用户逻辑跑 100MHz,直接在两个时钟域之间接信号是不行的,必须加异步 FIFO。verilog-axis 里的axis_async_fifo就是干这个用的,它的例化方式和axis_fifo类似,但传入两个时钟,写侧一个、读侧一个。

还有一个很常见的问题是位宽不匹配。协议栈的 AXI-Stream 数据位宽可能是 64bit 或者 256bit,但你的用户逻辑可能只想按字节流处理。这种场景可以用axis_adapter模块做位宽转换,它会自动处理tkeep的扩展和压缩,还会正确处理tlast的位置。

我曾经遇到过一个场景:用户逻辑按 32bit 发送数据,协议栈是 64bit 接口,直接拼接时忘了处理tkeep,导致最后一拍的有效字节判断错乱,PC 端收到的数据总是比发送的多 4 个字节。后来用axis_adapter一接就正常了。所以,涉及位宽转换,优先用现成 IP,不要自己手搓拼接逻辑。

5.4 配置寄存器的大坑:local 和 remote 别搞反

最后说一个特别容易犯的低级错误:配置寄存器里的local_portremote_portlocal_port是 FPGA 这一端的 UDP 端口,remote_port是 PC 那一端的 UDP 端口。发送数据时,协议栈会把local_port填进 UDP 头的源端口字段,把remote_port填进目的端口字段。

如果你把这两个值写反了,FPGA 发出去的包源端口和目的端口正好调换,PC 的网络调试助手如果监听在特定端口,就收不到数据。这类问题用 Wireshark 一眼就能看出来:抓包看到 UDP 头的源端口和目的端口跟预期不一致,就说明配置写反了。

还有一种情况是 AXI-Lite 写寄存器没有严格握手,导致某些寄存器值没有真正写进去。AXI-Lite 的握手规则和 AXI-Stream 类似,写地址通道awvalidawready、写数据通道wvalidwready都要同时为高才有效。我自己写初始化代码时,会把每次写操作整理成一个函数,比如:

# 伪代码示意 write_reg(0x04, local_mac_low) write_reg(0x08, local_mac_high) write_reg(0x10, local_ip) write_reg(0x14, local_port) write_reg(0x20, remote_ip) write_reg(0x24, remote_port)

写完remote_port之后,再读一遍确认值确实写入成功。如果读出的是 0 或者乱码,说明总线时序有问题,需要回头查状态机。

结尾

写到这里,verilog-ethernet 的核心脉络基本过了一遍。我个人学习这个工程时最大的体会是:开源项目能不能吃透,不在于你把每个模块的每一行代码都背下来,而在于你理解了数据是怎么流进去、怎么处理、怎么流出来的。verilog-ethernet 给了你一个特别好的机会,因为它的代码规范、层次清晰、testbench 完整,你完全可以通过仿真观察数据包的“一生”:从 PC 发出 ARP 请求,到 FPGA 应答,再到 UDP 数据被拆包、重组、回送,整个过程都能在波形和抓包里看得清清楚楚。

如果你照着这篇文章的思路,先从仿真跑通,再做回环实验,最后再用 Wireshark 抓包验证,我相信你收获的不只是“会用这个 IP”,而是对 FPGA 网络通信整个体系有了真正扎实的理解。等这个层面打通之后,再去看 PCIe、DDR、图像采集和网络传输结合起来的高速系统,你会发现很多概念都是相通的。最后再分享一个小技巧:遇到问题别急着改代码,先问自己“协议栈现在处于哪个状态、哪一层在丢包”,带着这个思路去抓包、看波形,问题往往比想象中更好定位。

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

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

立即咨询