4、MAC层设计(二):MAC层接收状态机设计、帧过滤与错误处理、全双工与半双工模式
上一讲我们把MAC发送通路捋了一遍,今天轮到接收端了。说实话,接收比发送要复杂那么一丢丢——你想想看,发送是你主动往外扔数据,节奏自己控制;接收呢,是线路上随时可能冒出来一个帧,你得时刻准备着。我早年做第一个千兆以太网项目时,接收状态机就踩过坑,今天把这些经验都抖出来。
4.1 接收状态机:从线缆到FIFO的旅程
MAC接收的核心,说白了就是一个有限状态机。它盯着GMII/RGMII接口上的RX_DV信号,一旦拉高,就知道帧来了。我习惯把接收状态机分成这么几个状态:
- IDLE:空闲态,等RX_DV拉高
- PREAMBLE:接收前导码(7字节0x55 + 1字节0xD5)
- DATA:接收帧本体,包括目的MAC、源MAC、类型/长度、载荷
- FCS:接收帧校验序列(4字节CRC32)
- ERROR:出错处理
这里有个细节——前导码要不要传给上层?我个人习惯是不传。前导码是物理层的东西,MAC层把它吃掉就行,只把目的MAC之后的字段往上送。嗯,这里要注意:有些设计会把前导码也存进FIFO,浪费存储不说,还容易把上层协议栈搞晕。
// 接收状态机核心代码(Verilog风格)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_state <= IDLE;
rx_ptr <= 0;
end else begin
case (rx_state)
IDLE: begin
if (rx_dv)
rx_state <= PREAMBLE;
end
PREAMBLE: begin
if (rx_ptr == 7) begin // 7字节0x55后,下一字节是SFD
if (rx_data == 8'hD5)
rx_state <= DATA;
else
rx_state <= ERROR; // SFD不对,直接报错
end
rx_ptr <= rx_ptr + 1;
end
DATA: begin
// 这里开始收目的MAC、源MAC、类型/长度、载荷
if (rx_ptr <= 1518) begin // 标准以太网最大帧长
// 存入FIFO
end else begin
rx_state <= ERROR; // 超长帧
end
rx_ptr <= rx_ptr + 1;
if (!rx_dv) begin
// 帧结束,检查是否收到至少64字节
if (rx_ptr >= 64)
rx_state <= FCS;
else
rx_state <= ERROR; // 帧过短(runt帧)
end
end
FCS: begin
// 校验CRC32
if (crc_ok)
rx_state <= IDLE;
else
rx_state <= ERROR;
end
ERROR: begin
// 丢弃该帧,重置指针
rx_ptr <= 0;
if (!rx_dv)
rx_state <= IDLE;
end
endcase
end
end
关键点:接收状态机必须处理所有异常情况——帧过短、帧过长、CRC错误、SFD错误。任何一个没处理好,都可能让整个协议栈挂死。我在项目中遇到过一种情况:对端设备发了超长帧(超过1518字节),状态机没做超长检测,结果接收FIFO被撑爆,后续所有帧都丢了。
4.2 帧过滤:不是你的帧,别乱收
以太网是共享介质,理论上你收到的帧里,大部分都不是发给你的。帧过滤就是干这个的——检查目的MAC地址,不是我的就扔。我见过一些新手直接把所有帧都收进来,让CPU去过滤,结果千兆网口上CPU直接被中断淹死。
帧过滤通常支持以下几种模式:
| 模式 | 说明 | 典型应用 |
|---|---|---|
| 单播过滤 | 只收目的MAC等于本机MAC的帧 | 普通主机 |
| 广播接收 | 收目的MAC为FF:FF:FF:FF:FF:FF的帧 | ARP请求 |
| 多播过滤 | 收目的MAC为多播地址的帧 | IGMP、组播视频 |
| 混杂模式 | 所有帧都收 | 网络抓包、交换机 |
你想想看,如果只做最简单的单播+广播过滤,其实就两个比较器的事。但实际项目中,多播过滤才是大头——多播地址有28位可变位,用哈希表做过滤是常见做法。我曾经在一个交换芯片项目里,用CRC16对多播地址做哈希,然后查一个64位的哈希表,误判率控制在1%以内,够用了。
避坑指南:帧过滤一定要在接收状态机里做,别等到数据都存进FIFO了再扔。我曾经见过一个设计,把所有帧都存进FIFO,然后让软件过滤——千兆网口下,FIFO深度根本不够,丢帧率高达30%。
4.3 错误处理:CRC、Runt、Jabber
接收端必须处理三类典型错误:
- CRC错误:帧尾的CRC32校验不过。原因可能是线路噪声、对端发送错误、或者PHY芯片有问题。我习惯的做法是:CRC错误的帧直接丢弃,但可以统计一下错误计数,方便排查问题。
- Runt帧:小于64字节的帧。这种帧在以太网规范里是非法的,直接丢弃。注意:有些老设备会发小于64字节的帧,别被它们带沟里。
- Jabber帧:大于1518字节的帧(如果支持Jumbo帧,上限可以到9000字节)。超长帧可能是对端设备故障,也可能是恶意攻击。直接丢弃并告警。
这里有个容易忽略的点:CRC校验的时机。你是在收完最后一个字节后立刻算CRC,还是等整个帧收完再算?我建议用流水线方式——边收边算,收完最后一字节时CRC结果也出来了,不耽误事。代码里用一个CRC32硬件加速器,每个时钟周期算一个字节,延迟只有一两个周期。
注意:CRC校验不能只算数据部分,必须把目的MAC、源MAC、类型/长度、载荷、以及FCS字段本身都算进去。有些新手只算了载荷,结果CRC永远不对——因为标准CRC32覆盖了整个帧(前导码和SFD除外)。
4.4 全双工 vs 半双工:两种模式,两套逻辑
全双工和半双工,说白了就是能不能同时收发。全双工模式下,发送和接收是两条独立的通路,互不干扰。半双工模式下,收发共用一条线路,得靠CSMA/CD协议来避免冲突。
现在大多数设备都跑全双工了,但半双工在10M/100M的老设备上还有。我建议你在设计MAC时,把两种模式都支持上,用寄存器配置切换。毕竟你也不知道客户会把你的IP用在哪。
全双工模式下,接收状态机相对简单——只管收,不用管线路是否空闲。半双工模式下就麻烦了:
- 接收过程中,如果检测到冲突(COL信号拉高),必须立即停止接收,发一个32位的Jam信号通知所有节点。
- 冲突后要执行退避算法(截断二进制指数退避),等待随机时间后重试。
- 帧间隙(IFG)必须严格保证96位时间,否则其他节点可能抢不到线路。
我记得有一次调试一个半双工系统,发现丢包率特别高。查了半天,原来是IFG没做够——发送完一帧后,我等了80位时间就开始发下一帧,结果对端还在处理上一帧,直接冲突了。改成96位后,问题解决。这种细节,文档上写得很清楚,但实际做起来就是容易忘。
总结一下:全双工模式适合高性能场景,半双工模式适合兼容老设备。如果你的设计只跑千兆以上,半双工可以不用管——千兆以太网标准里已经废弃了半双工模式。但百兆以下,还是老老实实把CSMA/CD做全吧。
4.5 本章知识体系
下面这张图把MAC接收的核心逻辑串起来了。从线缆进来的数据,经过状态机解析、帧过滤、错误检测,最终存入接收FIFO。全双工和半双工的区别主要体现在冲突检测和退避算法上。
这张图把今天讲的内容串起来了。从GMII接口进来的数据,先经过接收状态机解析,然后帧过滤决定要不要收,接着错误检测把关,最后存进FIFO。全双工和半双工的区别,主要体现在模式选择后的两条分支上。嗯,这个结构我用了好多年,稳定可靠。
个人建议:如果你刚开始做MAC设计,先把全双工模式调通,再考虑半双工。全双工的逻辑简单,容易验证。半双工涉及冲突检测和退避算法,调试起来比较头疼——你没法在仿真里模拟真实的线路冲突,得上板实测。
好了,MAC接收这块就聊到这儿。下一讲我们聊聊MAC层的流量控制和链路聚合,这两个话题在实际项目中也很关键。
公众号:蓝海资料掘金营,微信deep3321