简介:面向需要以FPGA直接驱动以太网物理层收发器的开发者,这份参考代码以AR8031 PHY为例,展示了基于Altera EP3C40系列器件的完整实现方案,包含寄存器配置、收发接口与时序控制等关键内容,通过调整相应寄存器可适配其他常见PHY芯片,尤其适合中高级FPGA工程师用于板级调试和网络通信设计。压缩包共2000个文件,约19.88MB,以cdb/hdb工程数据库、Verilog/VHDL源码、qip/tcl工程脚本及sof配置文件为主,兼顾工程完整性与源码可读性,便于直接打开工程或按模块查阅。目前已有1894人学习下载。借助清晰的工程结构和PHY读写示例,读者既能快速掌握以太网PHY的初始化与寄存器操作流程,又能借鉴其适配思路,显著降低开发同类驱动时的重复投入。
1. 以太网PHY芯片的FPGA驱动:不靠CPU,直接硬啃AR8031寄存器
在做嵌入式网络设备时,大家习惯用MAC+PHY芯片的方案,CPU通过MDIO/MDC接口配置PHY的寄存器,但你会发现CPU介入的配置过程,在高速数据传输和低延迟场景里就是个瓶颈。这套以FPGA驱动以太网PHY芯片的参考代码,就是用来解决这个问题的——它让FPGA直接接管AR8031的全部初始化、状态管理和链路协商,不经过CPU干预。适合那些需要极致时序控制、或者希望把CPU从网络底层事务里解放出来的开发者。
这套代码的价值在于,它把PHY芯片的底层寄存器操作全部用Verilog实现,包括经典的Marvell寄存器映射、链路状态轮询、自动协商触发、以及速率和双工模式的读取与应用。你会看到FPGA如何像软件驱动那样,管理一个硬件外设的状态机——只不过这个状态机是用时钟和组合逻辑堆出来的。对于想在FPGA上做完整以太网数据通路的人来说,这部分驱动是绕不开的基础,也让后续的RGMII接口数据收发有了可靠的物理层保证。
2. 整体架构:从RGMII到MDIO,驱动代码的层级拆分
2.1 RGMII接口与PHY芯片的物理连接
先从物理连接讲起。AR8031支持多种MAC接口模式,在参考代码中选用的是RGMII——Reduced Gigabit Media Independent Interface,用12根线完成千兆数据的收发。这里一定要搞清楚的,是RGMII的时序要求:发送路径上,FPGA要在时钟的上升沿和下降沿各送出一半数据;接收路径上,PHY芯片同样会在时钟的双沿送回数据,这就要求FPGA内部必须处理好DDR逻辑。
代码里与此对应的硬件描述,通常包含几个关键信号:txd[3:0]作为发送数据线,tx_ctl作为发送控制信号,以及tx_clk作为125MHz的吉比特时钟。在参考代码中,这些信号被定义为inout或者output类型,由PHY芯片的时钟输出直接驱动FPGA的接收逻辑。
接收路径上有一个常见的血泪经验:AR8031的rx_clk并不总是和数据严格对齐,它有一个约2ns的相位偏移。如果你的FPGA工程不做约束,综合工具往往给出一个相对保守的布局,结果就是高速收数时偶发CRC错误。解决的办法通常是在约束文件里对这些时钟组加上set_input_delay约束,甚至用IDELAY原语做微调。
2.2 MDIO/MDC控制通道的状态机设计
MDIO是管理PHY芯片的串行接口,它只有两根线:MDC作为时钟,MDIO作为双向数据线。FPGA驱动PHY,本质上就是通过这个接口读写PHY内部的寄存器。参考代码的核心,是实现了MDIO主控制器状态机,它有IDLE、START、OPCODE、ADDRESS、TURNAROUND、DATA等状态。
以读寄存器为例,状态机的推进过程是:IDLE状态下检测到读请求,进入START状态发送32位前导码,然后是OPCODE(读操作是10),紧接着是5位PHY地址和5位寄存器地址,再经过一个时钟周期的转向时间,FPGA释放MDIO总线控制权,PHY芯片开始驱动数据线。
这是个时序严格的过程,参考代码中状态机的跳转条件通常是基于一个分频时钟。比较稳妥的做法,是把MDC配置在2.5MHz以下,这是IEEE 802.3规定的标准频率上限。在某些FPGA工程里,也可以把MDC抬到12.5MHz,但这时对PCB走线和上下拉电阻的要求就会变高,稳定性也会打折扣。
2.3 寄存器读写控制器的Verilog实现要点
读写控制器的代码通常会封装成一个模块,对外提供的接口是地址、数据、读写使能和忙信号。这样做的好处是,上层逻辑(比如链路管理状态机)只需要给出寄存器地址和期望的数据,就可以启动一次MDIO操作,而不需要关心MDIO的繁琐时序。
代码中典型的读写操作,是包含一个计数器用来控制MDC时钟的翻转。在上升沿发送数据,在下降沿采样数据。参考代码中还会有对读回来的数据进行校验的逻辑——一方面是TURNAROUND阶段的处理,另一方是对数据位宽的对齐。这里有一个细节:MDIO读回来的数据是在MDC的下降沿有效的,因此采样时机必须在下降沿之后。
关于PHY地址,AR8031一般由硬件引脚配置,常见的是0x04。你在代码里会看到PHY_ADDR这个参数的定义,不同的板子可能不同,如果你改了PHY地址却读不到正确的ID寄存器,大概率就是这里的问题。
3. AR8031关键寄存器解析:从ID读取到链路状态监控
3.1 芯片ID与版本寄存器
做驱动移植的第一步,永远是验证MDIO通路是否正常。AR8031的寄存器2和寄存器3共同保存芯片的型号与版本信息,分别是0xDD01和0xA033。只有当这两个寄存器读回来的值和预期匹配时,才能确认FPGA和PHY之间的管理通道是通畅的。
参考代码中,通常会在初始化流程里先读取这两个寄存器,并把结果与常量比较。如果不匹配,代码会进入错误状态,通常会点亮一个LED或者在仿真波形里体现。这个设计很实用,因为MDIO是低速串行链路,一旦布线有问题,表现就是读回来全0或者全1。
我一般会建议在这个环节加一个延时重试机制——MDIO在极少数情况下会因为时序竞争读回错误的值,一次失败就复位并重新读取,会明显提高系统的健壮性。
3.2 自动协商控制与速率双工配置
AR8031的寄存器0和寄存器4是自动协商的核心。寄存器0是控制寄存器,bit 12是自动协商使能位,bit 13是速度选择,bit 8是双工模式。如果你想强制千兆全双工,就不开启自动协商,直接把bit 13置1、bit 8置1;如果你想用自动协商,则要正确设置寄存器4的advertised abilities。
参考代码中,自动协商的流程是:先向寄存器4写入期望通告的能力(比如1000BASE-T全双工),然后把寄存器0的bit 12置1以触发协商,接着等待PHY芯片更新寄存器1(状态寄存器)中的link status位。
这里有一个翻车点:寄存器1的bit 2是link状态,但它是一个锁存位,读一次就会自动清零。如果你的状态机在链接稳定的情况下反复读取并判断该位,会发现它时而为0时而为1,这是正常现象,表示这段时间内没有发生过链路断开,并不代表当前链路存在异常。要知道真正的当前链路状态,需要读取寄存器1的bit 0(它是实时状态位)。
3.3 链路状态监控与中断处理
驱动PHY芯片,不能只看初始化的结果,还必须实时监控链路状态的变化,否则就会出现网线拔掉但系统不自知的情况。AR8031支持两种方式:轮询状态寄存器,或者由PHY芯片产生中断信号。
参考代码中采用轮询方式的情况居多,因为FPGA实现中断控制器会额外消耗逻辑资源。轮询的核心是一个定时状态机——每隔一段时间(比如100ms),通过MDIO接口读取寄存器1,判断bit 0是否为1。如果链路断开,代码会记录断开时刻,并尝试重新发起自动协商;如果链路恢复,则重新初始化MAC侧的RGMII时序。
这种设计在嵌入式系统里非常实用,路由器、工业控制板卡都会用到类似逻辑。参考代码甚至会包含一个链路抖动滤波器——如果检测到链路状态变化,会连续读取几次确认状态稳定后再切换,避免因为PHY芯片在协商过程中出现的短暂抖动导致系统频繁复位。
4. 初始化序列与状态机跳转:让PHY芯片进入工作状态
4.1 上电后的初始化时序安排
AR8031在硬件复位释放之后,需要一段稳定时间才能接受MDIO操作。参考代码中的初始化状态机,会按照固定的顺序执行寄存器配置。首先是芯片复位配置,然后是使能必要的时钟,接着是自动协商配置,最后是等待链路变为UP状态。
这里的关键在于每个步骤之间的延时。你不能在复位释放之后立即读取寄存器,那大概率会失败。常见的做法是启动一个计数器,产生一个至少5ms的延时。在FPGA时钟为125MHz的情况下,这个延时意味着计数器要数到62万5千个周期。
初始化时序的参考代码如下(状态机风格模拟):
reg [2:0] init_state; localparam IDLE = 3'd0; localparam READ_ID = 3'd1; localparam CFG_AN = 3'd2; localparam WAIT_LINK = 3'd3; localparam DONE = 3'd4; case (init_state) IDLE: begin if (start_init) init_state <= READ_ID; end READ_ID: begin // 通过MDIO读寄存器2和3,验证芯片ID if (id_valid) init_state <= CFG_AN; else if (retry_cnt >= 3) init_state <= ERROR; end CFG_AN: begin // 写入寄存器4和0,开始自动协商 init_state <= WAIT_LINK; end WAIT_LINK: begin if (link_status) init_state <= DONE; end endcase这段代码的逻辑是:启动后首先验证芯片ID,如果连续三次读取失败,则进入错误状态;验证通过后配置自动协商,然后阻塞等待链路建立。注意,WAIT_LINK的状态需要配合一个超时计数器,否则如果网线没有接,系统会永远挂在这个状态,不能往下走。
在实际工程里,这里还会加入一个link_up标志位,作为后续RGMII数据收发使能的前置条件。链路没有建立时,MAC侧的发送逻辑不应该把数据送到线路上。
4.2 寄存器配置参数的映射与默认值
AR8031的寄存器配置有一些常用的参数组合。比如,开启自动协商之后,如果你想限制PHY只能工作在1000M全双工模式,可以这样配置:寄存器4写入0x01A0(指示PHY向对端通告1000BASE-T的能力),寄存器0写入0x1200(开启自动协商)。如果希望兼容100M网络,就需要把寄存器4的值改成包含100BASE-TX能力的值。
参考代码中会用一个局部参数数组来存放这些配置值,代码看起来会是这样的结构:
localparam [15:0] PHY_CFG_REG0 = 16'h1200; localparam [15:0] PHY_CFG_REG4 = 16'h01A0;在实际的项目里,这些值往往会做成可以在仿真中快速修改的宏定义。当你在调试板上遇到网络不通的情况,第一步就是检查寄存器0和4的值是否被正确写入。读回验证是一种好习惯——写入之后立即读回,用读回值判断写入是否成功,避免因为MDIO时序问题导致的静默失败。
4.3 阻塞式链路检测的潜在风险
上面提到,初始化状态机使用WAIT_LINK阻塞等待。但在真正的产品代码里,这种结构是有风险的——如果链路长时间无法建立,整个初始化流程会卡死。所以一般还会加入一个超时状态,让系统在超时后进入空闲状态,而不是死等。超时时间一般设为10秒。
另一种常用设计是定期重试。等链路建立完成之后,MDIO通道的管理权限可以交给更高层的逻辑,比如一个网络管理系统通过FPGA内部的寄存器映射表来动态调整PHY的功率控制或LED配置。参考代码中通常会把初始化状态机和运行时状态机分开,各自独立运行,避免状态混淆。
5. 避坑指南:AR8031驱动中常见的MDIO读回失败与链路抖动
在整理这套参考代码时,我把在项目中遇到的驱动问题归纳成几条,每条都是实际踩过的坑,写在这里方便你排查。
现象一:上电后读PHY ID寄存器,回来的数据全是0。
原因分析:U-Boot里常见的时钟配置缺乏对MAC域的明确初始化,导致xMII时钟没有被正确使能。也有可能是PHY芯片的复位释放时序不满足要求,芯片还没进入正常工作状态。
解决方案:检查FPGA复位释放后是否延时足够(建议至少10ms);检查硬件上的PHY地址引脚是否配置正确;在MDIO操作之前,先强制写一个复位寄存器的值,让PHY从复位状态解脱。
现象二:链路指示灯亮,但FPGA侧接收到的数据CRC全是错的。
原因分析:RGMII接收路径的时钟相位没有调整好,rx_clk与数据线之间的延迟关系不稳定。这是一个时序问题,不是逻辑问题。
解决方案:在约束文件中给rx_clk组设置set_input_delay,尝试不同的延迟值。或者在使用原语级代码时调整IDELAY的tap值。
现象三:链路偶尔会断,且重新协商的时间很长。
原因分析:多为供电不稳定,或者MDIO通道上存在外部干扰。
解决方案:在PCB上给PHY芯片的DVDD和AVDD增加足够的去耦电容;MDIO和MDC线上加适当的上拉电阻和串联电阻,降低干扰。
现象四:修改了PHY地址之后,MDIO仍然能读到数据但不匹配。
原因分析:AR8031的PHY地址是硬件引脚决定的,不同的引脚配置对应不同的地址,如果你的代码使用了错误的地址,会访问到总线上的其他设备。
解决方案:确认板子的地址电阻配置,读取寄存器2的实际值,回读验证后更新代码中的PHY_ADDR参数。
6. 二次开发技巧:把PHY驱动从状态机提到事务化API层
看完这套参考代码,你可以直接照搬的,是作为底层驱动的状态机设计;但如果你想把它融入更大的系统,我的建议是再封装一层事务化的API逻辑。具体来说,就是把一次MDIO读操作或一次链路状态查询,封装成一个可以被其他模块调用的独立任务,对外只暴露请求与响应接口。
module mdio_transaction #( parameter PHY_ADDR = 4'h4 )( input wire clk, input wire rst_n, input reg req_valid, input reg [4:0] tx_phy_addr, input reg [4:0] tx_reg_addr, input reg [15:0] tx_data, output reg [15:0] rx_data, output reg busy, output reg done );该模块封装了整套MDIO时序,让上层模块只需要发起读写请求即可。这个改造思路能显著提升工程的可维护性,因为你把物理层时序与业务逻辑解耦了。比如你要在系统里加入一个网络诊断模块,只要调用这个事务层接口,就能定时采集PHY的健康状态。
另一个值得做的改造,是把中断式链路检测加进去。FPGA内部可以通过一个专用的中断状态寄存器来记录链路变化,而不再需要周期性轮询。这个改造在低功耗场景下特别有效,因为只有在链路变化时系统才会被唤醒。
把那以后的每次FPGA网络开发流程,我都会在硬件定型前,先在仿真环境里做一次完整的状态机跑批验证,确保所有寄存器配置和时序约束已经拉齐,才会跑板级调试。这个教训花了我整整一周时间才消化——PHY驱动的坑不在复杂逻辑上,而在所有你以为它理所当然不会出错的细节里。希望这套参考代码和这份解析,能帮你躲开我踩过的那些坑。
本文还有配套的精品资源,点击获取