硬件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图来展示本章的核心逻辑:
这张图把整个链路串起来了。从上到下依次是UDP/IP协议栈、MAC层、接口层、PHY层、物理介质。右侧单独列出了CRC校验和它的硬件实现方式。
我个人觉得,理解这张图比背代码更重要。因为接口协议可能会变,但分层的思想不会变。你只要把每一层的职责搞清楚,换什么接口都不怕。
本章核心要点:
- GMII和RGMII的选择取决于引脚资源和时序预算
- CRC32用并行LFSR实现,注意初始值和输出取反
- 字节序、时序约束是常见的坑,提前做好仿真验证
- 分层设计思想比具体接口细节更重要
好了,这一章就到这里。MAC层和PHY层的接口是硬件协议栈的基石,搞懂了它,后面的IP层和UDP层就好办了。
公众号:蓝海资料掘金营,微信deep3321