云计算百科
云计算领域专业知识百科平台

FPGA 千兆以太网帧格式全解:从 IEEE 802.3 帧结构到 Verilog 收发实现

1. 为什么 FPGA 工程师必须吃透以太网帧格式

在 FPGA 上做千兆以太网(1000BASE-T / 1000BASE-X),很多人第一步就卡住:PHY 芯片已经链路 up,MAC 侧也收到了数据,但抓包工具里全是错帧,或者上位机根本收不到包。排查到最后,问题往往不在时序、不在时钟域,而是帧格式的某个字段写错了——前导码少了一个字节、FCS 算错了、IFG 不够、长度字段和实际负载对不上。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

以太网帧格式看起来简单:7 字节前导码 + 1 字节 SFD + 6 字节目的 MAC + 6 字节源 MAC + 2 字节类型/长度 + 46~1500 字节负载 + 4 字节 FCS。但真正落到 RTL 里,每一个字节都有讲究:

  • 前导码是 0x55 还是 0xAA?为什么是交替的 1010 还是 0101?
  • SFD 是 0xD5,它在 8 位数据通路上到底怎么对齐?
  • 类型字段 0x0800 是大端还是小端?在 8 位总线上先发高字节还是低字节?
  • 最小帧 64 字节是怎么算出来的?不含前导码还是含?
  • FCS 的 CRC-32 多项式是哪个?初值、反射、最终异或分别是什么?
  • 千兆模式下 GMII 是 8 位 125MHz,RGMII 是 DDR 双沿,SGMII 是串行 1.25Gbps,帧格式一样但接口时序完全不同。

如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

这篇文章只讲一件事:FPGA 里千兆以太网帧格式的完整定义,以及如何用 Verilog 把它一个字节一个字节地发出去、收进来。不讲 TCP/IP 协议栈,不讲 UDP 校验和,不讲 ARP 流程,只把二层帧格式和它的硬件实现讲透。

全文会覆盖:

  • IEEE 802.3 / Ethernet II 帧的逐字段定义与字节序;
  • 千兆以太网的物理层接口:GMII、RGMII、SGMII 的差异;
  • 发送方向:从帧缓存到 GMII 输出的完整状态机;
  • 接收方向:从 GMII 输入到帧解析、FCS 校验的状态机;
  • CRC-32 FCS 的并行/串行实现与逐位推导;
  • 前导码、SFD、IFG、背靠背帧的时序细节;
  • 最小帧填充、巨型帧、VLAN 标签扩展格式;
  • 完整可综合的 Verilog 代码与仿真验证方法;
  • 常见错误清单与调试手段。
  • 1.1 主要方案对比与整体处理流程

    在深入逐字段细节之前,先把本文涉及的主要方案放在一起横向对比,便于根据项目实际需求做取舍。下表从适用场景、优缺点、复杂度三个维度进行梳理:

    方案适用场景优点缺点复杂度
    Ethernet II 帧格式 绝大多数 IP/私有协议数据采集、图像传输 与主流协议栈兼容,EtherType 语义清晰,解析简单 无长度字段,短帧需依赖上层协议判断真实长度 低
    IEEE 802.3 帧格式 需要显式长度字段的 LLC/SNAP 场景、部分工业协议 自带长度字段,便于接收端裁剪填充 需额外解析 LLC/SNAP 头,实际使用较少 中
    GMII 接口 FPGA 与 PHY 同板、引脚资源充足 8 位并行、时序直观、调试方便 信号线约 25 根,引脚占用大 低
    RGMII 接口 板级设计主流、引脚受限场景 引脚数减半,成本低 DDR 双沿时序,需处理 1.5~2ns skew,时序约束复杂 中
    SGMII 接口 需要长距离/背板传输、使用高速收发器 串行差分,抗干扰强,走线简单 依赖 GTX/GTH 收发器,8b/10b 编码,调试门槛高 高
    逐位串行 CRC-32 低速链路、资源极度受限 逻辑极简,资源占用少 每周期仅 1 bit,千兆下无法满足吞吐 低
    8 位并行 CRC-32 千兆 MAC 发送/接收 FCS 计算 单周期处理 1 字节,满足 125MHz 吞吐 异或表达式长,需脚本生成或 IP 支持 中
    单帧缓存 低速、非连续发送场景 结构简单,BRAM 占用少 无法乒乓,吞吐受限 低
    乒乓双缓存 高吞吐、连续背靠背发送 发送与填充并行,吞吐高 控制逻辑复杂,BRAM 翻倍 高
    标准帧(≤1518 字节) 通用以太网互通 兼容所有交换机与网卡 单帧有效负载有限 低
    巨型帧(Jumbo) 大数据块传输、点对点直连 减少帧数、降低协议开销 非标准,需两端协商,交换机可能不支持 中
    802.1Q VLAN Tag 多业务隔离、优先级调度 单帧携带 VLAN 信息,灵活 帧长增加 4 字节,需解析 TPID/TCI 中
    QinQ 双标签 运营商级多租户隔离 双层标签,隔离粒度更细 帧长增加 8 字节,解析层级更多 高

    从表中可以看出:帧格式选 Ethernet II、接口按板级条件在 GMII/RGMII/SGMII 中取舍、CRC 用 8 位并行、缓存按吞吐需求决定是否乒乓,是绝大多数 FPGA 千兆以太网项目的合理起点。

    下面用一张流程图展示从上层应用请求到帧上线的整体处理流程,涵盖发送与接收两条主路径:

    #mermaid-svg-tPjaibC3Mz0Nwcjw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tPjaibC3Mz0Nwcjw .error-icon{fill:#552222;}#mermaid-svg-tPjaibC3Mz0Nwcjw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tPjaibC3Mz0Nwcjw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .marker.cross{stroke:#333333;}#mermaid-svg-tPjaibC3Mz0Nwcjw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tPjaibC3Mz0Nwcjw p{margin:0;}#mermaid-svg-tPjaibC3Mz0Nwcjw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .cluster-label text{fill:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .cluster-label span{color:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .cluster-label span p{background-color:transparent;}#mermaid-svg-tPjaibC3Mz0Nwcjw .label text,#mermaid-svg-tPjaibC3Mz0Nwcjw span{fill:#333;color:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .node rect,#mermaid-svg-tPjaibC3Mz0Nwcjw .node circle,#mermaid-svg-tPjaibC3Mz0Nwcjw .node ellipse,#mermaid-svg-tPjaibC3Mz0Nwcjw .node polygon,#mermaid-svg-tPjaibC3Mz0Nwcjw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .rough-node .label text,#mermaid-svg-tPjaibC3Mz0Nwcjw .node .label text,#mermaid-svg-tPjaibC3Mz0Nwcjw .image-shape .label,#mermaid-svg-tPjaibC3Mz0Nwcjw .icon-shape .label{text-anchor:middle;}#mermaid-svg-tPjaibC3Mz0Nwcjw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .rough-node .label,#mermaid-svg-tPjaibC3Mz0Nwcjw .node .label,#mermaid-svg-tPjaibC3Mz0Nwcjw .image-shape .label,#mermaid-svg-tPjaibC3Mz0Nwcjw .icon-shape .label{text-align:center;}#mermaid-svg-tPjaibC3Mz0Nwcjw .node.clickable{cursor:pointer;}#mermaid-svg-tPjaibC3Mz0Nwcjw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .arrowheadPath{fill:#333333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tPjaibC3Mz0Nwcjw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tPjaibC3Mz0Nwcjw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tPjaibC3Mz0Nwcjw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tPjaibC3Mz0Nwcjw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .cluster text{fill:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw .cluster span{color:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tPjaibC3Mz0Nwcjw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tPjaibC3Mz0Nwcjw rect.text{fill:none;stroke-width:0;}#mermaid-svg-tPjaibC3Mz0Nwcjw .icon-shape,#mermaid-svg-tPjaibC3Mz0Nwcjw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tPjaibC3Mz0Nwcjw .icon-shape p,#mermaid-svg-tPjaibC3Mz0Nwcjw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tPjaibC3Mz0Nwcjw .icon-shape .label rect,#mermaid-svg-tPjaibC3Mz0Nwcjw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tPjaibC3Mz0Nwcjw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tPjaibC3Mz0Nwcjw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tPjaibC3Mz0Nwcjw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    是

    否

    否

    是

    否

    是

    否

    是

    上层应用请求发送

    负载长度 < 46 字节?

    填充 0 至 46 字节

    按实际长度组帧

    填充 DA / SA / Type

    8 位并行 CRC-32 计算 FCS

    发送状态机: 前导码 7×0x55

    发送 SFD 0xD5

    逐字节发送 DA/SA/Type/Payload

    高字节先发 4 字节 FCS

    IFG 等待 12 个周期

    经 GMII/RGMII/SGMII 输出至 PHY

    PHY 输入 GMII 数据

    检测前导码 0x55

    检测到 SFD 0xD5?

    逐字节接收并写入帧缓存

    同步计算 CRC-32

    接收 4 字节 FCS

    CRC 校验通过?

    标记 err_crc 并丢弃

    长度 ≥ 64 字节?

    标记 err_len 并丢弃

    输出 rx_valid 与帧信息

    流程图中左侧为发送路径,右侧为接收路径。发送侧的关键是填充、组帧、CRC、前导码/SFD、FCS 字节序、IFG六个环节;接收侧的关键是前导码同步、SFD 识别、CRC 校验、长度检查四个环节。后续各章节将围绕这些环节逐一展开。

    2. 以太网帧格式的权威定义

    如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

    2.1 两种帧格式:Ethernet II 与 IEEE 802.3

    历史上存在两种帧格式,区别只在第 13~14 字节(从目的 MAC 算起的偏移 12~13)的语义:

    字段位置(从 DA 起)Ethernet IIIEEE 802.3
    0~5 目的 MAC 目的 MAC
    6~11 源 MAC 源 MAC
    12~13 EtherType(≥ 0x0600) Length(≤ 1500)
    14~ 负载 LLC/SNAP 头 + 负载

    判断规则很简单:这个 16 位字段的值 ≥ 0x0600(1536) 就按 EtherType 解释,≤ 0x05DC(1500) 就按长度解释。中间 1501~1535 是未定义区间。

    今天实际网络中 99% 以上是 Ethernet II,因为 IP 报文用 0x0800、ARP 用 0x0806、IPv6 用 0x86DD、VLAN 用 0x8100。FPGA 做数据采集、图像传输、高速采集卡,基本都用 Ethernet II。本文以 Ethernet II 为主,同时说明 802.3 长度字段的处理方式。

    2.2 完整帧结构(不含物理层封装)

    从 MAC 子层看,一个完整的以太网帧如下:

    +———+—–+——–+——–+——–+============+—–+
    |Preamble | SFD | DA | SA | Type | Payload | FCS |
    | 7 Byte |1Byte| 6 Byte | 6 Byte | 2 Byte | 46~1500 B |4Byte|
    +———+—–+——–+——–+——–+============+—–+

    各字段含义:

    • Preamble(前导码):7 个字节,每个字节 0x55,即二进制 0101_0101。在物理层上表现为 7 个周期的 5MHz 方波(千兆下是 125MHz 的 0101 交替),用于接收端 CDR(时钟数据恢复)锁定相位、均衡器收敛。
    • SFD(帧起始定界符):1 个字节 0xD5,二进制 1101_0101。前 6 位仍是 101010 的延续,最后两位 11 表示"后面开始是真正的数据"。接收端检测到 …10101011 这个模式就认为帧开始。
    • DA(Destination Address):6 字节目的 MAC 地址,先发第 0 字节(最高字节,OUI 部分)。
    • SA(Source Address):6 字节源 MAC 地址,同样先发高字节。
    • Type/Length:2 字节,先发高字节。EtherType 常见值见下表。
    • Payload:46~1500 字节。不足 46 字节要填充(padding)到 46,保证整帧(不含前导码和 SFD)至少 64 字节。
    • FCS(Frame Check Sequence):4 字节 CRC-32,覆盖范围是 DA + SA + Type + Payload + Padding,不包括前导码和 SFD。发送时按 CRC 计算结果的高字节先发还是低字节先发,是一个高频踩坑点,后面专门讲。

    2.3 常见 EtherType 取值

    EtherType协议
    0x0800 IPv4
    0x0806 ARP
    0x8100 IEEE 802.1Q VLAN Tag
    0x86DD IPv6
    0x8847 MPLS Unicast
    0x88CC LLDP
    0x88F7 PTP (IEEE 1588)
    0x8892 PROFINET
    0x88A4 EtherCAT
    0x22F0 AVTP (音视频传输)
    0x88B5 / 0x88B6 实验用 EtherType(本地实验推荐)

    做 FPGA 私有协议时,建议使用 0x88B5 或 0x88B6,这是 IEEE 保留给本地实验的 EtherType,不会和公网协议冲突。

    2.4 帧长约束

    项目数值说明
    最小帧长(不含前导码+SFD) 64 字节 DA+SA+Type+Payload+FCS
    最小负载 46 字节 64 – 6 – 6 – 2 – 4 = 46
    最大帧长(标准) 1518 字节 不含前导码+SFD,不含 VLAN Tag
    最大负载 1500 字节 MTU
    带 VLAN Tag 最大帧长 1522 字节 多了 4 字节 Tag
    巨型帧(Jumbo) 通常 9000 字节 非标准,需两端协商
    前导码+SFD 8 字节 物理层开销
    IFG(帧间隔) 12 字节时间 96 bit time,千兆下 96ns

    注意:64 字节最小帧不含前导码和 SFD。很多资料写"最小帧 64 字节",然后又说"加上前导码 8 字节共 72 字节",这两个说法都对,只是统计口径不同。在 RTL 里做长度检查时,要明确你统计的是从 DA 开始还是从 Preamble 开始。

    千兆速率下,一个最小帧(64 字节 = 512 bit)加上前导码(64 bit)和 IFG(96 bit),总共 672 bit,即 672ns。理论最大帧速率 = 1 / 672ns ≈ 1.488 Mpps。这个数字在做流量发生器时经常用到。

    3. 千兆以太网的物理层接口

    帧格式是 MAC 层的事,但 FPGA 要把它送出去,必须经过 PHY 接口。千兆以太网在 FPGA 侧常见的三种接口:

    3.1 GMII(Gigabit Media Independent Interface)

    GMII 是 IEEE 802.3 定义的标准接口:

    • 发送:gtx_clk(125MHz)、txd[7:0]、tx_en、tx_er;
    • 接收:rx_clk(从 PHY 恢复的 125MHz)、rxd[7:0]、rx_dv、rx_er;
    • 管理:mdc、mdio。

    8 位数据 × 125MHz = 1000Mbps。GMII 需要 25 根左右的信号线,引脚多,通常只在 FPGA 与 PHY 芯片同板时使用。

    关键时序:tx_en 拉高的第一个周期,txd 上就是前导码的第一个 0x55。tx_en 拉低的最后一个周期,txd 上是 FCS 的最后一个字节。tx_en 连续为高的周期数 = 帧长(含前导码和 SFD)。

    3.2 RGMII(Reduced GMII)

    RGMII 把 8 位数据砍成 4 位,用 DDR(双沿)传输,125MHz × 2 × 4bit = 1000Mbps。引脚数减半,是板级设计最常用的接口。

    • 发送:tx_clk(125MHz,由 MAC 提供)、txd[3:0]、tx_ctl;
    • 接收:rx_clk(PHY 恢复)、rxd[3:0]、rx_ctl。

    tx_ctl 在上升沿表示 tx_en,下降沿表示 tx_en XOR tx_er。数据 txd[3:0] 在上升沿是低 4 位,下降沿是高 4 位。也就是说,一个 GMII 字节在 RGMII 上被拆成两个半字节,分别在时钟的两个沿发送。

    RGMII 通常需要 PHY 内部或 FPGA 侧加 1.5~2ns 的时钟延迟(TXC 相对 TXD 的 skew),否则采样窗口会错。Xilinx 7 系列可以用 ODELAY/IDELAY 原语,或者在 PCB 上做等长绕线。

    3.3 SGMII(Serial GMII)

    SGMII 是串行接口,1.25Gbps 差分对,8b/10b 编码,有效速率 1000Mbps。FPGA 侧需要 GTX/GTH/GTY 收发器或者专用的 SGMII PHY。

    SGMII 的帧格式和 GMII 完全一样,只是物理编码不同。收发器内部完成 8b/10b 编解码和时钟恢复,对 MAC 层呈现的还是 8 位数据 + 有效信号。

    3.4 三种接口的帧格式一致性

    重要结论:无论 GMII、RGMII 还是 SGMII,MAC 层看到的帧格式完全相同。 前导码、SFD、DA、SA、Type、Payload、FCS 的字节内容和顺序不变,变的只是电气特性和位宽/时钟。

    所以本文的 RTL 设计以 GMII 为基准(8 位数据通路最直观),RGMII 只需要在 GMII 外面套一层 DDR 转换,SGMII 只需要把 GMII 接到收发器的 GMII 接口上。

    4. 字节序:最容易出错的地方

    4.1 网络字节序是大端

    以太网规定所有多字节字段按**大端(Big-Endian)**传输,即高字节先发。例如 EtherType = 0x0800,在 8 位数据总线上先出现 0x08,再出现 0x00。

    MAC 地址 00:1A:2B:3C:4D:5E,发送顺序是 0x00、0x1A、0x2B、0x3C、0x4D、0x5E。

    4.2 在 8 位总线上如何组织

    假设我们有一个 64 位宽的帧缓存,里面按小端存放了 MAC 地址,那么发送时需要做字节重排。最稳妥的做法是:帧缓存按字节数组组织,发送状态机按字节索引取数,不要试图用位拼接去"优化",那样极易出错。

    // 帧缓存:按字节存储,索引 0 是 DA 的第 0 字节
    reg [7:0] frame_buf [0:2047];

    发送时 frame_buf[0] 先发,frame_buf[1] 次之,以此类推。这样字节序问题在缓存填充阶段就解决了,发送状态机只管顺序取数。

    4.3 FCS 的字节序陷阱

    CRC-32 计算出来是一个 32 位值。以太网规定 FCS 字段传输时,CRC 结果的最高字节先发,但每个字节内部是低位先发(因为 CRC 是按位串行计算的,且以太网是 LSB first)。

    具体来说,如果 CRC 计算结果是 0x12345678,那么线上传输的字节顺序是 0x12、0x34、0x56、0x78,但每个字节在物理层是 LSB 先出。在 8 位 GMII 上,一个字节是一个周期,所以字节顺序就是 0x12 → 0x34 → 0x56 → 0x78。

    很多实现错误地把 0x78 先发,导致接收端 FCS 校验失败。记住:FCS 是 CRC 值的高字节在前。

    5. 发送方向 RTL 设计

    5.1 整体架构

    +———–+ +————-+ +———-+ +——+
    | 帧缓存 | –> | 发送状态机 | –> | CRC-32 | –> | GMII |
    | frame_buf | | tx_fsm | | 计算模块 | | 输出 |
    +———–+ +————-+ +———-+ +——+

    发送状态机负责:产生前导码和 SFD、按字节读出 DA/SA/Type/Payload、计算并插入 FCS、控制 IFG。

    5.2 状态机设计

    module eth_tx (
    input wire clk, // 125MHz
    input wire rst_n,

    // 帧请求接口
    input wire tx_req, // 请求发送一帧
    input wire [15:0] tx_len, // 负载长度(不含前导码、FCS)
    output reg tx_busy, // 正在发送
    output reg tx_done, // 发送完成(单周期脉冲)

    // 帧数据读取接口
    output reg [10:0] rd_addr, // 字节地址
    input wire [7:0] rd_data, // 字节数据
    output reg rd_en,

    // GMII 输出
    output reg [7:0] gmii_txd,
    output reg gmii_tx_en,
    output reg gmii_tx_er
    );

    // 状态定义
    localparam S_IDLE = 3'd0;
    localparam S_PREAMBLE= 3'd1;
    localparam S_SFD = 3'd2;
    localparam S_DATA = 3'd3;
    localparam S_FCS = 3'd4;
    localparam S_IFG = 3'd5;

    reg [2:0] state, next_state;
    reg [3:0] pre_cnt; // 前导码字节计数 0~6
    reg [10:0] byte_cnt; // 数据字节计数
    reg [1:0] fcs_cnt; // FCS 字节计数 0~3
    reg [7:0] ifg_cnt; // IFG 计数

    // CRC 计算模块接口
    reg crc_init;
    reg crc_en;
    reg [7:0] crc_data;
    wire [31:0] crc_out;

    // 帧总长度 = 负载长度 + 14(DA+SA+Type)
    wire [10:0] frame_len = tx_len + 11'd14;

    // CRC 模块例化
    crc32_8bit u_crc (
    .clk (clk),
    .rst_n (rst_n),
    .init (crc_init),
    .en (crc_en),
    .data (crc_data),
    .crc (crc_out)
    );

    // 状态转移
    always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
    state <= S_IDLE;
    else
    state <= next_state;
    end

    always @(*) begin
    next_state = state;
    case (state)
    S_IDLE: begin
    if (tx_req)
    next_state = S_PREAMBLE;
    end
    S_PREAMBLE: begin
    if (pre_cnt == 4'd6)
    next_state = S_SFD;
    end
    S_SFD: begin
    next_state = S_DATA;
    end
    S_DATA: begin
    if (byte_cnt == frame_len – 1)
    next_state = S_FCS;
    end
    S_FCS: begin
    if (fcs_cnt == 2'd3)
    next_state = S_IFG;
    end
    S_IFG: begin
    if (ifg_cnt == 8'd11)
    next_state = S_IDLE;
    end
    default: next_state = S_IDLE;
    endcase
    end

    // 输出逻辑
    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    gmii_txd <= 8'h00;
    gmii_tx_en <= 1'b0;
    gmii_tx_er <= 1'b0;
    pre_cnt <= 4'd0;
    byte_cnt <= 11'd0;
    fcs_cnt <= 2'd0;
    ifg_cnt <= 8'd0;
    rd_addr <= 11'd0;
    rd_en <= 1'b0;
    tx_busy <= 1'b0;
    tx_done <= 1'b0;
    crc_init <= 1'b0;
    crc_en <= 1'b0;
    crc_data <= 8'h00;
    end else begin
    tx_done <= 1'b0;
    crc_init <= 1'b0;
    crc_en <= 1'b0;
    rd_en <= 1'b0;

    case (state)
    S_IDLE: begin
    gmii_tx_en <= 1'b0;
    gmii_tx_er <= 1'b0;
    gmii_txd <= 8'h00;
    pre_cnt <= 4'd0;
    byte_cnt <= 11'd0;
    fcs_cnt <= 2'd0;
    ifg_cnt <= 8'd0;
    tx_busy <= 1'b0;
    if (tx_req) begin
    tx_busy <= 1'b1;
    crc_init <= 1'b1; // 进入前导码时初始化 CRC
    end
    end

    S_PREAMBLE: begin
    gmii_tx_en <= 1'b1;
    gmii_txd <= 8'h55;
    if (pre_cnt == 4'd6)
    pre_cnt <= 4'd0;
    else
    pre_cnt <= pre_cnt + 1'b1;
    end

    S_SFD: begin
    gmii_tx_en <= 1'b1;
    gmii_txd <= 8'hD5;
    end

    S_DATA: begin
    gmii_tx_en <= 1'b1;
    gmii_txd <= rd_data;
    crc_en <= 1'b1;
    crc_data <= rd_data;
    rd_en <= 1'b1;
    rd_addr <= byte_cnt;
    if (byte_cnt == frame_len – 1)
    byte_cnt <= 11'd0;
    else
    byte_cnt <= byte_cnt + 1'b1;
    end

    S_FCS: begin
    gmii_tx_en <= 1'b1;
    // 高字节先发
    case (fcs_cnt)
    2'd0: gmii_txd <= crc_out[31:24];
    2'd1: gmii_txd <= crc_out[23:16];
    2'd2: gmii_txd <= crc_out[15:8];
    2'd3: gmii_txd <= crc_out[7:0];
    endcase
    fcs_cnt <= fcs_cnt + 1'b1;
    end

    S_IFG: begin
    gmii_tx_en <= 1'b0;
    gmii_txd <= 8'h00;
    if (ifg_cnt == 8'd11) begin
    ifg_cnt <= 8'd0;
    tx_busy <= 1'b0;
    tx_done <= 1'b1;
    end else begin
    ifg_cnt <= ifg_cnt + 1'b1;
    end
    end
    endcase
    end
    end

    endmodule

    5.3 状态机要点解析

    前导码计数:pre_cnt 从 0 到 6,共 7 个周期,每个周期输出 0x55。注意进入 S_PREAMBLE 的第一个周期就要拉高 tx_en 并输出第一个 0x55,不能有气泡。

    SFD:单独一个状态,输出 0xD5,tx_en 保持高。

    数据段:byte_cnt 从 0 计数到 frame_len – 1,frame_len = tx_len + 14。这里 tx_len 是负载长度,14 是 DA+SA+Type。注意 rd_addr 的时序:当前周期给出地址,下一周期 rd_data 才有效,所以 gmii_txd <= rd_data 用的是上一周期地址对应的数据。如果帧缓存是同步读的 BRAM,需要额外一个周期的延迟对齐,实际实现时要把 tx_en 也延迟一拍,或者用预取(prefetch)方式。

    FCS:crc_out 在最后一个数据字节输入后的下一周期才有效(取决于 CRC 模块的流水线级数)。上面的代码假设 CRC 模块是单周期组合输出,实际用 8 位并行 CRC 时通常有 1~2 级流水,需要在 S_DATA 结束后插入等待周期,或者把 crc_out 打拍对齐。

    IFG:标准要求 96 bit time,千兆下 96ns = 12 个 125MHz 周期。代码里 ifg_cnt 从 0 到 11,共 12 个周期,符合要求。注意 IFG 期间 tx_en 必须为低。

    5.4 帧缓存的组织

    帧缓存建议用双端口 BRAM,写端口给上层应用(比如 ADC 采集、图像传感器),读端口给发送状态机。缓存深度至少 2048 字节,支持最大帧 1518 字节。

    // 双端口 BRAM 帧缓存
    reg [7:0] frame_buf [0:2047];

    always @(posedge clk) begin
    if (wr_en)
    frame_buf[wr_addr] <= wr_data;
    end

    always @(posedge clk) begin
    if (rd_en)
    rd_data <= frame_buf[rd_addr];
    end

    注意读延迟一拍,发送状态机要相应调整。

    6. CRC-32 FCS 的完整实现

    6.1 标准定义

    以太网 FCS 使用 CRC-32,参数如下:

    参数值
    多项式 0x04C11DB7(正常),0xEDB88320(反射)
    位宽 32
    初值 0xFFFFFFFF
    输入反射 是(LSB first)
    输出反射 是
    最终异或 0xFFFFFFFF

    标准写法(反射形式):

    crc = 0xFFFFFFFF;
    for each byte b:
    crc = crc ^ b;
    for i in 0..7:
    if (crc & 1)
    crc = (crc >> 1) ^ 0xEDB88320;
    else
    crc = crc >> 1;
    crc = crc ^ 0xFFFFFFFF;

    6.2 逐位串行实现

    module crc32_bitwise (
    input wire clk,
    input wire rst_n,
    input wire init,
    input wire en,
    input wire bit_in,
    output reg [31:0] crc
    );

    wire feedback = crc[0] ^ bit_in;

    always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
    crc <= 32'hFFFFFFFF;
    else if (init)
    crc <= 32'hFFFFFFFF;
    else if (en) begin
    crc[0] <= crc[1];
    crc[1] <= crc[2];
    crc[2] <= crc[3];
    crc[3] <= crc[4];
    crc[4] <= crc[5] ^ feedback;
    crc[5] <= crc[6];
    crc[6] <= crc[7];
    crc[7] <= crc[8];
    crc[8] <= crc[9] ^ feedback;
    crc[9] <= crc[10];
    crc[10] <= crc[11];
    crc[11] <= crc[12];
    crc[12] <= crc[13] ^ feedback;
    crc[13] <= crc[14];
    crc[14] <= crc[15];
    crc[15] <= crc[16];
    crc[16] <= crc[17] ^ feedback;
    crc[17] <= crc[18];
    crc[18] <= crc[19];
    crc[19] <= crc[20];
    crc[20] <= crc[21] ^ feedback;
    crc[21] <= crc[22];
    crc[22] <= crc[23];
    crc[23] <= crc[24];
    crc[24] <= crc[25] ^ feedback;
    crc[25] <= crc[26];
    crc[26] <= crc[27];
    crc[27] <= crc[28];
    crc[28] <= crc[29] ^ feedback;
    crc[29] <= crc[30];
    crc[30] <= crc[31];
    crc[31] <= feedback;
    end
    end

    endmodule

    这个实现每周期处理 1 bit,千兆下 8 位数据需要 8 个周期,太慢。实际要用 8 位并行。

    6.3 8 位并行实现

    8 位并行 CRC 的推导思路:把 8 个 bit 的串行迭代展开,得到当前 CRC 与输入字节的异或组合。标准结果如下(反射形式,多项式 0xEDB88320):

    module crc32_8bit (
    input wire clk,
    input wire rst_n,
    input wire init,
    input wire en,
    input wire [7:0] data,
    output reg [31:0] crc
    );

    wire [31:0] c;
    assign c = crc;

    wire [7:0] d;
    assign d = data;

    // 8 位并行 CRC-32(反射,多项式 0xEDB88320)
    wire [31:0] crc_next;
    assign crc_next[0] = c[8] ^ c[14] ^ c[20] ^ c[26] ^ c[30] ^ d[0] ^ d[6];
    assign crc_next[1] = c[9] ^ c[15] ^ c[21] ^ c[27] ^ c[31] ^ d[1] ^ d[7];
    assign crc_next[2] = c[10] ^ c[16] ^ c[22] ^ c[28] ^ d[2];
    assign crc_next[3] = c[11] ^ c[17] ^ c[23] ^ c[29] ^ d[3];
    assign crc_next[4] = c[12] ^ c[18] ^ c[24] ^ c[30] ^ d[4];
    assign crc_next[5] = c[13] ^ c[19] ^ c[25] ^ c[31] ^ d[5];
    assign crc_next[6] = c[14] ^ c[20] ^ c[26] ^ d[6];
    assign crc_next[7] = c[15] ^ c[21] ^ c[27] ^ d[7];
    assign crc_next[8] = c[0] ^ c[16] ^ c[22] ^ c[28] ^ d[0];
    assign crc_next[9] = c[1] ^ c[17] ^ c[23] ^ c[29] ^ d[1];
    assign crc_next[10] = c[2] ^ c[18] ^ c[24] ^ c[30] ^ d[2];
    assign crc_next[11] = c[3] ^ c[19] ^ c[25] ^ c[31] ^ d[3];
    assign crc_next[12] = c[4] ^ c[20] ^ c[26] ^ d[4];
    assign crc_next[13] = c[5] ^ c[21] ^ c[27] ^ d[5];
    assign crc_next[14] = c[6] ^ c[22] ^ c[28] ^ d[6];
    assign crc_next[15] = c[7] ^ c[23] ^ c[29] ^ d[7];
    assign crc_next[16] = c[8] ^ c[24] ^ c[30] ^ d[0];
    assign crc_next[17] = c[9] ^ c[25] ^ c[31] ^ d[1];
    assign crc_next[18] = c[10] ^ c[26] ^ d[2];
    assign crc_next[19] = c[11] ^ c[27] ^ d[3];
    assign crc_next[20] = c[12] ^ c[28] ^ d[4];
    assign crc_next[21] = c[13] ^ c[29] ^ d[5];
    assign crc_next[22] = c[14] ^ c[30] ^ d[6];
    assign crc_next[23] = c[15] ^ c[31] ^ d[7];
    assign crc_next[24] = c[16] ^ d[0];
    assign crc_next[25] = c[17] ^ d[1];
    assign crc_next[26] = c[18] ^ d[2];
    assign crc_next[27] = c[19] ^ d[3];
    assign crc_next[28] = c[20] ^ d[4];
    assign crc_next[29] = c[21] ^ d[5];
    assign crc_next[30] = c[22] ^ d[6];
    assign crc_next[31] = c[23] ^ d[7];

    always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
    crc <= 32'hFFFFFFFF;
    else if (init)
    crc <= 32'hFFFFFFFF;
    else if (en)
    crc <= crc_next;
    end

    endmodule

    6.4 并行 CRC 的推导方法

    上面那 32 行异或表达式不是背出来的,可以用脚本生成。思路:

  • 把 32 位 CRC 寄存器记为 c[31:0],输入字节记为 d[7:0];
  • 串行算法每处理 1 bit,做一次 feedback = c[0] ^ bit,然后右移并条件异或多项式;
  • 连续做 8 次,把中间变量全部代入,最终得到 c_next 关于 c 和 d 的异或表达式;
  • 用 Python 脚本符号化展开,输出 Verilog 赋值语句。
  • # 生成 8 位并行 CRC-32 表达式
    POLY = 0xEDB88320

    def step(crc, bit):
    fb = (crc & 1) ^ bit
    crc >>= 1
    if fb:
    crc ^= POLY
    return crc

    # 符号化:用 32 个变量表示 crc,8 个变量表示 data
    # 这里用整数位掩码模拟,实际生成时用符号运算库

    实际工程中更常用的做法是直接使用 Xilinx 的 crc32 IP 或者开源的 crc32_d8 模块,但理解推导过程对调试至关重要——当 FCS 校验失败时,你需要知道是初值错了、反射错了还是字节序错了。

    6.5 接收端 FCS 校验

    接收端有两种校验方式:

    方式一:重新计算并比较。把收到的 DA 到 Payload 重新算一遍 CRC,然后与收到的 FCS 比较。注意收到的 FCS 是"已经过最终异或"的值,所以比较时要先把计算结果异或 0xFFFFFFFF。

    方式二:把 FCS 也喂进 CRC。以太网 CRC 有一个性质:如果把 FCS 的 4 个字节也按顺序喂进 CRC 计算器(初值 0xFFFFFFFF,不取反),最终结果应该是 0x2144DF1C(这是一个固定常数,称为 CRC-32 的"魔术值")。这种方式不需要额外比较逻辑,直接判断 CRC 寄存器是否等于魔术值即可。

    方式二在硬件里更常用,因为省去了比较器和额外的寄存器。但要注意:魔术值 0x2144DF1C 是针对"反射输入、反射输出、初值全 1、最终异或全 1"这组参数的,参数不同魔术值也不同。

    7. 接收方向 RTL 设计

    7.1 整体架构

    +——+ +————-+ +———-+ +———–+
    | GMII | –> | 前导码检测 | –> | 帧解析 | –> | 帧缓存 |
    | 输入 | | SFD 对齐 | | CRC 校验 | | + 状态输出|
    +——+ +————-+ +———-+ +———–+

    接收状态机要完成:检测前导码、识别 SFD、逐字节接收、CRC 校验、长度检查、输出帧有效信号。

    7.2 接收状态机

    module eth_rx (
    input wire clk, // 125MHz(rx_clk)
    input wire rst_n,

    // GMII 输入
    input wire [7:0] gmii_rxd,
    input wire gmii_rx_dv,
    input wire gmii_rx_er,

    // 帧输出接口
    output reg rx_valid, // 帧有效(CRC 正确、长度合法)
    output reg rx_done, // 帧接收完成(单周期脉冲)
    output reg [10:0] rx_len, // 帧长度(DA 到 FCS,含 FCS)
    output reg [47:0] rx_da, // 目的 MAC
    output reg [47:0] rx_sa, // 源 MAC
    output reg [15:0] rx_type, // 类型/长度
    output reg [10:0] wr_addr,
    output reg [7:0] wr_data,
    output reg wr_en,

    // 错误统计
    output reg err_crc,
    output reg err_len
    );

    localparam S_IDLE = 3'd0;
    localparam S_PREAMBLE = 3'd1;
    localparam S_DATA = 3'd2;
    localparam S_FCS = 3'd3;
    localparam S_DONE = 3'd4;

    reg [2:0] state;
    reg [3:0] pre_cnt;
    reg [10:0] byte_cnt;
    reg [1:0] fcs_cnt;
    reg [7:0] fcs_buf [0:3];
    reg [7:0] byte_idx;

    // CRC 模块
    reg crc_init;
    reg crc_en;
    reg [7:0] crc_data;
    wire [31:0] crc_out;

    crc32_8bit u_crc (
    .clk (clk),
    .rst_n (rst_n),
    .init (crc_init),
    .en (crc_en),
    .data (crc_data),
    .crc (crc_out)
    );

    // 前导码检测:连续收到 0x55
    wire pre_match = (gmii_rxd == 8'h55);
    wire sfd_match = (gmii_rxd == 8'hD5);

    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    state <= S_IDLE;
    pre_cnt <= 4'd0;
    byte_cnt <= 11'd0;
    fcs_cnt <= 2'd0;
    rx_valid <= 1'b0;
    rx_done <= 1'b0;
    rx_len <= 11'd0;
    rx_da <= 48'd0;
    rx_sa <= 48'd0;
    rx_type <= 16'd0;
    wr_addr <= 11'd0;
    wr_data <= 8'd0;
    wr_en <= 1'b0;
    crc_init <= 1'b0;
    crc_en <= 1'b0;
    crc_data <= 8'd0;
    err_crc <= 1'b0;
    err_len <= 1'b0;
    end else begin
    rx_done <= 1'b0;
    wr_en <= 1'b0;
    crc_init <= 1'b0;
    crc_en <= 1'b0;

    case (state)
    S_IDLE: begin
    rx_valid <= 1'b0;
    pre_cnt <= 4'd0;
    byte_cnt <= 11'd0;
    fcs_cnt <= 2'd0;
    if (gmii_rx_dv && pre_match) begin
    state <= S_PREAMBLE;
    pre_cnt <= 4'd1;
    crc_init <= 1'b1;
    end
    end

    S_PREAMBLE: begin
    if (!gmii_rx_dv) begin
    state <= S_IDLE; // 前导码中断,丢弃
    end else if (sfd_match) begin
    state <= S_DATA;
    byte_cnt <= 11'd0;
    end else if (pre_match) begin
    pre_cnt <= pre_cnt + 1'b1;
    end else begin
    state <= S_IDLE; // 非法前导码
    end
    end

    S_DATA: begin
    if (!gmii_rx_dv) begin
    state <= S_IDLE; // 帧中断
    end else begin
    // 写入帧缓存
    wr_en <= 1'b1;
    wr_addr <= byte_cnt;
    wr_data <= gmii_rxd;

    // CRC 计算(DA 到 Payload)
    crc_en <= 1'b1;
    crc_data <= gmii_rxd;

    // 提取 DA/SA/Type
    case (byte_cnt)
    11'd0: rx_da[47:40] <= gmii_rxd;
    11'd1: rx_da[39:32] <= gmii_rxd;
    11'd2: rx_da[31:24] <= gmii_rxd;
    11'd3: rx_da[23:16] <= gmii_rxd;
    11'd4: rx_da[15:8] <= gmii_rxd;
    11'd5: rx_da[7:0] <= gmii_rxd;
    11'd6: rx_sa[47:40] <= gmii_rxd;
    11'd7: rx_sa[39:32] <= gmii_rxd;
    11'd8: rx_sa[31:24] <= gmii_rxd;
    11'd9: rx_sa[23:16] <= gmii_rxd;
    11'd10: rx_sa[15:8] <= gmii_rxd;
    11'd11: rx_sa[7:0] <= gmii_rxd;
    11'd12: rx_type[15:8] <= gmii_rxd;
    11'd13: rx_type[7:0] <= gmii_rxd;
    default: ;
    endcase

    byte_cnt <= byte_cnt + 1'b1;
    end
    end

    S_FCS: begin
    // 接收 4 字节 FCS
    fcs_buf[fcs_cnt] <= gmii_rxd;
    if (fcs_cnt == 2'd3) begin
    state <= S_DONE;
    end else begin
    fcs_cnt <= fcs_cnt + 1'b1;
    end
    end

    S_DONE: begin
    // 校验 CRC
    // 方式一:比较 crc_out 与 fcs_buf
    // 方式二:把 FCS 喂进 CRC,判断魔术值
    // 这里用方式一
    if (crc_out == {fcs_buf[0], fcs_buf[1], fcs_buf[2], fcs_buf[3]}) begin
    rx_valid <= 1'b1;
    err_crc <= 1'b0;
    end else begin
    rx_valid <= 1'b0;
    err_crc <= 1'b1;
    end

    // 长度检查
    rx_len <= byte_cnt + 11'd4; // 数据字节 + FCS
    if (byte_cnt + 11'd4 < 11'd64) begin
    err_len <= 1'b1;
    rx_valid <= 1'b0;
    end else begin
    err_len <= 1'b0;
    end

    rx_done <= 1'b1;
    state <= S_IDLE;
    end
    endcase
    end
    end

    endmodule

    7.3 接收状态机的关键细节

    前导码检测的鲁棒性:实际链路中,前导码可能因为 PHY 的时钟恢复而丢失前几个字节。标准允许接收端在检测到 SFD 之前的任意位置开始同步,只要看到 0xD5 就认为帧开始。上面的代码要求先看到 0x55 再看到 0xD5,在大多数场景下够用,但更鲁棒的做法是直接检测 0xD5,或者用移位寄存器检测 …10101011 的位模式。

    SFD 与数据的分界:0xD5 是 SFD,不属于帧内容,不参与 CRC 计算。代码里在 S_PREAMBLE 状态检测到 0xD5 后直接跳到 S_DATA,0xD5 本身不写入缓存也不喂 CRC,这是正确的。

    FCS 的接收:FCS 的 4 个字节要接收但不参与 CRC 计算(如果用方式一)。代码里 S_FCS 状态只把数据存入 fcs_buf,不使能 crc_en,正确。

    CRC 比较的时序:crc_out 在最后一个数据字节输入后的下一周期才有效。代码里在 S_DONE 状态比较,此时 crc_out 已经稳定。但如果 CRC 模块有流水线延迟,需要在 S_FCS 和 S_DONE 之间插入等待周期。

    长度检查:最小帧 64 字节(DA 到 FCS),代码里 byte_cnt 统计的是 DA 到 Payload 的字节数,加 4 是 FCS,所以 byte_cnt + 4 就是整帧长度。小于 64 判为错误。

    8. 前导码、SFD 与 IFG 的时序细节

    8.1 前导码的物理意义

    前导码 0x55 重复 7 次,在物理层上是 0101_0101 的交替模式。这个模式的作用:

  • 时钟恢复:接收端 CDR 需要足够的跳变沿来锁定频率和相位。全 0 或全 1 没有跳变,无法恢复时钟。
  • 均衡器收敛:自适应均衡器需要训练序列来调整抽头系数。
  • 信号检测:接收端通过检测前导码判断链路上有帧到来。
  • 千兆以太网使用 4D-PAM5 编码(1000BASE-T),前导码在物理层被编码成特定的符号序列,但 MAC 层看到的仍然是 0x55。

    8.2 SFD 的位模式

    SFD = 0xD5 = 1101_0101。注意以太网是 LSB first 传输,所以线上实际的位顺序是 1010_1011。前导码 0x55 = 0101_0101,LSB first 是 1010_1010。所以前导码 + SFD 的线上位流是:

    1010_1010 1010_1010 … 1010_1011

    即 7 组 1010_1010 加上最后的 1010_1011。接收端检测到 1011 这个模式(相对于前面的 1010 出现了连续两个 1)就知道数据开始了。

    8.3 IFG 的作用与实现

    IFG(Inter-Frame Gap)是帧与帧之间的最小间隔,标准规定 96 bit time。千兆下 96ns = 12 个 125MHz 周期。

    IFG 的作用:

  • 给接收端时间处理上一帧(CRC 校验、DMA 搬运);
  • 给 PHY 时间切换状态;
  • 保证公平性,防止某个节点独占链路。
  • 实现上,发送状态机在 S_IFG 状态保持 tx_en 为低,计数 12 个周期。注意 IFG 是从上一帧的最后一个字节(FCS 最后一字节)结束到下一帧前导码第一个字节开始的时间,所以计数要从 tx_en 拉低的那一拍开始。

    8.4 背靠背帧

    背靠背帧是指连续发送多个帧,帧间只有最小 IFG。千兆下理论最大帧速率:

    • 最小帧 64 字节 + 前导码 8 字节 + IFG 12 字节 = 84 字节 = 672 bit;
    • 672 bit / 1Gbps = 672ns;
    • 帧速率 = 1 / 672ns ≈ 1.488 Mpps。

    如果发送 1518 字节的大帧:

    • 1518 + 8 + 12 = 1538 字节 = 12304 bit;
    • 12304 / 1Gbps = 12.304us;
    • 帧速率 ≈ 81.3 Kpps。

    这两个数字在做流量测试时是基准。

    9. 最小帧填充与长度处理

    9.1 为什么需要填充

    以太网最早使用 CSMA/CD(载波监听多路访问/冲突检测),要求发送端在发送过程中能检测到冲突。为了保证冲突信号能传回发送端,帧必须足够长:

    • 最大网络直径 2500m(5 段中继);
    • 往返传播延迟约 51.2us;
    • 千兆下 51.2us × 1Gbps = 51200 bit = 6400 字节?不对,这是百兆的计算。

    实际千兆的槽时间(slot time)是 512 字节时间(4096 bit time),但因为千兆使用载波扩展(carrier extension),最小帧仍然是 64 字节。对于小于 64 字节的帧,MAC 层要填充到 64 字节。

    9.2 填充的实现

    填充发生在 Payload 之后、FCS 之前。填充字节内容任意(通常填 0),接收端根据 Type/Length 字段判断真实负载长度。

    // 发送时计算实际发送长度
    wire [10:0] payload_len_padded = (tx_len < 11'd46) ? 11'd46 : tx_len;
    wire [10:0] frame_len = payload_len_padded + 11'd14;

    如果 tx_len < 46,发送状态机在 Payload 结束后继续输出 0 直到 46 字节,然后计算 FCS。注意 FCS 要覆盖填充字节。

    9.3 接收端的长度处理

    接收端收到帧后,根据 Type/Length 字段判断:

    • 如果字段值 ≥ 0x0600,是 EtherType,负载长度 = 帧长 – 14 – 4;
    • 如果字段值 ≤ 0x05DC,是长度字段,负载长度 = 该值,剩余的是填充。

    对于 EtherType 帧,上层协议(如 IP)自己知道真实长度(IP 头里有 Total Length),所以填充字节不影响解析。

    10. VLAN 标签与扩展帧格式

    10.1 802.1Q VLAN Tag

    VLAN Tag 插入在 SA 和 Type 之间,共 4 字节:

    +——–+——–+——+—–+——–+=========+—–+
    | DA | SA | TPID | TCI | Type | Payload | FCS |
    | 6 Byte | 6 Byte |2Byte |2Byte| 2 Byte | … |4Byte|
    +——–+——–+——+—–+——–+=========+—–+

    • TPID:Tag Protocol Identifier,固定 0x8100;
    • TCI:Tag Control Information,16 位,包含:
      • PCP(3 位):优先级 0~7;
      • DEI(1 位):丢弃 eligible 指示;
      • VID(12 位):VLAN ID,0~4095。

    带 VLAN Tag 的帧最大长度 1522 字节(1518 + 4)。

    10.2 QinQ 双标签

    QinQ(802.1ad)在 VLAN Tag 外面再套一层:

    DA | SA | TPID=0x88A8 | TCI | TPID=0x8100 | TCI | Type | Payload | FCS

    外层 TPID 是 0x88A8,内层是 0x8100。最大帧长 1526 字节。

    10.3 FPGA 中的 VLAN 处理

    如果 FPGA 只做透传,不需要解析 VLAN,直接把整个帧当负载处理即可。如果需要识别 VLAN,要在 Type 字段位置判断是否为 0x8100 或 0x88A8,如果是则跳过 4 字节继续读 Type。

    // 简化的 VLAN 识别
    reg [15:0] type_field;
    reg is_vlan;
    reg [15:0] vlan_id;

    always @(posedge clk) begin
    if (byte_cnt == 11'd12)
    type_field[15:8] <= gmii_rxd;
    if (byte_cnt == 11'd13) begin
    type_field[7:0] <= gmii_rxd;
    is_vlan <= (type_field[15:8] == 8'h81) && (gmii_rxd == 8'h00);
    end
    end

    11. 完整顶层集成与仿真

    11.1 顶层模块

    module eth_top (
    input wire clk_125m,
    input wire rst_n,

    // 用户发送接口
    input wire user_tx_req,
    input wire [15:0] user_tx_len,
    output wire user_tx_busy,
    output wire user_tx_done,

    // 用户接收接口
    output wire user_rx_valid,
    output wire user_rx_done,
    output wire [10:0] user_rx_len,
    output wire [47:0] user_rx_da,
    output wire [47:0] user_rx_sa,
    output wire [15:0] user_rx_type,

    // GMII
    output wire [7:0] gmii_txd,
    output wire gmii_tx_en,
    output wire gmii_tx_er,
    input wire [7:0] gmii_rxd,
    input wire gmii_rx_dv,
    input wire gmii_rx_er
    );

    // 发送帧缓存
    wire [10:0] tx_rd_addr;
    wire [7:0] tx_rd_data;
    wire tx_rd_en;

    // 接收帧缓存
    wire [10:0] rx_wr_addr;
    wire [7:0] rx_wr_data;
    wire rx_wr_en;

    eth_tx u_tx (
    .clk (clk_125m),
    .rst_n (rst_n),
    .tx_req (user_tx_req),
    .tx_len (user_tx_len),
    .tx_busy (user_tx_busy),
    .tx_done (user_tx_done),
    .rd_addr (tx_rd_addr),
    .rd_data (tx_rd_data),
    .rd_en (tx_rd_en),
    .gmii_txd (gmii_txd),
    .gmii_tx_en (gmii_tx_en),
    .gmii_tx_er (gmii_tx_er)
    );
    如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存
    eth_rx u_rx (
    .clk (clk_125m),
    .rst_n (rst_n),
    .gmii_rxd (gmii_rxd),
    .gmii_rx_dv (gmii_rx_dv),
    .gmii_rx_er (gmii_rx_er),
    .rx_valid (user_rx_valid),
    .rx_done (user_rx_done),
    .rx_len (user_rx_len),
    .rx_da (user_rx_da),
    .rx_sa (user_rx_sa),
    .rx_type (user_rx_type),
    .wr_addr (rx_wr_addr),
    .wr_data (rx_wr_data),
    .wr_en (rx_wr_en),
    .err_crc (),
    .err_len ()
    );

    endmodule

    11.2 仿真测试平台

    `timescale 1ns / 1ps

    module tb_eth;

    reg clk;
    reg rst_n;
    reg tx_req;
    reg [15:0] tx_len;
    wire tx_busy;
    wire tx_done;

    wire [7:0] gmii_txd;
    wire gmii_tx_en;
    wire gmii_tx_er;

    // 环回:发送直接接接收
    wire [7:0] gmii_rxd = gmii_txd;
    wire gmii_rx_dv = gmii_tx_en;
    wire gmii_rx_er = gmii_tx_er;

    wire rx_valid;
    wire rx_done;
    wire [10:0] rx_len;
    wire [47:0] rx_da;
    wire [47:0] rx_sa;
    wire [15:0] rx_type;

    eth_top u_dut (
    .clk_125m (clk),
    .rst_n (rst_n),
    .user_tx_req (tx_req),
    .user_tx_len (tx_len),
    .user_tx_busy(tx_busy),
    .user_tx_done(tx_done),
    .user_rx_valid(rx_valid),
    .user_rx_done (rx_done),
    .user_rx_len (rx_len),
    .user_rx_da (rx_da),
    .user_rx_sa (rx_sa),
    .user_rx_type (rx_type),
    .gmii_txd (gmii_txd),
    .gmii_tx_en (gmii_tx_en),
    .gmii_tx_er (gmii_tx_er),
    .gmii_rxd (gmii_rxd),
    .gmii_rx_dv (gmii_rx_dv),
    .gmii_rx_er (gmii_rx_er)
    );

    // 125MHz 时钟
    initial clk = 0;
    always #4 clk = ~clk; // 8ns 周期 = 125MHz

    initial begin
    rst_n = 0;
    tx_req = 0;
    tx_len = 0;
    #100;
    rst_n = 1;
    #100;

    // 发送一帧 64 字节负载
    @(posedge clk);
    tx_req <= 1;
    tx_len <= 16'd64;
    @(posedge clk);
    tx_req <= 0;

    // 等待发送完成
    wait (tx_done);
    #1000;

    // 检查接收结果
    if (rx_valid && rx_len == 11'd82) // 64 + 14 + 4
    $display("PASS: frame received, len=%0d", rx_len);
    else
    $display("FAIL: rx_valid=%b, len=%0d", rx_valid, rx_len);

    $finish;
    end

    endmodule

    11.3 仿真要点

    环回测试:把 gmii_txd 直接接到 gmii_rxd,gmii_tx_en 接到 gmii_rx_dv,可以验证收发状态机的配合。注意实际中接收端有延迟,环回时接收状态机会在发送的同时启动,需要确认前导码检测逻辑能正确处理。

    CRC 验证:用一个已知的帧(比如 Wireshark 抓到的真实帧)作为输入,检查计算出的 FCS 是否与抓包一致。这是验证 CRC 实现最直接的方法。

    边界测试:分别测试 46 字节(最小负载)、1500 字节(最大负载)、1518 字节(最大帧)三种情况。

    12. 常见错误清单

    12.1 前导码错误

    • 错误:前导码只发了 6 个 0x55。后果:部分 PHY 无法锁定,帧丢失。
    • 错误:前导码用了 0xAA。后果:位模式反了,接收端检测不到。
    • 错误:SFD 用了 0xD5 但发送时 LSB/MSB 搞反,变成 0xAB。后果:接收端认为前导码还没结束。

    12.2 字节序错误

    • 错误:MAC 地址按小端发送,00:1A:2B:3C:4D:5E 发成 5E:4D:3C:2B:1A:00。后果:交换机学不到正确 MAC,帧被丢弃。
    • 错误:EtherType 0x0800 发成 0x0008。后果:上层协议栈识别不了 IP 包。
    • 错误:FCS 低字节先发。后果:接收端 CRC 校验失败,帧被丢弃。

    12.3 CRC 错误

    • 错误:初值用了 0 而不是 0xFFFFFFFF。后果:FCS 全错。
    • 错误:忘记最终异或 0xFFFFFFFF。后果:FCS 与标准差一个固定值。
    • 错误:CRC 覆盖范围包含了前导码和 SFD。后果:FCS 错误。
    • 错误:CRC 覆盖范围漏了填充字节。后果:小帧的 FCS 错误。

    12.4 时序错误

    • 错误:tx_en 比 txd 晚一拍。后果:第一个字节丢失或错位。
    • 错误:IFG 不足 12 个周期。后果:背靠背发送时接收端丢帧。
    • 错误:FCS 计算完成前就切换到发送状态。后果:FCS 字节是旧值。

    12.5 长度错误

    • 错误:最小帧没填充到 64 字节。后果:交换机可能丢弃( runt frame)。
    • 错误:长度字段统计包含了 FCS。后果:上层解析错位。
    • 错误:最大帧超过 1518 字节但没协商巨型帧。后果:被丢弃。

    13. 调试手段与工具

    13.1 用 Wireshark 抓包验证

    最直接的方法:FPGA 发送帧,PC 用 Wireshark 抓包。如果 Wireshark 显示 “Malformed Packet” 或 “Bad FCS”,说明帧格式有问题。Wireshark 可以显示每个字段的十六进制值,逐字段对比。

    13.2 用 ILA 观察 GMII 信号

    Xilinx Vivado 的 ILA 可以抓取 gmii_txd、gmii_tx_en 的波形。触发条件设为 gmii_tx_en 上升沿,观察前导码、SFD、数据、FCS 的字节序列是否符合预期。

    13.3 用环回模式自测

    PHY 芯片通常支持内部环回(loopback),MAC 发送的帧直接回到 MAC 接收。这样可以排除外部链路问题,专注调试 MAC 逻辑。

    13.4 用已知帧做黄金参考

    从 Wireshark 导出一个真实帧的十六进制数据,作为测试平台的输入,对比 FPGA 计算出的 FCS 与抓包中的 FCS。如果不一致,逐位排查 CRC 实现。

    14. 性能优化与工程实践

    14.1 流水线设计

    发送状态机的数据通路可以流水化:地址生成、BRAM 读、CRC 计算、GMII 输出各占一级,提高时序余量。但要注意各级之间的握手和气泡处理。

    14.2 多帧缓存与乒乓

    高吞吐场景下,单帧缓存不够用。可以用乒乓缓冲:一个缓存发送时,另一个缓存接收上层数据。切换时要注意帧边界对齐。

    14.3 时钟域 crossing

    GMII 的 rx_clk 是从 PHY 恢复的时钟,和 FPGA 系统时钟不同源。接收数据要跨时钟域到系统时钟域,用异步 FIFO。发送方向 tx_clk 通常由 FPGA 提供,和系统时钟同源,不需要 CDC。

    14.4 资源估算

    一个基本的千兆 MAC(发送 + 接收 + CRC)在 Xilinx 7 系列上大约占用:

    • LUT:800~1200;
    • FF:600~900;
    • BRAM:2~4 块(帧缓存);
    • 时序:125MHz 下 WNS 容易满足。

    如果加 VLAN 解析、多队列、统计计数器,资源会翻倍。

    15. 总结

    以太网帧格式的核心就三句话:

  • 前导码 7×0x55 + SFD 0xD5,然后是大端字节序的 DA/SA/Type/Payload/FCS;
  • FCS 是 CRC-32,覆盖 DA 到 Payload(含填充),初值全 1,反射,最终异或全 1,高字节先发;
  • 最小帧 64 字节(不含前导码),IFG 12 字节时间,千兆下 8 位数据 125MHz。
  • 把这三点落实到 RTL,就是本文的发送状态机和接收状态机。调试时优先用 Wireshark 抓包对比,用 ILA 看波形,用环回排除链路问题。

    帧格式不难,难在细节。前导码少一个字节、FCS 字节序反了、CRC 初值错了,任何一个都会让帧在链路上消失得无影无踪。把本文的代码跑通,把每个字段的字节序和时序搞清楚,千兆以太网就不再是黑盒。

    16. 常见错误清单与调试手段

    前面第 12 章按「前导码 / 字节序 / CRC / 时序 / 长度」五类做了定性归纳,但真正在板子上调不通时,工程师需要的是可执行的排查路径:看到什么现象、对应哪条根因、用什么手段定位、怎么改。本节把千兆以太网 FPGA 调试中最高频的问题整理成一张速查表,每条都给出「现象 / 根因 / 定位方法 / 修复方式」四列,建议打印出来贴在工位上。
    如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

    16.1 高频问题速查表

    #问题现象根因定位方法修复方式
    1 FCS 字节序反了 Wireshark 显示 “Bad FCS” 或 “Malformed Packet”;接收端 err_crc 持续拉高;用 ILA 看 FCS 四字节与 Wireshark 计算值「整体倒序」 把 CRC 结果的低字节 crc_out[7:0] 先发,而以太网要求高字节 crc_out[31:24] 先发 抓一帧,把 ILA 上 FCS 四字节与 Wireshark 的 “Frame check sequence” 逐字节对比;若完全逆序即为本条 发送状态机 S_FCS 按 [31:24]→[23:16]→[15:8]→[7:0] 顺序输出;接收端比较时按 {fcs_buf[0],fcs_buf[1],fcs_buf[2],fcs_buf[3]} 拼接
    2 前导码少发一个 0x55 部分 PHY 链路指示灯正常但收不到帧;Wireshark 抓不到任何包;某些 PHY 能收、某些不能(对 CDR 锁定时间敏感) pre_cnt 计数范围写成 0~5(6 个字节)或状态机进入 S_PREAMBLE 首拍有气泡,实际只发出 6 个 0x55 ILA 触发 gmii_tx_en 上升沿,数 txd==0x55 的连续周期数,应为 7;同时确认 tx_en 拉高首拍 txd 就是 0x55 pre_cnt 改为 0~6 共 7 拍;S_IDLE→S_PREAMBLE 转移当拍即输出 0x55 并拉高 tx_en,不留空拍
    3 IFG 不足 12 周期 单帧收发正常,背靠背连续发送时丢帧率随速率上升;交换机端口统计 FCS Error / Alignment Error 增长 ifg_cnt 上限写成 10 或 11 但计数起点晚一拍,实际只有 10~11 个空闲周期;或 IFG 期间 tx_en 未完全拉低 ILA 抓连续两帧,测量前一帧 FCS 末字节到下一帧首个 0x55 之间的 tx_en==0 周期数,应 ≥ 12 ifg_cnt 从 0 计到 11 共 12 拍,且从 tx_en 拉低那一拍开始计数;IFG 期间强制 tx_en=0、txd 可置 0
    4 tx_en 有气泡 帧被截断或长度异常;Wireshark 报 “Short Frame”;接收端 rx_len 比预期小 状态机在 S_DATA 中因 BRAM 读延迟未对齐,插入了 tx_en=0 的等待周期;或 tx_en 由组合逻辑产生,出现毛刺 ILA 抓 gmii_tx_en,检查一帧内是否为连续高电平(中间不允许有低电平);统计高电平周期数是否等于帧长 用预取(prefetch)方式提前一拍给地址,保证 rd_data 与 tx_en 同拍对齐;tx_en 必须寄存器输出,禁止组合产生
    5 RGMII skew 未加导致采样错位 链路 up 但误码率高;小流量偶发丢包,大流量几乎全丢;换一块板子现象不同(走线长度差异) RGMII 的 TXC 相对 TXD 需要 1.5~2ns 延迟,PCB 未做等长绕线且 FPGA 侧未加 ODELAY,采样点落在数据跳变沿 用示波器测 TXC 与 TXD 相位关系;或在 FPGA 内用 IDELAY 扫描 tap 值,观察误码率随 tap 变化的「浴盆曲线」 发送侧加 ODELAY(Xilinx 7 系列约 tap 值对应 1.5~2ns),接收侧加 IDELAY 并把 tap 扫到浴盆中心;或 PCB 上对 TXC 做延迟绕线
    6 CRC 初值 / 反射配置错 FCS 与标准值差一个固定偏移;或所有帧 FCS 全错但长度、字节序都对 初值用了 0x00000000 而非 0xFFFFFFFF;或漏了输入/输出反射(应 LSB first);或漏了最终异或 0xFFFFFFFF 用固定测试向量(如 ASCII “123456789” 的 CRC-32 应为 0xCBF43926)单独仿真 CRC 模块,与标准值对比 初值 0xFFFFFFFF、输入输出均反射(多项式用 0xEDB88320)、最终异或 0xFFFFFFFF,三者缺一不可
    7 最小帧未填充到 46 字节 小包(如 ARP、TCP ACK)被交换机丢弃;Wireshark 报 “Runt Frame”;大包正常 发送状态机直接按 tx_len 组帧,未做 tx_len < 46 时补 0 的处理;或填充了但 FCS 未覆盖填充字节 抓一个短包,看线上帧长是否 < 64 字节;检查 CRC 计算是否包含了填充区 payload_len_padded = (tx_len < 46) ? 46 : tx_len;填充字节(通常 0)必须参与 CRC 计算,且在 FCS 之前发出
    8 SFD 对齐错位 接收端偶尔把数据首字节当成 SFD,或整帧偏移一个字节;rx_da 解析出明显错误的 MAC 接收状态机在 8 位通路上按字节匹配 0xD5,但前导码因 PHY 时钟恢复丢失前几字节,导致 0xD5 出现在非预期位置;或位序(LSB/MSB)理解错误 ILA 抓 rxd 序列,人工核对 …0x55,0x55,0xD5,DA[0]… 的字节边界;对比 rx_da 与发送端 DA 接收端直接检测 0xD5 作为帧起始(不强制先见 7 个 0x55);确认以太网 LSB first,0xD5 线上位流为 1010_1011
    9 tx_en 与 txd 相位错一拍 首字节丢失,接收端 DA 首字节变成 0x55 或 0xD5;整帧数据左移一字节 tx_en 与 txd 不在同一拍更新,tx_en 晚一拍拉高,导致首拍 txd 上的 0x55 未被采到 ILA 同时抓 tx_en 与 txd,确认 tx_en 上升沿那一拍的 txd 是否为 0x55 tx_en 与 txd 在同一 always 块、同一时钟沿赋值,保证严格同拍
    10 接收端 CRC 比较时序错 err_crc 随机误报,同一帧有时通过有时不通过;仿真通过、上板失败 crc_out 有 1~2 级流水延迟,在 S_DONE 比较时 CRC 尚未稳定;或 FCS 四字节被误喂进 CRC 仿真中打印 crc_out 与 fcs_buf 的时序关系;检查 S_FCS 状态是否误使能 crc_en 在 S_FCS 与 S_DONE 之间插入等待周期,或把 crc_out 打拍对齐后再比较;S_FCS 状态禁止使能 crc_en
    11 帧缓存读延迟未对齐 数据整体错位一个字节;或首字节为上一帧残留数据 同步读 BRAM 有一拍读延迟,状态机按「当拍给地址当拍取数」编写,实际取到的是上一地址数据 仿真中观察 rd_addr 与 rd_data 的对应关系,确认是否差一拍 地址提前一拍给出(prefetch),或把 tx_en/txd 整体延迟一拍对齐
    12 巨型帧未协商被丢弃 大包(>1518 字节)全部丢失,小包正常;交换机端口统计 Oversize 增长 发送端发了 Jumbo 帧但链路对端 / 交换机未开启 Jumbo Frame 支持 抓包确认帧长是否 > 1518;查交换机 Jumbo 配置 两端同时开启 Jumbo 并统一 MTU;或把负载拆分到多个标准帧

    16.2 按现象反查的排查顺序

    上表按问题归类,实际调试时更常见的是「先看到现象,再反查根因」。建议按下面的顺序逐层收敛,避免一上来就怀疑 CRC:

    #mermaid-svg-q6mk7ERFNSej9QSI{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q6mk7ERFNSej9QSI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q6mk7ERFNSej9QSI .error-icon{fill:#552222;}#mermaid-svg-q6mk7ERFNSej9QSI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q6mk7ERFNSej9QSI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q6mk7ERFNSej9QSI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q6mk7ERFNSej9QSI .marker.cross{stroke:#333333;}#mermaid-svg-q6mk7ERFNSej9QSI svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q6mk7ERFNSej9QSI p{margin:0;}#mermaid-svg-q6mk7ERFNSej9QSI .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-q6mk7ERFNSej9QSI .cluster-label text{fill:#333;}#mermaid-svg-q6mk7ERFNSej9QSI .cluster-label span{color:#333;}#mermaid-svg-q6mk7ERFNSej9QSI .cluster-label span p{background-color:transparent;}#mermaid-svg-q6mk7ERFNSej9QSI .label text,#mermaid-svg-q6mk7ERFNSej9QSI span{fill:#333;color:#333;}#mermaid-svg-q6mk7ERFNSej9QSI .node rect,#mermaid-svg-q6mk7ERFNSej9QSI .node circle,#mermaid-svg-q6mk7ERFNSej9QSI .node ellipse,#mermaid-svg-q6mk7ERFNSej9QSI .node polygon,#mermaid-svg-q6mk7ERFNSej9QSI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q6mk7ERFNSej9QSI .rough-node .label text,#mermaid-svg-q6mk7ERFNSej9QSI .node .label text,#mermaid-svg-q6mk7ERFNSej9QSI .image-shape .label,#mermaid-svg-q6mk7ERFNSej9QSI .icon-shape .label{text-anchor:middle;}#mermaid-svg-q6mk7ERFNSej9QSI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q6mk7ERFNSej9QSI .rough-node .label,#mermaid-svg-q6mk7ERFNSej9QSI .node .label,#mermaid-svg-q6mk7ERFNSej9QSI .image-shape .label,#mermaid-svg-q6mk7ERFNSej9QSI .icon-shape .label{text-align:center;}#mermaid-svg-q6mk7ERFNSej9QSI .node.clickable{cursor:pointer;}#mermaid-svg-q6mk7ERFNSej9QSI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q6mk7ERFNSej9QSI .arrowheadPath{fill:#333333;}#mermaid-svg-q6mk7ERFNSej9QSI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q6mk7ERFNSej9QSI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q6mk7ERFNSej9QSI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q6mk7ERFNSej9QSI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q6mk7ERFNSej9QSI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q6mk7ERFNSej9QSI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q6mk7ERFNSej9QSI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q6mk7ERFNSej9QSI .cluster text{fill:#333;}#mermaid-svg-q6mk7ERFNSej9QSI .cluster span{color:#333;}#mermaid-svg-q6mk7ERFNSej9QSI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q6mk7ERFNSej9QSI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q6mk7ERFNSej9QSI rect.text{fill:none;stroke-width:0;}#mermaid-svg-q6mk7ERFNSej9QSI .icon-shape,#mermaid-svg-q6mk7ERFNSej9QSI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q6mk7ERFNSej9QSI .icon-shape p,#mermaid-svg-q6mk7ERFNSej9QSI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q6mk7ERFNSej9QSI .icon-shape .label rect,#mermaid-svg-q6mk7ERFNSej9QSI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q6mk7ERFNSej9QSI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q6mk7ERFNSej9QSI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q6mk7ERFNSej9QSI :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    抓不到

    无波形

    有波形

    能抓到但报错

    Bad FCS

    Runt / Short

    Oversize

    Malformed

    现象: 收不到包 / 错帧

    Wireshark 能抓到帧吗?

    PHY 链路 up 且 ILA 上 tx_en 有波形?

    查发送状态机: tx_req 握手 / 状态机卡死

    查前导码 7×0x55 与 SFD 0xD5 是否完整

    查 RGMII skew / GMII 时序约束

    报错类型?

    查 CRC 初值/反射/异或 与 FCS 字节序

    查最小帧填充与 tx_en 气泡

    查巨型帧协商与长度字段

    查 DA/SA/Type 字节序与 VLAN 偏移

    排查的核心原则是从物理层往协议层收敛:先确认链路和时序(skew、tx_en 波形),再确认帧边界(前导码、SFD、IFG),最后才查字段内容(字节序、CRC、长度)。绝大多数「玄学丢包」最终都落在前两层。
    如果需要系统学习FPGA,这里有一份整理的FPGA汇总课程资料,包含BD教程、Vivado教程、IP 核教程等,有需要的朋友可以保存:链接: https://pan.baidu.com/s/1pIFS8GEYu_jH1np4ZLG2Kw?pwd=wbbw 提取码: wbbw 整理不易,尽快保存

    16.3 三个最容易被忽略的细节

    第一,tx_en 的气泡比想象中更致命。 很多实现为了省一个 BRAM 而用组合逻辑直接输出 tx_en,综合后出现毛刺或与 txd 不同拍,接收端会把这一拍当成帧结束,整帧被截断。tx_en 必须寄存器输出,且与 txd 严格同拍。

    第二,CRC 的「三个参数」必须同时正确。 初值、反射、最终异或,任何一个错都会让 FCS 全错,但错误表现不同:初值错是「差一个固定偏移」,反射错是「完全对不上」,漏最终异或是「差 0xFFFFFFFF」。用固定测试向量单独验证 CRC 模块,比在整条链路上猜要快得多。

    第三,RGMII 的 skew 是板级问题,不是逻辑问题。 同一份 RTL 在不同板子上表现不同,几乎一定是 skew 没调好。用 IDELAY 扫描 tap 值画误码率曲线,找到浴盆底部,比反复改逻辑有效得多。

    把这张表和这套排查顺序用熟,千兆以太网调试就从「碰运气」变成了「按图索骥」。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » FPGA 千兆以太网帧格式全解:从 IEEE 802.3 帧结构到 Verilog 收发实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!