FPGA直驱AR8031 PHY:RGMII接口实现精简MAC与ARP/ICMP 简介这份资源面向需要让FPGA直接驱动以太网PHY芯片的开发者提供了一套可参考的Verilog实现代码。示例基于Altera EP3C40系列FPGA与AR8031 PHY芯片核心思路是通过寄存器配置完成链路建立并可根据代码中的寄存器定义迁移适配到其他PHY型号适合具备一定FPGA与以太网基础的工程师参考。压缩包共约2000个文件整体19.88MB以cdb、hdb等Quartus工程数据库文件为主同时包含315个v与6个sv源文件、21个vhd、81个tdf以及qsf、qpf等工程配置文件和若干txt说明完整保留了工程编译与综合所需的目录结构。目前已有1895人学习下载。读者可从中获取PHY初始化、MDIO管理接口、收发时序控制等关键模块的实现思路并借助工程文件快速复现与二次开发减少从零调试以太网链路的时间成本。1. 一块没有 MAC 的 PHY怎么让 FPGA 把包发出去手头有一块以太网接口板上面焊着一颗 AR8031 PHY旁边就是 FPGA但中间没有独立 MAC 芯片。很多人第一反应是去找现成的 MAC IP结果发现要么授权费劝退要么接口对不上。其实 AR8031 这类 PHY 支持 RGMII 直连模式FPGA 完全可以用普通 IO 模拟 RGMII 时序自己实现一个精简的 MAC 收发逻辑。这份参考代码解决的就是这个场景不依赖硬核 MAC用 FPGA 直接驱动 PHY 完成链路建立、ARP 请求和 ICMP 回显。适合已经能跑通 FPGA 基本工程、想打通以太网物理层到数据链路层的硬件工程师和逻辑设计者。代码结构不复杂但 RGMII 的时序约束和 PHY 寄存器配置是绕不开的两道坎下面按实际调试顺序拆开讲。2. RGMII 接口时序与 PHY 上电初始化从复位到链路 UP2.1 为什么选 RGMII 而不是 MIIAR8031 支持 MII、RMII、RGMII 三种 MAC 侧接口。MII 需要 4 位数据线加独立收发时钟引脚多但时序宽松RMII 把数据线压到 2 位时钟提到 50MHz引脚少但对 FPGA 的 IO 时序要求更高RGMII 用 4 位数据线在 125MHz 时钟的上下沿都采样等效 8 位数据宽度引脚数和带宽平衡得最好。常见做法是选 RGMII因为大多数中低端 FPGA 的 IO 在 125MHz DDR 下还能收敛而 RMII 的 50MHz 单沿采样虽然频率低但建立保持窗口更窄反而容易翻车。RGMII 的关键点在于TX 侧由 FPGA 输出TXC时钟和数据TXD[3:0]、控制信号TX_CTLRX 侧由 PHY 输出RXC时钟和数据RXD[3:0]、控制信号RX_CTL。数据在时钟的上升沿传低 4 位下降沿传高 4 位。控制信号也复用TX_CTL 在上升沿表示 TXEN下降沿表示 TXERRRX_CTL 上升沿表示 RXDV下降沿表示 RXERR。这个复用关系如果搞反表现就是链路能 UP 但发不出包或者收到的全是错包。2.2 PHY 复位与寄存器配置流程AR8031 上电后需要先复位再通过 MDIO 写寄存器。MDIO 是两线接口MDC时钟和MDIO双向数据。FPGA 侧需要实现一个 MDIO 主机控制器按 IEEE 802.3 的帧格式读写 PHY 寄存器。帧格式是 32 位前导码加 2 位起始码、2 位操作码、5 位 PHY 地址、5 位寄存器地址、2 位转向和 16 位数据。下面是一个简化的 MDIO 写时序代码片段用状态机实现// MDIO 写寄存器状态机简化版 // 输入clk 为 25MHzphy_addr 为 5 位 PHY 地址reg_addr 为 5 位寄存器地址 // wr_data 为 16 位待写数据 localparam IDLE 3d0; localparam PRE 3d1; // 前导码 32 个 1 localparam ST 3d2; // 起始码 01 localparam OP 3d3; // 操作码 10 表示写 localparam ADDR 3d4; // PHY 地址 寄存器地址 localparam TA 3d5; // 转向位 10 localparam DATA 3d6; // 16 位数据 reg [4:0] bit_cnt; reg [31:0] shift_reg; always (posedge clk) begin case (state) IDLE: if (start) begin shift_reg 32hFFFFFFFF; // 前导码 bit_cnt 0; state PRE; end PRE: begin mdc ~mdc; if (mdc) begin mdio_out 1b1; bit_cnt bit_cnt 1; if (bit_cnt 31) begin bit_cnt 0; state ST; end end end ST: begin // 发送 01 mdc ~mdc; if (mdc) begin mdio_out (bit_cnt 0) ? 1b0 : 1b1; bit_cnt bit_cnt 1; if (bit_cnt 1) begin bit_cnt 0; state OP; end end end // ... 后续状态类似依次发送操作码、地址、转向和数据 endcase end这段代码的逻辑说明MDC 时钟由 FPGA 分频产生通常不超过 2.5MHzAR8031 手册里标称最高 25MHz但实际调试时降到 1MHz 左右更稳。前导码是 32 个连续的 1作用是让 PHY 的 MDIO 接口同步。起始码固定为01操作码10表示写、01表示读。PHY 地址由硬件引脚决定常见接法是上下拉电阻组合代码里phy_addr要跟板子实际连接一致。转向位在写操作时是10读操作时是Z0这里容易写错写错的表现是寄存器读回来全是 1 或全是 0。参数说明clk建议用 25MHz 晶振直接驱动不要用 PLL 倍频到很高再分频因为 MDC 的占空比和相位关系会影响建立保持。bit_cnt的位宽要够前导码 32 位需要 5 位计数器。shift_reg在写操作里其实可以省掉直接按位输出更直观这里保留是为了兼容读操作的移位采样。2.3 关键寄存器配置与链路建立AR8031 需要配置的寄存器不多但有几个必须写对。常见做法是上电后先读PHY ID寄存器确认通信正常然后写BMCR做软复位再写ANAR和ANLPAR配置自协商能力最后写AR8031 扩展寄存器把 RGMII 延时调好。寄存器地址名称配置值作用0x00BMCR0x9140自协商使能、重启自协商0x04ANAR0x01E1通告 100M 全双工、10M 全双工0x091000BASE-T 控制0x0300使能 1000M 全双工0x1DRGMII 延时控制0x0005TX 延时 1.5nsRX 延时 1.5nsRGMII 延时是调试中最容易踩的坑。AR8031 内部有可编程延时线如果 PCB 走线长度不匹配或者 FPGA 侧没有做 IO 延时约束采样点就会偏。常见做法是先把 PHY 侧延时设成默认值用示波器看 RXC 和 RXD 的相位关系再微调0x1D寄存器的值。如果手头没有示波器就写一个简单的收发测试FPGA 发固定包PHY 环回看收到的是不是原包。收不到就每次改一个延时档位直到通为止。链路 UP 的判断方法是读BMSR寄存器的 bit 2Link Status。这个位是自清零的读一次之后会清零下次读之前需要先读一次丢弃再读第二次才是真实状态。这个细节在 AR8031 手册里有写但很多人第一次调的时候会以为链路没起来其实是读法不对。3. 用 FPGA 逻辑实现精简 MAC收发状态机与 CRC 校验3.1 发送路径从字节流到 RGMII 时序发送路径的核心是把上层给的字节流加上前导码、SFD、目的地址、源地址、类型/长度、数据和 FCS然后按 RGMII 的 DDR 时序输出。前导码是 7 个0x55SFD 是0xD5这两个是固定格式。目的地址和源地址各 6 字节类型/长度 2 字节数据长度可变FCS 是 4 字节 CRC32。下面是一个发送状态机的核心片段// 发送状态机按字节输出每个字节拆成高低 4 位在 DDR 输出 // tx_data 为 8 位待发字节tx_valid 为有效指示 localparam TX_IDLE 2d0; localparam TX_PRE 2d1; // 前导码 SFD localparam TX_DATA 2d2; // 数据 FCS reg [3:0] nibble_cnt; reg [7:0] tx_byte; reg [1:0] state; always (posedge clk_125m) begin case (state) TX_IDLE: if (tx_valid) begin tx_byte 8h55; // 前导码 nibble_cnt 0; state TX_PRE; end TX_PRE: begin // 每个字节分两个时钟沿输出 if (nibble_cnt 0) begin txd tx_byte[3:0]; tx_ctl 1b1; nibble_cnt 1; end else begin txd tx_byte[7:4]; tx_ctl 1b1; nibble_cnt 0; // 前导码发完 7 个后发 SFD if (pre_cnt 6) begin tx_byte 8hD5; pre_cnt 0; state TX_DATA; end else begin tx_byte 8h55; pre_cnt pre_cnt 1; end end end TX_DATA: begin // 从 FIFO 读数据计算 CRC发完数据后发 FCS // ... end endcase end逻辑说明clk_125m是 RGMII 的参考时钟125MHz。每个字节在时钟的上升沿输出低 4 位下降沿输出高 4 位所以一个字节占一个时钟周期。tx_ctl在发送期间保持高表示 TXEN 有效。前导码发 7 个0x55后发0xD5然后进入数据状态。数据状态里需要同时做两件事把数据字节输出以及把字节送入 CRC32 计算模块。CRC 算完后取反低字节先发。参数说明tx_byte的位序是低 4 位先发对应 RGMII 的 DDR 上升沿。nibble_cnt用来标记当前是低 4 位还是高 4 位。pre_cnt计数前导码个数到 6 的时候切换到 SFD。这里没有写完整的 FIFO 接口和 CRC 模块实际代码里需要补上但状态机的骨架就是这样。3.2 接收路径从 RGMII 采样到 CRC 校验接收路径比发送麻烦一点因为 RXC 是 PHY 输出的FPGA 需要用这个时钟去采样 RXD。常见做法是用 IDDR 原语把 DDR 数据拆成两个单沿数据或者用IDELAY加ISERDES做更精确的采样。如果 FPGA 不支持 IDDR也可以用逻辑在 RXC 的上下沿分别寄存但要注意时序约束。接收状态机的流程是检测到 RX_CTL 上升沿后开始找 SFD找到 SFD 后开始收数据同时算 CRC收完后比较 CRC 是否正确。如果正确把数据写入 FIFO如果错误丢弃并置错误标志。// 接收状态机用 IDDR 拆 DDR 数据 // RXC 为 PHY 输出时钟RXD 为 4 位数据RX_CTL 为控制 genvar i; generate for (i 0; i 4; i i 1) begin : iddr_rxd IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED), .INIT_Q1(1b0), .INIT_Q2(1b0), .SRTYPE(SYNC) ) iddr_inst ( .Q1(rxd_low[i]), // 上升沿数据 .Q2(rxd_high[i]), // 下降沿数据 .C(RXC), .CE(1b1), .D(RXD[i]), .R(1b0), .S(1b0) ); end endgenerate // 拼接成完整字节 wire [7:0] rx_byte {rxd_high, rxd_low}; // 接收状态机 localparam RX_IDLE 2d0; localparam RX_SFD 2d1; localparam RX_DATA 2d2; always (posedge RXC) begin case (rx_state) RX_IDLE: if (rx_ctl_rise) begin rx_state RX_SFD; byte_cnt 0; end RX_SFD: begin if (rx_byte 8hD5) begin rx_state RX_DATA; crc_init 1b1; end end RX_DATA: begin // 写入 FIFO同时更新 CRC if (rx_ctl_fall) begin // 收完检查 CRC if (crc_ok) begin fifo_wr 1b1; end else begin crc_err 1b1; end rx_state RX_IDLE; end end endcase end逻辑说明IDDR原语把 RXD 的 DDR 数据拆成rxd_low和rxd_high两个单沿信号分别对应上升沿和下降沿的数据。拼接时注意位序rxd_high是高 4 位rxd_low是低 4 位。rx_ctl_rise和rx_ctl_fall是 RX_CTL 的边沿检测信号用来判断帧的开始和结束。CRC 校验模块在crc_init有效时复位然后每个字节更新一次收完后比较结果。参数说明DDR_CLK_EDGE选SAME_EDGE_PIPELINED是为了让 Q1 和 Q2 在同一个时钟沿输出方便后续拼接。如果选OPPOSITE_EDGEQ1 和 Q2 会差半个周期拼接时需要额外对齐。crc_init在 SFD 之后拉高一个周期复位 CRC 寄存器。crc_ok是比较结果通常 CRC32 的多项式是0x04C11DB7初值全 1结果取反。3.3 CRC32 校验的两种实现方式CRC32 可以用查表法或移位法。查表法速度快但需要 256 个 32 位表项占用 Block RAM移位法省资源但每个字节需要 8 个时钟周期。在 125MHz 的 RGMII 下每个字节只有 8ns 的处理时间移位法如果跑不到 125MHz 就会成为瓶颈。常见做法是用查表法把表存在分布式 RAM 里或者用 4 位查表加两级流水。下面是一个简化的查表法 CRC32 代码// CRC32 查表法每个时钟处理一个字节 // crc_reg 为当前 CRC 值byte_in 为输入字节 reg [31:0] crc_reg; wire [7:0] index crc_reg[7:0] ^ byte_in; wire [31:0] crc_next {8h00, crc_reg[31:8]} ^ crc_table[index]; always (posedge clk) begin if (crc_init) begin crc_reg 32hFFFFFFFF; end else if (crc_en) begin crc_reg crc_next; end end // crc_table 用 case 语句或 ROM 实现这里省略具体表项逻辑说明index是当前 CRC 低 8 位与输入字节的异或结果用来查表。crc_next是移位后的 CRC 与查表结果的异或。crc_init有效时初值设为全 1crc_en有效时每个时钟更新一次。发送时数据字节依次送入最后取反输出接收时包括 FCS 在内的所有字节都送入最后比较结果是否为0xDEBB20E3取反后的固定值。参数说明crc_table的生成多项式是0x04C11DB7可以用 Python 脚本生成也可以在网上找现成的表。如果资源紧张可以用 4 位查表每个字节分两次处理但控制逻辑会复杂一些。注意发送和接收的 CRC 初值和取反方式要一致否则会出现发出去的包自己能收但对方收不到的情况。4. 上板调试与避坑链路不通、CRC 错包、时序不收敛4.1 链路 UP 但 ping 不通现象MDIO 读BMSR显示 Link Status 为 1但 PC 端 ping 不通。原因通常是 RGMII 延时不对或者 TX_CTL 的复用关系搞反。先检查0x1D寄存器的延时值AR8031 默认是 0需要根据 PCB 走线长度调整。如果 TX 延时不够PHY 采样不到正确的数据如果 RX 延时不够FPGA 采样不到正确的数据。解决方法是写一个简单的回环测试FPGA 发固定包PHY 内部环回看收到的是不是原包。如果不通每次改一个延时档位直到通为止。4.2 CRC 错包率居高不下现象能收到包但 CRC 错误计数器一直在涨。原因可能是 RXC 的采样点偏了或者 IDDR 的DDR_CLK_EDGE选错。常见做法是用IDELAY对 RXC 做微调步进 78ps 左右找到眼图中心。如果 FPGA 不支持IDELAY可以在 RXC 上加一级反相器相当于移半个周期有时候能碰巧采到。另一个原因是 CRC 模块的初值或取反方式不对检查发送和接收的 CRC 配置是否一致。4.3 时序报告显示 RGMII 路径不收敛现象综合实现后时序报告里 RGMII 的建立保持时间为负。原因是 125MHz DDR 的时序余量本来就小如果 IO 约束没写对工具会按默认值布线。解决方法是手动写set_input_delay和set_output_delay参考 PHY 手册里的建立保持时间。AR8031 的 RGMII 时序要求是TXC 上升沿前数据有效窗口约 1nsRXC 上升沿后数据有效窗口约 1ns。约束里要把这些值写进去工具才会按这个目标去布线和优化。4.4 MDIO 读回来全是 0xFFFF现象读PHY ID寄存器返回0xFFFF说明 MDIO 通信没建立。原因可能是 MDC 频率太高或者 PHY 地址不对。先把 MDC 降到 1MHz 以下再确认phy_addr跟板子上的上下拉电阻一致。AR8031 的 PHY 地址由PHYAD[4:0]引脚决定常见接法是全部下拉为 0或者按位组合。如果地址不对MDIO 帧会被 PHY 忽略读回来就是全 1。4.5 发送正常但接收不到任何包现象PC 端能收到 FPGA 发的 ARP 请求但 FPGA 收不到 PC 的 ARP 回复。原因可能是 RX_CTL 的边沿检测逻辑有问题或者 FIFO 的写使能没接对。先检查rx_ctl_rise和rx_ctl_fall是否在正确的时钟域下生成如果 RXC 和系统时钟不同源需要做跨时钟域处理。另一个常见原因是 FIFO 的读侧没有及时读走数据导致溢出。可以在 FIFO 上加一个溢出标志调试时看这个标志有没有拉高。5. 进阶把 ARP 和 ICMP 跑通验证端到端链路5.1 ARP 请求与回复的极简实现ARP 包的结构是以太网头 14 字节ARP 头 28 字节。ARP 头里包括硬件类型、协议类型、硬件地址长度、协议地址长度、操作码、发送方 MAC、发送方 IP、目标 MAC、目标 IP。FPGA 收到 ARP 请求后如果目标 IP 是自己的就构造一个 ARP 回复把发送方 MAC 和 IP 填进去目标 MAC 和 IP 填成请求方的。// ARP 回复构造收到请求后交换发送方和目标方的 MAC/IP // 输入req_src_mac, req_src_ip, req_dst_ip // 输出reply_dst_mac req_src_mac, reply_src_mac 本机 MAC // reply_src_ip req_dst_ip, reply_dst_ip req_src_ip always (posedge clk) begin if (arp_req_valid req_dst_ip local_ip) begin // 构造回复包 eth_dst req_src_mac; eth_src local_mac; eth_type 16h0806; arp_op 16h0002; // 回复 arp_src_mac local_mac; arp_src_ip local_ip; arp_dst_mac req_src_mac; arp_dst_ip req_src_ip; arp_reply_valid 1b1; end end逻辑说明arp_req_valid是接收路径解析出的 ARP 请求有效信号req_dst_ip是请求里的目标 IP。如果目标 IP 等于本机 IP就构造回复。回复包的以太网目的地址是请求方的源 MAC源地址是本机 MAC类型是0x0806。ARP 操作码0x0002表示回复。发送方 MAC/IP 填本机目标 MAC/IP 填请求方的源 MAC/IP。参数说明local_mac和local_ip是本地配置的 MAC 和 IP通常用参数或寄存器配置。arp_reply_valid拉高一个周期后发送状态机开始读这些字段并组包。注意 ARP 包的长度是 42 字节不含 FCS发送时要在数据字段后补 18 字节的填充因为以太网最小帧长是 60 字节不含 FCS。5.2 ICMP 回显请求与回复ICMP 回显请求就是 ping 包。收到 ping 包后把类型从0x08改成0x00重新算校验和然后发回去。ICMP 校验和是 16 位反码求和计算方法是把 ICMP 头和数据按 16 位分组逐组相加溢出的位回卷最后取反。// ICMP 校验和计算16 位反码求和 // data 为 16 位输入valid 为有效指示 reg [31:0] sum; always (posedge clk) begin if (icmp_init) begin sum 32h0; end else if (icmp_valid) begin sum sum {16h0, data}; // 溢出的位回卷 if (sum[31:16] ! 0) begin sum {16h0, sum[15:0]} sum[31:16]; end end end wire [15:0] checksum ~sum[15:0];逻辑说明sum是 32 位累加器每次加一个 16 位数据。如果高 16 位不为 0说明有溢出把高 16 位加到低 16 位上。最后取反得到校验和。发送时先把校验和字段填 0算完后再填回去。接收时包括校验和字段一起算结果应该是0xFFFF。参数说明icmp_init在 ICMP 头开始时拉高一个周期复位累加器。icmp_valid在每个 16 位数据有效时拉高。注意 ICMP 数据长度可能是奇数最后一个字节要补 0 再算。校验和的字节序是大端高字节在前。5.3 端到端验证方法与一个习惯验证顺序是先确认 MDIO 能读到PHY ID再确认链路 UP然后发 ARP 请求看 PC 有没有回复最后 ping 通。每一步都有对应的计数器或调试寄存器不要跳步。我一般会在代码里留一个debug_reg把链路状态、CRC 错误计数、收发包计数都映射进去用 MDIO 读出来看。这样不用接示波器也能定位大部分问题。从那以后我每次调以太网 PHY都强制先跑一遍 MDIO 读写测试确认寄存器能读能写再往下做。这个习惯帮我省了很多来回改代码的时间。希望帮到你。本文还有配套的精品资源点击获取