从近似0基础开始FPGA开发 -- part.10 verilog-ethernet开源UDP协议栈工程学习

项目地址: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-ethernetmaster分支。为了避免以后仓库变化造成阅读差异,建议实际项目中固定到经过验证的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.varp_cache.v ARP状态控制和ARP缓存
arp_eth_rx.varp_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.veth_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.vaxis_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上可以使用以下环境之一:

  1. Vivado Tcl Shell配合已安装的GNU Make;
  2. MSYS2或Git Bash;
  3. WSL;
  4. 不运行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/MakefileSYN_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=1tready=0时,发送端必须保持tdatatlasttuser不变,直到握手完成。

4.2 Header和Payload分离

verilog-ethernet的UDP、IP和Ethernet接口通常不是把所有内容混成一条字节流,而是分成:

  1. 一组并行Header字段;
  2. 一条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_INTERVALARP_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

接收方向的主要工作为:

  1. 接收已经由IP层解析的IPv4 Header和Payload;
  2. 从IP Payload前8 Byte中提取UDP源端口、目的端口、长度和校验和;
  3. 将剩余字节作为UDP Payload输出;
  4. 根据UDP长度生成tlast
  5. 检查报文是否提前结束,并输出错误状态。
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_txeth_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系列可使用IODDRBUFG;7系列示例常使用IODDRBUFR。最终必须结合器件、引脚所在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_regno_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_ctlphy_tx_clk/txd/tx_ctl
MAC AXIS rx_axis_*tx_axis_*
Ethernet rx/tx_eth_hdr_valid/readyeth_payload_axis_*
UDP rx/tx_udp_hdr_valid/readyudp_payload_axis_*
ARP ARP请求、应答、Cache查询状态
错误 rx_error_bad_fcsip_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_porthdr_valid/ready
只回第一包 匹配状态未在tlast清除,或不匹配包未消费 match_cond_reg、丢弃通路
长包丢失或错位 FIFO深度、反压或长度不一致 FIFO状态、UDP length、tlast
最后一字节异常 tlasttdata未同步保持 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/txudp_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逐层深入。这样每个模块都有明确的输入、输出和验证目标,阅读效率会高很多。

下一步可以在这个示例基础上进行三项改造:

  1. 将固定UDP Echo改成用户AXI-Stream FIFO回环;
  2. 增加ICMP Echo Reply,使开发板能够响应Ping;
  3. 根据KU5P开发板原理图重新设计100 MHz输入时钟、125 MHz/90°时钟、RGMII引脚和时序约束。

完成这三项后,开源工程就不再只是"能运行的示例",而会成为适配自己硬件平台的可复用UDP通信模块。

Fin


参考资料

  1. alexforencich/verilog-ethernet GitHub仓库
  2. verilog-ethernet README
  3. fpganinja/taxi 后续项目
  4. KC705 RGMII示例
  5. cocotb官方文档
  6. cocotbext-eth
相关推荐
冬奇Lab2 小时前
一天一个开源项目(第215篇):Langflow - 可视化拖拽构建 AI 应用的低代码平台
人工智能·开源·资讯
GitCode官方2 小时前
本周 G-Star 开源项目推荐
开源·g-star·atomgit
ChampaignWolf3 小时前
YAAI 生态一周年:把 ABAP 变成 Agent 工具栈的开源组合拳
开源·abap·mcp·开源ai·yaai
MatrixOrigin4 小时前
Astra 正式开源了:面向长期复杂工作的企业级 Agent Runtime
开源·astra·矩阵起源·matrix origin·contextpipe
梦想的颜色4 小时前
【AI速览】2026 最新 开箱即用型开源成品 Agent :DeepSeek Harness 、Pi-Agent、 Opencode 横向 全面 对比
开源·agent·opencode·dsh·piagent
萧鼎5 小时前
safetensors 库的安装、核心语法与实战用法,安全保存模型权重
python·开源·教程·python库·safetensors
@小匠6 小时前
1Password-开源计划申请免费使用资格
开源
Behaviour7 小时前
DeepSeek V4.1 Flash 发布开源,Harness v0.1.5同日适配
人工智能·语言模型·开源·aigc·ai编程
狗凯之家源码网7 小时前
全开源 H5 棋牌对战系统修复优化与二次开发实测
开源·php·棋牌对战