第四章:ARP协议解析——从报文到RTL实现

各位同学,今天我们来聊聊ARP协议解析。说实话,ARP这个协议看起来简单,但要在硬件里把它实现得既高效又可靠,里面门道还真不少。我在做第一个网络芯片项目时,就因为在ARP缓存表的老化机制上栽了跟头,导致整板网络不通,排查了整整三天……嗯,从那以后我对ARP就格外上心了。

4.1 ARP报文格式——你得看懂这28个字节

ARP报文,说白了就是28个字节的固定结构。你想想看,以太网帧头14字节加上ARP报文28字节,总共42字节,再补6个字节的填充,凑够64字节的最小帧长。这个细节我在项目里吃过亏——当时没注意填充,结果交换机直接把我的ARP请求给丢了。

来看ARP报文的详细格式:

字段 长度(字节) 说明
硬件类型 2 以太网为0x0001
协议类型 2 IPv4为0x0800
硬件地址长度 1 MAC地址为6
协议地址长度 1 IPv4地址为4
操作码 2 1=请求,2=响应
发送方MAC 6 源MAC地址
发送方IP 4 源IP地址
目标MAC 6 请求时为全0
目标IP 4 要查询的IP地址

我个人习惯在RTL设计里,把这28个字节按64位宽度对齐,这样一拍就能处理4个字段。为什么?因为AXI总线通常是64位或128位,对齐后处理逻辑会干净很多。

4.2 请求与响应流程——状态机里的两个关键路径

ARP的请求响应流程,说白了就是两个动作:

  1. 收到ARP请求:检查目标IP是否匹配本机。匹配则回复ARP响应,不匹配则丢弃。
  2. 收到ARP响应:提取发送方的MAC-IP映射,更新缓存表。

这里有个坑——我曾经以为ARP请求只发一次就够了。实际上,标准规定如果没收到响应,需要重试3次,每次间隔1秒。这个重试逻辑在RTL里实现时,你得小心状态机的设计,别让重试把缓存表给污染了。

下面是我常用的ARP解析状态机:

// ARP解析状态机(简化版)
typedef enum {
    IDLE,
    PARSE_HEADER,    // 解析硬件类型和协议类型
    CHECK_OPCODE,    // 检查操作码
    EXTRACT_SENDER,  // 提取发送方信息
    CHECK_TARGET,    // 检查目标IP
    GEN_REPLY,       // 生成响应
    UPDATE_CACHE,    // 更新缓存
    DROP_PACKET      // 丢弃报文
} arp_state_t;

你想想看,这个状态机里最关键的其实是CHECK_TARGET状态。如果目标IP匹配,就进入GEN_REPLY;如果不匹配,但操作码是响应,就进入UPDATE_CACHE。这个分支判断,我建议用组合逻辑做,别拖到时钟沿,否则会浪费一个周期。

重要提示:ARP响应报文里,发送方MAC和IP就是我们要缓存的信息。但注意——ARP请求里也有发送方信息,同样需要缓存。这叫"免费ARP"学习机制,很多新手会漏掉。

4.3 RTL缓存表设计——CAM还是RAM?这是个问题

ARP缓存表,说白了就是个MAC-IP映射表。但硬件实现时,有两种主流方案:

  • CAM(内容可寻址存储器):用IP地址查MAC,一拍出结果。速度快,但面积大、功耗高。
  • RAM + 哈希:对IP做哈希,存到RAM里。面积小,但可能有哈希冲突。

我个人习惯在小型设计里用CAM,因为逻辑简单,不容易出bug。但如果你要支持256条以上的缓存条目,那还是用哈希RAM吧,否则芯片面积会爆炸。

来看一个简单的CAM实现思路:

// 4条目CAM示例(Verilog风格)
// 每个条目包含:valid, ip_addr, mac_addr, age
reg [31:0] cam_ip   [0:3];
reg [47:0] cam_mac  [0:3];
reg        cam_valid[0:3];
reg [3:0]  cam_age  [0:3];  // 老化计数器

// 查找逻辑:并行比较所有条目
always @(*) begin
    hit = 1'b0;
    hit_idx = 2'b0;
    for (i = 0; i < 4; i = i + 1) begin
        if (cam_valid[i] && cam_ip[i] == target_ip) begin
            hit     = 1'b1;
            hit_idx = i;
        end
    end
end

嗯,这里要注意——CAM的并行比较会消耗大量组合逻辑。如果你用FPGA实现,LUT资源会吃得很厉害。我建议条目数控制在16以内,超过16就老老实实用哈希。

4.4 老化机制——别让缓存变成"死缓存"

ARP缓存是有生命周期的。标准建议超时时间为20分钟,但实际项目中,我一般设成5分钟。为什么?因为网络拓扑变化时,20分钟的老化时间太长了,设备断网后要等20分钟才能恢复通信,这谁能忍?

老化机制的核心是:每个缓存条目配一个计数器,每隔一定时间(比如1秒)减1。减到0时,标记为无效。如果收到新的ARP响应,重置计数器。

设计技巧:我习惯把老化计数器做成4位,最大计数值15。如果1秒减1,那么最大老化时间是15秒。这个时间对于局域网来说足够了。如果你需要更长的老化时间,可以扩展计数器位数,或者降低时钟频率。

但有个特殊情况——静态ARP条目。比如网关的MAC地址,你肯定不希望它被老化掉。所以缓存表里要加一个static标志位,静态条目不参与老化。

4.5 状态机整合——把上面所有东西串起来

好了,我们把ARP解析、缓存查找、老化机制整合到一个状态机里。这是我实际项目中用过的架构:

// 主状态机(带缓存管理)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state <= IDLE;
    end else begin
        case (state)
            IDLE: begin
                if (arp_pkt_valid) state <= PARSE_HDR;
                else if (age_tick)  state <= AGE_CHECK;  // 每秒触发一次老化
            end
            
            PARSE_HDR: begin
                // 解析硬件类型和协议类型
                if (hw_type == 16'h0001 && proto_type == 16'h0800)
                    state <= CHECK_OP;
                else
                    state <= DROP;
            end
            
            CHECK_OP: begin
                if (opcode == 16'h0001)      // ARP请求
                    state <= CHECK_TARGET;
                else if (opcode == 16'h0002) // ARP响应
                    state <= UPDATE_CACHE;
                else
                    state <= DROP;
            end
            
            // ... 后续状态省略
        endcase
    end
end

避坑指南:我曾经在AGE_CHECK状态里直接修改了CAM条目,结果导致正在进行的ARP查找得到了错误结果。正确的做法是:老化操作和查找操作要互斥,或者用双端口RAM。我的建议是——老化用单独的时钟域,或者用"写优先"的RAM。

4.6 知识体系总览

下面这张图是我手绘的ARP协议解析引擎架构,你可以看到各个模块之间的关系:

ARP协议解析引擎架构图 以太网帧输入 ARP报文解析 主状态机 ARP缓存表 老化定时器 ARP响应输出 图例: 输入 解析 状态机 缓存 老化 输出 虚线表示老化模块触发缓存更新,实线表示数据流

从这张图你可以看到,ARP解析引擎的核心就是三个模块的协同:报文解析、状态机控制、缓存管理。老化机制作为后台任务,定期扫描缓存表。我个人建议在实现时,把老化模块做成独立的有限状态机,这样主状态机不会被老化操作阻塞。

好了,ARP协议解析的内容就讲到这里。记住一句话:ARP看似简单,但RTL实现时处处是坑。多想想边界情况,比如广播MAC、全0IP、重复IP等,你的设计才能经得起考验。


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