第三章 MAC层设计(一):以太网帧结构解析、CRC32校验原理与实现、MAC层发送状态机设计
各位同学,今天我们正式进入MAC层的设计。说实话,MAC层是整个网络协议栈里最“实在”的一层——它不跟你玩虚的,直接跟物理层的比特流打交道。我当年刚接触FPGA网络开发时,觉得MAC层不就是拼个帧嘛,结果第一次上板调试,抓出来的数据全是乱的……嗯,从那以后我老老实实把帧结构背得滚瓜烂熟。
3.1 以太网帧结构:别小看这14个字节
先看标准以太网帧长什么样。我习惯把帧结构分成三块:头部、数据、尾部。头部固定14字节,尾部是4字节的FCS(帧校验序列)。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 目的MAC地址 | 6 | 接收方MAC |
| 源MAC地址 | 6 | 发送方MAC |
| 类型/长度 | 2 | 0x0800表示IPv4,0x0806表示ARP |
| 数据载荷 | 46~1500 | 上层协议数据,不足46字节需填充 |
| FCS(CRC32) | 4 | 对整个帧(不含前导码)做CRC校验 |
这里有个坑,我必须要提醒你:前导码(Preamble)和帧起始定界符(SFD)不属于MAC帧。前导码是物理层加的,共8字节(7字节0x55 + 1字节0xD5)。你在FPGA里做MAC设计时,收到的第一个有效字节应该是目的MAC的第一个字节。
3.2 CRC32校验原理:多项式除法在硬件里的玩法
CRC32说白了就是多项式除法。以太网用的是CRC-32标准,生成多项式是:
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
对应的二进制是 0x04C11DB7。注意,这个多项式是“反转”的——什么意思?你查资料时会发现有的写0xEDB88320,其实是一个东西,只是位序不同。我个人习惯用0x04C11DB7,因为跟IEEE标准一致。
计算过程分三步:
- 在数据后面补32个0
- 用生成多项式做模2除法(异或运算)
- 余数取反,得到32位CRC值
你想想看,如果每个字节都做一次除法,那速度太慢了。FPGA里我们通常用并行CRC算法,一次处理一个字节或四个字节。我当年在10G以太网项目里,用的是64位并行CRC,一个时钟周期处理8个字节。
3.3 CRC32的Verilog实现:一个可综合的模块
下面给出一个经典的字节并行CRC32模块。这个代码我用了很多年,从百兆到万兆都能用,只是时钟频率不同。
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,
output reg crc_valid
);
// 内部寄存器
reg [31:0] crc_reg;
// CRC32多项式(反转后)
parameter CRC_POLY = 32'hEDB88320;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
crc_reg <= 32'hFFFFFFFF;
crc_out <= 32'h0;
crc_valid <= 1'b0;
end else if (data_valid) begin
// 逐字节计算CRC
crc_reg <= next_crc(crc_reg, data_in);
crc_valid <= 1'b0;
end else begin
// 数据结束后输出结果(取反)
crc_out <= ~crc_reg;
crc_valid <= 1'b1;
end
end
// 组合逻辑:计算下一个CRC值
function [31:0] next_crc;
input [31:0] crc;
input [7:0] data;
reg [31:0] new_crc;
integer i;
begin
new_crc = crc ^ {24'h0, data};
for (i = 0; i < 8; i = i + 1) begin
if (new_crc[0])
new_crc = (new_crc >> 1) ^ CRC_POLY;
else
new_crc = (new_crc >> 1);
end
next_crc = new_crc;
end
endfunction
endmodule
3.4 MAC发送状态机:从数据到比特流的转换
MAC发送状态机是整个发送通路的核心。我习惯把它设计成5个状态:
状态机的工作流程:
- IDLE:等待发送请求。有帧要发时,跳转到PREAMBLE。
- PREAMBLE:发送8字节前导码(7字节0x55 + 1字节0xD5)。发送完成后进入DATA。
- DATA:发送MAC帧头(14字节)+ 数据载荷。注意这里要处理填充逻辑——如果数据不足46字节,自动填充0。
- FCS:发送4字节CRC32校验值。这个值在DATA阶段就要并行计算好。
- IFG:帧间隔,至少96个bit time(12字节)。这是以太网的规矩,给接收端留出处理时间。
3.5 发送状态机的Verilog实现框架
module mac_tx (
input wire clk,
input wire rst_n,
input wire tx_start, // 发送启动信号
input wire [7:0] tx_data, // 待发送数据
input wire tx_data_valid, // 数据有效
output reg [7:0] gmii_txd, // GMII接口数据
output reg gmii_tx_en, // GMII接口使能
output reg tx_done // 发送完成
);
// 状态定义
localparam IDLE = 3'd0;
localparam PREAMBLE = 3'd1;
localparam DATA = 3'd2;
localparam FCS = 3'd3;
localparam IFG = 3'd4;
reg [2:0] state, next_state;
reg [3:0] preamble_cnt; // 前导码计数器
reg [15:0] byte_cnt; // 字节计数器
reg [31:0] crc_value; // CRC计算结果
// 状态转移逻辑(略,参考状态图)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 输出逻辑
always @(*) begin
gmii_txd = 8'h00;
gmii_tx_en = 1'b0;
tx_done = 1'b0;
case (state)
IDLE: begin
// 等待tx_start
end
PREAMBLE: begin
gmii_tx_en = 1'b1;
if (preamble_cnt < 7)
gmii_txd = 8'h55; // 前导码
else
gmii_txd = 8'hD5; // SFD
end
DATA: begin
gmii_tx_en = 1'b1;
gmii_txd = tx_data; // 发送MAC帧数据
end
FCS: begin
gmii_tx_en = 1'b1;
// 按字节发送CRC值(大端序)
case (byte_cnt[1:0])
2'b00: gmii_txd = crc_value[31:24];
2'b01: gmii_txd = crc_value[23:16];
2'b10: gmii_txd = crc_value[15:8];
2'b11: gmii_txd = crc_value[7:0];
endcase
end
IFG: begin
gmii_tx_en = 1'b0; // 发送空闲
if (byte_cnt == 12) // IFG结束
tx_done = 1'b1;
end
endcase
end
endmodule
3.6 关于字节序的提醒
以太网采用大端序(Big-Endian),即高位字节先发送。比如MAC地址 00:11:22:33:44:55,先发0x00,再发0x11,以此类推。CRC32也是大端序,先发高位字节。
这一点在调试时特别容易出错。我记得有一次,同事把CRC的字节序搞反了,结果接收端一直报错。查了三天才发现是字节序问题。所以,写代码前先确认字节序,这个习惯能帮你省下大量调试时间。
好了,MAC层发送部分就讲到这里。下一节我们会深入MAC接收状态机,以及如何处理接收到的错误帧。记住:发送是“主动”的,接收是“被动”的,接收端的设计往往更复杂。
公众号:蓝海资料掘金营,微信deep3321