简介ZYNQ 7020实现以太网ARP地址解析FPGA驱动是面向嵌入式开发者与FPGA学习者的工程资源专注解决在Xilinx ZYNQ SoC上利用可编程逻辑加速网络协议栈中ARP地址解析的问题。资源共242个文件、压缩包约1.91MB包含Verilog/VHDL源码.v/.vhdl/.vh、Xilinx约束文件.xdc、综合布局布线后的设计检查点.dcp、IP核配置.xci以及仿真与运行脚本.sh/.tcl并附有eth_arp_test.bit位流文件可直接下载至7020器件验证ARP帧收发与缓存逻辑。目前已有172人学习适合中高级FPGA开发者参考硬件接口设计、中断处理、性能优化与测试验证等完整开发流程也可作为扩展IPv6 NDP或其它协议硬件加速的基础框架。1. ZYNQ 7020 的 ARP 地址解析为什么值得交给 FPGA 去做做 ZYNQ 7020 以太网最容易被轻视的就是 ARP。很多工程启动 Linux 后第一件事就是处理 ARP 广播CPU 为此要频繁跑中断和协议栈真遇到广播风暴PS 侧会被二层报文拖到丢帧。这份资源干的事是把 ARP 地址解析从 ARM 侧下沉到 PL 侧FPGA 直接收帧、识别 EtherType 0x0806、判断请求或响应、回包并维护缓存PS 侧只留一个 AXI-Lite 寄存器接口和对应的 C 驱动库。它适合三类场景工业网关上不想让 Linux 协议栈参与低频广播的最小系统、对二层响应时延敏感的板卡、以及想通过寄存器把网络控制权交给上层应用的方案。下面按我拆这套资源时的实际顺序来写从帧格式讲到驱动调用再把最容易翻车的几个细节单独拉出来。2. ARP 帧与寄存器边界字节序、填充和一条 AXI-Lite2.1 ARP 协议原理先看清请求帧里哪 28 字节决定结果先不谈代码把 ARP 帧按字节拆开。一个完整的 ARP 请求在以太网帧里是 14 字节以太网头加 28 字节 ARP 报文字段如下字段长度ARP 请求值ARP 响应值目的 MAC6FF:FF:FF:FF:FF:FF广播请求方 MAC单播源 MAC6请求方 MAC被解析方 MACEtherType20x08060x0806硬件类型20x00010x0001协议类型20x08000x0800硬件地址长度10x060x06协议地址长度10x040x04操作码20x00010x0002发送方 MAC6请求方 MAC被解析方 MAC发送方 IP4请求方 IP被解析方 IP目标 MAC6全 0请求方 MAC目标 IP4要解析的 IP请求方 IP在 FPGA 里做解析逻辑上就两个动作收到请求且目标 IP 等于本机 IP就构造响应回出去收到响应且目标 IP 等于本机 IP就把发送方 MAC 写进缓存。这个判断的核心是操作码字段所以 RTL 里一看到 0x0806 就要继续往下读 8 字节确认 opcode 是 1 还是 2再决定是回包还是缓存。字节序是这里最容易错的地方。MAC 和 IP 在帧里都是大端也就是网络字节序。FPGA 侧按字节流处理时无所谓但一旦进到 32 位寄存器PS 驱动就得用htonl之类的方式把 IP 转成帧内顺序。比如 192.168.1.10 在帧里依次是 C0 A8 01 0A在 32 位寄存器里读出来应该是 0xC0A8010A而不是 0x0A01A8C0。这一步错后面的缓存表全是反的。2.2 PL 侧寄存器表FPGA 驱动库与 RTL 的唯一契约这套资源里 PL 侧和 PS 侧能对上靠的是一张固定寄存器表。AXI-Lite 本身只保证 4 字节对齐读写真正的内容完全由 RTL 决定。常见的布局如下偏移名称方向说明0x00REG_CTRLRWbit0 STARTbit1 FLUSHbit4 ANS_EN0x04REG_STATUSRObit0 BUSYbit1 DONEbit2 TIMEOUTbit3 CACHE_HIT0x08REG_MAC_LORW本机 MAC 低 32 位0x0CREG_MAC_HIRW本机 MAC 高 16 位放在低半字0x10REG_LOCAL_IPRW本机 IP网络字节序0x14REG_TARGET_IPRW要解析的目标 IP0x18REG_RESP_MAC_LORO解析结果低 32 位0x1CREG_RESP_MAC_HIRO解析结果高 16 位控制寄存器必须和状态机联动。驱动写REG_CTRL.STARTFPGA 先看缓存里有没有目标 IP 的条目有就直接把结果放到REG_RESP_MAC并置 DONE没有则启动发送 ARP 请求等对方响应后写缓存、置 DONE。如果超时没等到就置 TIMEOUT。这套设计的好处是软件不用关心二层收发的细节只用一个寄存器偏移表就能完成「请求 IP → 得到 MAC」的完整闭环。2.3 为什么不用 PS GEM 协议栈而要 FPGA 驱动库ZYNQ 7020 的 PS 侧自带 GEM 以太网控制器很多人第一反应是用它配 Linux。GEM 只是一个 MAC 引擎收到 ARP 报文后照样要把数据交给 CPU走中断、分配 skb、查路由、更新邻居表。低速场景没问题但一旦链路广播报文密集CPU 就被迫高频响应协议栈层面的处理延迟也跟着放大。FPGA 做的 ARP 卸载思路不同PHY 过来的 RGMII 数据直接进 PL 逻辑ARP 请求由状态机判断并回包整个过程不经过 ARM。响应时延只取决于线速和状态机周期这是车载以太网和 TSN 方案里常说的硬件卸载思路。选型边界也要说清楚如果只是点到点、低频广播GEM 加 Linux 协议栈完全够如果要做端口隔离、二层线速响应或者不想让协议栈参与噪声广播才值得把 ARP 下沉到 PL 侧。3. 移植 FPGA ARP 模块从 RGMII 端口到发送状态机3.1 顶层接口125 MHz 时钟、RGMII 四线和 AXI-Lite 对齐先看数据面。PHY 向 FPGA 提供 125 MHz 时钟RGMII 在时钟上下沿各采 4 位数据等效千兆速率。顶层端口一般是这样一组端口方向说明clk125输入PHY 提供的 125 MHz 参考时钟rgmii_rx_ctl、rgmii_rx_data[3:0]输入接收控制与数据rgmii_tx_ctl、rgmii_tx_data[3:0]输出发送控制与数据s_axi_aclk、s_axi_aw/ar/w/r/b 相关输入PS 侧 AXI-Lite 从接口RGMII 是 DDR 信号RTL 里一般用 IDDR/ODDR 原语把上下沿数据合并成 8 位 GMII 域再进状态机。这里最容易被忽略的是时钟域AXI-Lite 来自 PS频率可能是 100 MHz 或 150 MHz和 PL 的 125 MHz 不是同一个域。寄存器里的 START、FLUSH 这类控制位进 125 MHz 域之前至少要打两拍同步否则状态机偶尔会漏触发或抖动。资源里如果已经做了同步建议不要为了省资源删掉那两级寄存器。3.2 接收解析状态机识别帧类型走到操作码判断发送路径先不谈接收路径是整个模块的心脏。状态机不复杂但跳转条件必须严格。下面是一段参考骨架状态名和实际工程可以直接对应localparam S_IDLE 4d0; localparam S_ETH_HDR 4d1; localparam S_ARP_HDR 4d2; localparam S_OPCODE 4d3; localparam S_LEARN 4d4; localparam S_TX_REPLY 4d5; localparam S_DONE 4d6; always (posedge clk125 or negedge rst_n) begin if (!rst_n) begin state S_IDLE; rx_byte_cnt 16d0; end else begin case (state) S_IDLE: begin rx_byte_cnt 16d0; if (rx_valid) state S_ETH_HDR; end S_ETH_HDR: begin // 以太网头共14字节第13字节是EtherType高字节 if (rx_valid rx_byte_cnt 12 rx_data 8h08) state S_ARP_HDR; else if (rx_valid rx_byte_cnt 12) state S_IDLE; else if (rx_valid) rx_byte_cnt rx_byte_cnt 1; end S_ARP_HDR: begin // 第14字节必须为0x06走到第21字节判操作码 if (rx_valid rx_byte_cnt 13 rx_data ! 8h06) state S_IDLE; else if (rx_valid rx_byte_cnt 21) state S_OPCODE; else if (rx_valid) rx_byte_cnt rx_byte_cnt 1; end default: state S_IDLE; endcase end end这段代码的逻辑是以太网头 14 字节里第 13 字节是 EtherType 高字节 0x08第 14 字节是低字节 0x06ARP 报文从第 15 字节开始硬件类型高字节 0x00、低字节 0x01然后一路数到第 21 字节相对帧头才是操作码。rx_byte_cnt是相对帧起始的字节计数这个计数必须从帧首字节开始复位否则任何错位都会让状态机把 IP 字段当 MAC 读。实战中我把这段写成参数化模块EtherType 判断直接给16h0806一个常量比逐字节判断要少两个状态。3.3 发送回应填充 pad、CRC 与 60 字节下限一个 ARP 请求只有 42 字节14 字节以太网头加 28 字节 ARP但以太网最小帧长是 64 字节含 FCS。FPGA 构造 ARP 响应时如果直接把 42 字节发出去对端网卡大概率会当成 runt 帧丢弃。正确做法是补 pad 到 60 字节再追加 4 字节 CRC32凑满 64 字节。常见做法是在发送计数器里分三段0~41 字节按 ARP 响应内容发42~59 字节补 0x0060~63 字节发计算好的 CRC。CRC 可以直接调用 Vivado 里的 CRC 生成 IP也可以让 MAC 层配置为不自动补 FCS。资源里一般默认 RTL 自算 CRC因为这样不依赖 PHY 的 offload 能力。移植时要特别注意帧头目的 MAC 在响应里必须是请求方的单播 MAC不能继续用广播地址否则交换机和网卡会直接丢弃。4. PS 侧驱动库一个接口把 ARP 解析变成查表4.1 驱动接口五个函数覆盖一条完整解析链路驱动库的粒度按寄存器表走核心函数就是初始化、设 IP、发起解析、清缓存。参考接口如下#define REG_CTRL 0x00 #define REG_STATUS 0x04 #define REG_MAC_LO 0x08 #define REG_MAC_HI 0x0C #define REG_LOCAL_IP 0x10 #define REG_TARGET_IP 0x14 #define REG_RESP_MAC_LO 0x18 #define REG_RESP_MAC_HI 0x1C #define CTRL_START (1u 0) #define CTRL_FLUSH (1u 1) #define STATUS_BUSY (1u 0) #define STATUS_DONE (1u 1) #define STATUS_TIMEOUT (1u 2) void arp_drv_init(uintptr_t base, uint8_t mac[6], uint32_t ip) { writel((mac[0] 24) | (mac[1] 16) | (mac[2] 8) | mac[3], base REG_MAC_LO); writel(mac[4] | (mac[5] 8), base REG_MAC_HI); writel(ip, base REG_LOCAL_IP); } int arp_drv_resolve(uintptr_t base, uint32_t target_ip, uint8_t mac[6]) { int i; uint32_t status; uint32_t lo, hi; writel(target_ip, base REG_TARGET_IP); writel(CTRL_START, base REG_CTRL); for (i 0; i 100; i) { status readl(base REG_STATUS); if (status STATUS_DONE) break; if (status STATUS_TIMEOUT) return -ETIMEDOUT; usleep(100); } if (status STATUS_DONE) { lo readl(base REG_RESP_MAC_LO); hi readl(base REG_RESP_MAC_HI); mac[0] (lo 24) 0xff; mac[1] (lo 16) 0xff; mac[2] (lo 8) 0xff; mac[3] lo 0xff; mac[4] hi 0xff; mac[5] (hi 8) 0xff; return 0; } return -EIO; }关键点是usleep(100)轮询 100 次整体超时 10 ms。ARP 在一个正常交换网络里的响应周期远小于这个值所以轮询比中断更省代码。如果驱动里用中断可以等寄存器 DONE 拉升后触发wake_up但对大多数应用没有必要。读 MAC 结果时按最高字节到最低字节拆回mac[0]到mac[5]对应帧内顺序这样上层拿到的是可直接 printf 的标准 MAC 数组。4.2 用户态访问mmap /dev/mem 与 UIO 的选择Linux 下访问这段寄存器有两种常见路径。一种是应用层直接/dev/mem加 mmap好处是调试快坏处是要 root 权限且地址必须页对齐另一种是写一个很小的 UIO 驱动把 FPGA 寄存器映射成字符设备还能绑定 DONE 中断。资源里的驱动库在裸机下可以直接当普通 C 函数用在 Linux 下建议包一层 mmapstatic uintptr_t arp_dev_map; int arp_dev_init(uintptr_t phys, size_t len) { int fd open(/dev/mem, O_RDWR | O_SYNC); if (fd 0) return -1; arp_dev_map (uintptr_t)mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, phys ~0xFFF); close(fd); return arp_dev_map ? 0 : -1; }注意mmap传入的物理地址必须按 4 KB 页对齐基址的低 12 位要加到映射结果的地址上这是很多人第一次跑挂的地方。初始化时一次映射完成后续所有寄存器访问都基于arp_dev_map offset不要再反复 open/dev/mem。4.3 超时重传与缓存老化策略ARP 请求发出去后对方不一定立即在比如目标设备刚上电、还在协商 PHY。驱动里不能只等一次 10 ms 就返回失败常见做法是重传 2 到 3 次间隔从 100 ms 开始递增。第一次等 100 ms第二次 200 ms总超时控制在几百毫秒量级。PL 侧缓存如果有条目resolve会直接命中并返回 CACHE_HIT 状态不会产生网络报文但如果链路拔插过旧 MAC 可能已经失效驱动必须在 link down 或网络事件通知时主动触发 FLUSH 寄存器把缓存清空。另一个办法是给缓存加老化RTL 里维护一个时间戳超时后自动清条目不过这会增加 PL 逻辑面积简单场景用驱动定时清理更划算。5. 避坑ZYNQ 7020 上 5 个反复翻车的 ARP/以太网细节5.1 现象tcpdump 看到 who-hasFPGA 就是不回这是最典型的上板问题。PC 上抓包能看到 ARP 请求发出去了目标 IP 也确实是 FPGA 绑定的 IP但没有任何 reply。原因基本出在 RGMII 收发采样上。RGMII 是 DDR 信号FPGA 侧必须保证采样点落在数据稳定区很多板卡的 PHY 默认不带 internal delayPCB 走线又不够长导致时钟沿正好落在数据跳变沿上。解决方法是先确认 PHY 的 delay 配置Vivado 里 RGMII IP 通常有 internal delay 开关或者通过 MDIO 接口读写 PHY 的 delay 寄存器把 TX 和 RX 的 delay 都打开。我一般会先用小循环发包同时抓 PHY 的 link 状态确认 autoneg 结果是 1000 Mbps 而不是协商成了 100 Mbps因为不同速率下 RGMII 时序完全不同。5.2 现象仿真能回上板抓包却是坏帧仿真里很多人只发 42 字节的 ARP 包RTL 仿真环境不会报错误但真实网卡要求最小 64 字节。结果就是 PC 明明收到了 ARP reply长度却不对Wireshark 直接标成 runt frame 或其他错误。原因就是发送逻辑没有做 pad 填充。解决方法是强制发送计数器从 0 到 63 走完前 42 字节按 ARP 响应填中间补 0x00最后 4 字节发 CRC。如果不想自己处理 CRC可以在 PHY 配置里打开 CRC offload但要注意每个 PHY 的寄存器不一样。资源驱动库里如果已经带 pad 逻辑检查它是否把tx_byte_cnt 60设为 FIFO 写结束信号而不是tx_byte_cnt 42。5.3 现象驱动读出来的 MAC 是 00:11:22:ff:ff:ff后 16 位全是 F缓存读出来前半段正确后半段全是 0xFF或者全 0。这说明 RTL 侧的响应写入逻辑和 PS 侧的 MAC 位序没对齐。常见原因是 RTL 把 48 位 MAC 存进了两个 32 位寄存器但低寄存器放的是「低地址字节在前」PS 侧拆的时候又按字节顺序读于是拼出了错位数据。解决方法是统一按帧内字节顺序来mac[0]是目的 MAC 第一个字节在寄存器里放最高 8 位。驱动拆回 MAC 的代码要和 RTL 写入顺序严格一致最好在头文件里写清楚注释否则换人维护后大概率会再翻一次。5.4 现象ARP 能通但无法跨网段通信板卡和 PC 不在同一网段时arp_drv_resolve直接拿目标 IP 发广播结果永远没人回。这是因为 ARP 只在同一广播域内有效跨网段时应该解析的是网关的 MAC而不是目标主机的 MAC。解决方式是在驱动上层做一次路由判断如果目标 IP 不在本机网段就先把下一跳通常就是网关的 IP 传给arp_drv_resolve等链路层帧发到网关再由网关做三层转发。这个判断在资源里不会替你完成因为驱动只管 ARP不管路由。不要试图在 FPGA 里实现三层转发ZYNQ 7020 的 PS 侧跑 Linux 才是该干这个活的地方。5.5 现象拔插网线后第一次解析慢甚至解析失败链路断开重插后FPGA 缓存和对方设备都可能有旧 ARP 条目。如果对方已经换了 IP 或 MAC旧缓存会持续命中导致上层拿到一个根本不通的目标 MAC。原因就是缓存没有老化也没有在链路事件时清空。解决方法是驱动在上电、PHY link down/up、网络重配这三类事件里调用 FLUSH 寄存器。更彻底的方案是在 RTL 里给每条缓存加一个最后更新时间戳超过 60 秒自动失效这样即使驱动漏调 FLUSH缓存也不会永久脏。我的习惯是驱动和 RTL 两层都做RTL 老化作为兜底驱动 FLUSH 作为主动补救。6. 验收一次完整的 ARP 交互抓包、驱动调用和缓存核对先把 PC 网卡固定成静态 IP比如 192.168.1.50FPGA 通过驱动配置本机 IP 为 192.168.1.99。PC 端开一个抓包窗口sudo tcpdump -i eth0 -nn -e arp然后在板卡上调用一次解析uint8_t mac[6]; int ret arp_drv_resolve(base, 0xC0A80132, mac); printf(ret%d mac%02x:%02x:%02x:%02x:%02x:%02x\n, ret, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);0xC0A80132 就是 192.168.1.50 的网络字节序。正常情况下 tcpdump 会抓到两条ARP, Request who-has 192.168.1.50 tell 192.168.1.99 ARP, Reply 192.168.1.50 is-at 00:1e:06:xx:xx:xx先核对 request 里的源 MAC 是否等于驱动写进REG_MAC的本机 MAC再核对 reply 里的 SPA 是否是 192.168.1.50。如果这两条都正确接着读REG_STATUSDONE 位置 1CACHE_HIT 位为 0。第二次调用同一个 IP 时CACHE_HIT 应变 1tcpdump 里不会再出现新的 request说明缓存逻辑生效。我一般会顺手验证一个反向场景把 PC 网线拔掉再插回然后立刻重新解析。如果第一次时间明显变长或返回旧 MAC说明缓存老化没有生效这是整个链路里最值得先修的隐患。从那以后每次动 PHY 配置或改 MIO 引脚我都会强制先把静态 ARP 抓包和缓存读回这一整套流程跑一遍确认二层没有暗坑再往上叠 TCP/UDP。希望这套检查思路也能帮到你。本文还有配套的精品资源点击获取