资讯动态

MDIO控制器设计实战:Verilog/VHDL双实现与Clause 22/45解析

发布时间:2026/10/5 5:37:44 来源:尧图企业网站定制
做FPGA或嵌入式网络开发的朋友应该都遇到过这种情况板子上的PHY芯片死活不通ping不通、协商不上翻遍数据手册发现关键寄存器全都停在默认值。这时候你最需要的就是一根能直接捅进PHY内部的管理总线——MDIO。这套接口标准、实现思路、以及Verilog/VHDL两种写法的完整对比就是我这篇文章想一次性讲透的东西。MDIOManagement Data Input/Output是IEEE 802.3定义的管理接口用MDC时钟和MDIO数据两根线就能读写PHY芯片内部的寄存器用来配置工作模式、查询链路状态、获取协商结果。很多项目直接用IP核但自己做一个小巧的MDIO控制器不仅省资源、可控性强而且对理解链路层和PHY之间的交互极有帮助。这篇文章我把我实际做过的一套MDIO控制器完整实现拆开讲Verilog为主VHDL对照代码可以直接拿去仿真也能改改频率参数就上板。1. MDIO控制器设计思路为什么选状态机而非计数器1.1 MDIO是干什么的一根时钟一根数据如何读写PHYMDIO接口本质上是一个双线串行管理接口一条MDC时钟线一条MDIO数据线由MAC或FPGA作为主设备主动发起操作PHY作为从设备被动响应。它做的事情非常单纯写寄存器、读寄存器。可恰恰是这套“单纯”的读写承担了几乎所有PHY初始化、掉电/上电控制、link状态轮询、协商强制、EEE配置等功能。比如常见的8211系列千兆PHY复位之后默认寄存器配置不一定满足你的硬件设计比如时钟源选择、LED灯行为、传输延迟补偿等这些都必须通过MDIO在PHY被接入网络之前配好。实际上你拿逻辑分析仪抓一颗PHY上电后的MDIO波形会看到MAC侧一帧接一帧的配置写入整个初始化序列就是靠这些看起来不起眼的寄存器操作拼出来的。我自己做这个控制器的背景很简单项目里PHY直接挂在FPGA上软件通过一个低速总线接口来触发MDIO读写我不想去依赖厂商IP也不想在硬件调试阶段干瞪眼。于是就用最典型的“状态机分频计数”结构从零手写一个干净、好移植、可综合的MDIO主控制器。1.2 Clause 22和Clause 45怎么选帧格式对比MDIO协议分成两种IEEE 802.3 Clause 22和Clause 45读PHY寄存器之前必须先搞清楚你手里的芯片支持哪一种或者是否两者都支持。Clause 22是最早的规范寄存器空间只有5位地址共32个寄存器绝大多数百兆PHY和部分千兆PHY用的就是它比如很多通用百兆PHY芯片就是Clause 22。Clause 45是为了扩展管理能力设计的它把“寄存器地址”拆成了设备地址Device Address5位和寄存器地址Register Address16位能访问更大的地址空间同时支持MMDMemory-Mapped Device访问模式。百兆时代几乎用不到千兆/万兆PHY的先进功能、EEE、TX/RX眼图测试等都依赖Clause 45。我见很多新人上来不看数据手册默认写成Clause 22结果读到一块Clause 45的PHY时读回的数据全是0xFFFF或者CRC不对。这里有一个小技巧数据手册里一般会写明“Clause 22/45 MDIO interface”兼容的PHY会明确支持两种模式。更实际的判断方法是看PHY寄存器0Clause 22的寄存器0通常是控制寄存器但Clause 45的寄存器0往往是MMD地址寄存器读出来的含义完全不同。两种帧格式的差异我整理了一张表项目Clause 22Clause 45帧头32位全1前导码32位全1前导码ST0100OP读10写01读11写00地址字段PHY地址5位 寄存器地址5位设备地址5位 寄存器地址16位TA读周期为Z0写周期为10读周期为Z0写周期为10数据16位16位典型用途基本PHY控制和状态千兆/万兆扩展管理、EEE、MMDClause 45还有一个特点同一个OP码既可以做“地址写/读”操作也可以做“数据写/读”操作因为管理和数据通路是分开的。实际编码时要特别注意OP和ST的组合不然设备地址会对不上。1.3 控制器内部结构分频、状态机、移位寄存器三层设计一个MDIO主控制器个人建议不要图省事把所有逻辑塞在一条大状态机里而是拆成三层时钟分频层、协议状态机层、数据与三态驱动层。这样每一层的职责都单一出问题也容易定位。时钟分频层负责由系统时钟生成MDC时钟。MDC并不是低速到可以随便给的信号IEEE对它有明确的频率要求100M以太网标准里MDC最大2.5MHz1000M以太网可以到12.5MHz。实测下来很多PHY对MDC并不苛刻但为了保证兼容性分频逻辑最好做成“任意分频可配”这样你换平台、换PHY、或者干脆拿SignalTap抓波形时都能灵活调整。协议状态机层是整个控制器的核心。它根据外部命令生成一帧完整的MDIO时序前导码、ST、OP、地址、TA、数据。状态机的跳转最好和MDC边沿严格对齐不然波形上会出现毛刺。数据与三态驱动层负责把并行的读写数据转换成移位数据。因为MDIO数据在MDC下降沿变化、上升沿被采样所以输出移位和输入采样都要安排在专门的边沿这是整个模块在时序上最容易出问题的地方后面会详细说。把这三层拆开之后整体结构很清晰代码可读性也高。调试的时候哪个环节有问题直接看对应的信号不用在一坨状态机里翻来翻去。2. Verilog代码逐段拆解完整实现一个可直接仿真的MDIO控制器2.1 端口定义与参数设计下面这套Verilog代码是我在实际项目中用过的版本做了两处精简一是把状态机流程固定成单次读写二是把Clause 45的扩展接口用参数预留方便按需打开。完整的模块端口如下module mdio_controller #( parameter CLK_FREQ 50_000_000, // 系统时钟频率单位Hz parameter MDC_FREQ 2_500_000, // MDC时钟频率单位Hz parameter PHY_ADDR 5d5, // 默认PHY地址可被软件覆盖 parameter CLAUSE45 0 // 0: Clause 22, 1: Clause 45 )( input wire clk, input wire rst_n, // 寄存器访问接口类似简单内存映射 input wire start, // 单次操作启动脉冲 input wire op_write, // 1: 写寄存器, 0: 读寄存器 input wire [4:0] phy_addr, // 当CLAUSE450时有效 input wire [4:0] reg_addr, // Clause 22寄存器地址 input wire [4:0] dev_addr, // Clause 45设备地址 input wire [15:0] write_data, // 写数据 output reg [15:0] read_data, // 读数据 output reg busy, // 忙标志 output reg done, // 完成脉冲 output reg mdc, // MDC时钟 inout wire mdio // MDIO数据线三态 );参数设计这块我重点解释两个点。第一个是CLK_FREQ和MDC_FREQ为什么不直接写死成常数因为项目里可能存在多种系统时钟。比如同一个控制器用在50MHz的FPGA工程和一个100MHz的工程里如果分频参数写死了换环境就要改代码。用参数定义并换算分频系数仿真和上板都能直接复用。第二个是CLAUSE45参数。很多人会把Clause 22和Clause 45写成完全不同的两个模块其实没必要。Clause 45的帧结构里只有ST、OP、地址段不一样数据段和TA段完全一致控制状态机的跳转条件即可并不需要重新设计一套状态机。我后面会讲怎么用参数切换模式这样代码量能少一半。2.2 分频设计和MDC边沿对齐MDC生成是这个模块最简单也最容易写错的地方。MDC是主设备主动驱动的时钟不是PHY反馈回来的所以它的频率、占空比、边沿位置完全由你决定。为了采样稳定MDIO输出数据必须和MDC下降沿对齐MDIO输入数据在MDC上升沿被采样这在IEEE规范里是前提。分频逻辑我用了一个计数器clk_div_cnt从0计数到DIV_CNT - 1。其中DIV_CNT CLK_FREQ / MDC_FREQ / 2因为一个MDC周期要高低电平各占一半实际操作时要取整这里50MHz分到2.5MHzDIV_CNT就是10一个MDC半周期10个系统时钟整周期20个时钟。代码如下localparam DIV_CNT CLK_FREQ / MDC_FREQ / 2; reg [$clog2(DIV_CNT)-1:0] div_cnt; wire mdc_rise, mdc_fall; always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 0; mdc 1b0; end else if (div_cnt DIV_CNT - 1) begin div_cnt 0; mdc ~mdc; end else begin div_cnt div_cnt 1b1; end end assign mdc_rise (div_cnt DIV_CNT - 1) !mdc; assign mdc_fall (div_cnt DIV_CNT - 1) mdc;这里有一个细节mdc_rise和mdc_fall不是简单的“MDC上升沿”和“MDC下降沿”它们是系统时钟域里的脉冲信号宽度只有一个系统时钟周期表示“下一个时刻MDC会翻转”。这样设计的好处是所有状态机动作都还在系统时钟域内不需要跨时钟域也不会出现组合逻辑采样MDC边沿的麻烦。为什么我不直接生成边沿因为MDC本身是寄存器输出它在div_cnt DIV_CNT-1时刻翻转翻转前后会有一定的传播延迟。如果组合逻辑去采posedge mdc会和内部状态机产生竞争。用脉冲信号代替真正边沿就能让状态机在系统时钟的上升沿统一动作逻辑上非常干净。实测下来这种写法在FPGA上时序非常稳几乎不会出现亚稳态问题。2.3 主状态机从IDLE到数据阶段的逐级跳转状态机是本模块的核心。我把MDIO帧拆成8个阶段IDLE、PRE、ST、OP、ADDR、TA、DATA、DONE。每一位数据的进退都靠位计数器bit_cnt控制bit_cnt在mdc_fall时递增表示一个MDC周期处理完一位。状态定义如下localparam IDLE 4d0; localparam PRE 4d1; // 32位全1前导码 localparam ST 4d2; // 起始码 localparam OP 4d3; // 操作码 localparam ADDR 4d4; // PHY地址 寄存器地址 localparam TA 4d5; // 总线转向周期 localparam DATA 4d6; // 16位数据 localparam DONE 4d7; // 完成状态状态跳转的核心思路每个阶段都有一个预设的位长度phase_len当bit_cnt计数到phase_len-1时跳转到下一个阶段否则留在当前阶段继续移位。always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 0; ... end else begin case (state) IDLE: begin if (start) state PRE; bit_cnt 0; end PRE: begin if (mdc_fall) begin if (bit_cnt 31) state ST; bit_cnt 0; else bit_cnt bit_cnt 1b1; end end ... endcase end end写状态机的时候有一个我自己踩过的坑状态跳转条件里混用了mdc_rise和mdc_fall。比如在TA阶段前一位还是主设备驱动后一位就要释放总线这个切换动作必须发生在mdc_fall时但读数据的采样又必须在mdc_rise时。如果把两个边沿写反仿真波形看起来是完整的上板之后就会发现读数偶尔错位。经验法则输出侧包括数据线驱动、总线释放统一在mdc_fall动作输入采样统一在mdc_rise动作。这样和PHY的时序完全吻合不需要额外调整。2.4 输出移位、总线释放和读数据采样MDIO总线是半双工的同一根线既能被主设备驱动也能被从设备驱动。因此必须有三态控制主设备说话时驱动数据线主设备等待从设备回话时数据线要释放成高阻态让PHY来驱动。我在代码里用mdio_out寄存器和mdio_oe输出使能信号来控制三态reg mdio_out; reg mdio_oe; assign mdio mdio_oe ? mdio_out : 1bz; assign mdio_in mdio;状态机在PRE、ST、OP、ADDR阶段mdio_oe始终为1输出移位寄存器把当前待发送位赋给mdio_out。到了TA阶段分两种情况读操作TA的第一位主设备输出1第二位释放总线mdio_oe0让PHY从第三位开始驱动数据写操作TA的两位都是主设备输出10后续DATA阶段继续保持输出。真正的读数据采样在DATA阶段进行。我维护一个16位的接收移位寄存器read_shift在mdc_rise时把mdio_in打进最低位然后循环左移。这样16个MDC周期后read_shift就是完整的读结果。上板调试时很多人遇到读回全0xFFFF第一反应是PHY地址不对其实更常见的是总线释放时机出了问题。如果TA阶段该释放的时候没有释放PHY的驱动信号被主设备拉死数据线一直保持高电平读回自然全是1。碰到这种问题优先用逻辑分析仪抓一下TA阶段波形看主设备是否及时释放了三态。// DATA阶段读操作与写操作的分支处理 DATA: begin if (mdc_fall) begin if (op_write_reg) begin mdio_out write_data[15 - bit_cnt]; mdio_oe 1b1; end else begin mdio_oe 1b0; // 保持释放 end if (bit_cnt 15) begin state DONE; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end if (mdc_rise !op_write_reg) begin read_shift {read_shift[14:0], mdio_in}; end end2.5 完整Verilog代码把以上各段拼接起来就是一个可综合、可仿真的完整控制器。为了不占太大篇幅我按关键部分分段呈现但所有信号定义和状态逻辑是完整连贯的。// // MDIO Master Controller (Clause 22 / Clause 45) // ----------------------------------------------------- // 特性 // - 可参数化MDC频率 // - 支持Clause 22 / Clause 45切换 // - 单次读写握手输出done脉冲 // - 符合IEEE 802.3时序下降沿输出、上升沿采样 // module mdio_controller #( parameter CLK_FREQ 50_000_000, parameter MDC_FREQ 2_500_000, parameter PHY_ADDR 5d5, parameter CLAUSE45 0 )( input wire clk, input wire rst_n, input wire start, input wire op_write, input wire [4:0] phy_addr, input wire [4:0] reg_addr, input wire [4:0] dev_addr, input wire [15:0] write_data, output reg [15:0] read_data, output reg busy, output reg done, output reg mdc, inout wire mdio ); localparam DIV_CNT CLK_FREQ / MDC_FREQ / 2; localparam IDLE 4d0, PRE 4d1, ST 4d2, OP 4d3; localparam ADDR 4d4, TA 4d5, DATA 4d6, DONE 4d7; reg [3:0] state, next_state; reg [$clog2(DIV_CNT)-1:0] div_cnt; reg [5:0] bit_cnt; reg [15:0] read_shift; reg op_write_reg; reg [15:0] write_data_reg; reg mdio_out; reg mdio_oe; wire mdio_rise, mdio_fall; wire mdio_in; assign mdio mdio_oe ? mdio_out : 1bz; assign mdio_in mdio; // MDC分频 always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 0; mdc 1b0; end else if (div_cnt DIV_CNT - 1) begin div_cnt 0; mdc ~mdc; end else begin div_cnt div_cnt 1b1; end end assign mdio_rise (div_cnt DIV_CNT - 1) !mdc; assign mdio_fall (div_cnt DIV_CNT - 1) mdc; // 主状态机 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 0; op_write_reg 1b0; write_data_reg 16d0; mdio_out 1b1; mdio_oe 1b1; read_shift 16d0; read_data 16d0; busy 1b0; done 1b0; end else begin done 1b0; case (state) IDLE: begin if (start) begin op_write_reg op_write; write_data_reg write_data; busy 1b1; state PRE; bit_cnt 0; end end PRE: begin if (mdio_fall) begin mdio_out 1b1; mdio_oe 1b1; if (bit_cnt 31) begin state ST; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end end ST: begin if (mdio_fall) begin mdio_out CLAUSE45 ? 1b0 : 1b0; if (bit_cnt 1) begin state OP; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end end OP: begin if (mdio_fall) begin if (CLAUSE45) begin mdio_out op_write_reg ? 1b0 : 1b1; end else begin mdio_out op_write_reg ? 1b1 : 1b0; end if (bit_cnt 1) begin state ADDR; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end end ADDR: begin if (mdio_fall) begin if (CLAUSE45) begin if (bit_cnt 5) mdio_out dev_addr[4 - bit_cnt]; else mdio_out reg_addr[15 - (bit_cnt - 5)]; end else begin if (bit_cnt 5) mdio_out phy_addr[4 - bit_cnt]; else mdio_out reg_addr[4 - (bit_cnt - 5)]; end if (bit_cnt (CLAUSE45 ? 20 : 9)) begin state TA; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end end TA: begin if (mdio_fall) begin if (bit_cnt 0) begin mdio_out 1b1; mdio_oe 1b1; end else if (bit_cnt 1) begin if (op_write_reg) begin mdio_out 1b0; mdio_oe 1b1; end else begin mdio_oe 1b0; // 读操作释放总线 mdio_out 1bz; end state DATA; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end end DATA: begin if (mdio_rise) begin if (!op_write_reg) read_shift {read_shift[14:0], mdio_in}; end if (mdio_fall) begin if (op_write_reg) begin mdio_out write_data_reg[15 - bit_cnt]; mdio_oe 1b1; end else begin mdio_oe 1b0; end if (bit_cnt 15) begin state DONE; bit_cnt 0; if (!op_write_reg) read_data read_shift; end else begin bit_cnt bit_cnt 1b1; end end end DONE: begin busy 1b0; done 1b1; mdio_oe 1b1; mdio_out 1b1; state IDLE; end default: state IDLE; endcase end end endmodule写这套代码时有几个值得注意的细节。ST阶段在Clause 22下是01Clause 45下是00所以我在ST里固定输出0第二位才区分。其实Clause 22的ST高电平是“不是有效帧”的标记你在那一位输出0或1对某些PHY影响不大但规范写法要严格区分避免误触发某些厂商的特有模式。OP阶段的编码也要小心。Clause 22里写操作是01读操作是10Clause 45里写操作是00读操作是11。代码里用op_write_reg判断并赋值这样的设计比在外部拼好OP码再送进来更安全因为外部总线上的组合逻辑毛刺可能直接跑到MDIO上去。3. VHDL完整代码与Verilog对比一次搞懂两种语言的差异3.1 VHDL侧完整实现实体、架构与状态机VHDL版本的代码对应上面Verilog模块功能和端口完全一致。VHDL的强类型和枚举类型在写状态机时相当舒服能减少很多因为状态编码写错导致的bug。library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity mdio_controller is generic ( CLK_FREQ : integer : 50_000_000; MDC_FREQ : integer : 2_500_000; PHY_ADDR : std_logic_vector(4 downto 0) : 00101; CLAUSE45 : boolean : false ); port ( clk : in std_logic; rst_n : in std_logic; start : in std_logic; op_write : in std_logic; phy_addr : in std_logic_vector(4 downto 0); reg_addr : in std_logic_vector(4 downto 0); dev_addr : in std_logic_vector(4 downto 0); write_data : in std_logic_vector(15 downto 0); read_data : out std_logic_vector(15 downto 0); busy : out std_logic; done : out std_logic; mdc : out std_logic; mdio : inout std_logic ); end entity; architecture rtl of mdio_controller is type state_t is (IDLE, PRE, ST, OP, ADDR, TA, DATA, DONE); signal state : state_t : IDLE; signal div_cnt : integer range 0 to 1023 : 0; signal bit_cnt : integer range 0 to 63 : 0; signal mdio_rise : boolean; signal mdio_fall : boolean; signal mdio_in : std_logic; signal mdio_out : std_logic : 1; signal mdio_oe : std_logic : 1; signal read_shift : std_logic_vector(15 downto 0); signal op_write_reg : std_logic; constant DIV_CNT : integer : CLK_FREQ / MDC_FREQ / 2; begin -- 三态缓冲 mdio mdio_out when mdio_oe 1 else Z; mdio_in mdio; -- MDC生成 process(clk, rst_n) begin if rst_n 0 then div_cnt 0; mdc 0; elsif rising_edge(clk) then if div_cnt DIV_CNT - 1 then div_cnt 0; mdc not mdc; else div_cnt div_cnt 1; end if; end if; end process; mdio_rise (div_cnt DIV_CNT - 1) and (mdc 0); mdio_fall (div_cnt DIV_CNT - 1) and (mdc 1); -- 主状态机 process(clk, rst_n) begin if rst_n 0 then state IDLE; bit_cnt 0; op_write_reg 0; mdio_out 1; mdio_oe 1; read_shift (others 0); read_data (others 0); busy 0; done 0; elsif rising_edge(clk) then done 0; case state is when IDLE if start 1 then op_write_reg op_write; busy 1; state PRE; bit_cnt 0; end if; when PRE if mdio_fall then mdio_out 1; mdio_oe 1; if bit_cnt 31 then state ST; bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; when ST if mdio_fall then mdio_out 0; if bit_cnt 1 then state OP; bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; when OP if mdio_fall then if CLAUSE45 then mdio_out not op_write_reg; else mdio_out op_write_reg; end if; if bit_cnt 1 then state ADDR; bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; when ADDR if mdio_fall then if bit_cnt 5 then if CLAUSE45 then mdio_out dev_addr(4 - bit_cnt); else mdio_out phy_addr(4 - bit_cnt); end if; else if CLAUSE45 then mdio_out reg_addr(15 - (bit_cnt - 5)); else mdio_out reg_addr(4 - (bit_cnt - 5)); end if; end if; if bit_cnt 20 then state TA; bit_cnt 0; elsif bit_cnt 9 then state TA; bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; when TA if mdio_fall then if bit_cnt 0 then mdio_out 1; mdio_oe 1; elsif bit_cnt 1 then if op_write_reg 1 then mdio_out 0; mdio_oe 1; else mdio_oe 0; mdio_out Z; end if; state DATA; bit_cnt 0; else bit_cnt bit_cnt 1; end if; end if; when DATA if mdio_rise then if op_write_reg 0 then read_shift read_shift(14 downto 0) mdio_in; end if; end if; if mdio_fall then if op_write_reg 1 then mdio_out write_data(15 - bit_cnt); mdio_oe 1; else mdio_oe 0; end if; if bit_cnt 15 then state DONE; bit_cnt 0; if op_write_reg 0 then read_data read_shift; end if; else bit_cnt bit_cnt 1; end if; end if; when DONE busy 0; done 1; mdio_oe 1; mdio_out 1; state IDLE; when others state IDLE; end case; end if; end process; end architecture;这里有一个VHDL常见的坑mdio声明为inout std_logic在生成三态时用了mdio mdio_out when mdio_oe 1 else Z。因为std_logic是九值逻辑仿真时不指定上拉会看到U或X。我个人习惯在testbench里对mdio施加H弱上拉模拟PHY不驱动时总线默认高电平的状态这样波形更贴近实际。VHDL侧用boolean做泛型会比Verilog用整数参数更直观比如CLAUSE45 : boolean : false判断时候直接if CLAUSE45 then编译期就把分支确定下来综合结果里不会有冗余逻辑。这是VHDL在重型项目里更受欢迎的原因之一泛型表达能力更强。3.2 两种语言差异工程实践中的迁移心得用两套语言写同一个控制器最大的感受是Verilog写得快但VHDL查错容易。这句话不是玄学而是两种语言设计哲学导致的必然。对比项VerilogVHDL类型系统弱类型位宽不匹配自动扩展/截断强类型位宽不匹配编译报错库引用几乎不需要额外库必须显式use库和包端口定义简单直接entity声明较长但结构清晰状态机参数或宏定义自由编码枚举类型直观安全泛型/参数parameter/localparamgeneric/constant三态处理assign 条件表达式std_logic的条件信号赋值模拟驱动自由赋值宽容类型转换频繁严格团队维护风格差异大备注易缺失风格统一自文档化强从Verilog移植到VHDL时最容易翻车的是信号赋值方向。VHDL里process内的信号赋值是最后赋值生效也就是在一个时钟沿前、后多次赋值最终只有一个值能决定寄存器内容。这和Verilog的阻塞/非阻塞赋值不太一样。比如在process(clk, rst_n)里对mdio_out在不同状态下分别赋值VHDL会被综合成多路选择器不会像Verilog那样产生意外锁存器但前提是每个分支都要给出明确的赋值目标否则综合工具会推断出锁存器。另一个迁移上的差异在数组和切片。VHDL里reg_addr(15 - (bit_cnt - 5))这种写法在追求严谨的同时也带来一定的麻烦索引必须保证在合法范围内但integer和std_logic_vector混用时经常需要类型转换。建议在VHDL侧尽量把位计数信号定义成integer用整数运算控制索引比拼接unsigned再用to_integer转换更简洁。如果你团队里既有Verilog又有VHDL工程师我的建议是状态机部分用VHDL枚举数据通路部分用Verilog的简洁赋值两套代码通过接口模块对接。实际项目里混合语言综合完全没问题关键是接口信号的位宽、方向、极性要提前约定好不然编译过了、仿真挂了、上板更乱。4. 仿真、上板与常见问题排查让MDIO控制器稳定跑起来4.1 testbench搭建与PHY仿真模型控制器写好之后第一件事不是上板而是在仿真里把时序跑通。MDIO这种低速接口虽然逻辑简单但如果你直接对空载总线仿真会发现读操作根本读不到数据因为没有任何设备去驱动mdio。所以testbench里必须写一个简易的PHY行为模型。最简单的PHY模型只需要做两件事收到写帧时把数据存到寄存器堆收到读帧时把对应寄存器的值驱动到mdio上。可以不用完整解析帧只需要根据主设备的MDC时钟在TA之后按位送回数据就能验证控制器的时序是否正确。// 简化PHY模型只响应寄存器0x10的读写 reg [15:0] phy_reg [31:0]; wire mdio_oe; reg mdio_tb_out; reg mdio_tb_oe; assign mdio mdio_oe ? mdio_out : (mdio_tb_oe ? mdio_tb_out : 1bz); assign mdio_in mdio; initial begin phy_reg[0] 16h3900; phy_reg[1] 16h782d; phy_reg[16] 16hdead; end在testbench里给mdio加上拉是必须的pullup(mdio)否则总线在释放状态下是z仿真波形里会显示成高阻读数自然不正确。上拉之后总线默认高电平符合MDIO规范。实测中我发现很多新手在这栽跟头仿真波形里读数据全是1第一反应是状态机有问题其实只是testbench少了一个pullup。4.2 常见问题与对策速查表我把做这个控制器过程中遇到的最典型的几个问题列一张表方便你直接对照排查。现象最常见原因排查顺序仿真报“failure to obtain a verilog simulation license”仿真工具License异常或路径问题1. 重启License服务 2. 检查LM_LICENSE_FILE3. 确认工具版本和工程版本匹配读回数据全是0xFFFF总线未释放/上拉缺失/PHY地址错误1. 抓TA阶段mdio_oe 2. 确认testbench有pullup 3. 确认phy_addr读回数据全0上拉缺失或PHY模型未正确驱动1. 检查PHY模型TA后是否驱动总线 2. 检查采样边沿写操作成功但读操作数据错位采样边沿用错在下降沿采了数据1. 改为mdc_rise时采mdio_in状态机卡在PRE不跳转分频参数导致mdc_fall脉冲不进1. 检查DIV_CNT是否为2的整数次幂 2. 检查mdc_fall信号波形上板后第一次读写失败重复读写正常复位后MDIO总线未稳定或PHY未就绪1. 增加控制器启动延迟 2. PHY复位后等待一段时间再访问PHY地址正确但读写老出错MDC频率超过该PHY支持上限1. 降低MDC到2.5MHz重试 2. 查看PHY数据手册支持频率上面最值得强调的一条是License问题。很多人写的代码本身没问题却因为仿真环境问题浪费半天时间。Vivado或Quartus里遇到failure to obtain a verilog simulation license先确认自己的工程仿真版本和安装版本是否一致再检查系统环境变量里有没有冲突的License路径。这类问题几乎不影响逻辑设计但排查起来非常恶心建议项目一开始就把仿真环境固定下来不要中途换工具版本。4.3 上板调试时的一些实战经验仿真全部通过之后上板调试又有一批新的坑等着你。这些经验没写在代码注释里也没出现在数据手册里但能省下大把时间。第一用逻辑分析仪抓MDIO波形时尽量抓完整的地址和数据段不要只抓开头几位。MDIO虽然只有两根线但一帧数据包含前导码、地址、TA、数据任何一个环节错位波形看起来都会“很对但不对”。我惯用的写法是设置SignalTap或ILA采样深度为MDC的至少32倍然后把mdio和mdc一起抓下来用光标功能对照帧格式逐段检查。这样定位问题的速度比一遍遍猜原因快得多。第二写操作后马上读同一个寄存器是排查PHY寄存器配置是否生效的最快方法。如果写进去的值和读出来的不一致先看是不是这个寄存器本身写1清0或要求特定访问方式然后再怀疑时序问题。很多PHY的状态寄存器是只读的配置寄存器是只写的你读一个只写寄存器当然读到默认值这属于正常行为。第三多PHY环境下建议给控制器增加一个phy_addr的可编程接口而不只是编译期参数。因为我遇到过客户板子在调试时突然换PHY地址跳线如果地址只能通过重编译修改效率太低了。把地址做成寄存器配置项比做参数更灵活。这算是一个“面向未来”的设计。第四关于PHY复位后的等待时间耐性要足。几乎所有PHY在复位之后都需要一段稳定的准备时间短则几毫秒长则几十毫秒这个时间内MDIO接口可能完全不响应。控制器里加一个复位后延时计数器等时间到了再允许start信号可以避免第一笔读写就失败。我经手的项目里至少有一半的“偶尔读写失败”问题根因都是复位时序没处理好。第五如果有条件优先使用超过16位深度的逻辑分析仪采样把start、busy、done这些控制信号和MDIO波形一起抓。这样能直接把软件层的读写请求和硬件波形关联起来。软件以为写了一次寄存器其实MDIO上发了两帧这通常是总线接口信号毛刺导致start被检测到多次。解决办法是让start信号严格打两拍同步并只在IDLE状态接收我实测能避免绝大多数重复触发问题。我个人做完这套MDIO控制器后最大的体会是管理总线这种东西看着简单但真正要稳定跑起来考验的并不是协议理解而是对时序边沿、总线释放、复位时序这些细节的把控。Verilog版和VHDL版代码我都保留在项目里后续如果要增加Clause 45的增强管理、增加自动重试、或者接一个寄存器配置队列都可以在这套骨架上继续做。把基础打牢了后面扩展起来会顺手很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑