第47章:以太网ARP测试实验
上一章我们搞定了 PHY 的配置通道,这一章让开发板真正接入网络:实现 ARP 协议——它是以太网通信的"第一步",没有它,后面所有的 IP 通信都无从谈起。
本章在讲什么?(先看这个)
本章要实现双向的 ARP 功能:
- 电脑发 ARP 请求 → 开发板识别后返回 ARP 应答(告诉电脑自己的 MAC 地址);
- 按下触摸按键 → 开发板主动发 ARP 请求,电脑返回应答。
要实现它,你必须先弄懂四块知识:以太网帧格式、ARP 协议格式、RGMII 接口、CRC32 校验。本章一个一个拆。
核心概念拆解
47.1 ARP 协议:IP 地址换 MAC 地址的"查号台"
以太网通信以"帧"为单位,帧头里必须写目的主机的 MAC 地址。问题来了:我们只知道对方 IP(比如 192.168.1.102),怎么知道它的 MAC?这就是 ARP(Address Resolution Protocol,地址解析协议) 的任务。
工作流程非常像"宿舍楼喊话":
- ARP 请求(广播):主机 A 在局域网里大喊:"谁是 192.168.1.10?请告诉 192.168.1.102!"——目的 MAC 填广播地址
ff_ff_ff_ff_ff_ff,所有主机都会收到; - ARP 应答(单播):只有 IP 对得上的主机 B 回应:"192.168.1.10 的 MAC 是 00-11-22-33-44-55",并直接发给 A;
- ARP 缓存:A 把这对 IP↔MAC 记在小本本上,下次直接用(缓存一般 20 分钟过期)。
47.2 以太网 MAC 帧格式:所有协议的"集装箱"

| 字段 | 长度 | 说明 |
|---|---|---|
| 前导码 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 位。

关键信号一览:
| 信号 | 方向 | 说明 |
|---|---|---|
| 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 程序设计:四大模块协作
总体架构

数据通路是:
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)就说明双向链路都通了。
初学者容易踩的坑
- 忘记 Padding:ARP 只有 28 字节,不发够 46 字节数据段,电脑网卡会直接丢弃。
- CRC 字节序:发送时忘记"取反+高低位互换",数据包 CRC 校验必错,表现为电脑完全收不到包。
- RGMII 时钟域混淆:接收逻辑必须用
gmii_rx_clk(由 RXC 经 BUFG 而来),发送用gmii_tx_clk,两个时钟域的信号不能混用。 - IDELAY 忘加参考时钟:IDELAYE2 必须搭配 IDELAYCTRL 和 200MHz 参考时钟(PLL 输出),漏掉 200MHz 时钟综合会报错或时序异常。
- 出错状态跳转:接收状态机出错时直接回 idle,会把同一包的后续字节误认为新的前导码,出现"幽灵包"。
- 广播地址写错:ARP 请求的目的 MAC 必须是 ff_ff_ff_ff_ff_ff,写成了对方 MAC 反而会让请求永远无人应答。
小结 & 下一步
本章信息量不小,复盘一下收获:
- 协议层:以太网帧格式、ARP 请求/应答流程、最小帧长与填充规则;
- 接口层:RGMII 的 DDR 采样原理,IDDR/ODDR/IDELAY/BUFG/BUFIO 原语的使用;
- 校验层:crc32_d8 查表法校验及发送时的字节序处理;
- 架构层:收发状态机的设计,以及"出错先等本包结束再复位"的工程思维。
ARP 解决了"找对方"的问题。下一章 ICMP 测试实验,我们让开发板回复 ping 命令——这是检验网络连通性最直接的方式。