项目地址:alexforencich/verilog-ethernet
本文承接前面的FPGA网络通信与UDP逻辑设计,从"阅读一个成熟开源工程"的角度,分析
verilog-ethernet的目录结构、协议分层、AXI-Stream接口、千兆RGMII数据路径、UDP回环示例以及仿真方法。
前言
在前面的文章中,我们已经按照Ethernet MAC、ARP、IPv4和UDP的顺序,对FPGA网络协议栈进行了模块划分和逻辑设计。
自己从零编写协议栈,可以帮助我们理解每个字段是如何生成的;而阅读一个成熟的开源工程,则能够进一步学习:
- 大型RTL工程如何划分模块;
- 帧头与Payload如何分别握手;
- 不同协议层之间如何通过AXI-Stream连接;
- ARP查询失败、帧提前结束等异常如何上报;
- RGMII、GMII、10G XGMII如何复用同一套上层协议逻辑;
- Testbench如何构造真实Ethernet、ARP、IP和UDP报文。
verilog-ethernet正是一个非常适合学习上述内容的工程。它包含1G、10G和25G Ethernet相关模块,提供ARP、IPv4、UDP、MAC、RGMII/XGMII以及PTP等逻辑,并使用cocotb构建了较完整的仿真环境。
但在正式展开前,需要先说明一个重要信息:
verilog-ethernet仓库已经被作者标记为Deprecated。作者说明后续新功能、Bug修复和商业支持转移到新项目fpganinja/taxi。因此,本文将verilog-ethernet作为Verilog协议栈学习和已有工程维护参考;如果准备从零开始一个需要长期维护的新产品,应同时评估taxi。
本文分析基于verilog-ethernet的master分支。为了避免以后仓库变化造成阅读差异,建议实际项目中固定到经过验证的Commit,而不是始终跟随分支最新状态。
一、这个工程能够实现什么
从功能上看,verilog-ethernet不是单独的"UDP模块",而是一组可以按需求拼装的Ethernet RTL组件。
text
┌──────────────────────────────────────────────────────────────────┐
│ verilog-ethernet │
├────────────────┬────────────────┬────────────────┬───────────────┤
│ 协议栈模块 │ MAC模块 │ PHY接口模块 │ 公共基础模块 │
├────────────────┼────────────────┼────────────────┼───────────────┤
│ ARP │ 1G MAC │ MII/GMII/RGMII │ FIFO │
│ IPv4 │ 10G/25G MAC │ XGMII │ Arbiter/Mux │
│ UDP │ FCS插入/检查 │ 10GBASE-R │ LFSR/CRC │
│ UDP Checksum │ PAUSE控制 │ SERDES适配 │ CDC │
│ PTP │ 帧填充/IFG │ DDR IO │ AXI-Stream │
└────────────────┴────────────────┴────────────────┴───────────────┘
官方README推荐的顶层选择如下。
| 使用场景 | 推荐协议栈顶层 | 数据宽度 |
|---|---|---|
| 1G,只需要IPv4和ARP | ip_complete |
8 bit |
| 1G,需要UDP、IPv4和ARP | udp_complete |
8 bit |
| 10G/25G,只需要IPv4和ARP | ip_complete_64 |
64 bit |
| 10G/25G,需要UDP、IPv4和ARP | udp_complete_64 |
64 bit |
对于当前基于RGMII的千兆UDP通信,我们主要关注8 bit数据路径:
text
eth_mac_1g_rgmii_fifo
↕ AXI-Stream Ethernet Frame
eth_axis_rx / eth_axis_tx
↕ Ethernet Header + Payload AXIS
udp_complete
↕ UDP Header + Payload AXIS
用户逻辑
二、仓库目录结构
下载工程后,根目录的核心结构如下。
text
verilog-ethernet/
├── rtl/ # Ethernet、ARP、IP、UDP、PTP等核心RTL
├── tb/ # 各个基础模块的Testbench
├── example/ # 多种FPGA开发板的完整示例工程
├── lib/axis/ # AXI-Stream公共组件
├── syn/ # Vivado等工具使用的综合约束脚本
├── scripts/ # 代码生成或工程辅助脚本
├── README.md # 模块说明和接口约定
├── tox.ini # Python/cocotb测试环境版本
└── COPYING # MIT License
2.1 rtl目录
rtl目录不是按照"板卡"组织,而是按照"功能"组织。这样同一个协议模块可以复用于不同FPGA和不同PHY接口。
| 文件或文件组 | 功能 |
|---|---|
arp.v、arp_cache.v |
ARP状态控制和ARP缓存 |
arp_eth_rx.v、arp_eth_tx.v |
ARP帧解析与生成 |
ip.v |
8 bit IPv4收发模块 |
ip_complete.v |
IPv4与ARP的集成顶层 |
udp.v |
8 bit UDP收发模块 |
udp_complete.v |
UDP、IPv4和ARP集成顶层 |
udp_checksum_gen.v |
UDP长度、IP长度和UDP校验和生成 |
eth_axis_rx.v、eth_axis_tx.v |
Ethernet Header接口与完整AXIS帧之间转换 |
eth_mac_1g.v |
1G GMII MAC |
eth_mac_1g_rgmii_fifo.v |
带跨时钟FIFO的RGMII三速MAC顶层 |
axis_gmii_rx.v、axis_gmii_tx.v |
AXIS帧与GMII字节流转换 |
rgmii_phy_if.v |
GMII与RGMII DDR接口转换 |
axis_eth_fcs_* |
Ethernet FCS计算、插入和检查 |
lfsr.v |
通用并行LFSR/CRC组合逻辑 |
*_64.v |
面向10G/25G的64 bit数据路径 |
2.2 example目录
example目录提供了面向多种板卡的完整工程,例如KC705、VCU118、ZCU102、Arty以及部分Intel FPGA板卡。
每个示例通常包含:
text
example/<BOARD>/<design>/
├── rtl/fpga.v # 板级顶层:引脚、时钟、复位
├── rtl/fpga_core.v # 协议栈和UDP Echo核心
├── tb/ # 该板级核心的cocotb测试
├── fpga.xdc # 板卡引脚及基础时钟约束
├── eth.xdc # Ethernet接口约束
├── Makefile # 构建入口
└── README.md # 板卡配置、编译和测试说明
学习时不建议一开始从rtl/udp_ip_rx.v逐行阅读。更高效的方式是先选一个与目标接口接近的完整示例,再从fpga_core.v向下追踪模块实例。
对于Xilinx 1G RGMII工程,可以参考:
text
example/KC705/fpga_rgmii/
它使用RGMII PHY接口,默认实现UDP Echo,整体结构与多数外接千兆PHY的Xilinx板卡比较接近。
三、下载与准备工程
3.1 Git下载
bash
git clone https://github.com/alexforencich/verilog-ethernet.git
cd verilog-ethernet
为了让后续工程可重复,建议记录当前Commit:
bash
git rev-parse HEAD
如果团队已经验证某个版本,可以直接固定:
bash
git checkout <已经验证的commit-id>
3.2 Windows下运行Makefile
该工程的示例大量使用GNU Make。在Windows上可以使用以下环境之一:
- Vivado Tcl Shell配合已安装的GNU Make;
- MSYS2或Git Bash;
- WSL;
- 不运行Makefile,直接按照Makefile中的源文件列表创建Vivado工程。
无论采用哪种方式,命令行都必须能够找到Vivado:
bash
vivado -version
如果出现:
text
vivado: No such file or directory
说明Vivado可执行程序没有加入当前终端的PATH,并不是Verilog源码缺失。可以先从Vivado安装目录启动settings64.bat,或将Vivado的bin目录加入环境变量,然后重新打开终端。
3.3 不要只添加一个顶层文件
udp_complete.v不是完全独立的单文件IP。它还会例化UDP、IP、ARP、仲裁器和AXI-Stream FIFO等模块。
对于1G RGMII UDP Echo,依赖关系可简化为:
text
fpga_core
├── eth_mac_1g_rgmii_fifo
│ ├── eth_mac_1g_rgmii
│ │ ├── rgmii_phy_if
│ │ └── eth_mac_1g
│ │ ├── axis_gmii_rx
│ │ ├── axis_gmii_tx
│ │ └── lfsr
│ ├── axis_async_fifo
│ └── axis_async_fifo_adapter
├── eth_axis_rx
├── eth_axis_tx
├── udp_complete
│ ├── udp
│ │ ├── udp_ip_rx
│ │ ├── udp_ip_tx
│ │ └── udp_checksum_gen
│ └── ip_complete
│ ├── ip
│ └── arp
└── axis_fifo # UDP Payload回环缓存
如果直接把udp_complete.v加入Vivado,综合时会继续报出一系列"module not found"。最稳妥的方法是参考对应示例的fpga/Makefile中SYN_FILES列表,把列出的RTL和lib/axis/rtl依赖一起加入工程。
四、先理解工程里的AXI-Stream约定
这个工程最值得学习的部分之一,是它没有为每个协议层重新发明一套数据接口,而是统一使用AXI-Stream风格的握手。
README中给出的公共信号含义如下。
| 信号 | 含义 |
|---|---|
tdata |
数据,位宽由数据路径决定 |
tkeep |
当前数据拍中哪些字节有效,主要用于64 bit模块 |
tvalid |
发送端声明当前数据有效 |
tready |
接收端声明能够接收数据 |
tlast |
当前数据拍为一帧的最后一拍 |
tuser |
与tlast && tvalid一起表示坏帧或错误帧 |
4.1 一次有效传输
text
clk _/‾\_/‾\_/‾\_/‾\_/‾\_/‾\_
tvalid ____/‾‾‾‾‾‾‾‾‾‾‾\________
tready ________/‾‾‾‾‾‾‾‾‾\______
tdata ----<D0><D0><D1><D2><D3>----
↑ ↑ ↑ ↑
valid && ready时才完成传输
当tvalid=1而tready=0时,发送端必须保持tdata、tlast和tuser不变,直到握手完成。
4.2 Header和Payload分离
verilog-ethernet的UDP、IP和Ethernet接口通常不是把所有内容混成一条字节流,而是分成:
- 一组并行Header字段;
- 一条Payload AXI-Stream。
以UDP发送为例:
text
Header通道:
s_udp_hdr_valid ──┐
s_udp_hdr_ready ◄─┤ 一次握手锁存IP、端口、长度等全部Header字段
source_ip ──┤
dest_ip ──┤
source_port ──┤
dest_port ──┘
Payload通道:
s_udp_payload_axis_tdata ─────<D0><D1><D2>...<DN>
s_udp_payload_axis_tvalid ─────/‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
s_udp_payload_axis_tready ─────/‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
s_udp_payload_axis_tlast ___________________/‾\
这样做的优点是:
- 上层逻辑无需自己串行发送UDP头;
- Header只握手一次,Payload可以连续流式传输;
- 仲裁器能够同时处理Header和对应Payload;
- 8 bit和64 bit数据路径可以保持一致的协议层接口形式。
4.3 s_和m_不要理解反了
该工程遵循常见命名:
s_*:Slave/Sink输入,本模块接收;m_*:Master/Source输出,本模块发送。
因此对udp_complete来说:
text
s_udp_*:用户逻辑送入协议栈,准备向网络发送
m_udp_*:协议栈从网络解析后,送给用户逻辑
这与"TX必须叫m_axis,RX必须叫s_axis"的直觉不完全相同,阅读时要始终站在当前模块边界判断方向。
五、udp_complete顶层结构
udp_complete是1G UDP协议栈最重要的集成模块。它自身并不包含RGMII引脚,而是以Ethernet Header加Payload AXIS的形式连接MAC侧。
text
udp_complete
┌──────────────────────────────────────────────────────────────┐
│ │
│ Ethernet RX ─► ip_complete ─► Protocol分类 ─┬─ UDP ─► m_udp │
│ └──────► m_ip │
│ │
│ s_udp ─► UDP组包 ─┐ │
│ ├─ ip_arb_mux ─► ip_complete ─► Ethernet TX│
│ s_ip ────────────┘ │
│ │
│ ARP状态机 + ARP Cache │
└──────────────────────────────────────────────────────────────┘
5.1 主要参数
| 参数 | 默认值 | 作用 |
|---|---|---|
ARP_CACHE_ADDR_WIDTH |
9 | ARP缓存地址宽度 |
ARP_REQUEST_RETRY_COUNT |
4 | ARP请求重试次数 |
ARP_REQUEST_RETRY_INTERVAL |
125000000*2 |
ARP重试间隔,默认按125 MHz计算 |
ARP_REQUEST_TIMEOUT |
125000000*30 |
ARP缓存/请求超时相关时间,默认按125 MHz计算 |
UDP_CHECKSUM_GEN_ENABLE |
1 | 是否使能UDP发送校验和生成 |
UDP_CHECKSUM_PAYLOAD_FIFO_DEPTH |
2048 | UDP校验和Payload FIFO深度 |
UDP_CHECKSUM_HEADER_FIFO_DEPTH |
8 | UDP校验和Header FIFO深度 |
这里有一个很重要的移植点:ARP_REQUEST_RETRY_INTERVAL和ARP_REQUEST_TIMEOUT使用时钟周期表示。如果协议栈时钟不是125 MHz,必须按照实际时钟频率重新换算。
5.2 配置接口
verilog
.local_mac (local_mac),
.local_ip (local_ip),
.gateway_ip (gateway_ip),
.subnet_mask (subnet_mask),
.clear_arp_cache(clear_arp_cache)
示例配置:
verilog
wire [47:0] local_mac = 48'h02_00_00_00_00_01;
wire [31:0] local_ip = {8'd192, 8'd168, 8'd1, 8'd10};
wire [31:0] gateway_ip = {8'd192, 8'd168, 8'd1, 8'd1};
wire [31:0] subnet_mask = {8'd255, 8'd255, 8'd255, 8'd0};
如果目的IP与本地IP在同一子网,协议栈查询目的主机MAC;如果不在同一子网,则应向gateway_ip对应的网关MAC发送。
5.3 为什么同时暴露IP接口和UDP接口
udp_complete不仅暴露s_udp/m_udp,还保留s_ip/m_ip接口。
接收方向中,IPv4的Protocol字段为8'h11时进入UDP模块;其他IPv4协议从m_ip_*接口输出。发送方向中,外部IP数据与UDP模块生成的IP数据通过ip_arb_mux仲裁后,共用IP/ARP发送通路。
因此:
- UDP业务直接使用
m_udp/s_udp; - ICMP、自定义IP协议等可以通过
m_ip/s_ip扩展; udp_complete本身不会自动完成ICMP Echo Reply。
如果希望开发板能够响应ping,需要在外部IP接口后增加ICMP解析与应答逻辑,或者使用包含相应扩展的上层封装。
六、UDP模块内部如何收发
udp.v主要例化三个子模块:
text
┌──────────────┐
IP RX ────────►│ udp_ip_rx │────► UDP Header + Payload
└──────────────┘
UDP Header ──┐ ┌───────────────────┐ ┌──────────────┐
UDP Payload ─┴──►│ udp_checksum_gen │──►│ udp_ip_tx │──► IP TX
└───────────────────┘ └──────────────┘
6.1 udp_ip_rx
接收方向的主要工作为:
- 接收已经由IP层解析的IPv4 Header和Payload;
- 从IP Payload前8 Byte中提取UDP源端口、目的端口、长度和校验和;
- 将剩余字节作为UDP Payload输出;
- 根据UDP长度生成
tlast; - 检查报文是否提前结束,并输出错误状态。
text
IP Payload输入: <SRC_PORT><DST_PORT><LEN><CHECKSUM><UDP DATA...>
│
UDP Header输出: 源端口、目的端口、长度、校验和
UDP AXIS输出: <D0><D1>...<DN>
6.2 udp_ip_tx
发送方向执行相反操作:锁存UDP Header字段,将8 Byte UDP头放在Payload前,然后向IP层输出Protocol=8'h11的IP报文。
6.3 udp_checksum_gen
UDP校验和需要同时覆盖:
- IPv4伪首部;
- UDP Header;
- UDP Payload;
- 奇数字节时的补0。
由于校验和通常要在发送UDP头之前确定,而计算又需要遍历整个Payload,所以模块内部使用FIFO缓存Header和Payload。计算完成后,再带着正确的长度与校验和向后级发送。
text
输入Header/Payload
│
├──► Payload FIFO ─────────────────────┐
│ │
└──► 累加伪首部、UDP头和Payload ─► checksum
│
▼
输出Header + 回放Payload FIFO
如果关闭UDP_CHECKSUM_GEN_ENABLE,发送侧可直接使用用户给定的s_udp_checksum。IPv4允许UDP校验和为0,但在需要更强错误检测的工程中建议保留校验和。
七、IP与ARP模块的协作
7.1 ip_complete
ip_complete把IPv4处理和ARP处理封装在一起。
text
┌───────────┐
Ethernet RX ────────────►│ IP/ARP分类│
└─────┬─────┘
┌──────────┴──────────┐
▼ ▼
IPv4 RX ARP RX
│ │
▼ ▼
上层IP ARP Cache
上层IP TX ─► 路由判断 ─► ARP查询 ─► IPv4组帧 ─► Ethernet TX
发送一个UDP包时,用户逻辑只提供目的IP,不需要自己提供目的MAC。IP/ARP层会根据子网掩码选择:
text
目的IP与本地IP同网段 → 查询目的IP的MAC
目的IP与本地IP不同网段 → 查询Gateway IP的MAC
7.2 第一次UDP发送为什么会先出现ARP
如果ARP Cache中没有对应表项,协议栈不能立即发送UDP帧,而是先广播ARP Request。
text
用户提交UDP Header
│
▼
ARP Cache Miss
│
▼
发送ARP Request ──► 等待ARP Reply ──► 写入Cache
│
▼
继续发送UDP帧
若多次重试仍无应答,ip_tx_error_arp_failed会产生错误指示。
7.3 ARP Cache不是软件表格
arp_cache.v使用可综合RTL实现地址缓存,主键是IPv4地址,值是48 bit MAC地址。缓存深度由ARP_CACHE_ADDR_WIDTH决定。
调试发送不出去的问题时,可以观察:
- 是否发出了ARP Request;
- 是否收到了目标主机的ARP Reply;
clear_arp_cache是否被意外持续拉高;ip_tx_error_arp_failed是否产生脉冲;- 本地IP、网关和子网掩码是否配置正确。
八、Ethernet Header与完整MAC帧的转换
udp_complete输出的是"Ethernet Header字段 + Payload AXIS",而MAC接收的是"从目的MAC开始的一整帧AXIS数据"。中间由eth_axis_tx与eth_axis_rx转换。
8.1 发送方向:eth_axis_tx
text
输入:
dest_mac、src_mac、eth_type + Payload AXIS
输出AXIS:
┌──────────┬──────────┬───────────┬─────────────────┐
│ DEST MAC │ SRC MAC │ EtherType │ Ethernet Payload│
│ 6 Byte │ 6 Byte │ 2 Byte │ N Byte │
└──────────┴──────────┴───────────┴─────────────────┘
MAC层随后继续插入Preamble、SFD、必要的Padding、FCS和IFG。
8.2 接收方向:eth_axis_rx
text
完整Ethernet AXIS帧
│
├──► Header:dest_mac、src_mac、eth_type
│
└──► Payload AXIS:tdata、tvalid、tready、tlast、tuser
这种分层非常清晰:
eth_mac_1g_*只关心Ethernet物理帧发送和接收;eth_axis_*只关心MAC Header的串并转换;ip_complete/udp_complete关心协议字段;- 用户逻辑只处理UDP Header和Payload。
九、1G RGMII MAC数据路径
对于外部接口为RGMII的开发板,推荐首先阅读eth_mac_1g_rgmii_fifo.v。
text
逻辑时钟域 PHY引脚
TX AXIS ─► 异步FIFO ─► axis_gmii_tx ─► rgmii_phy_if ─► RGMII TX
│
RX AXIS ◄─ 异步FIFO ◄─ axis_gmii_rx ◄─ rgmii_phy_if ◄─ RGMII RX
9.1 为什么推荐带FIFO版本
eth_mac_1g_rgmii_fifo同时具有:
- 用户逻辑时钟
logic_clk; - GTX发送时钟
gtx_clk及90°相移时钟gtx_clk90; - PHY提供的
rgmii_rx_clk; - TX/RX异步FIFO;
- 帧级丢弃和状态统计。
因此,用户逻辑可以在统一的logic_clk域处理协议,而RGMII RX仍使用PHY恢复时钟采样。
9.2 关键参数
verilog
eth_mac_1g_rgmii_fifo #(
.TARGET("XILINX"),
.IODDR_STYLE("IODDR"),
.CLOCK_INPUT_STYLE("BUFG"),
.USE_CLK90("TRUE"),
.ENABLE_PADDING(1),
.MIN_FRAME_LENGTH(64),
.TX_FIFO_DEPTH(4096),
.TX_FRAME_FIFO(1),
.RX_FIFO_DEPTH(4096),
.RX_FRAME_FIFO(1)
) eth_mac_inst (...);
参数取值需要根据器件系列和时钟资源调整。工程源码的注释给出了典型选择:UltraScale系列可使用IODDR和BUFG;7系列示例常使用IODDR和BUFR。最终必须结合器件、引脚所在Bank和Vivado时序报告确认。
9.3 MAC配置与状态
verilog
.cfg_ifg (8'd12),
.cfg_tx_enable(1'b1),
.cfg_rx_enable(1'b1)
常用状态包括:
| 状态信号 | 含义 |
|---|---|
tx_error_underflow |
发送过程中数据断流 |
tx_fifo_overflow |
TX FIFO溢出 |
rx_error_bad_frame |
接收到错误帧 |
rx_error_bad_fcs |
FCS校验错误 |
rx_fifo_overflow |
RX FIFO溢出 |
tx/rx_fifo_good_frame |
FIFO完成一个正确帧 |
speed[1:0] |
当前接口速率状态 |
这些信号不要全部悬空。上板初期至少应接入ILA或错误计数器,否则发生偶发丢包时很难判断是协议错误还是FIFO/物理接口问题。
十、rgmii_phy_if如何完成DDR转换
rgmii_phy_if.v负责GMII和RGMII之间的接口转换,并处理10/100/1000 Mbps速率适配。
10.1 接收方向
RGMII每个时钟周期传输一个字节的两个Nibble:
text
phy_rgmii_rx_clk __/‾‾\__/‾‾\__/‾‾\__
↑ ↓ ↑ ↓
phy_rgmii_rxd[3:0] <低4><高4>
mac_gmii_rxd[3:0] = 上升沿采样值
mac_gmii_rxd[7:4] = 下降沿采样值
源码通过ssio_ddr_in完成双沿采样,同时采样RX_CTL:
text
RX_DV = RX_CTL上升沿采样值
RX_ER = RX_CTL上升沿值 XOR RX_CTL下降沿值
10.2 发送方向
千兆模式下:
text
RGMII TX上升沿:GMII_TXD[3:0],TX_EN
RGMII TX下降沿:GMII_TXD[7:4],TX_EN XOR TX_ER
源码通过通用oddr.v封装不同厂商和器件的DDR输出原语。USE_CLK90="TRUE"时,发送RGMII时钟使用90°相移的clk90。
10.3 10M/100M并非简单降低系统时钟
rgmii_phy_if仍以较高基准时钟运行,通过内部计数器生成时钟使能和重复Nibble,实现10/100/1000 Mbps适配。源码中speed编码为:
text
2'b00:10 Mbps
2'b01:100 Mbps
2'b10:1000 Mbps
如果项目只要求固定千兆,可以先在千兆模式下跑通;如果需要三速自适应,还要保证PHY协商速率与MAC内部speed检测一致。
10.4 RGMII延时仍然要结合PHY配置
逻辑能够正确完成DDR转换,不代表板级时序天然正确。还需要确认:
- PHY工作在RGMII、RGMII-ID、TXID还是RXID模式;
- TX/RX延时由PHY还是FPGA提供;
gtx_clk90的相位是否符合接口要求;- RGMII输入/输出XDC是否完整;
- 是否加载了仓库
syn/vivado中的接口约束脚本。
重复添加延时或完全没有延时,都可能导致偶发rx_error_bad_fcs。
十一、UDP Echo示例的核心逻辑
KC705 RGMII示例默认监听:
text
FPGA IP: 192.168.1.128
UDP端口: 1234
收到目的端口为1234的UDP包后,示例交换源/目的IP和端口,并通过FIFO回送原Payload。
11.1 Echo数据路径
text
PC发送UDP
│
▼
m_udp Header + Payload
│
├── 目的端口==1234?── 否 ──► 消耗并丢弃
│
是
▼
Payload写入axis_fifo ──► Payload读出 ──► s_udp Payload
│
└── Header交换:
TX源IP = 本地IP
TX目的IP = RX源IP
TX源端口 = RX目的端口
TX目的端口 = RX源端口
TX长度 = RX长度
11.2 Header交换
下面代码按照原工程思路简化,重点展示Echo关系:
verilog
wire port_match = (rx_udp_dest_port == 16'd1234);
assign tx_udp_ip_ttl = 8'd64;
assign tx_udp_ip_source_ip = local_ip;
assign tx_udp_ip_dest_ip = rx_udp_ip_source_ip;
assign tx_udp_source_port = rx_udp_dest_port;
assign tx_udp_dest_port = rx_udp_source_port;
assign tx_udp_length = rx_udp_length;
assign tx_udp_checksum = 16'd0;
11.3 为什么需要Payload FIFO
接收Header时,发送侧可能还未完成ARP查询或后级尚未Ready。如果直接把RX Payload组合连接到TX Payload,一旦产生反压,就可能阻塞接收链路。
加入axis_fifo后:
text
UDP RX ──► FIFO写入 FIFO读出 ──► UDP TX
│ ▲
└── 吸收瞬时反压 ────┘
但FIFO并不能无限吸收数据。如果上位机持续以线速发送,而业务侧无法及时处理,最终仍会溢出。因此产品设计还需要错误统计、限速或流量控制策略。
11.4 不匹配报文也必须被正确消费
如果目的端口不是1234,不能简单把ready永久拉低。否则该帧会一直占据接口,后面的正确报文也无法进入。
正确思路是:
text
端口匹配 → Header交给Echo逻辑,Payload写FIFO
端口不匹配 → Header握手完成,Payload持续ready并丢弃到tlast
这也是原示例中使用match_cond_reg和no_match_reg锁存整帧选择结果的原因:一旦开始处理一个包,选择状态要保持到tlast,不能在Payload传输中途改变。
十二、最小化集成到自己的KU5P工程
以KU5P、外部RGMII PHY和用户侧UDP AXI-Stream为目标,建议将工程分为三层。
text
ku5p_udp_top
├── clock_reset
│ ├── 100 MHz差分时钟输入
│ ├── MMCM生成125 MHz
│ ├── MMCM生成125 MHz/90°
│ └── 各时钟域复位同步
├── ethernet_stack
│ ├── eth_mac_1g_rgmii_fifo
│ ├── eth_axis_rx / eth_axis_tx
│ └── udp_complete
└── user_logic
├── UDP端口过滤
├── RX AXIS FIFO
└── TX AXIS数据源或回环
12.1 建议对外接口
verilog
module ku5p_udp_top (
input wire sys_clk_p,
input wire sys_clk_n,
input wire rgmii_rx_clk,
input wire [3:0] rgmii_rxd,
input wire rgmii_rx_ctl,
output wire rgmii_tx_clk,
output wire [3:0] rgmii_txd,
output wire rgmii_tx_ctl,
output wire phy_reset_n
);
顶层内部再固定或寄存以下网络参数:
verilog
localparam [47:0] LOCAL_MAC = 48'h02_00_00_00_00_01;
localparam [31:0] LOCAL_IP = {8'd192, 8'd168, 8'd1, 8'd10};
localparam [31:0] GATEWAY_IP = {8'd192, 8'd168, 8'd1, 8'd1};
localparam [31:0] SUBNET_MASK = {8'd255, 8'd255, 8'd255, 8'd0};
localparam [15:0] UDP_PORT = 16'd6000;
12.2 用户接口如何简化
原始udp_complete会输出大量UDP/IP Header字段。可以在外部再封装一层,只向用户保留常用信号:
text
RX:src_ip、src_port、length、tdata、tvalid、tready、tlast、tuser
TX:dest_ip、dest_port、length、tdata、tvalid、tready、tlast、tuser
注意:只保留Payload AXIS而完全不保留目标IP、端口和长度是不够的。UDP发送Header必须在Payload前明确提交,因此至少要有一组帧级元数据接口。
12.3 KU5P器件参数
KU5P属于Kintex UltraScale+系列。可从以下方向开始配置:
verilog
.TARGET("XILINX"),
.IODDR_STYLE("IODDR"),
.CLOCK_INPUT_STYLE("BUFG"),
.USE_CLK90("TRUE")
但原工程通用DDR封装覆盖多个系列,实际使用的原语、时钟缓冲和约束必须在目标Vivado版本中综合检查。不要仅凭参数名正确就认为RGMII时序已经完成。
十三、Vivado工程集成步骤
13.1 建立目录
建议不要修改下载的原仓库,而是在自己的工程中建立明确边界:
text
project/
├── src/
│ ├── top/
│ └── user/
├── third_party/
│ └── verilog-ethernet/
├── constrs/
└── sim/
这样后续升级或对比上游代码时更容易。
13.2 添加源码
最稳妥的方式是先复制对应示例Makefile中的SYN_FILES列表。对于1G RGMII UDP工程,至少会涉及:
text
RGMII:rgmii_phy_if、ssio_ddr_in、iddr、oddr
MAC:eth_mac_1g_rgmii_fifo、eth_mac_1g_rgmii、eth_mac_1g
GMII:axis_gmii_rx、axis_gmii_tx、lfsr
Ethernet:eth_axis_rx、eth_axis_tx、eth_arb_mux
UDP:udp_complete、udp、udp_ip_rx、udp_ip_tx、udp_checksum_gen
IP:ip_complete、ip、ip_eth_rx、ip_eth_tx、ip_arb_mux
ARP:arp、arp_cache、arp_eth_rx、arp_eth_tx
AXIS库:axis_fifo、axis_async_fifo、axis_async_fifo_adapter
仲裁:arbiter、priority_encoder
Vivado中将Verilog标准设置为Verilog 2001兼容即可。源码使用了:
verilog
`default_nettype none
这会阻止拼写错误的信号名被自动声明为隐式Wire,是一个很好的工程习惯。如果报错"未声明信号",应修正端口或Wire名称,而不是删除这条语句掩盖问题。
13.3 添加约束
需要同时处理:
- 系统差分时钟引脚和周期;
- RGMII RX/TX引脚与IOSTANDARD;
- PHY复位、MDIO/MDC等板级信号;
- RGMII输入输出时延;
- 异步FIFO与复位同步的时序例外;
- 仓库
syn/vivado中的模块级Tcl约束。
引脚约束必须来自自己的开发板原理图,不能直接复制KC705的XDC。
13.4 先做语法检查,再做时序检查
推荐顺序:
text
① Elaborated Design:确认无端口重复、位宽错误和缺失模块
② Synthesis:确认无Latch、组合环和多驱动
③ Implementation:确认时钟和IO约束生效
④ Report Timing Summary:检查RGMII和内部时钟域
⑤ 生成Bitstream并上板
网络协议栈"综合通过"只代表逻辑能够映射,并不代表RGMII采样窗口正确。
十四、运行cocotb仿真
工程测试使用cocotb,并配合cocotbext-eth模拟GMII/RGMII端口,Scapy负责构造与解析真实协议报文。
仓库固定测试环境中包含:
text
pytest、cocotb、cocotb-test、cocotbext-axi、cocotbext-eth、scapy
同时需要安装Icarus Verilog或选择其他受支持仿真器。
14.1 建立Python虚拟环境
bash
python -m venv .venv
Linux/WSL激活:
bash
source .venv/bin/activate
Windows PowerShell激活:
powershell
.venv\Scripts\Activate.ps1
为了复现原仓库测试,优先参考tox.ini中的固定版本,不建议一开始全部安装最新版。不同大版本的cocotb接口可能已经变化。
14.2 运行KC705 RGMII核心测试
bash
cd example/KC705/fpga_rgmii/tb/fpga_core
make SIM=icarus
生成波形:
bash
make SIM=icarus WAVES=1
也可以在仓库根目录使用pytest执行指定测试:
bash
pytest -s example/KC705/fpga_rgmii/tb/fpga_core/test_fpga_core.py
14.3 Testbench做了什么
KC705示例Testbench的主要流程为:
text
Scapy构造Ether/IP/UDP包
│
▼
cocotbext-eth通过RGMII发送给DUT
│
▼
DUT收到UDP并准备Echo,但ARP Cache未命中
│
▼
Testbench接收DUT发出的ARP Request
│
▼
Testbench向DUT发送ARP Reply
│
▼
DUT发出UDP Echo
│
▼
Scapy逐字段比较MAC、IP、端口和Payload
这比只比较tdata更可靠,因为它同时验证了:
- RGMII DDR收发;
- Ethernet MAC组帧和FCS;
- ARP请求与缓存更新;
- IPv4 Header生成;
- UDP端口交换;
- Payload完整性。
14.4 仿真波形建议观察
| 层级 | 观察信号 |
|---|---|
| RGMII | phy_rx_clk/rxd/rx_ctl、phy_tx_clk/txd/tx_ctl |
| MAC AXIS | rx_axis_*、tx_axis_* |
| Ethernet | rx/tx_eth_hdr_valid/ready、eth_payload_axis_* |
| UDP | rx/tx_udp_hdr_valid/ready、udp_payload_axis_* |
| ARP | ARP请求、应答、Cache查询状态 |
| 错误 | rx_error_bad_fcs、ip_tx_error_arp_failed等 |
十五、上板UDP Echo测试
以FPGA配置为192.168.1.128:1234为例,上位机配置静态IP:
text
上位机IP:192.168.1.100
子网掩码:255.255.255.0
默认网关:可留空
15.1 先验证ARP
powershell
arp -d 192.168.1.128
ping 192.168.1.128
arp -a
需要说明:原始UDP Echo示例会正确响应ARP,但没有自动实现ICMP Echo Reply。因此ping可能显示超时;只要arp -a中已经出现FPGA的MAC地址,ARP通路就可能已经正常。
15.2 Netcat测试
Linux或安装了Netcat的环境中:
bash
netcat -u 192.168.1.128 1234
输入文字并回车,正常情况下会收到相同内容。
15.3 Python测试
python
import socket
FPGA = ("192.168.1.128", 1234)
PC_IP = "192.168.1.100"
PC_PORT = 5000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((PC_IP, PC_PORT))
sock.settimeout(2.0)
tx_data = bytes(range(256))
sock.sendto(tx_data, FPGA)
try:
rx_data, remote = sock.recvfrom(2048)
print("remote:", remote)
print("length:", len(rx_data))
print("result:", "PASS" if rx_data == tx_data else "FAIL")
except socket.timeout:
print("FAIL: UDP Echo timeout")
finally:
sock.close()
15.4 Wireshark过滤器
text
arp || (ip.addr == 192.168.1.128 && udp.port == 1234)
首次通信时正常报文顺序为:
text
PC → Broadcast:ARP Who has 192.168.1.128?
FPGA → PC: ARP Reply
PC → FPGA: UDP 5000 → 1234
FPGA → PC: UDP 1234 → 5000
如果只看到ARP Request,看不到ARP Reply,检查RX MAC、ARP接收和TX MAC;如果ARP正常但没有UDP回包,检查端口过滤、Header握手、Payload FIFO和s_udp_hdr_ready。
十六、ILA在线调试建议
16.1 第一组:物理接口状态
text
rgmii_rx_clk、rgmii_rxd、rgmii_rx_ctl
rgmii_tx_clk、rgmii_txd、rgmii_tx_ctl
speed、phy_reset_n
16.2 第二组:MAC与FIFO错误
text
rx_error_bad_frame
rx_error_bad_fcs
rx_fifo_overflow
tx_fifo_overflow
tx_error_underflow
rx_fifo_good_frame
tx_fifo_good_frame
16.3 第三组:UDP接口
text
rx_udp_hdr_valid/ready
rx_udp_dest_port
rx_udp_ip_source_ip
rx_udp_payload_axis_tdata/valid/ready/last/user
tx_udp_hdr_valid/ready
tx_udp_dest_port
tx_udp_ip_dest_ip
tx_udp_payload_axis_tdata/valid/ready/last/user
16.4 第四组:协议栈错误
text
ip_rx_error_invalid_header
ip_rx_error_invalid_checksum
ip_rx_error_payload_early_termination
ip_tx_error_arp_failed
udp_rx_error_header_early_termination
udp_rx_error_payload_early_termination
udp_tx_error_payload_early_termination
对于脉冲型错误信号,推荐再加一个保持寄存器或计数器,否则ILA触发条件不合适时很容易错过。
verilog
always @(posedge clk) begin
if (rst)
bad_fcs_count <= 32'd0;
else if (rx_error_bad_fcs)
bad_fcs_count <= bad_fcs_count + 1'b1;
end
十七、常见问题
| 现象 | 主要原因 | 检查方向 |
|---|---|---|
| Vivado报缺少模块 | 只添加了顶层,未加入AXIS库和下级RTL | 对照示例SYN_FILES |
make找不到Vivado |
Vivado没有进入当前终端PATH | 运行settings64.bat或配置PATH |
| 能综合但Link不起来 | PHY复位、参考时钟、MDIO或模式配置错误 | PHY寄存器和板级原理图 |
| Link正常但大量Bad FCS | RGMII延时/约束错误 | PHY RGMII-ID、clk90、XDC |
| 第一次UDP长时间无输出 | 正在进行ARP查询 | Wireshark看ARP Request/Reply |
| 一直出现ARP Failed | IP/掩码/网关错误或对端不回ARP | 配置字段和ARP通路 |
| UDP包进入但不回环 | 端口不匹配或Header未握手 | dest_port、hdr_valid/ready |
| 只回第一包 | 匹配状态未在tlast清除,或不匹配包未消费 |
match_cond_reg、丢弃通路 |
| 长包丢失或错位 | FIFO深度、反压或长度不一致 | FIFO状态、UDP length、tlast |
| 最后一字节异常 | tlast与tdata未同步保持 |
tvalid && tready握手 |
| 仿真Python导入失败 | cocotb系列版本不匹配 | 使用tox.ini固定版本 |
ping不通但UDP正常 |
示例未实现ICMP Echo Reply | 通过ARP表和UDP测试判断 |
十八、推荐的源码阅读顺序
第一次面对几十个RTL文件时,可以按照一条UDP Echo数据路径阅读。
第一阶段:先看顶层连接
text
example/KC705/fpga_rgmii/rtl/fpga_core.v
目标:弄清MAC、eth_axis_rx/tx、udp_complete和Payload FIFO如何连接。
第二阶段:理解UDP/IP/ARP分层
text
udp_complete.v
→ udp.v
→ udp_ip_rx.v
→ udp_ip_tx.v
→ udp_checksum_gen.v
→ ip_complete.v
→ ip.v
→ arp.v
→ arp_cache.v
目标:理解Header通道与Payload通道、Protocol分流以及ARP Cache Miss处理。
第三阶段:阅读数据链路层
text
eth_axis_rx.v / eth_axis_tx.v
→ eth_mac_1g_rgmii_fifo.v
→ eth_mac_1g_rgmii.v
→ eth_mac_1g.v
→ axis_gmii_rx.v / axis_gmii_tx.v
→ rgmii_phy_if.v
目标:理解Ethernet Header串并转换、Preamble/FCS/IFG、FIFO跨时钟域和RGMII DDR接口。
第四阶段:阅读Testbench
text
example/KC705/fpga_rgmii/tb/fpga_core/test_fpga_core.py
目标:理解Scapy如何构造报文、仿真PHY如何收发,以及ARP和UDP Echo如何逐字段验证。
十九、从这个工程中可以学到的RTL设计方法
19.1 Header与Payload解耦
帧级元数据一次握手,连续数据使用AXI-Stream传输。这种结构不仅适合UDP,也适合采集数据、DMA描述符和PCIe TLP处理。
19.2 选择信号必须保持整帧
Mux/Demux不能只在第一拍组合判断后就放任条件变化,而应锁存选择结果直到tlast。这是所有分组流设计的共性要求。
19.3 反压必须贯穿整条链路
任何一级tready=0都可能逐级向上传播。设计状态机时必须只在valid && ready时更新字节计数器和状态。
19.4 错误要显式暴露
工程为Header提前结束、Payload提前结束、IP校验和错误、ARP失败、Bad FCS和FIFO溢出分别提供状态信号。这比只输出一个笼统的error更利于上板定位。
19.5 板级逻辑与协议逻辑分离
fpga.v负责引脚、时钟和板级资源;fpga_core.v负责协议栈;rtl公共模块不绑定某块板卡。移植时主要替换板级顶层和约束,上层协议逻辑可以保持稳定。
二十、工程使用边界
verilog-ethernet功能较完整,但它不是软件操作系统中的全功能网络协议栈。使用前需要明确边界:
- 核心是面向硬件数据流的Ethernet/ARP/IPv4/UDP组件;
udp_complete不等于自动支持所有IP协议;- ICMP Echo Reply需要外部扩展;
- 产品级使用仍要完成PHY管理、时序约束、异常统计和压力测试;
- 仓库已停止后续维护,新项目应评估
taxi; - 使用和再分发时应保留MIT License要求的版权与许可声明。
对于学习者而言,它最大的价值并不是"一键生成一个UDP IP",而是提供了一份可以沿数据路径逐层阅读的成熟RTL样本。
总结
本文从工程学习的角度梳理了verilog-ethernet的整体结构,并重点分析了1G RGMII UDP数据路径。
完整的接收方向为:
text
RGMII PHY
→ rgmii_phy_if
→ eth_mac_1g
→ MAC RX FIFO
→ eth_axis_rx
→ ip_complete
→ udp
→ 用户UDP Header与Payload AXIS
完整的发送方向为:
text
用户UDP Header与Payload AXIS
→ udp
→ ip_complete/ARP Cache
→ eth_axis_tx
→ MAC TX FIFO
→ eth_mac_1g
→ rgmii_phy_if
→ RGMII PHY
如果是第一次学习该工程,建议不要尝试一次看懂全部源码。先跑通一个UDP Echo仿真,再观察Header与Payload握手;随后沿udp_complete → ip_complete → eth_axis → MAC → RGMII逐层深入。这样每个模块都有明确的输入、输出和验证目标,阅读效率会高很多。
下一步可以在这个示例基础上进行三项改造:
- 将固定UDP Echo改成用户AXI-Stream FIFO回环;
- 增加ICMP Echo Reply,使开发板能够响应Ping;
- 根据KU5P开发板原理图重新设计100 MHz输入时钟、125 MHz/90°时钟、RGMII引脚和时序约束。
完成这三项后,开源工程就不再只是"能运行的示例",而会成为适配自己硬件平台的可复用UDP通信模块。
Fin