☰
FPGA直接驱动以太网PHY:从MDIO到RGMII的完整实现
2026/10/16 4:31:48 网站建设 项目流程

简介:面向FPGA开发者,这份参考代码解决的是如何用Altera公司的EP3C40系列FPGA直接驱动以太网PHY芯片AR8031的问题。工程完整覆盖了PHY寄存器配置、MDIO管理接口、RGMII收发数据通路、时钟复位处理等关键模块,并给出针对其他PHY芯片的寄存器适配思路,方便使用者移植到不同型号的收发器。压缩包约19.88MB,共包含2000个文件,以Quartus工程设置与编译数据库文件(qpf、qsf、qip、cdb、hdb)为主,同时包含Verilog与VHDL硬件描述源码(v、vhd、tdf),以及sof位流、引脚分配bsf、时序约束sdc和仿真脚本等,类型划分清晰,目录结构完整,可直接在Quartus II中打开工程进行综合与仿真。此外还附带了项目说明文档,有助于理解各文件用途和整体设计。目前已有1894人学习下载,适合有一定FPGA基础、正在调试以太网接口的工程师参考,也可作为学习PHY芯片驱动开发的入门样例。

1. FPGA直接驱动以太网PHY:从MDIO寄存器到RGMII收发链路

一块FPGA板子上接了千兆PHY芯片,第一反应往往是去找现成的MAC IP核。但IP核没授权或者工具版本对不上,项目就直接卡在起跑线。这份参考代码的价值在于:它不依赖付费MAC IP,直接在FPGA里用RGMII和MDIO两条链路把AR8031这颗PHY芯片接管起来,完成初始化、自协商、数据收发全流程。反直觉的是,整份代码里最核心、最不容易一把过的模块并不是数据收发,而是初始化时的MDIO寄存器读写——寄存器通路一通百通,不通后面全是空中楼阁。

它适合刚接手FPGA以太网开发的工程师,也适合想把PHY从黑匣子变成可控外设的开发者。代码做到链路层,MAC层以上留给你自己扩展。它能让你在一天之内看到以下结果:PHY被成功配置、link状态拉高、数据在IO上进出,而不是卡在数据手册里出不来。后面几章我会按“接口识别→初始化→数据收发→排查→改造”的顺序,把这套参考代码拆开过一遍。

2. 硬件接口与顶层架构:RGMII和MDIO两路分工

2.1 先认三组信号:数据、管理、配置

FPGA和PHY之间的连接并没有想象中那么多。以AR8031为例,按功能分三组。数据信号组是RGMII接口的引脚:TXD[3:0]、TX_CTL、TXC,以及RXD[3:0]、RX_CTL、RXC。TXC和RXC在千兆模式下是125MHz时钟,TXD和RXD都以DDR方式在时钟的上下沿各传半个字节,合起来每个时钟周期传一个完整字节。把收发两个字面拆开看,发送方向的TXD是FPGA输出,接收方向的RXD是PHY输出。

管理接口就是MDIO,只有MDC和MDIO两根线。MDC是FPGA输出的时钟,MDIO是双向数据线。PHY这边有配套的寄存器地址空间,FPGA通过MDIO对寄存器进行读写,从而控制PHY的工作模式、查看链路状态。MDIO的基本读写帧是32位前导码加14位命令,加上16位数据,实际速度很慢,MDC通常工作在2.5MHz左右。参考代码里没有调用任何IP核,直接用状态机逐位操作这两根线,这就是后面第三章的主要内容。

第三组是配置引脚。这类引脚不在MAC接口列表里,而是PHY上电时的strap引脚。板子上的上下拉电阻决定PHY地址、RGMII还是SGMII模式、主从时钟来源等。参考代码的XDC约束文件里一般只约束RGMII和MDIO,但硬件原理图上的几个电阻是决定初始化能否成功的前提。常见问题是板子上把PHY地址拉成了001,代码里却还按000去访问,就会出现MDIO读写无响应。

RGMII接口信号的含义可以用一个表格来表示,这块在调试时几乎每个引脚都要核对一遍:

信号方向说明
TXD[3:0]FPGA -> PHYDDR模式,与TXC上下沿对齐
TX_CTLFPGA -> PHYTX_EN信号,高表示数据有效
TXCFPGA -> PHY千兆125MHz,百兆25MHz
RXD[3:0]PHY -> FPGA接收数据,同样DDR对齐
RX_CTLPHY -> FPGARX_DV与错误标志混合编码
RXCPHY -> FPGA接收时钟由PHY恢复输出
MDCFPGA -> PHY管理时钟,建议低于2.5MHz
MDIO双向管理数据线,读时需要释放总线

表里RX_CTL那行的“混合编码”值得多说一句。RGMII规定,RX_CTL在RXC的上升沿表示RX_DV,在下降沿表示RX_DV异或RX_ERR。也就是说不能把它当一个普通信号来接,得在FPGA内部用DDR方式采样,然后组合还原出真实的数据有效信号。TX_CTL逻辑上相对简单,就是TX_EN信号,但发送时同样要做DDR输出,因为它也要在TXC的上下沿同时传信息。

2.2 参考代码顶层怎么拆:看清模块骨架

拿到FPGA工程先别急着跑仿真,我习惯先打开目录结构,把每个文件对应到硬件链路上。这份参考代码的典型目录长这样:

eth_phy_top/ rtl/ eth_phy_top.v // 顶层,例化收发与MDIO模块 mdio_phy_ctrl.v // MDIO读写状态机,负责寄存器配置 rgmii_rx.v // 接收侧DDR采样与对齐 rgmii_tx.v // 发送侧数据锁存与DDR输出 async_fifo.v // 跨时钟域FIFO包装 sim/ phy_model.v // PHY行为模型,模拟MDIO寄存器 tb_top.sv // 顶层测试用例 syn/ eth_phy_top.xdc // 引脚与时序约束

eth_phy_top.v是唯一对外可见的模块,它把RGMII引脚、MDIO引脚、复位和时钟引出来,内部例化三个子模块:mdio_phy_ctrl负责启动初始化时序,rgmii_rx处理接收字节流,rgmii_tx处理发送字节流。初始化完成后,mdio_phy_ctrl会拉高一个phy_ready信号,rgmii_rx和rgmii_tx才开始工作。

数据流走向是:内部用户逻辑把待发送的字节写到发送FIFO,rgmii_tx从FIFO读数据并拼成DDR时序输出到TXD;另一侧,rgmii_rx把RXC上的DDR数据拆成完整的8bit,写入异步FIFO,再由用户时钟域的读侧取走。PHY侧有一个较小的监控模块循环读取状态寄存器,将link up、协商速度等信息输出成几个标志位。这块在仿真时很有用,因为在板子上看链路状态还要动用示波器,仿真里直接看寄存器值就能判断PHY是否正常。

这种“初始化通道和数据通道分开”的结构是值得保留的好设计。MDIO状态机工作在慢速时钟下,数据链路工作在125MHz下,两者在顶层通过phy_ready握手,互不干扰。后面改代码时,如果想把MDIO部分换成串行管理接口,或者改成某种自检模式,也不会波及数据通路。

3. MDIO状态机与PHY启动序列:读不回ID,后面全白搭

3.1 MDIO读写状态机:逐位操作也能稳定读写

MDIO协议是典型的串行管理接口。每帧结构为:32位前导码全1,接着2位ST码,2位操作码OP(读为10,写为01),5位PHY地址,5位寄存器地址,2位转向时间,16位数据。这里没有神秘之处,但写代码时很考验对时机条件的理解:MDIO的数据必须在MDC上升沿之前的低电平阶段稳定下来。参考代码的写法是把mdc和mdio分开控制,mdc上升沿来之前先给mdio赋值。

// mdio_phy_ctrl.v 中按位发送的核心片段 task send_bit(input bit_val); begin MDIO_O = bit_val; // 数据先就绪 MDC_O = 1'b0; // 保持低电平 #MDC_HALF_PERIOD; MDC_O = 1'b1; // 上升沿,PHY采样 #MDC_HALF_PERIOD; MDC_O = 1'b0; // 回到低电平 end endtask

这段代码的问题是仿真里面能用#延时,综合之后没有意义。参考代码如果直接上板,就会把send_bit改写成可综合状态机。状态机每次在MDC上升沿前的状态变化中把数据放到MDIO_O,再拉高MDC,再拉低MDC,如此循环。所以看仿真代码时,一种是任务加延时;看综合代码时,一定用的是计数器。

另一种常见的实现是按位循环移位。先把要发送的内容拼成一个32位的移位寄存器,然后用一个计数器逐个送出:

reg [31:0] tx_buf; // 装填好的发送帧 reg [ 5:0] bit_cnt; always @(posedge clk_mdc) begin if (start) begin tx_buf <= {1'b0, 1'b1, 1'b0, 1'b1, 5'h00, 5'h01, 2'b10, 16'h0000}; bit_cnt <= 6'd0; end else if (bit_cnt < 32) begin MDIO_O <= tx_buf[31]; tx_buf <= {tx_buf[30:0], 1'b0}; bit_cnt <= bit_cnt + 1; end end

这个状态机的逻辑说明:tx_buf按帧格式拼好,bit_cnt从0数到31,每个MDC上升沿输出tx_buf[31],再左移一位。读操作要麻烦一些,在第32位之后插入两个转向时钟周期,第一个周期继续输出高阻,第二个周期释放总线,然后从MDIO_I采回16位。采样时刻要避开MDC边沿附近,常见做法是在MDC下降沿后半个周期采样,避免受到上升沿数据变化的干扰。

3.2 PHY复位与自协商配置:寄存器0、寄存器9和0x04怎么配

MDIO通路通了,下一步是给PHY写寄存器。AR8031遵循IEEE 802.3的寄存器布局,0x00是控制寄存器,0x01是状态寄存器,0x02和0x03是PHY ID寄存器,0x04是自协商广播寄存器,0x09和0x0A是1000BASE-T控制与状态寄存器。参考代码的初始化序列大体是:软复位、等复位完成、写自协商广播、检查link状态。常见初始化序列如下:

// eth_phy_top.v 中的配置流程,step由状态机驱动 case (cfg_step) 0: begin // 读取寄存器0x02,判断PHY ID高16位 rd_phy(5'h00, 5'h02); end 1: begin // 读到的值应该与器件手册相符 if (rd_data[15:0] == 16'h004D) cfg_step <= 2; else begin cfg_err <= 1'b1; cfg_step <= 7'b111; end end 2: begin // 软复位:写0x00的bit15为1 wr_phy(5'h00, 5'h00, 16'h8000); end // 后续步骤接着写自协商寄存器 endcase

寄存器0x00的bit15是软件复位位,写入1会让PHY内部复位,完成之后此位自动清0。写完复位之后建议至少等待几百微秒,再把自协商相关寄存器写好。AR8031的自协商广播建议这样配:寄存器0x04写入0x01E1,表示广播10BASE-T半双工、10BASE-T全双工、100BASE-TX半双工和100BASE-TX全双工;寄存器9写入0x0200,表示广播1000BASE-T全双工。这样在千兆交换机下不会出现断流。填值的时候不要忘记bit0到bit4各代表一个能力位,少配一个就相当于放弃一种速率。

实际调试时会有玄学问题,比如写寄存器0x00的值看数据手册怎么都对,但PHY就是协商不到千兆。后来发现是漏写了寄存器9。这是因为百兆自协商通告写在0x04里,而千兆通告是另一套寄存器,两个都要写。参考代码把这两个地址拆成单独步骤,注释写得很清楚,抄代码的时候不要自作主张合并。

3.3 回读PHY ID:确认初始化第一次成功

对PHY芯片做初始化,第一步永远是读ID。AR8031的PHY ID分布在寄存器0x02和0x03,各16位。参考代码在初始化状态机里单独留了读ID逻辑,读出来的值会锁在phy_id_reg中:

reg [31:0] phy_id_reg; wire phy_id_ok = (phy_id_reg[31:16] == 16'h004D);

这段代码把高16位固定值和型号比对,低16位在不同版本硅片上有差异,所以只比对高16位。如果仿真或上板时phy_id_ok拉不高,说明MDIO通路还没通,这不是自协商能解决的,要回头查时钟、查引脚约束、查MDC频率。从那以后我的习惯是:初始化代码里永远保留这个回读断言,板子一上电就看这个信号,比肉眼盯着示波器省事。先验证再配置,后面才谈数据收发。

4. RGMII收发通路实现:DDR采样与跨时钟域隔离

4.1 接收侧DDR采样:把RGMII还原成字节流

第2章提到,RX_CTL是混合编码,RXD本身也是DDR。接收侧要做的第一件事是还原数据和有效信号。代码常例化IDDR原语:

// rgmii_rx.v 中的核心例化,对4bit数据各自做DDR采样 genvar i; generate for (i = 0; i < 4; i = i + 1) begin: rxd_ddr IDDR #(.DDR_CLK_EDGE("SAME_EDGE")) u_iddr_rxd ( .Q0(rxd_low[i]), // 上升沿采到的低4bit .Q1(rxd_high[i]), // 下降沿采到的高4bit .C(rxc_int), // PHY恢复的RXC .D(rxd[i]) ); end endgenerate

IDDR原语把DDR输入拆成两路单沿数据。SAME_EDGE模式表示Q0和Q1在同一个时钟沿更新,下游逻辑不用处理半个时钟周期的错位,直接拼接即可。拼接的方法是rx_byte = {rxd_high, rxd_low},注意高位在前,拼好的字节再送进后续模块。RX_CTL也是同样的DDR方式采样,上升沿出来的rx_ctl_r就是RX_DV,下降沿出来的rx_ctl_f要与rx_ctl_r异或后作为错误标志。参考代码里一般不会真的去组合出error信号,而是直接把rx_ctl_r作为写使能,rx_ctl_f挂在调试总线上。

RGMII的DDR采集还有一个容易忽略的问题:PHY输出的RXC与RXD存在相对偏斜。规范允许RXD相对RXC有最多约2ns的偏斜,在125MHz边缘附近稍有不慎就会采到中间态。参考代码在XDC里针对输入延迟做了约束:

create_clock -name rgmii_rxc -period 8.000 [get_ports {RXC}] set_input_delay -clock rgmii_rxc -max 2.000 [get_ports {RXD[*]}] set_input_delay -clock rgmii_rxc -min 0.500 [get_ports {RXD[*]}]

如果板子上PHY和FPGA距离较长,还需要把RX_CTL的delay也加进去。这类约束不是摆设,它影响的是综合在RXC到IDDR数据端这条路径上的时序分析结果。不加约束,工具默认RXC和RXD同源同时到达,结果布线可能把数据路径拉得很长,上板后偶尔丢字节。

4.2 发送侧:TX_CTL与TXD的产生

发送侧的DDR输出同样绕不开原语。FPGA内部要发出的信号实际上是8位并行数据和1位使能,但RGMII引脚上需要DDR输出。以Xilinx的OSERDESE2为例,它可以完成这种并转串:

OSERDESE2 #( .DATA_RATE_OQ("DDR"), .DATA_WIDTH(4) ) u_oserdes_txctl ( .D1(tx_en_clk), // 上升沿要发出去的使能 .D2(tx_err_clk), // 下降沿发出去的错误标志位 .OQ(tx_ctl), .CLK(txc_int), .CLKDIV(tx_clk_div), .RST(!rst_n) );

发送方向,TXD[3:0]四个引脚分别例化一个OSERDESE2,D1/D2分别是内部8位数据的高4bit和低4bit。关键是TX_CTL也要走DDR。如果图省事把它直接接到内部tx_en,而内部tx_en是按单沿时机输出的,TXC下降沿处就会因为错位出现毛刺。很多深夜里查CRC error查到最后,根因都是TX_CTL的DDR路径没走。

另外要做的工作是发送字节缓冲。参考代码通常在rgmii_tx里放一个同步FIFO,用户逻辑把待发数据写入,发送逻辑在TXC域里读出,再拆成DDR输出。这样用户逻辑不需要在125MHz下严格对齐节拍,只要FIFO不满就可以继续写。FIFO深度不必太大,做以太网一包最多1518字节,深度1024足够缓存一整包。

4.3 时钟域与异步FIFO:别让125MHz把你锁死

整个链路涉及多个时钟域:MDIO对应的慢速配置时钟、RXC对应的PHY接收时钟、以及用户逻辑自己的时钟。参考代码里数据通路没有简单地把所有逻辑都放在RXC域里,而是在接收侧插入异步FIFO:

// 用xpm_fifo_async把RXC域和用户时钟域隔离 xpm_fifo_async #( .READ_DATA_WIDTH(8), .WRITE_DATA_WIDTH(8), .WRITE_DEPTH(1024), .FIFO_MEMORY_TYPE("auto") ) u_rx_fifo ( .wr_clk(rxc_int), // 写侧时钟 .wr_en(rx_wr_en), .din(rx_byte), .rd_clk(user_clk), // 用户逻辑时钟 .rd_en(user_rd_en), .dout(user_rx_byte), .full(wr_full), .empty(rd_empty) );

读侧把异步FIFO的读时钟接到用户逻辑的工作时钟,用户逻辑不必非得跑在125MHz,也不怕PHY的时钟和本地时钟存在小的频差。实际项目里如果直接把RXC当全局时钟用,用户逻辑的时序收敛会很痛苦,尤其当PHY的时钟输入到BUFG的路径上又铺了不少组合逻辑时。常见做法是只在RXC域里做DDR采样和字节拼装,拼完立刻进FIFO。

发送侧一般不需要异步FIFO,因为发送时钟是FPGA自己给的,本地时钟和用户时钟是同源的,用同步FIFO即可。但如果发送端内部接了某些慢速总线,发送路径上的同步FIFO也可以起到平滑作用。所以参考代码给收发两侧各保留一个FIFO,是给二次开发留的缓冲余地。

5. 避坑排查:哪些细节能让参考代码变成废纸

5.1 现象:PHY ID读回全F,MDIO像没接

上板后第一步读PHY ID,读回来0xFFFFFFFF,这是最常见的翻车现场。很多人的第一反应是怀疑芯片坏了,其实绝大多数情况下是MDIO链路没有真正建立。

原因大致有三种:MDC时钟太快,PHY来不及响应;MDIO引脚方向没切换,读的时候还在输出;或者PHY地址和代码里的地址不匹配。MDIO的规范速度在2.5MHz左右,如果FPGA直接给125MHz,PHY的串行状态机根本跟不上。引脚方向问题则出现在读操作,状态机没有把MDIO_O置为高阻,总线被FPGA自己拉死。

解决方法是先分频再调试。把MDC分频到2.5MHz,读方向在转向时钟里至少留出两个周期给总线释放,然后重新跑仿真确认phy_id_ok能拉高。之后上板,用示波器量MDC和MDIO波形,先看有没有时钟,再看数据线上的前导码。

5.2 现象:link灯亮,但收不到数据包

链路协商成功了,灯也亮了,但FPGA用户侧始终收不到完整包。这类问题最浪费工时,因为所有人都以为链路通就是数据通。

原因多半是RX_CTL的混合编码还原错了。RGMII规范里RX_CTL在上升沿给RX_DV,下降沿给RX_DV和RX_ERR的异或值。如果只采样了上升沿,或者把下降沿值当成独立信号,就会出现数据写入使能比实际数据早一拍或者晚一拍的情况,包里全是错位字节。

解决方法是把RX_CTL也走IDDR,然后把rx_ctl_r和rx_ctl_f还原成真正的rx_valid。具体要盯的是rx_ctl_r拉高的时刻是否和第一个字节出现在RXD上的时刻对齐。仿真里用phy_model产生一帧典型数据,直接对比波形:如果valid上升沿比第一个字节早了半个时钟周期,在FIFO写使能路径上加一拍延迟,或者在拼接逻辑里做字节对齐。

5.3 现象:CRC校验错,偶发丢字节

收发都能跑,但上板跑一段时间就出现CRC错误,或者接收侧字节偶发丢失。这类问题最隐蔽,因为不是必现,频率又不足以用逻辑分析仪抓。

常见原因是跨时钟域处理不彻底。比如发送侧用户时钟和TXC不同源,却直接用了同步FIFO;或者接收侧把RXC域信号直接拉到用户时钟域做判断。另一个高频原因是外部PHY和FPGA之间的时序约束缺失,导致布局布线结果差,时序收敛边界状态不稳定。

解决方法是把所有跨时钟路径都收进FIFO,不要用打拍方式处理连续数据流。然后把XDC里的输入输出延迟约束补全,至少对RXC和RXD、TXC和TXD这两组路径做set_input_delay和set_output_delay。补完约束再跑一次时序收敛,通常能发现有一两条路径在8ns周期下只剩不到0.1ns的余量,这种路径就是定时炸弹。

5.4 现象:仿真过了,上板链路不亮

仿真里PHY模型工作正常,phy_id_ok也拉高了,但一上板link灯就是不亮。这种一般不是逻辑问题,而是硬件配置引脚和代码逻辑打架。

常见原因是PHY的strap引脚在板子上被电阻拉到了某种固定模式,比如强制100M模式,代码里却往寄存器写了自协商,两边冲突。还有一种是PHY地址引脚和复位时序没对上,PHY留在复位状态,MDIO永远无响应。

解决方法是先查原理图:PHY地址若干bit、模式选择引脚、时钟来源引脚分别被拉成什么电平,把strap值列成表,确认和代码里访问的PHY地址一致。然后检查复位上电时序,很多PHY要求参考时钟稳定后至少10ms才能撤复位,FPGA逻辑里要做一个延时计数器,而不是简单把外部复位反相接过去。

6. 二次开发改造:把参考代码改成可复用的MAC前端

6.1 移植到别的板子:引脚、PHY地址和时钟频率

把参考代码搬到自己的板子,有三处必须改:引脚约束、PHY地址、RGMII模式。RGMII在AR8031上可能支持多种模式,比如RGMII to MII或者RGMII with clock delay。参考代码默认是不加内部delay、由FPGA端用IDDR和ODDR满足DDR时序。如果你的板子在PHY侧加了延迟电阻,那么XDC里的input delay和output delay数值都要按板子走线长度重新评估。

PHY地址这一项最容易漏。参考代码里把PHY地址作为参数提出来了:

parameter PHY_ADDR = 3'b000;

在AR8031上,strap引脚配置PHY地址,常见取值000到001。代码里所有rd_phy、wr_phy的PHY地址参数都来自PHY_ADDR,移植时只需要改这一处。寄存器地址不推荐参数化,因为不同PHY的寄存器0x04和0x09布局差异较大,宁可多写两条配置宏,不要为了省事把寄存器地址也套成变量。

6.2 改造成参数化配置列表:后续换PHY芯片不慌

参考代码把初始化序列写成case分支,每步配置一个寄存器。这样做直观,但换PHY芯片时改动量大。更通用的做法是把配置步骤定义成数组,每条记录包含PHY地址、寄存器地址、数据值,和一个结束标志:

localparam [31:0] INIT_SEQ [0:5] = '{ {3'b000, 5'h02, 32'h00000000}, // 占位,读ID用 {3'b000, 5'h00, 32'h00008000}, // 软复位 {3'b000, 5'h00, 32'h00001000}, // 开自协商 {3'b000, 5'h04, 32'h000001E1}, // 广播百兆能力 {3'b000, 5'h09, 32'h00000200}, // 广播千兆全双工 {3'b000, 5'h00, 32'h00000000} // 结束 };

这种改造比硬编码的case多一层地址解码逻辑,但换来的是初始化序列和代码主体分离。我一般把INIT_SEQ放在独立.vh头文件里,换PHY时只改头文件。需要增加一步寄存器配置时,不用重新梳理状态机,只需要在数组末尾补一条。上板验证时也方便,把某一行的数据值改成错误值,看phy_id_ok或者link状态是否按预期变化,能快速判断配置流程是否正确执行。

6.3 向上扩展:收下来的字节流怎么接MAC

参考代码做到字节流这一层,再往上就要自己实现MAC。一个比较顺手的做法是用AXI-Stream接口包一层,把接收侧FIFO的读侧变成AXI-Stream从端,发送侧FIFO的写侧变成AXI-Stream主端。这样后续接DMA或者接软核处理报文都很顺手。

接口大概长这样:

// AXI-Stream从端:接收方向,接口由rx_fifo读侧产生 axis_rx_if.master axis_rx; // tdata[7:0], tvalid, tready, tlast // AXI-Stream主端:发送方向,接口由tx_fifo写侧产生 axis_tx_if.slave axis_tx; // tdata[7:0], tvalid, tready, tlast

tlast在MAC层对应一帧的结束,RG MII接收侧怎么知道一帧结束?参考代码里只给了rx_ctl信号,帧结束可以由外部MAC根据包长计数器识别,也可以把rx_ctl_f异或信息利用起来。我的经验是,先用接收侧包计数器配合用户逻辑做简单组帧,在仿真里对比收发的包长度,验证DDR采样不会丢字节,再接DMA就省心很多。

这套代码本身只解决PHY驱动,不解决协议栈。曾有段时间我也想着直接在FPGA里把ARP、UDP全写了,后来发现工作量翻倍。正确的做法是把参考代码当成一个可复用的外设前端,MAC层和IP层放给软核或者专用IP。从那以后我每次换PHY芯片都先读回ID再配寄存器,不再跳过;改造代码时也强制把PHY地址和寄存器序列提成参数,仿真、上板、查错都照着同一套流程走,希望能帮到跟我一样在PHY上摔过跟头的开发者。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询