硬件UDP/IP协议栈设计(一):MAC层与PHY层接口(GMII/RGMII)、CRC校验的硬件实现

各位同学,今天我们来聊聊硬件UDP/IP协议栈里最基础、也最绕不开的一环——MAC层和PHY层的接口设计。说白了,这就是你的FPGA芯片和外部物理收发器之间的“握手协议”。

我个人习惯把这一层比作“翻译官”。你的FPGA内部跑的是数字逻辑,而网线上传的是模拟信号。PHY芯片负责模数转换,MAC层负责把数据打包成帧。中间怎么沟通?靠的就是GMII或RGMII接口。

GMII vs RGMII:选哪个?

先看GMII(Gigabit Media Independent Interface)。它是个并行接口,数据位宽8位,时钟频率125MHz。发送和接收各有独立的时钟、数据和控制信号。好处是时序简单,坏处是引脚太多——整整24根线。

我在项目中遇到过一块板子,BGA封装的FPGA,引脚资源紧张得要命。那时候我就想,要是能用RGMII就好了。

RGMII(Reduced GMII)把数据位宽砍到4位,时钟频率还是125MHz,但用双沿采样(DDR)来达到同样的吞吐量。说白了,就是时钟的上升沿和下降沿各传4位数据,合起来还是8位。引脚数从24根降到12根,省了一半。

嗯,这里要注意:RGMII的时序约束比GMII复杂。因为双沿采样对时钟和数据之间的skew更敏感。我建议新手先从GMII入手,等把协议栈调通了,再考虑换成RGMII来节省引脚。

核心区别总结:

  • GMII:8位并行,125MHz单沿,24引脚,时序简单
  • RGMII:4位并行,125MHz双沿,12引脚,时序复杂

接口信号详解

我们以GMII为例,看看具体有哪些信号。你想想看,发送和接收其实是两条独立的通路,各自有数据、时钟、控制信号。

信号组 信号名 方向 说明
发送 GTX_CLK MAC→PHY 125MHz发送时钟
发送 TXD[7:0] MAC→PHY 8位发送数据
发送 TX_EN MAC→PHY 发送使能
发送 TX_ER MAC→PHY 发送错误指示
接收 RX_CLK PHY→MAC 125MHz接收时钟
接收 RXD[7:0] PHY→MAC 8位接收数据
接收 RX_DV PHY→MAC 接收数据有效
接收 RX_ER PHY→MAC 接收错误指示

我曾经被RX_DV和RX_ER的组合状态坑过一次。这两个信号组合起来能表示好几种状态:正常数据、载波扩展、错误帧等等。如果你没仔细看PHY芯片的数据手册,很容易把错误帧当成正常数据收进来。

CRC校验的硬件实现

接下来是重头戏——CRC校验。以太网帧尾部有个4字节的FCS(Frame Check Sequence),用的就是CRC32算法。生成多项式是:

G(x) = x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1

说白了,CRC就是多项式除法。发送端计算余数,接收端重新计算,如果余数为0,说明帧没出错。

硬件实现CRC,我推荐用LFSR(线性反馈移位寄存器)。32位的LFSR,每个时钟周期处理1位数据。但千兆以太网一个时钟周期要处理8位数据,所以得用并行CRC算法。

我的经验:并行CRC的推导过程很繁琐,但好在有现成的工具。我一般用Python脚本生成Verilog代码,输入多项式、数据位宽,输出就是完整的CRC计算逻辑。别手算,容易出错。

下面是一个简化的并行CRC32核心代码片段,处理8位数据:

module crc32_d8 (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  data_in,
    input  wire        data_valid,
    output reg  [31:0] crc_out
);

    reg [31:0] crc_reg;
    wire [31:0] next_crc;

    // 并行CRC32计算逻辑(简化版)
    assign next_crc[0]  = crc_reg[24] ^ crc_reg[30] ^ data_in[0] ^ data_in[6];
    assign next_crc[1]  = crc_reg[25] ^ crc_reg[31] ^ data_in[1] ^ data_in[7] ^ data_in[6];
    // ... 实际有32个等式,这里省略

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            crc_reg <= 32'hFFFFFFFF;
        else if (data_valid)
            crc_reg <= next_crc;
    end

    assign crc_out = ~crc_reg;  // 输出取反

endmodule

嗯,这里要注意:CRC的初始值要设为全1,最后输出要取反。这是IEEE 802.3标准规定的。我见过有人忘了取反,结果怎么都对不上。

避坑指南

我曾经在一个项目里,CRC校验总是报错。查了两天才发现,问题出在字节序上。以太网是大端传输,但我的数据缓存模块用的是小端。CRC计算时字节顺序搞反了,结果自然不对。

所以我的建议是:

  • 先确认你的数据字节序和以太网标准一致
  • 用仿真工具抓取完整的以太网帧,手动计算CRC做对比
  • 别忘了处理前导码和帧起始定界符(SFD)——它们不参与CRC计算

警告:RGMII接口的时序约束一定要做对。我曾经见过一个团队,RGMII的时钟和数据之间差了1ns,结果10%的帧都CRC错误。后来加了IODELAY才解决。

知识体系结构图

下面我用一张SVG图来展示本章的核心逻辑:

MAC层与PHY层接口知识体系 UDP/IP协议栈 MAC层(介质访问控制) 帧封装/解封装 · CRC32校验 · 流量控制 MAC与PHY接口 GMII(24引脚,8位并行,125MHz单沿) RGMII(12引脚,4位双沿,125MHz DDR) PHY层(物理层收发器) 模数转换 · 编码解码 · 链路协商 网线/光纤(物理介质) CRC32校验 多项式除法 硬件实现 LFSR · 并行CRC

这张图把整个链路串起来了。从上到下依次是UDP/IP协议栈、MAC层、接口层、PHY层、物理介质。右侧单独列出了CRC校验和它的硬件实现方式。

我个人觉得,理解这张图比背代码更重要。因为接口协议可能会变,但分层的思想不会变。你只要把每一层的职责搞清楚,换什么接口都不怕。

本章核心要点:

  1. GMII和RGMII的选择取决于引脚资源和时序预算
  2. CRC32用并行LFSR实现,注意初始值和输出取反
  3. 字节序、时序约束是常见的坑,提前做好仿真验证
  4. 分层设计思想比具体接口细节更重要

好了,这一章就到这里。MAC层和PHY层的接口是硬件协议栈的基石,搞懂了它,后面的IP层和UDP层就好办了。


公众号:蓝海资料掘金营,微信deep3321