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 流程,只把二层帧格式和它的硬件实现讲透。
全文会覆盖:
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)的语义:
| 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 取值
| 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 行异或表达式不是背出来的,可以用脚本生成。思路:
# 生成 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 的交替模式。这个模式的作用:
千兆以太网使用 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 的作用:
实现上,发送状态机在 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. 总结
以太网帧格式的核心就三句话:
把这三点落实到 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 值画误码率曲线,找到浴盆底部,比反复改逻辑有效得多。
把这张表和这套排查顺序用熟,千兆以太网调试就从「碰运气」变成了「按图索骥」。
网硕互联帮助中心







评论前必须登录!
注册