3. TSN交换机端口PHY接口配置与链路检测

任务说明

本案例在FPGA侧实现TSN交换机端口与外部PHY芯片之间的RGMII接口配置,并完成链路状态检测、自协商使能及速率适配。核心工作包括:MDIO管理接口读写PHY寄存器、RGMII数据路径的DDR收发、链路状态轮询与速率切换。

硬件与依赖

  • FPGA开发板(如Xilinx Artix-7/Kintex-7系列),外部PHY芯片(如Marvell 88E1512或TI DP83867)
  • PHY与FPGA之间连接:RGMII(TXC/RXC、TXD[3:0]/RXD[3:0]、TX_CTL/RX_CTL)+ MDC/MDIO
  • 参考时钟:25MHz晶振输入PHY,FPGA提供125MHz(1000M)或25MHz(100M)RGMII时钟
  • 开发工具:Vivado 2020.2及以上

整体结构

FPGA RGMII TX/RX MDIO Master 外部PHY 88E1512 自协商/链路 TXC / TXD[3:0] / TX_CTL (DDR) RXC / RXD[3:0] / RX_CTL (DDR) MDC / MDIO (管理接口)

关键实现

以下代码实现MDIO读写状态机与链路检测逻辑。MDIO时钟分频至2.5MHz,PHY地址为5'h01。通过读取PHY寄存器1(BMSR)的bit2判断链路状态,bit5判断自协商完成;读取寄存器0(BMCR)的bit13/bit6获取速率信息。

module phy_ctrl #(
    parameter CLK_DIV = 50  // 125MHz / 50 = 2.5MHz MDC
)(
    input  wire        clk,
    input  wire        rst_n,
    // MDIO接口
    output reg         mdc,
    output reg         mdio_oe,
    output reg         mdio_o,
    input  wire        mdio_i,
    // 链路状态输出
    output reg  [1:0]  link_speed,   // 00:10M 01:100M 10:1000M
    output reg         link_up,
    output reg         an_done
);

    localparam PHY_ADDR = 5'h01;
    localparam REG_BMCR = 5'h00;
    localparam REG_BMSR = 5'h01;

    // 时钟分频
    reg [5:0] div_cnt;
    reg       mdc_rise, mdc_fall;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            div_cnt <= 0; mdc <= 0;
            mdc_rise <= 0; mdc_fall <= 0;
        end else begin
            mdc_rise <= 0; mdc_fall <= 0;
            if (div_cnt == CLK_DIV-1) begin
                div_cnt <= 0;
                mdc <= ~mdc;
                if (~mdc) mdc_rise <= 1;
                else      mdc_fall <= 1;
            end else
                div_cnt <= div_cnt + 1;
        end
    end

    // MDIO状态机
    localparam S_IDLE  = 3'd0, S_ST   = 3'd1, S_OP   = 3'd2,
               S_ADDR  = 3'd3, S_REG  = 3'd4, S_TA   = 3'd5,
               S_DATA  = 3'd6, S_WAIT = 3'd7;

    reg [2:0]  state;
    reg [4:0]  bit_cnt;
    reg [15:0] rd_data;
    reg [15:0] wr_data;
    reg [4:0]  reg_addr;
    reg        is_read;
    reg        start;
    reg        done;

    // 轮询控制
    reg [19:0] poll_timer;
    reg        poll_en;
    reg [1:0]  poll_step;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= S_IDLE; bit_cnt <= 0; mdio_oe <= 0;
            mdio_o <= 1; done <= 0; start <= 0;
            link_up <= 0; an_done <= 0; link_speed <= 2'b10;
            poll_timer <= 0; poll_en <= 1; poll_step <= 0;
        end else begin
            done <= 0;
            case (state)
                S_IDLE: begin
                    mdio_oe <= 0; mdio_o <= 1;
                    if (start) begin
                        bit_cnt <= 0; state <= S_ST;
                    end
                end
                S_ST: begin  // 32'b01 起始码
                    mdio_oe <= 1;
                    if (mdc_fall) begin
                        mdio_o <= (bit_cnt == 0) ? 1'b0 : 1'b1;
                        if (bit_cnt == 1) state <= S_OP;
                        bit_cnt <= bit_cnt + 1;
                    end
                end
                S_OP: begin  // 读:10 写:01
                    if (mdc_fall) begin
                        mdio_o <= is_read ? (bit_cnt==0 ? 1'b1 : 1'b0)
                                          : (bit_cnt==0 ? 1'b0 : 1'b1);
                        if (bit_cnt == 1) begin state <= S_ADDR; bit_cnt <= 0; end
                        else bit_cnt <= bit_cnt + 1;
                    end
                end
                S_ADDR: begin  // PHY地址5bit
                    if (mdc_fall) begin
                        mdio_o <= PHY_ADDR[4-bit_cnt];
                        if (bit_cnt == 4) begin state <= S_REG; bit_cnt <= 0; end
                        else bit_cnt <= bit_cnt + 1;
                    end
                end
                S_REG: begin  // 寄存器地址5bit
                    if (mdc_fall) begin
                        mdio_o <= reg_addr[4-bit_cnt];
                        if (bit_cnt == 4) begin state <= S_TA; bit_cnt <= 0; end
                        else bit_cnt <= bit_cnt + 1;
                    end
                end
                S_TA: begin  // 转向
                    if (is_read) begin
                        if (mdc_fall && bit_cnt == 0) begin
                            mdio_oe <= 0; bit_cnt <= 1;
                        end else if (mdc_fall && bit_cnt == 1) begin
                            state <= S_DATA; bit_cnt <= 0; rd_data <= 0;
                        end
                    end else begin
                        if (mdc_fall) begin
                            mdio_o <= (bit_cnt == 0) ? 1'b1 : 1'b0;
                            if (bit_cnt == 1) begin state <= S_DATA; bit_cnt <= 0; end
                            else bit_cnt <= bit_cnt + 1;
                        end
                    end
                end
                S_DATA: begin
                    if (is_read) begin
                        if (mdc_rise) begin
                            rd_data <= {rd_data[14:0], mdio_i};
                            if (bit_cnt == 15) begin
                                state <= S_WAIT; done <= 1;
                            end else bit_cnt <= bit_cnt + 1;
                        end
                    end else begin
                        if (mdc_fall) begin
                            mdio_o <= wr_data[15-bit_cnt];
                            if (bit_cnt == 15) begin
                                state <= S_WAIT; done <= 1;
                            end else bit_cnt <= bit_cnt + 1;
                        end
                    end
                end
                S_WAIT: begin
                    mdio_oe <= 0; mdio_o <= 1;
                    state <= S_IDLE;
                end
            endcase

            // 轮询调度
            if (poll_en) begin
                if (poll_timer == 20'd1_000_000) begin
                    poll_timer <= 0;
                    case (poll_step)
                        2'd0: begin  // 读BMSR
                            reg_addr <= REG_BMSR; is_read <= 1;
                            start <= 1; poll_step <= 1;
                        end
                        2'd1: begin
                            if (done) begin
                                link_up  <= rd_data[2];
                                an_done  <= rd_data[5];
                                start <= 0; poll_step <= 2;
                            end
                        end
                        2'd2: begin  // 读BMCR
                            reg_addr <= REG_BMCR; is_read <= 1;
                            start <= 1; poll_step <= 3;
                        end
                        2'd3: begin
                            if (done) begin
                                link_speed <= rd_data[13] ? 2'b10 :
                                              rd_data[6]  ? 2'b01 : 2'b00;
                                start <= 0; poll_step <= 0;
                            end
                        end
                    endcase
                end else
                    poll_timer <= poll_timer + 1;
            end
        end
    end

endmodule

RGMII数据路径要点

  • TX侧:FPGA在TXC上升沿发送TXD[3:0]低4位,下降沿发送高4位;TX_CTL同理承载TXEN与TXERR。
  • RX侧:PHY在RXC双边沿输出RXD[3:0]与RX_CTL,FPGA需用IDDR原语采样。
  • 1000M模式下RGMII时钟125MHz,100M/10M模式下时钟分别为25MHz/2.5MHz,需根据link_speed动态切换时钟源。
  • 建议在TXC/RXC路径上加入IOBUF与IDELAY/ODELAY用于时序校准。

运行与验证

  1. 在Vivado中创建工程,添加上述phy_ctrl.v,绑定引脚:mdc→PHY_MDC,mdio_o/mdio_oe/mdio_i→三态IOBUF→PHY_MDIO。
  2. 综合实现后下载bitstream,用ILA抓取link_up、an_done、link_speed信号。
  3. 插入网线后观察:link_up应在数百毫秒内拉高,an_done同步置1,link_speed与对端协商结果一致(千兆为2'b10)。
  4. 拔掉网线,link_up应拉低;更换百兆对端,link_speed应变为2'b01。
  5. 用示波器测量MDC频率应为2.5MHz,MDIO在MDC上升沿采样数据稳定。

常见问题

  • MDIO无响应:检查PHY地址是否与硬件strap一致,MDC分频是否过高。
  • 链路始终down:确认PHY复位时序(RESET_N至少保持10ms低电平),以及自协商是否被禁用。
  • RGMII数据错位:检查TXC/RXC是否加了2ns延迟(PHY内部延迟或PCB走线补偿)。