
项目背景写一个完整的MDIO控制器其实比想象中要麻烦得多。头一回做以太网交换机项目时我在PHY芯片的寄存器读写上栽了大跟头——板卡回来CPU通过MDIO总线读PHY的状态寄存器读回来的数据永远都是0xFFFF。排查了整整两天最后才发现问题不在逻辑而在时序上MDC时钟频率没按手册压到2.5MHz以内PHY芯片根本来不及响应。这篇文章就把这个经验完整的讲一遍。我会从MDIO协议基本机制说起然后给出一个可直接用的Verilog控制器实现附上同样功能的VHDL版本做对比最后聊聊实际调试中那些文档里不会写的坑。不管你是刚接触FPGA的在校生还是正在做交换板卡的硬件工程师看完应该都能照着自己的场景把代码搭起来。1. 内容整体设计与思路拆解1.1 MDIO控制器的核心职责MDIOManagement Data Input/Output是IEEE 802.3协议定义的管理接口用来访问以太网PHY芯片内部的寄存器。它只有两根线MDC时钟和MDIO数据。但就这么两根线承载着链路状态查询、速率协商配置、环回测试、中断管理这些关键功能。控制器的核心职责说白了就两件事CPU或者逻辑要通过MDIO总线往PHY寄存器里写数据或者把PHY寄存器的值读回来。因为MDIO是串行协议一次完整操作至少需要64个MDC时钟周期写操作或者更长的读操作而且数据是一位一位在线上传输的所以控制器的设计核心就是状态机——把串行收发过程切分成若干阶段按顺序执行。很多初学者会问能不能直接用一个移位寄存器把数据发出去理论可以但实际工程一定会加状态机。原因很简单读操作里有总线方向切换有等待PHY响应的时间槽这些都不是单纯移位能搞定的。而且状态机便于插入超时判断、错误重试、中断上报这些功能后续维护也容易。1.2 Verilog实现和VHDL实现对比的意义同一个功能用Verilog和VHDL各写一遍放在一篇文章里对比这有实际价值。很多FPGA团队是Verilog起步的但遇到某些特殊IP核、特定客户的代码交付要求或者要维护老项目时就不得不面对VHDL代码。两种语言的差异体验一遍就清楚Verilog的端口声明简洁行为描述自由度大但容易写飞VHDL强类型约束多写起来啰嗦但综合结果往往更规整。具体到MDIO控制器两者在设计思路上一模一样变的是语法外壳。看完对比你至少能在这两种语言之间无障碍切换——这在实际项目里比想象中重要得多。2. MDIO协议机制与关键细节2.1 帧格式与时序窗口MDIO协议的帧结构在各个协议版本里略有差异但最基础、最常用的Clause 22格式是这样的先是一段32位的 preamble全1然后依次是2位起始码ST、2位操作码OP、5位PHY地址、5位寄存器地址、2位转换时间TA最后是16位数据。写操作的格式是前导码 ST01 OP01 PHYAD[4:0] REGAD[4:0] TA(这两个时钟周期由主机驱动为10) DATA[15:0]。整个过程全部由主机FPGA驱动PHY不加任何干预。读操作就有意思了在前面发完地址之后TA周期的两个时钟里第一个时钟主机释放MDIO总线输出高阻第二个时钟开始由PHY驱动数据线。也就是说总线所有权在TA阶段发生了交接。这个交接点是整个MDIO控制器最容易出错的地方后面写代码时会专门处理。还有一点需要注意preamble在有些PHY芯片上是可省略的但千万、千万、千万不要默认所有PHY都支持省略实际项目中我们一律发完整的32位前导码。多花32个时钟周期基本可以忽略换来的是绝对稳妥的兼容性这笔账怎么算都划算。2.2 MDC时钟频率与分频计算MDC的时钟频率Clause 22规定最高2.5MHzClause 45支持最高12.5MHz。实际工程中如果系统时钟是50MHz给MDC做一次分频除以20得到2.5MHz如果系统时钟是125MHz那就除以50同样得到2.5MHz。可能有朋友会问我的PHY芯片100MHz MDC也能跑为什么不往高里配答案很简单信号完整性。MDIO线走在PCB上是有寄生电容的MDC频率越高MDIO数据线上的建立时间越紧张尤其当PHY芯片距离FPGA比较远的时候时序余量不足会导致偶发性读错误。这类问题在实验室里很难复现到了现场却频繁出现。所以设计默认按最保守的2.5MHz来只有确认线路很短、PHY手册明确支持更高的频率时才会考虑把MDC提上去。分频模块的Verilog写法非常简单核心代码就是一个计数器module mdio_clk_div ( input wire clk, input wire rst_n, output reg mdc ); parameter DIV 20; // 50MHz / 20 2.5MHz reg [7:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 8d0; mdc 1b0; end else begin if (cnt (DIV/2 - 1)) begin cnt 8d0; mdc ~mdc; end else begin cnt cnt 1b1; end end end endmodule2.3 寄存器读写场景与控制器需求实际项目中CPU访问PHY寄存器往往是通过片内总线比如AXI-Lite或者简单SRAM接口下发指令给MDIO控制器然后控制器自动完成串行时序。这种设计模式下控制器需要提供以下接口命令字读还是写、PHY地址、寄存器地址、写数据写操作时用、读数据读操作完成后返回、忙标志BUSY、完成标志DONE。有些设计还要求支持多笔连续读写。这时候可以从两个方向做控制器本身只完成单次操作由上层模块发起多次请求这是最常见的做法或者控制器内部加FIFO自动逐条处理这在需要大量配置PHY的高级特性时能减轻CPU负担。不过用FIFO做复杂调度时务必在文档里写清楚命令执行的时序关系免得后续维护的人看不懂。3. Verilog控制器完整实现与拆解3.1 模块端口定义我通常把MDIO控制器封装成下面这个接口简洁而且容易对接总线module mdio_controller ( input wire clk, // 系统时钟 50MHz input wire rst_n, // 异步复位低有效 // CPU/逻辑侧接口 input wire start, // 启动一次读写操作 input wire rw, // 1: 读 0: 写 input wire [4:0] phy_addr, // PHY地址 input wire [4:0] reg_addr, // 寄存器地址 input wire [15:0] wr_data, // 写数据 output wire [15:0] rd_data, // 读数据 output wire busy, // 忙标志 output wire done, // 操作完成脉冲宽度为1个系统时钟 // MDIO物理接口 output wire mdc, // 管理时钟 inout wire mdio // 管理数据 );这里MDC信号直接由分频计数器产生而MDIO是三态总线用inout类型。三态控制是重点地方不对会出现总线冲突轻则功能异常重则器件发热甚至损坏。3.2 状态机定义与设计思路整个控制器的核心状态机我设计成9个状态分别是IDLE、PREAMBLE、CTRL、ADDR、TURNAROUND、RD_DATA、WR_DATA、DONE_ST和WAIT。比起有些人用计数器硬顶实现显式状态机的好处是一目了然review代码时别人一看就知道总线所有权在谁手里。状态跳转如下localparam IDLE 4d0; localparam PREAMBLE 4d1; localparam CTRL 4d2; localparam ADDR 4d3; localparam TURNAROUND 4d4; localparam RD_DATA 4d5; localparam WR_DATA 4d6; localparam DONE_ST 4d7; localparam WAIT 4d8;IDLE状态等待start信号拉高PREAMBLE状态连续输出32个1CTRL状态发送ST和OP总共4个bitADDR状态发送PHY地址和寄存器地址总共10个bitTURNAROUND处理2个时钟的总线交接RD_DATA和WR_DATA分别处理16位数据的读和写DONE_ST拉高done信号WAIT保证一次操作结束后tir保持足够时间避免背靠背操作把PHY芯片冲晕。3.3 关键分频模块与全局状态机完整的Verilog代码为了叙述连贯我拆成几个片段来讲。先是MDC分频和主状态机的框架部分parameter DIV 20; // MDC分频系数50MHz时钟下得到2.5MHz MDC reg [7:0] div_cnt; reg mdc_r; reg [5:0] bit_cnt; reg [15:0] shift_reg; reg [3:0] state; reg mdio_dir; // 1: 输出 0: 输入高阻 reg mdio_out_r; reg rw_r; reg phy_addr_r; reg [4:0] reg_addr_r; reg [15:0] rd_data_r; reg done_r; reg busy_r; // MDC产生逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 8d0; mdc_r 1b0; end else if (div_cnt (DIV/2 - 1)) begin div_cnt 8d0; mdc_r ~mdc_r; end else begin div_cnt div_cnt 1b1; end end assign mdc mdc_r;主状态机的跳转逻辑我按时钟域的严格写法来做所有的状态跳转和数据移位都发生在MDC的上升沿。这样做的好处是MDIO数据线和MDC之间天然满足建立时间和保持时间的要求不用再做额外的跨时钟域处理。always (posedge mdc_r or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 6d0; shift_reg 16d0; mdio_dir 1b1; mdio_out_r 1b1; rd_data_r 16d0; done_r 1b0; busy_r 1b0; end else begin done_r 1b0; case (state) IDLE: begin busy_r 1b0; if (start) begin busy_r 1b1; rw_r rw; phy_addr_r phy_addr; reg_addr_r reg_addr; shift_reg wr_data; bit_cnt 6d0; state PREAMBLE; end end // 后续状态见代码片段 endcase end end这里有个细节状态跳转使用posedge mdc_r但IDLE对start信号的采样用的是这个时钟域的上升沿。由于start来自系统逻辑通常是AXI总线或CPU接口会跟MDC域有相位差。工程做法是在模块入口处把start信号同步到mdc_r域再采电平。我这版代码为了清晰没有加那级同步实际使用中要补上。3.4 各状态的具体实现PREAMBLE状态连续发32个1。我直接用计数bit_cnt从0到31每次输出mdio_out_r 1b1计数到31后进入CTRL状态PREAMBLE: begin mdio_dir 1b1; mdio_out_r 1b1; if (bit_cnt 6d31) begin bit_cnt 6d0; state CTRL; end else begin bit_cnt bit_cnt 1b1; end endCTRL状态发送ST和OP。ST固定是01读操作OP是10写操作OP是01。我把4个bit展开成case去判断CTRL: begin mdio_dir 1b1; case (bit_cnt) 6d0: mdio_out_r 1b0; // ST bit0 6d1: mdio_out_r 1b1; // ST bit1 6d2: mdio_out_r rw_r; // OP bit0: 读为1, 写为0 6d3: mdio_out_r ~rw_r; // OP bit1: 读为0, 写为1 default: mdio_out_r 1b1; endcase if (bit_cnt 6d3) begin bit_cnt 6d0; state ADDR; end else begin bit_cnt bit_cnt 1b1; end endADDR状态先发送5位PHY地址再发送5位寄存器地址总共10个bit。发送顺序是MSB先出即从phy_addr[4]开始一路送到phy_addr[0]再接reg_addr[4]到reg_addr[0]ADDR: begin mdio_dir 1b1; if (bit_cnt 6d5) mdio_out_r phy_addr_r[4 - bit_cnt]; else mdio_out_r reg_addr_r[9 - bit_cnt]; // 注意索引换算 if (bit_cnt 6d9) begin bit_cnt 6d0; state TURNAROUND; end else begin bit_cnt bit_cnt 1b1; end endTURNAROUND状态这是总线交接的关键。写操作时2个TA位都发1读操作时第一个TA位MDIO输出高阻相当于释放总线第二个TA位也保持高阻但此时要开始准备采样了TURNAROUND: begin if (rw_r) begin // 读操作释放总线 mdio_dir 1b0; mdio_out_r 1b1; // 高阻时数据线上逻辑1靠外部上拉实际没有驱动 if (bit_cnt 6d1) begin bit_cnt 6d0; state RD_DATA; end else begin bit_cnt bit_cnt 1b1; end end else begin // 写操作继续驱动总线 mdio_dir 1b1; mdio_out_r 1b1; if (bit_cnt 6d1) begin bit_cnt 6d0; state WR_DATA; end else begin bit_cnt bit_cnt 1b1; end end end读数据阶段16个时钟周期里每个时钟的上沿采样一次mdio总线RD_DATA: begin mdio_dir 1b0; // 始终释放总线 rd_data_r {rd_data_r[14:0], mdio}; if (bit_cnt 6d15) begin bit_cnt 6d0; state DONE_ST; end else begin bit_cnt bit_cnt 1b1; end end写数据阶段之前暂存在shift_reg里的数据按MSB先行发出去这里有一个易错点shift_reg本身是16位寄存器每次只送最高位送完一个bit后左移一位把下一个待发送bit移到最高位WR_DATA: begin mdio_dir 1b1; mdio_out_r shift_reg[15]; shift_reg {shift_reg[14:0], 1b0}; if (bit_cnt 6d15) begin bit_cnt 6d0; state DONE_ST; end else begin bit_cnt bit_cnt 1b1; end endDONE_ST状态拉高done信号一个周期随即回到IDLEDONE_ST: begin done_r 1b1; busy_r 1b0; state IDLE; end3.5 对外输出与三态缓冲处理最后对外输出时MDIO三态缓冲的写法是很多新人踩坑的地方。正确写法是assign mdio mdio_dir ? mdio_out_r : 1bz; assign rd_data rd_data_r; assign busy busy_r; assign done done_r;这个三态控制就是核心点。mdio_dir为1时FPGA强力驱动总线向外输出数据mdio_dir为0时输出高阻相当于把总线让出去。读操作时TURNAROUND状态把mdio_dir拉低之后所有读数据采样阶段都保持拉低直到回到IDLE为止。万一不小心写成了mdio mdio_dir ? mdio_out_r : 1b0那读操作根本读不到PHY的数据因为总线一直被钳在低电平。我见过不止一个新手在这里纠结半天。4. VHDL版本实现与对比分析4.1 VHDL状态机与三态控制同款功能用VHDL写状态机思路完全一致差异主要在语法和强类型约束上。VHDL的端口声明里inout端口需要使用std_logic类型而不能用std_logic_vector的bit位。核心状态机的VHDL片段如下architecture Behavioral of mdio_controller is type state_type is (IDLE, PREAMBLE, CTRL, ADDR, TURNAROUND, RD_DATA, WR_DATA, DONE_ST); signal state : state_type : IDLE; signal bit_cnt : integer range 0 to 63 : 0; signal shift_reg : std_logic_vector(15 downto 0) : (others 0); signal rd_data_s : std_logic_vector(15 downto 0) : (others 0); signal mdio_dir : std_logic : 1; signal mdio_out_s : std_logic : 1; signal mdc_s : std_logic : 0; signal div_cnt : integer range 0 to 31 : 0; begin -- MDC分频 process(clk, rst_n) begin if rst_n 0 then mdc_s 0; div_cnt 0; elsif rising_edge(clk) then if div_cnt (DIV/2 - 1) then div_cnt 0; mdc_s not mdc_s; else div_cnt div_cnt 1; end if; end if; end process; -- 主状态机 process(mdc_s, rst_n) begin if rst_n 0 then state IDLE; bit_cnt 0; ... elsif rising_edge(mdc_s) then case state is when IDLE if start 1 then rw_reg rw; phy_addr_reg phy_addr; reg_addr_reg reg_addr; shift_reg wr_data; bit_cnt 0; state PREAMBLE; end if; when PREAMBLE mdio_dir 1; mdio_out_s 1; if bit_cnt 31 then bit_cnt 0; state CTRL; else bit_cnt bit_cnt 1; end if; -- 其余状态省略跟Verilog一一对应 end case; end if; end process; -- 三态缓冲 mdio mdio_out_s when mdio_dir 1 else Z; end Behavioral;VHDL里值得注意的点是integer类型直接做bit_cnt不需要人工考虑位宽综合器会自动分配合理的硬件位数。但这种便利也有代价如果代码里对bit_cnt的无符号比较范围写错比如写成了when bit_cnt 100综合时会报错。好在VHDL的强类型检查会在编译阶段就拦住这类问题而不是等到仿真时才发现。4.2 Verilog与VHDL实现的关键差异项目选型时Verilog和VHDL各有侧重。我把两种实现方式的核心差异整理成一张表方便你对照参考对比维度Verilog实现VHDL实现端口声明input/output直接写在模块头简洁entity端口用port()显式声明类型需明确std_logic是std_logic_vector状态机类型通常用localparam配合整型(integer或reg[3:0])用枚举类型type state_type is(...)可读性更强三态控制assign mdio dir ? out : 1bz用when else或process内给inout赋值数据位宽需要手工定义reg[4:0]、reg[5:0]等整型不指定位宽部分场景更省事但边界检查依赖工具链综合风格自由度大写法灵活容易写出不可综合代码需要经验强类型约束压制了不自然语法综合结果通常更可控工程习惯国内外主流资料多多数IP核和开源项目首选军工、欧洲项目常见部分老核心代码存量巨大考虑到大多数FPGA团队的手头IP和demo都是Verilog为主而新项目的代码交接、维护也都绕不开Verilog所以本文主体实现我用Verilog来做。但如果你的团队有同事习惯VHDL两份完整代码对照着看互相翻译理解起来就非常顺畅了。4.3 VHDL常量定义与类型转换的实践心得VHDL里定义常量直接用constant关键字constant DIV : integer : 20; constant PREAMBLE_LEN : integer : 32;这在Verilog里对应parameter。区别在于VHDL的常量在包package里定义后可以用在多个模块里Verilog的parameter更像语言的宏作用域灵活性弱一些。实际维护VHDL老代码时经常遇到上百个常量放在一个package文件里的情况这时候改分频系数只需要动一个文件编译全工程自动更新。类型转换是VHDL开发里最常见的问题。比如从mdio总线上采回来的数据在VHDL里是std_logic类型但rd_data寄存器是std_logic_vector不能直接用位拼接运算符{}得这样写rd_data_s rd_data_s(14 downto 0) mdio;这种拼接方式跟Verilog的移位效果一样但语法上更接近数组操作。建议在团队内部统一用拼接方式来移位别用循环移位函数否则代码可读性会明显下降。5. MDIO控制器在真实项目中的集成要点5.1 如何和总线对接控制器写好了接下来要接进系统。我常用的做法是给控制器包一层总线适配器比如把AXI4-Lite接口转成控制器的start/rw/phy_addr/reg_addr/wr_data/busy/done信号。因为MDIO操作本身是慢速操作一次读写最快也要64个MDC周期在2.5MHz下就是25.6us。对AXI总线而言这个延迟已经足够让CPU在wait状态的边缘试探了。为保证CPU不超时适配器里必须加FIFO或阻塞等待逻辑别指望总线的ready信号能一直拉低。如果对接的是简单SRAM接口Cortex-M系列CPU的总线就是这样那更简单——把片选和写使能用AND门组合出start脉冲即可read数据时直接返回rd_data寄存器的值。5.2 PHY地址和寄存器地址的传播问题实际项目中一个交换芯片可能带多个SGMII/SerDes端口每个端口外接一颗PHYPHY地址通过芯片外部引脚上拉下拉电阻配置。调试时如果读不到预期的PHY ID第一请确认PHY地址匹配第二要确认MDIO总线上的上拉电阻是否到位——MDIO是OD门/三态总线线上必须有一个上拉电阻把空闲电平拉高。读操作中PHY释放总线后data线是靠上拉电阻维持高电平的如果这个电阻缺失或者阻值太大读回来的数据会全部变成0这类问题用示波器一眼就能看出来MDIO线上没有明显的脉冲翻转。5.3 与IIC接口的对比和选型思路热词里有人提到“host访问电口模块是iic接口”这涉及到项目选型问题有些PHY芯片考虑到引脚数和外围成本会提供IIC管理接口作为替代MDIO的选项。两种接口的区别在于MDIO是两线串行、基于时钟沿采样IIC也是两线但多了器件寻址和ACK机制协议更复杂。从FPGA实现角度说IIC控制器比MDIO控制器麻烦不少因为IIC需要处理从机响应、总线仲裁、时钟同步这些机制。只要PHY芯片同时提供MDIO和IIC接口我一律选MDIO——FPGA侧逻辑简单CPU侧软件配置也直接调试手段也更成熟。除非板卡空间紧张到连MDIO的上拉电阻都不想放否则不要轻易走IIC线管理PHY。6. 常见问题与排查技巧实录6.1 读回数据全为1这是刚调通MDIO时最容易看到的现象。一般原因有三个一是PHY地址不对读到了不存在的PHY二是MDIO总线上没有上拉电阻PHY返回数据时总线被拉高三是读操作时状态机的总线交接时机错了采样时PHY还没成功接管总线。排查顺序从简单到复杂先用示波器看MDC和MDIO的波形重点看TURNAROUND阶段MDIO线上的阻抗变化再确认PHY芯片地址引脚的实际电平最后检查代码里TURNAROUND状态释放总线的时机是否正确。6.2 读操作偶发错误写操作正常这种情况通常指向时序余量不足。MDC频率过高、PCB走线过长、或者FPGA侧IO的drive strength设置得太弱都可能导致读数据时采样点偏离数据有效窗口。解决办法是把MDC分频系数调大一倍比如从2.5MHz降到1.25MHz或者调整采样的时钟沿从上升沿改成下降沿再试。经验法则是没有必要追求MDC最高频率慢一点换来的可靠性收益远大于那几十微秒的性能损失。6.3 仿真通过但上板失败仿真里MDIO控制器跑得欢快无比一到板子上就歇菜最常见的坑是复位和异步信号处理。我的板级调试流程是这样的先用VIOVirtual IO或者ILA实时抓取MDC、MDIO、state、bit_cnt这些内部信号确认状态机有没有按预期跑起来然后用VIO手动触发一次读操作对比读回的数据和PHY手册上默认寄存器值最后挂上总线监视器长时间跑读操作观测是否出现偶发错误。另外必须检查复位信号MDIO控制器如果用到了MDC域的逻辑复位信号最好同步到MDC域再使用避免异步复位释放时状态机处于亚稳态。这一点在verilog代码里就要提前规划好不然现场慢慢查会查到怀疑人生。6.4 上电后PHY芯片还没就绪PHY芯片从上电到管理接口就绪通常需要几十毫秒甚至上百毫秒。如果FPGA在此时立刻发起MDIO读操作很可能得到无效数据。解决办法是在控制器外围加一个上电延时逻辑开机后先等待PHY芯片的电源和时钟都起来、管理接口就绪再允许CPU发起MDIO访问。具体延时值参考PHY芯片数据手册里的power management时序参数。如果不加这个延时你会遇到一个奇怪现象只有开机第一次读失败之后手动重读又全部正常——就是这个问题。7. 完整工程代码与验证环境建议7.1 仿真testbench的关键场景写testbench时如果只是给控制器喂一个start信号就完事那验证覆盖率会非常差。一个好的testbench至少要覆盖以下场景读操作整个过程PHY侧返回固定数据比如0xABCD检查控制器读出的rd_data是否匹配写操作整个过程PHY侧接收数据并抓取最后一拍数据对比写进去的wr_data连续读写操作检查IDLE状态回到位后能否正确响应下一个start请求复位操作在操作中途拉低复位检查状态机能否回到IDLEdone和busy信号是否正常拉低。PHY侧的仿真模型我通常写一个简单的行为模型响应读操作时在TURNAROUND后的第一个数据位开始逐个输出寄存器值响应写操作时把数据接收下来存到一个reg里供testbench检查。这样验证效率最高。7.2 可配置参数的完整设计思路把下面几个参数做成parameter会让控制器适用性大幅提升MDC分频系数DIV不同系统时钟频率下都能调到2.5MHz支持的最大PHY地址宽度现在的代码固定了5位如果将来做Clause 45地址位需要扩到更大是否支持省略preamble通过寄存器配置位控制方便在不支持省略的PHY和需要省时间的场景之间切换。再次强调preamble的省略只在确认目标PHY支持时才用默认总是发送完整的32位。这不仅是协议兼容性问题也是团队review代码时的一个明确验收标准。8. 实际项目中调试与验证的经验补充分享8.1 调试波形判读的实用技巧在ILA或者逻辑分析仪里MDIO总线数据看着是一串串脉冲不知道怎么判断对不对。我的经验是先预设一个已知的PHY寄存器比如PHY的ID寄存器地址02和03这两个寄存器的值是芯片厂商烧死的固定值最好用来验证读链路。读这两个寄存器的值跟数据手册对照两者一致说明控制器读写通路至少八九不离十了。如果连读ID都不对先别急着改代码。用示波器测量MDC的周期是否精确等于设计值再用软件模拟发一个已知数据帧在总线上看PHY是否响应。往往问题出在时钟电路或IO配置而不是MDIO逻辑本身。8.2 常见PHY寄存器速查调试MDIO控制器时有几个寄存器是必读的寄存器0Control基本控制包括软复位、速率选择、AN使能等。上电后要检查bit15是否为1表示软复位完成。寄存器1Status链路状态、AN完成状态。bit2是链路up/down信号调试网口时必须每天看好几次。寄存器2和3PHY ID High/Low厂商唯一标识用于验证读写通路是否正常的最好人选。寄存器4Auto-Negotiation Advertisement广播PHY支持的速率和双工模式配置不当时会出现协商不上的问题。调节这些寄存器时最好一步一步来每改一个bit就读回来确认一次。因为MDIO是低速串行口写入之后需要几个微秒甚至毫秒生效不要连续发多个配置命令免得后面的命令覆盖前面的效果。8.3 上拉电阻与PCB布局的工程细节最后提一个很多人不重视的硬件细节MDC和MDIO两根线的PCB布局和上拉电阻。MDC是输出时钟建议串联一个33欧姆到47欧姆的电阻视驱动器强度而定减少反射MDIO是双向线数据线上必须有一个上拉电阻阻值一般选择4.7K到10K取决于总线上挂了多少颗PHY。PHY芯片越多上拉电阻可以适当小一些但最小不要低于2.2K防止灌电流过大。布线层面MDC和MDIO最好并行走线减少二者之间的时序偏差。如果PHY芯片和FPGA距离超过5厘米那就把MDC频率降到1MHz以下这部分余量是保证现场不出隐形故障的关键。