第47章:以太网ARP测试实验

上一章我们搞定了 PHY 的配置通道,这一章让开发板真正接入网络:实现 ARP 协议——它是以太网通信的"第一步",没有它,后面所有的 IP 通信都无从谈起。

本章在讲什么?(先看这个)

本章要实现双向的 ARP 功能:

  1. 电脑发 ARP 请求 → 开发板识别后返回 ARP 应答(告诉电脑自己的 MAC 地址);
  2. 按下触摸按键 → 开发板主动发 ARP 请求,电脑返回应答。

要实现它,你必须先弄懂四块知识:以太网帧格式、ARP 协议格式、RGMII 接口、CRC32 校验。本章一个一个拆。

核心概念拆解

47.1 ARP 协议:IP 地址换 MAC 地址的"查号台"

以太网通信以"帧"为单位,帧头里必须写目的主机的 MAC 地址。问题来了:我们只知道对方 IP(比如 192.168.1.102),怎么知道它的 MAC?这就是 ARP(Address Resolution Protocol,地址解析协议) 的任务。

工作流程非常像"宿舍楼喊话":

  1. ARP 请求(广播):主机 A 在局域网里大喊:"谁是 192.168.1.10?请告诉 192.168.1.102!"——目的 MAC 填广播地址 ff_ff_ff_ff_ff_ff,所有主机都会收到;
  2. ARP 应答(单播):只有 IP 对得上的主机 B 回应:"192.168.1.10 的 MAC 是 00-11-22-33-44-55",并直接发给 A;
  3. ARP 缓存:A 把这对 IP↔MAC 记在小本本上,下次直接用(缓存一般 20 分钟过期)。

47.2 以太网 MAC 帧格式:所有协议的"集装箱"

图47.1.5 以太网帧格式

字段 长度 说明
前导码 Preamble 7 字节 0x55 交替同步码
帧起始界定符 SFD 1 字节 固定 0xd5
目的 MAC 6 字节 单播/组播/广播地址
源 MAC 6 字节 发送端地址
长度/类型 2 字节 ≥1536 时表示上层协议:0x0800=IP,0x0806=ARP
数据 46~1500 字节 1500 即 MTU
FCS 4 字节 CRC32 校验

两个容易被忽略的规则:

  • 最小 46 字节:ARP 数据只有 28 字节,后面必须填充 18 字节(Padding,一般填 0);
  • 帧间隙 IFG:相邻两帧之间至少间隔 96 bit time,给设备留出恢复时间。

47.3 RGMII 接口:4 根线跑出千兆速率的秘密

FPGA 内部逻辑喜欢 8 位宽的 GMII 接口(单沿采样),但 PHY 芯片(YT8531)用的是 RGMII 接口——数据线只有 4 位,靠双沿采样(DDR)补回带宽:时钟仍为 125MHz,上升沿传低 4 位,下降沿传高 4 位。

图47.1.8 MAC侧与PHY侧接口连接

关键信号一览:

信号 方向 说明
ETH_RXC PHY→FPGA 接收时钟,1000M 时 125MHz
ETH_RXCTL PHY→FPGA 接收数据有效(含错误信息异或)
ETH_RXD[3:0] PHY→FPGA 接收数据
ETH_TXC FPGA→PHY 发送时钟
ETH_TXCTL FPGA→PHY 发送数据有效
ETH_TXD[3:0] FPGA→PHY 发送数据

时序上有个硬件细节:开发板通过上下拉电阻配置了 RXC 相对 RXD 延时约 2ns 的模式,让数据落在时钟边沿的"中间",采样窗口更充裕。这直接影响后面 IDELAY 的配置。

47.4 CRC32 校验:为什么发送端必须做

CRC32 是网络通信里最常用的校验(生成多项式 G(x)=x³²+x²⁶+…+1)。本实验只对发送做校验、不对接收做校验,原因是:

  • 接收的数据对不对,从解析结果就能大致判断;
  • 发送的数据如果 CRC 不对,会被电脑网卡直接丢包,ARP 就失败了。

实现上没有用逐 bit 移位的笨办法,而是用 crc32_d8:查表法一次算一个字节,一个时钟周期出结果,满足千兆网实时性(代价是多吃一些 LUTRAM)。

47.4 程序设计:四大模块协作

总体架构

图47.4.2 ARP测试实验系统框图

数据通路是:

ARP控制模块 → ARP模块(收/发/CRC) ↔ GMII_TO_RGMII模块 ↔ PHY芯片
                    ↑
              PLL提供200MHz给IDELAYCTRL

GMII_TO_RGMII:原语三件套

接收方向(rgmii_rx):4 位 RGMII 数据 → 8 位 GMII 数据。用了三个 FPGA 原语:

//输入延时(补偿走线偏差,须搭配IDELAYCTRL)
IDELAYE2 #(
    .IDELAY_TYPE     ("FIXED"),
    .IDELAY_VALUE    (IDELAY_VALUE),   //延时n个tap(每个约78ps)
    .REFCLK_FREQUENCY(200.0)
) u_delay_rxd (...);
//双沿采样→单沿双位
IDDR #(
    .DDR_CLK_EDGE("SAME_EDGE_PIPELINED")
) u_iddr_rxd (
    .Q1 (gmii_rxd[i]),     //上升沿采到低4位
    .Q2 (gmii_rxd[4+i]),   //下降沿采到高4位
    .C  (rgmii_rxc_bufio), //BUFIO专用IO时钟
    .D  (rgmii_rxd_delay[i])
);

代码用 generate for 循环例化 4 组 IDELAYE2+IDDR——需要重复例化时,这是标准省代码写法。时钟则分成两路:BUFG 全局时钟给内部逻辑,BUFIO 专供 IO 口采样。

发送方向(rgmii_tx):用 ODDR 原语做逆变换,8 位单沿 → 4 位双沿:

ODDR #(
    .DDR_CLK_EDGE("SAME_EDGE")
) ODDR_inst (
    .Q  (rgmii_txd[i]),
    .C  (gmii_tx_clk),
    .D1 (gmii_txd[i]),    //上升沿送低4位
    .D2 (gmii_txd[4+i])   //下降沿送高4位
);

ARP 接收模块:状态机逐字段解析

接收状态机的五个状态正好对应帧的各段:

st_idle → st_preamble(7个55+1个d5) → st_eth_head(14字节帧头)
        → st_arp_data(28字节ARP数据) → st_rx_end → 回idle

一个重要的设计决策:中间状态解析出错时跳到 st_rx_end 而不是 st_idle。因为这一包数据还没接收完,如果立刻回空闲态,剩下的有效字节会被误当成前导码解析。st_rx_end 会等 gmii_rx_dv=0(本包结束)才回 idle。

解析 ARP 数据的核心代码:

st_arp_data : begin
    if(gmii_rx_dv) begin
        rx_cnt <= rx_cnt + 5'd1;
        if(rx_cnt == 5'd6)
            op_code[15:8] <= gmii_rxd;                      //操作码
        else if((rx_cnt >= 5'd8) && (rx_cnt < 5'd14))
            src_mac_temp <= {src_mac_temp[39:0],gmii_rxd};  //源MAC地址
        else if((rx_cnt >= 5'd14) && (rx_cnt < 5'd18))
            src_ip_temp <= {src_ip_temp[23:0],gmii_rxd};    //源IP地址
        else if(rx_cnt == 5'd28) begin
            //判断目的IP是不是开发板的IP
            if(des_ip_temp == BOARD_IP) begin
                if((op_code == 16'd1) || (op_code == 16'd2)) begin
                    arp_rx_done <= 1'b1;
                    src_mac     <= src_mac_temp;
                    src_ip      <= src_ip_temp;
                    if(op_code == 16'd1)
                        arp_rx_type <= 1'b0;   //ARP请求
                    else
                        arp_rx_type <= 1'b1;   //ARP应答
                end
            end
        end
    end
end

三重过滤一目了然:IP 是不是我的?操作码是不是 1(请求)或 2(应答)?都通过才算收包成功。

ARP 发送模块:数组拼帧 + 补零 + 发 CRC

发送模块用三个数组把整帧内容备好:

reg [7:0] preamble[7:0]  ; //前导码+SFD
reg [7:0] eth_head[13:0] ; //以太网首部
reg [7:0] arp_data[27:0] ; //ARP数据

发送状态机依次把三段数组搬上 GMII 总线。两个细节:

细节一,Padding 补零。ARP 数据 28 字节不足 46 字节,发送时直接用三目运算符补齐:

st_arp_data : begin
    gmii_tx_en <= 1'b1;
    gmii_txd   <= (tx_cnt < 6'd28) ? arp_data[tx_cnt] : 8'd0; //28字节后补0
    crc_en     <= 1'b1;
    ...
end

细节二,CRC 发送顺序。CRC32 发送前要按位取反、高低位互换,这是以太网规范的要求:

st_crc : begin
    gmii_tx_en <= 1'b1;
    if(tx_cnt == 6'd0)
        gmii_txd <= {~crc_next[0],~crc_next[1],...}; //crc_next高字节取反发送
    ...
end

另外发送前会检查 des_mac/des_ip 是否已被更新——第一次发请求时目的 MAC 是广播地址,收到应答后缓存下对方 MAC,下次就能点对点发送了。

ARP 控制模块:两种触发来源

if(pos_touch_key == 1'b1) begin        //按下触摸按键→主动发ARP请求
    arp_tx_en   <= 1'b1;
    arp_tx_type <= 1'b0;
end
else if((arp_rx_done == 1'b1) && (arp_rx_type == 1'b0)) begin
    arp_tx_en   <= 1'b1;               //收到别人的请求→回应答
    arp_tx_type <= 1'b1;
end

仿真验证的巧思

TB 里把发送、接收环回起来,一个顶层就能同时测收和发:

assign gmii_rx_dv = gmii_tx_en;   //发送的数据直接喂给接收端
assign gmii_rxd   = gmii_txd;

激励流程:先发一个 ARP 请求包 → 等 tx_done → 延时 → 再发一个 ARP 应答包,一次仿真覆盖两种包型。

47.5 下载验证

  • 电脑 IP 设为 192.168.1.102,开发板 IP 固化为 192.168.1.10(参数 BOARD_IP);
  • 按下触摸按键,开发板发 ARP 请求;在电脑 cmd 里先 ping 192.168.1.10,让电脑主动发请求,开发板会自动应答;
  • arp -a 命令查看电脑的 ARP 缓存表,能看到 192.168.1.10 对应的 MAC 地址(00-11-22-33-44-55)就说明双向链路都通了。

初学者容易踩的坑

  1. 忘记 Padding:ARP 只有 28 字节,不发够 46 字节数据段,电脑网卡会直接丢弃。
  2. CRC 字节序:发送时忘记"取反+高低位互换",数据包 CRC 校验必错,表现为电脑完全收不到包。
  3. RGMII 时钟域混淆:接收逻辑必须用 gmii_rx_clk(由 RXC 经 BUFG 而来),发送用 gmii_tx_clk,两个时钟域的信号不能混用。
  4. IDELAY 忘加参考时钟:IDELAYE2 必须搭配 IDELAYCTRL 和 200MHz 参考时钟(PLL 输出),漏掉 200MHz 时钟综合会报错或时序异常。
  5. 出错状态跳转:接收状态机出错时直接回 idle,会把同一包的后续字节误认为新的前导码,出现"幽灵包"。
  6. 广播地址写错:ARP 请求的目的 MAC 必须是 ff_ff_ff_ff_ff_ff,写成了对方 MAC 反而会让请求永远无人应答。

小结 & 下一步

本章信息量不小,复盘一下收获:

  • 协议层:以太网帧格式、ARP 请求/应答流程、最小帧长与填充规则;
  • 接口层:RGMII 的 DDR 采样原理,IDDR/ODDR/IDELAY/BUFG/BUFIO 原语的使用;
  • 校验层:crc32_d8 查表法校验及发送时的字节序处理;
  • 架构层:收发状态机的设计,以及"出错先等本包结束再复位"的工程思维。

ARP 解决了"找对方"的问题。下一章 ICMP 测试实验,我们让开发板回复 ping 命令——这是检验网络连通性最直接的方式。