本帖最后由 UT发布 于 2025-4-3 15:00 编辑
软件版本:TD_5.6.4_Release_97693
操作系统: WIN10 64bit
硬件平台:适用安路 (Anlogic)FPGA
1 以太网 PHY 芯片介绍 使用以太网协议进行数据传输时,经常需要将数据进行编解码和数模转换才能让其在物理介质(网线)上传输,这些功能被定义在在 OSI 模型中的物理层中,通常 FPGA 芯片不具备这样的功能,这就需要使用外部的 PHY 芯片来完成这些功能。常见的千兆以太网 PHY 芯片有 YT8531 , RTL8211 , 88E1518 等, YT8531 芯片的结构功能框图如下图所示。
以太网 MAC 层通过 RGMII 接口和 PHY 芯片的 PCS 层连接, PCS ( Physical Coding Sublayer ,物理编码子层)主要完成数据的编码 / 解码、加扰 / 解扰。 PMA ( Physical Medium Attachment ,物理媒介子层)在发送端通过 DAC 模块完成数据的数模转换,在接收端具有时钟恢复、均衡器、 ADC 等功能模块。在 PHY 芯片上有一组内部寄存器的配置接口 MDIO ( Management Data Input/Output Interface )和 MDC ,通过一定的时序规范可以读取 PHY 芯片内部寄存器的配置信息,或者将配置信息写入 PHY 芯片中。 LED 控制器可以通过控制 LED 的亮灭指示当前以太网协商的速率和工作状态。
PHY 芯片的一些功能需要通过引脚的硬件连接来配置, YT8531 的引脚功能如下图所示。
PHYAD 引脚控制 PHY 芯片的物理地址,与 IIC 总线类似,一组 MDIO 总线可以连接多个设备,每个设备都需要设置独立的地址,主机利用这个地址对不同的设备进行访问。通过 4.7 千欧的电阻对 TXDLY 和 RXDLY 进行上拉或下拉,可以控制 PHY 芯片是否对发送时钟和接收时钟进行延时,对 1000Mbps 速率,上拉会给时钟端 2ns 延时,以让时钟的边沿处于数据的中心,更容易采集到稳定的数据。 CFG_EXT 引脚控制是否开启芯片内部的 LDO 电源供电, CFG_LDO0 和 CFG_LDO1 引脚控制 IO 电压标准。
一般 PHY 芯片仅通过硬件连接配置即可正常使用,若还需要修改其它功能如速率、半双工等,需要使用软件配置的方法,即通过 MDIO 接口配置 PHY 寄存器。 PHY 寄存器的地址空间为 5 位, 802.3 协议中规定了地址为 0-15 的寄存器功能,其余寄存器可由厂商自定义。寄存器 0 为 PHY 控制寄存器,该寄存器的功能如下图所示。
Reset :软件复位,将位 15 设置为 1 来复位 PHY 芯片,复位过程中该位保持为 1 ,复位完成后自动清零,复位完成前 PHY 芯片不接受写入寄存器的操作。
Loopback :位 14 默认为 0 ,当设置为 1 时 PHY 芯片为回环模式,从 MAC 发送过来的数据会通过 PCS 端直接回环到 MAC 接收端,通常应用于故障诊断中。
Autoneg_En :位 12 为自动协商使能,设置为 1 时端口的工作模式通过和对端进行自动协商来确定,设置为 0 时通过其它位的相应配置来确定。
Speed_Selection :位 13 和位 6 联合控制端口的速率,前提是自动协商关闭,该设置才起作用。位 13 为低位,位 6 为高位, Speed_Selection 设置为 2 ’b00 时,端口速率为 10Mbps ,设置为 2 ’b01 时,端口速率为 100Mbps ,设置为 2 ’b10 时,端口速率为 1000Mbps , 2 ’b11 保留。
Duplex_Mode :位 8 控制端口是全双工模式还是半双工模式,需要自动协商关闭该设置才起作用。
Power_Down :位 11 设置为 1 时, PHY 芯片掉电,当从掉电中恢复时,会进行软件复位和自动协商。
通过配置寄存器 0 就能将 PHY 配置成我们想要的工作状态,其它寄存器的功能可以在手册中查看。
2 MDIO 接口设计
2.1 MDIO 时序介绍 MDIO 接口在协议 802.3 中定义,主要用于以太网的 MAC 和 PHY 之间, MAC 层器件通过 MDIO 配置 PHY 芯片的工作模式或者获取当前的工作状态。 MDIO 又称为 SMI ( Serial Management Interface ,串行管理接口),由 MDIO 数据线和 MDC 时钟线两条信号线组成,是串行半双工的数据接口,其接口通信协议规范如下图所示。
Preamble : 32 位前导码,连续发送 32 位高电平用于 PHY 芯片同步。
ST : 2 ’b01 作为数据开始的标志。
OP : 2 位操作码, 2 ’b10 时表示读操作, 2 ’b01 时表示写操作。
PHYAD : PHY 硬件地址,高 2 位默认为 2 ’b00 ,低 3 位由 PHY 芯片引脚的上下拉控制,通过该字段决定和哪个 PHY 芯片通信。
REGAD :寄存器地址,用于设置该次操作的寄存器对象。
TA :空闲时间,防止总线上产生竞争,在写操作时, FPGA 驱动信号线传输 2 ’b10 。读操作时, FPGA 释放总线,将 MDIO 驱动成高阻态,在传输第一个比特时, PHY 芯片也驱动信号线为高阻态,传输第二个比特时, PHY 芯片接过总线控制权,将信号线驱动为低电平。
DATA : 16 位数据,写操作时由 FPGA 驱动,读操作时由 PHY 芯片驱动。
以 PHY 芯片硬件地址为 0x01 为例, MDIO 接口的读写时序如下图所示。
MDC 时钟由 FPGA 侧驱动,时钟下降沿更新数据,上升沿采样数据。当总线处于空闲状态或一次读写操作完成时, FPGA 和 PHY 芯片都必须释放总线,即将 MDIO 信号线驱动为高阻态。
2.2 MDIO 程序设计
MDIO 驱动器主要包含 MDIO 收发数据状态机、 MDC 时钟分频器、移位发送模块、移位接收模块、总线忙控制模块。 MDIO 和 MDC 的输出逻辑和时序通过状态机控制。
接口信号功能如下:
IO_phy_mdio 为 MDIO 双向数据线。
O_phy_mdc 为 MDC 时钟线。
I_reg_addr 为操作的寄存器地址。
I_phy_addr 为 PHY 的物理地址。
I_wr_data 为写入寄存器的数据。
O_rd_data 为从寄存器读出的数据。
I_mdio_mode 为总线的操作模式,置 1 时为写操作,置 0 时为读操作。
I_mdio_req 为握手请求信号。
O_mdio_busy 为握手响应信号,当空闲状态接收到请求时,该信号拉高,直到一次操作完成后,该信号拉低,等待下一次握手请求信号。
IDLE :空闲状态,状态机处于空闲状态时,将总线置为高阻态,当接收到 REQ 请求时,开始一次操作,进入 PREAMBLE 状态。
PREAMBLE :发送前导码,发送 32bit 的逻辑 1 ,用于同步,发送完成后进入 INFO 状态
INFO :发送开始标志、操作码、物理地址、寄存器地址共 14bit 数据,发送完成后进入 TA 状态。
TA : 2 个时钟周期用来转换总线控制权。
RD_DATA :将总线上的串行数据转成 16bit 并行数据。
WR_DATA :将需要写入寄存器的数据由并行转成串行输出。
PAUSE :一次操作完成后暂停一段时间间隔,才能允许下一次操作。
module uimdio_ctrl#(
parameter DIV_NUM = 25
)
( input wire I_clk ,//主时钟
input wire I_rstn ,//复位,低有效
input wire [4:0] I_phy_addr ,//硬件地址
input wire [4:0] I_reg_addr ,//寄存器地址
input wire I_mdio_req ,//操作请求
input wire I_mdio_mode ,//操作模式,0write,1read
input wire [15:0] I_wr_data ,//写寄存器数据
output reg [15:0] O_rd_data ,//读寄存器数据
output wire O_mdio_busy ,//操作忙标志
output reg O_mdio_done ,//读完成标志
output wire O_phy_mdc ,//MDIO时钟线
inout wire IO_phy_mdio //MDIO数据线
);
localparam IDLE = 0;//空闲
localparam PREAMBLE = 1;//前导码
localparam INFO = 2;//START + OP + PHYADDR + REGADDR
localparam TA = 3;//控制权转换
localparam WR_DATA = 4;//写寄存器
localparam RD_DATA = 5;//读寄存器
localparam PAUSE = 6;//两次操作间隔
reg [2:0] cur_st ;
reg [2:0] nxt_st ;
reg [5:0] st_cnt ;
reg div_clk ;
reg [15:0] div_cnt ;
reg mdio_out ;
reg mdio_en ;
assign O_mdio_busy = ~(cur_st == IDLE);
assign O_phy_mdc = ~div_clk;//时钟反转,数据在时钟的下降沿跳变
assign IO_phy_mdio = mdio_en ? mdio_out : 1'bz;
//时钟分频计数器
always@(posedge I_clk or negedge I_rstn) begin
if(!I_rstn) begin
div_cnt <= 0;
div_clk <= 0;
end
else if(div_cnt < DIV_NUM - 1) begin
div_cnt <= div_cnt + 1;
div_clk <= div_clk;
end
else begin
div_cnt <= 0;
div_clk <= ~div_clk;
end
end
//状态机计数器
always@(posedge div_clk or negedge I_rstn) begin
if(!I_rstn)
st_cnt <= 0;
else if(nxt_st != cur_st)
st_cnt <= 0;
else
st_cnt <= st_cnt + 1;
end
//三段式状态机
always@(posedge div_clk or negedge I_rstn) begin
if(!I_rstn)
cur_st <= IDLE;
else
cur_st <= nxt_st;
end
always@(*) begin
if(!I_rstn)
nxt_st <= IDLE;
else begin
case(cur_st)
IDLE:begin
if(I_mdio_req)
nxt_st <= PREAMBLE;
else
nxt_st <= IDLE;
end
PREAMBLE:begin
if(st_cnt == 'd31)
nxt_st <= INFO;
else
nxt_st <= PREAMBLE;
end
INFO:begin
if(st_cnt == 'd13)
nxt_st <= TA;
else
nxt_st <= INFO;
end
TA:begin
if(st_cnt == 'd1)
if(I_mdio_mode)
nxt_st <= WR_DATA;
else
nxt_st <= RD_DATA;
else
nxt_st <= TA;
end
WR_DATA:begin
if(st_cnt == 'd15)
nxt_st <= PAUSE;
else
nxt_st <= WR_DATA;
end
RD_DATA:begin
if(st_cnt == 'd16)
nxt_st <= PAUSE;
else
nxt_st <= RD_DATA;
end
PAUSE:begin
if(st_cnt == 'd31)
nxt_st <= IDLE;
else
nxt_st <= PAUSE;
end
default:begin
nxt_st <= IDLE;
end
endcase
end
end
always@(posedge div_clk or negedge I_rstn) begin
if(!I_rstn)
mdio_out <= 0;
else if(cur_st == PREAMBLE)
mdio_out <= 1;
else if(cur_st == INFO) begin
case(st_cnt)
'd0 :mdio_out <= 0;
'd1 :mdio_out <= 1;
'd2 :mdio_out <= I_mdio_mode ? 0 : 1;
'd3 :mdio_out <= I_mdio_mode ? 1 : 0;
'd4 :mdio_out <= I_phy_addr[4];
'd5 :mdio_out <= I_phy_addr[3];
'd6 :mdio_out <= I_phy_addr[2];
'd7 :mdio_out <= I_phy_addr[1];
'd8 :mdio_out <= I_phy_addr[0];
'd9 :mdio_out <= I_reg_addr[4];
'd10 :mdio_out <= I_reg_addr[3];
'd11 :mdio_out <= I_reg_addr[2];
'd12 :mdio_out <= I_reg_addr[1];
'd13 :mdio_out <= I_reg_addr[0];
endcase
end
else if(cur_st == TA) begin
case(st_cnt)
'd0 :mdio_out <= I_mdio_mode ? 1 : 1'bz;
'd1 :mdio_out <= I_mdio_mode ? 0 : 1'bz;
endcase
end
else if(cur_st == WR_DATA)
mdio_out <= I_mdio_mode ? I_wr_data[15 - st_cnt] : 0;
else
mdio_out <= 0;
end
always@(posedge div_clk or negedge I_rstn) begin
if(!I_rstn)
mdio_en <= 0;
else if(cur_st == IDLE || cur_st == PAUSE)
mdio_en <= 0;
else if(cur_st == TA)
mdio_en <= I_mdio_mode ? 1 : 0;
else if(cur_st == PREAMBLE)
mdio_en <= 1;
else
mdio_en <= mdio_en;
end
always@(posedge div_clk or negedge I_rstn) begin
if(!I_rstn)
O_mdio_done <= 0;
else if(cur_st == RD_DATA && st_cnt == 'd16)
O_mdio_done <= 1;
else
O_mdio_done <= 0;
end
//MDIO读数据,使用mdc时钟采样
always@(posedge O_phy_mdc or negedge I_rstn) begin
if(!I_rstn)
O_rd_data <= 0;
else if(cur_st == RD_DATA && st_cnt >= 1)
O_rd_data <= {O_rd_data[14:0], IO_phy_mdio};
else
O_rd_data <= O_rd_data;
end
endmodule 复制代码
为了方便操作 MDIO 驱动器的读写,需要增加一级模块封装,该模块完成对请求信号及操作类型的控制,来对 PHY 寄存器进行配置。在 PHY 寄存器配置完成后,通过按键可以进行读操作,将寄存器的配置信息读取出来。
在上电后等待一定的时间间隔,释放复位,通过握手信号控制驱动器的工作状态,当 busy 信号为 0 时,拉高 req 信号,直到 busy 信号为高,代表驱动器响应成功,直到 busy 再次拉低,进行下一次操作。通过计数器控制写操作执行的次数,当达到指定次数时,寄存器配置完成,拉高 cfgdone 信号,表示不再继续进行写操作。
第一次写操作在 bit11 的位置写入 1 ,让 PHY 芯片掉电,第二次写操作再写入速度模式和全双工模式。
module uiphy_cfg#(
parameter SPEED = 2'b01,
parameter CLK_FREQ = 32'd25_000_000
)
(
input wire I_clk ,
input wire I_rstn ,
input wire I_keycap ,
output wire O_phyrst ,
output wire O_phy_mdc ,
inout wire IO_phy_mdio
);
localparam CFG_NUM = 3;
localparam IDLE = 0;
localparam REQ = 1;
localparam DONE = 2;
reg [23:0] cnt;
reg [5:0] index;
reg [23:0] gap_cnt;
reg [1:0] state;
reg cfgdone;
reg mdio_req;
reg mode;
wire [4:0] reg_addr;
wire [15:0] rd_data;
reg [15:0] wr_data;
wire mdio_busy;
wire mdio_done;
wire start_lock;
assign start_lock = cnt == 1250_000;
assign reg_addr = cfgdone ? 5'h0 : 5'h0;
always@(posedge I_clk or negedge O_phyrst) begin
if(!O_phyrst)
cnt <= 0;
else if(cnt < 1250_000)
cnt <= cnt + 1'b1;
else
cnt <= cnt;
end
always@(posedge I_clk or negedge O_phyrst) begin
if(!O_phyrst)
state <= IDLE;
else if(start_lock) begin
case(state)
IDLE:begin
if(cfgdone == IDLE)
state <= REQ;
else if(cfgdone && I_keycap)
state <= REQ;
else
state <= IDLE;
end
REQ:begin
if(mdio_busy)
state <= DONE;
else
state <= REQ;
end
DONE:begin
if(!mdio_busy)
state <= IDLE;
else
state <= DONE;
end
default:begin
state <= IDLE;
end
endcase
end
end
always@(posedge I_clk or negedge O_phyrst) begin
if(!O_phyrst)
mdio_req <= 0;
else if(mdio_req && mdio_busy)
mdio_req <= 0;
else if(state == REQ)
mdio_req <= 1;
else
mdio_req <= 0;
end
always@(posedge I_clk or negedge O_phyrst) begin
if(!O_phyrst)
mode <= 0;
else if(state == REQ && cfgdone == 1)
mode <= 0;
else if(state == REQ)
mode <= 1;
else
mode <= mode;
end
always@(posedge I_clk or negedge O_phyrst) begin
if(!O_phyrst)
cfgdone <= 0;
else if(index == CFG_NUM - 1 && !mdio_busy)
cfgdone <= 1;
end
always@(posedge I_clk or negedge O_phyrst) begin
if(!O_phyrst)
index <= 0;
else if(state == DONE && !mdio_busy && !cfgdone)
index <= index + 1;
else
index <= index;
end
always@(*) begin
case(index)
0:wr_data <= 16'h0800;
1:wr_data <= {2'b00, SPEED[0], 6'b0000_10, SPEED[1], 6'b0000_00};
2:wr_data <= {2'b10, SPEED[0], 6'b0000_10, SPEED[1], 6'b0000_00};
default:wr_data <= 16'b8000;
endcase
end
uiphyrst#(
.CLK_FREQ (CLK_FREQ)
)
uiphyrst
(
.I_CLK (I_clk ),
.I_rstn (I_rstn ),
.I_phyrst (I_rstn ),
.O_phyrst (O_phyrst )
);
uimdio_ctrl #(
.DIV_NUM (50 )
)
uimdio_ctrl
(
.I_clk (I_clk ),
.I_rstn (O_phyrst ),
.I_phy_addr (5'b00100 ),
.I_reg_addr (reg_addr ),
.I_mdio_req (mdio_req ),
.I_mdio_mode (mode ),
.I_wr_data (wr_data ),
.O_rd_data (rd_data ),
.O_mdio_busy (mdio_busy ),
.O_mdio_done (mdio_done ),
.O_phy_mdc (O_phy_mdc ),
.IO_phy_mdio (IO_phy_mdio )
);
endmodule 复制代码
`timescale 1ns/1ps
module cfg_tb;
reg clk,rst;
wire phyrst,phy_mdc,phy_mdio;
pullup(phy_mdio);
uiphy_cfg #(
.SPEED(2'b01),
.CLK_FREQ(32'd25_000_000)
) inst_uiphy_cfg (
.I_clk (clk),
.I_rstn (~rst),
.O_phyrst (phyrst),
.O_phy_mdc (phy_mdc),
.IO_phy_mdio (phy_mdio)
);
always #20 clk = ~clk;
initial begin
clk = 0;
rst = 1;
#200;
rst = 0;
end
endmodule 复制代码
通过 modelsim 进行仿真验证,抓取 mdio_req 为高电平的时间段,可见 mdio_req 拉高后, MDIO 驱动器将 mdio_busy 也拉高进行响应,当 mdio_busy 拉高时, mido_req 立即拉低,握手成功。
mdio 信号线上的数据如下图所示,空闲时总线为高阻态,在 mdio_busy 拉高后,进行写操作,总线先发送 32bit 的逻辑 1 ,接着按顺序发送开始标志 2 ’b01 、操作码 2 ’b01 、物理地址 5 ’b00100 、寄存器地址 5 ’b00000 、 TA2 ’b10 共 16bit 数据,然后发送数据 16 ’h0800 ,全部发送完成后,总线置为高阻态。
通过 MDIO 将 PHY 芯片的工作模式设置为全双工、 100Mbps 速率,在 PC 中的“网络和 Internet ”设置中查看以太网的配置信息,可以看到链接速度为 100Mbps ,证明 PHY 芯片配置成功。
然后在程序中设置按键控制读寄存器 0 操作,通过 chipwatcher 抓取波形,上板验证效果如下图所示。读出寄存器 0 的配置信息为 16 ’h2100 ,即为写入的配置信息。
3 RGMII 接口设计
3.1 RGMII 接口时序
在 2.2 节我们对 RGMII 接口的功能及引脚做了介绍, RGMII 接口的时序如下图所示。
在普通模式下,数据在时钟的边沿变化,如果不做额外处理,数据无法被稳定采样,所以 PHY 芯片还提供了延迟模式来保证数据在稳定的区间内被采样。发送端或接受端被设置成延时模式时,时钟在 PHY 芯片内部会添加 2ns 的延时,让时钟的边沿对齐数据有效区间的中心。延时模式通过 PHY 芯片的引脚上下拉控制。
RGMII 接口是双沿采样的,每个时钟周期传输一个字节的数据。传输数据时,在时钟上升沿传输一个字节的低 4bit ,在时钟下降沿传输一个字节的高 4bit ,所以千兆速率下 RGMII 接口的采样时钟为 125MHz ,而接口速率为 1000Mbps 。 FPGA 内部逻辑难以处理双沿数据,需要在接收引脚或发送引脚的 IOB 上将双沿数据转换成单沿数据或将单沿数据转换成双沿数据, IOB 中的 IDDR 和 ODDR 可以用来实现这些功能。
PH1_LOGIC_IDDR 是安路 PH1 系列 FPGA IO 上将双沿采样时钟数据变换为单沿数据的单元。具有数据对齐的双速率 D 触发器,并具有同步 / 异步复位功能。可以将输入的 DDR 数据转为 1 位 SDR 数据同步到内部时钟网络。
该单元的引脚功能如下:
clk :采样时钟。
d : IO 上输入的 DDR 数据。
rst :复位信号,高有效。
q0 : 1 位上升沿数据输出。
q1 : 1 位下降沿数据输出。
该单元可以配置成 PIPED 模式或 NONE 模式, PIPED 模式下时序如下图所示。
NONE 模式下时序图如下图所示。
工作在 PIPED 模式下时, q0 和 q1 对应一个完整字节的数据,所以在对 RGMII 接口采样时使用 PIPED 模式。
PH1_LOGIC_ODDR 时 FPGA IO 上将内部单沿数据变换为双沿数据后输出的单元,具有同步 / 异步复位功能。可以将数据双倍率输出到 IO 口上。
该单元的引脚功能如下:
clk :同步时钟。
d0 : 1 位上升沿输入数据。
d1 : 1 位下降沿输入数据。
rst :复位信号,高有效。
q : 1 位 DDR 双沿输出数据。
该单元的工作时序如下图所示。
3.2 RGMII 模块设计 RGMII 接口输入输出模块 rgmii_interface 的代码如下:
module rgmii_interface (
// 指示当前运行速度为10/100
input speed_10_100, //设置当前的运行速率
//RGMII发送时序调整,当PHY 接收来自FPGA的TX信号,没有使用内部2ns延迟情况需要在fpga端设置2ns延迟
input gmii_tx_clk_d,
input gmii_tx_reset_d,
// 以下端口是RGMII物理接口:这些端口将位于FPGA的引脚上
output [3:0] rgmii_txd,
output rgmii_tx_ctl,
output rgmii_txc,
input [3:0] rgmii_rxd,
input rgmii_rx_ctl,
input rgmii_rxc,
// 以下端口连接到 TEMAC核 的 内部GMII接口模块
input gmii_tx_reset,
input gmii_tx_clk, // ggmii_tx_clk: 125mhz
input [7:0] gmii_txd,
input gmii_tx_en,
input gmii_tx_er,
input gmii_rx_reset,
output gmii_rx_clk, //output: 125mhz(1gbps) 25mhz(100mbps) 2.5mhz(10mbps)
output reg [7:0] gmii_rxd,
output reg gmii_rx_dv,
output reg gmii_rx_er,
output gmii_crs,
output gmii_col,
// 以下信号为RGMII状态信号
output reg link_status,
output reg [1:0] clock_speed,
output reg duplex_status
);
//----------------------------------------------------------------------------
// 模块 内部 信号
//----------------------------------------------------------------------------
reg [3:0] gmii_txd_falling; // gmii_txd信号在gmii_tx_clk的下降沿锁存。
wire rgmii_txc_odelay; // RGMII接收器时钟ODDR输出.
wire rgmii_tx_ctl_odelay; // RGMII控制信号ODDR输出.
wire [3:0] rgmii_txd_odelay; // RGMII数据ODDR输出.
wire rgmii_tx_ctl_int; // 内部RGMII传输控制信号.
wire rgmii_rx_ctl_delay;
wire [3:0] rgmii_rxd_delay;
wire rgmii_rx_ctl_reg; // 内部RGMII接收器控制信号.
reg tx_en_to_ddr;
wire gmii_rx_dv_reg;
wire gmii_rx_er_reg;
wire [7:0] gmii_rxd_reg;
wire inband_ce; //RGMII带内状态寄存器 使能输出信号
wire rgmii_rxc_int;
//==============================================================================
// RGMII 发送逻辑
//==============================================================================
//----------------------------------------------------------------------------
// RGMII 发送器时钟管理:rgmii_txc
//----------------------------------------------------------------------------
// 产生 rgmii_txc 时钟.
PH1_LOGIC_ODDR rgmii_txc_ddr
(
.q (rgmii_txc_odelay), //output 125mhz(1gbps) 25mhz(100mbps) 2.5mhz(10mbps)
.clk (gmii_tx_clk_d),
.d0 (1'b1),
.d1 (1'b0),
.rst (gmii_tx_reset_d)
);
assign rgmii_txc = rgmii_txc_odelay;
//---------------------------------------------------------------------------
// RGMII 发送逻辑 : rgmii_txd
//---------------------------------------------------------------------------
// 1Gbps gmii_txd 8位有效; rgmii_txc双沿使能发送8位
// 10/100Mbps gmii_txd 仅低四位有效,rgmii_txc双沿使能重复发送低四位
// 1Gbps时,125mhz的rgmii_txc,上升沿发送低四位,下降沿发送高四位。 一个时钟周期8bit, 125*8=1Gbps
// 100Mbps时,25mhz的rgmii_txc,上升沿发送低四位,下降沿发送低四位。 相当于一个时钟周期只发一个4bit, 25*4=100mbps
// 10Mbps时同100mbps,数据有效位在每byte的低四位,虽然一个时钟周期重复发低四位,相当于一个时钟周期只发一个低4位。
always @ (speed_10_100, gmii_txd)begin
if (speed_10_100 == 1'b0) // 1Gbps gmii_txd 8位有效
gmii_txd_falling <= gmii_txd[7:4];
else // 10/100Mbps gmii_txd高四位无效,rgmii_txc双沿使能发送低四位
gmii_txd_falling <= gmii_txd[3:0];
end
genvar i;
generate for (i=0; i<4; i=i+1)begin : txdata_out_bus
PH1_LOGIC_ODDR rgmii_txd_out
(
.q (rgmii_txd_odelay[i]),
.clk (gmii_tx_clk),
.d0 (gmii_txd[i]),
.d1 (gmii_txd_falling[i]),
.rst (gmii_tx_reset)
);
assign rgmii_txd[i] = rgmii_txd_odelay[i];
end
endgenerate
//---------------------------------------------------------------------------
// RGMII 发送逻辑 : rgmii_tx_ctl
//---------------------------------------------------------------------------
// 编码 rgmii ctl 信号
assign rgmii_tx_ctl_int = gmii_tx_en ^ gmii_tx_er;
// 需要逻辑以确保 错误信号 将在整个时钟相位内,将tx_ctl发送为低电平
always @(speed_10_100 or gmii_tx_en or gmii_tx_er)begin
if (speed_10_100)
tx_en_to_ddr = gmii_tx_en & (!gmii_tx_er );
else
tx_en_to_ddr = gmii_tx_en;
end
// oDDR primitive
PH1_LOGIC_ODDR ctl_output
(
.q (rgmii_tx_ctl_odelay),
.clk (gmii_tx_clk),
.d0 (tx_en_to_ddr),
.d1 (rgmii_tx_ctl_int),
.rst (gmii_tx_reset)
);
assign rgmii_tx_ctl = rgmii_tx_ctl_odelay;
//==============================================================================
// RGMII 接收逻辑
//==============================================================================
//---------------------------------------------------------------------------
// RGMII 接收逻辑:rgmii_rxc
//---------------------------------------------------------------------------
PH1_PHY_IOCLK u_rgmii_rxc_int
(
.clkin (rgmii_rxc),
.clkout (rgmii_rxc_int)
);
PH1_PHY_SCLK_V2 u_rgmii_rxc
(
.ce(1'b1),
.clkin (rgmii_rxc),
.clkout (gmii_rx_clk)
);
//---------------------------------------------------------------------------
// RGMII 接收逻辑:rgmii_rxd ----> gmii_rxd_reg
//---------------------------------------------------------------------------
// 1Gbps gmii_rxd 8位有效; rgmii_rxc双沿使能接收8位
// 10/100Mbps gmii_rxd 仅低四位有效,rgmii_rxc双沿使能接收高低四位数据相同
// 1gbps时,125mhz的rgmii_rxc,上升沿接收四位数据(对应低4bit),下降沿接收四位数据(对应高4bit)。 一个时钟周期8bit, 125mhz*8=1gbps
// 100mbps时,25mhz的rgmii_rxc,上升沿接收四位数据,下降沿接收四位数据(高四位和低四位数据相同)。 相当于单沿采样,一个时钟周期只收一个4bit(数据有效位在每byte的低四位:高低四位重复), 25mhz*4=100mbps
// 10mbps时同100mbps,虽然一个时钟周期上下沿重复接收相同四位数据,相当于一个时钟周期只收一个4bit。
genvar j;
generate for (j=0; j<4; j=j+1)begin : rxdata_bus
assign rgmii_rxd_delay[j]=rgmii_rxd[j];
end
endgenerate
// Instantiate Double Data Rate Input flip-flops.
// DDR_CLK_EDGE attribute specifies output data alignment from IDDR component
genvar k;
generate for (k=0; k<4; k=k+1)begin : rxdata_in_bus
PH1_LOGIC_IDDR #(
.PIPEMODE ("PIPED")
)
rgmii_rx_data_in
(
.q0 (gmii_rxd_reg[k]),
.q1 (gmii_rxd_reg[k+4]),
.clk (rgmii_rxc_int),
.d (rgmii_rxd_delay[k]),
.rst (1'b0)
);
end
endgenerate
//---------------------------------------------------------------------------
// RGMII 接收逻辑:rgmii_rx_ctl ------> gmii_rx_dv、gmii_rx_er
//---------------------------------------------------------------------------
assign rgmii_rx_ctl_delay = rgmii_rx_ctl;
PH1_LOGIC_IDDR #(
.PIPEMODE ("PIPED")
)
rgmii_rx_ctl_in
(
.q0 (gmii_rx_dv_reg),
.q1 (rgmii_rx_ctl_reg),
.clk (rgmii_rxc_int),
.d (rgmii_rx_ctl_delay),
.rst (1'b0)
);
// 解码 gmii_rx_er signal
assign gmii_rx_er_reg = gmii_rx_dv_reg ^ rgmii_rx_ctl_reg;
//----------------------------------------------------------------------------
// 接收逻辑:内部信号给到输出端口:gmii_rxd、gmii_rx_dv、gmii_rx_er、gmii_col、gmii_crs
//----------------------------------------------------------------------------
always @ (posedge gmii_rx_clk )begin
gmii_rxd <= gmii_rxd_reg;
gmii_rx_dv <= gmii_rx_dv_reg;
gmii_rx_er <= gmii_rx_er_reg;
end
// 从RGMII创建GMII格式的冲突和载波侦听信号
assign gmii_col = (gmii_tx_en | gmii_tx_er) & (gmii_rx_dv_reg | gmii_rx_er_reg);
assign gmii_crs = (gmii_tx_en | gmii_tx_er) | (gmii_rx_dv_reg | gmii_rx_er_reg);
//==============================================================================
// RGMII 状态寄存器
//==============================================================================
// 在帧间间隔期间启用带内状态寄存器
assign inband_ce = !(gmii_rx_dv_reg || gmii_rx_er_reg);
always @ (posedge gmii_rx_clk or posedge gmii_rx_reset)begin
if (gmii_rx_reset) begin
link_status <= 1'b0;
clock_speed[1:0] <= 2'b0;
duplex_status <= 1'b0;
end
else if (inband_ce) begin
link_status <= gmii_rxd_reg[0];
clock_speed[1:0] <= gmii_rxd_reg[2:1];
duplex_status <= gmii_rxd_reg[3];
end
end
endmodule 复制代码
3.3 数据转换模块设计 rgmii_interface 模块只实现了千兆速率下的 RGMII 接口收发,而 100Mps 和 10Mps 速率的采样时钟分别为 25MHz 和 2.5MHz ,数据单沿传输。 100M 和 10M 以太网的内部逻辑时钟为 12.5MHz 和 1.25MHz ,数据位宽为 8 位,这就需要额外设计一个模块进行跨时钟域处理,并且实现 8bit 数据和 4bit 数据的相互转换,称为 rgmii_cdc 模块。
rgmii_cdc 模块的设计思路与 mac 层类似,在发送端和接收端各使用两个异步 FIFO ,一个用来缓存数据,即 data_fifo ,一个用来缓存长度队列,即 len_fifo 。对写入 data_fifo 的数据数量进行计数,当写入最后一个数据时,将计数的值也写入 len_fifo 中。当 len_fifo 为非空时,代表有一帧数据等待读出,这时候将缓存的长度信息读取出来,然后通过计数器读取出一个完的帧。
rgmii_cdc模块的代码如下:
module rgmii_cdc
(
input wire speed_10_100 ,
input wire I_rst ,
input wire I_gmii_rclk ,
input wire I_gmii_rvalid ,
input wire [7:0] I_gmii_rdata ,
input wire I_gmii_tclk ,
output reg O_gmii_tvalid ,
output reg [7:0] O_gmii_tdata ,
input wire I_rx_clk ,
output reg O_rx_valid ,
output reg [7:0] O_rx_data ,
input wire I_tx_clk ,
input wire I_tx_valid ,
input wire [7:0] I_tx_data
);
reg [7:0] tx_gap;
reg [15:0] tx_len_fifo_din;
wire tx_len_fifo_wren;
wire tx_len_fifo_rdemtpy;
reg tx_len_fifo_rden;
wire [15:0] tx_len_fifo_dout;
reg tx_data_fifo_rden;
wire [7:0] tx_data_fifo_dout;
reg [15:0] tx_len;
reg tx_valid_reg;
reg tcnt_10_100;
reg [15:0] tx_cnt;
reg tx_lock;
reg tx_run;
reg tx_en;
assign tx_len_fifo_wren = tx_valid_reg && ~I_tx_valid;
always@(posedge I_tx_clk)
tx_valid_reg <= I_tx_valid;
always@(posedge I_tx_clk or posedge I_rst) begin
if(I_rst)
tx_len_fifo_din <= 16'd0;
else if(I_tx_valid)
tx_len_fifo_din <= tx_len_fifo_din + 1'b1;
else
tx_len_fifo_din <= 16'd0;
end
udp_pkg_buf #(
.DATA_WIDTH_W (8 ),
.DATA_WIDTH_R (8 ),
.ADDR_WIDTH_W (12 ),
.ADDR_WIDTH_R (12 ),
.SHOW_AHEAD_EN (1'b1 ),
.OUTREG_EN ("NOREG" )
)
tx_data_fifo
(
.rst (I_rst ),
.clkw (I_tx_clk ),
.we (I_tx_valid ),
.di (I_tx_data ),
.clkr (I_gmii_tclk ),
.re (tx_data_fifo_rden ),
.dout (tx_data_fifo_dout ),
.wrusedw (),
.rdusedw ()
);
udp_pkg_buf #(
.DATA_WIDTH_W (16 ),
.DATA_WIDTH_R (16 ),
.ADDR_WIDTH_W (6 ),
.ADDR_WIDTH_R (6 ),
.SHOW_AHEAD_EN (1'b1 ),
.OUTREG_EN ("NOREG" )
)
tx_len_fifo
(
.rst (I_rst ),
.clkw (I_tx_clk ),
.we (tx_len_fifo_wren ),
.di (tx_len_fifo_din ),
.clkr (I_gmii_tclk ),
.re (tx_len_fifo_rden ),
.dout (tx_len_fifo_dout ),
.wrusedw (),
.rdusedw (),
.empty_flag (tx_len_fifo_rdemtpy)
);
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_lock <= 1'b0;
else if(!tx_lock && !tx_len_fifo_rdemtpy)
tx_lock <= 1'b1;
else if(tx_cnt == 16'd0 && tx_gap == 8'd10 && ~speed_10_100)
tx_lock <= 1'b0;
else if(tx_cnt == 16'd0 && tx_gap == 8'd22 && speed_10_100)
tx_lock <= 1'b0;
else
tx_lock <= tx_lock;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_len_fifo_rden <= 1'b0;
else if(!tx_lock && !tx_len_fifo_rdemtpy)
tx_len_fifo_rden <= 1'b1;
else
tx_len_fifo_rden <= 1'b0;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_len <= 16'd0;
else if(tx_len_fifo_rden)
tx_len <= tx_len_fifo_dout;
else
tx_len <= tx_len;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_run <= 1'b0;
else if(tx_lock && tx_len_fifo_rden)
tx_run <= 1'b1;
else if(tx_cnt == tx_len - 1'b1 && ~speed_10_100)
tx_run <= 1'b0;
else if(tx_cnt == tx_len - 1'b1 && tcnt_10_100 && speed_10_100)
tx_run <= 1'b0;
else
tx_run <= tx_run;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_data_fifo_rden <= 1'b0;
else if(tcnt_10_100 && tx_run)
tx_data_fifo_rden <= 1'b1;
else if(speed_10_100)
tx_data_fifo_rden <= 1'b0;
else if(tx_lock && tx_len_fifo_rden && ~speed_10_100)
tx_data_fifo_rden <= 1'b1;
else if(tx_cnt >= tx_len - 1'b1)
tx_data_fifo_rden <= 1'b0;
else
tx_data_fifo_rden <= tx_data_fifo_rden;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tcnt_10_100 <= 1'b0;
else if(tx_run && speed_10_100)
tcnt_10_100 <= tcnt_10_100 + 1'b1;
else
tcnt_10_100 <= 1'b0;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_gap <= 8'd0;
else if(~speed_10_100 && tx_gap == 8'd10)
tx_gap <= 8'd0;
else if(speed_10_100 && tx_gap == 8'd22)
tx_gap <= 8'd0;
else if((tx_lock && tx_cnt == tx_len - 1'b1) || tx_gap > 8'd0)
tx_gap <= tx_gap + 1'b1;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_cnt <= 16'd0;
else if(tx_cnt == tx_len)
tx_cnt <= 16'd0;
else if(tx_data_fifo_rden)
tx_cnt <= tx_cnt + 1'b1;
else
tx_cnt <= tx_cnt;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
tx_en <= 1'b0;
else if(tx_run && speed_10_100)
tx_en <= 1'b1;
else if(~tx_run)
tx_en <= 1'b0;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
O_gmii_tvalid <= 1'b0;
else if(~speed_10_100)
O_gmii_tvalid <= tx_data_fifo_rden;
else if(speed_10_100 && ~tx_en)
O_gmii_tvalid <= 1'b0;
else if(speed_10_100 && tx_en)
O_gmii_tvalid <= 1'b1;
else
O_gmii_tvalid <= 1'b0;
end
always@(posedge I_gmii_tclk or posedge I_rst) begin
if(I_rst)
O_gmii_tdata <= 8'd0;
else if(tx_run && ~speed_10_100)
O_gmii_tdata <= tx_data_fifo_dout;
else if(tx_en && speed_10_100)
O_gmii_tdata <= tcnt_10_100 ? {4'd0,tx_data_fifo_dout[3:0]} : {4'd0,tx_data_fifo_dout[7:4]};
else
O_gmii_tdata <= 8'd0;
end
//==================================================================
//==================================================================
reg [15:0] rx_len_fifo_din;
wire rx_len_fifo_wren;
wire rx_len_fifo_rdemtpy;
reg rx_len_fifo_rden;
wire [15:0] rx_len_fifo_dout;
reg [7:0] rx_data_fifo_din;
reg rx_data_fifo_wren;
reg rx_data_fifo_rden;
wire [7:0] rx_data_fifo_dout;
reg gmii_rvalid_reg;
reg [15:0] rx_len;
reg [15:0] rx_cnt;
reg rx_lock;
reg rcnt_10_100;
assign rx_len_fifo_wren = gmii_rvalid_reg && ~I_gmii_rvalid;
always@(posedge I_gmii_rclk)
gmii_rvalid_reg <= I_gmii_rvalid;
always@(posedge I_gmii_rclk or posedge I_rst) begin
if(I_rst)
rx_len_fifo_din <= 0;
else if(~I_gmii_rvalid)
rx_len_fifo_din <= 0;
else if(I_gmii_rvalid && ~speed_10_100)
rx_len_fifo_din <= rx_len_fifo_din + 1;
else if(I_gmii_rvalid && speed_10_100 && rcnt_10_100)
rx_len_fifo_din <= rx_len_fifo_din + 1;
else
rx_len_fifo_din <= rx_len_fifo_din;
end
always@(posedge I_gmii_rclk or posedge I_rst) begin
if(I_rst)
rcnt_10_100 <= 1'b0;
else if(I_gmii_rvalid && speed_10_100)
rcnt_10_100 <= rcnt_10_100 + 1'b1;
else
rcnt_10_100 <= 1'b0;
end
always@(posedge I_gmii_rclk or posedge I_rst) begin
if(I_rst)
rx_data_fifo_wren <= 1'b0;
else if(~speed_10_100)
rx_data_fifo_wren <= I_gmii_rvalid;
else if(speed_10_100)
rx_data_fifo_wren <= rcnt_10_100;
end
always@(posedge I_gmii_rclk or posedge I_rst) begin
if(I_rst)
rx_data_fifo_din <= 8'd0;
else if(speed_10_100)
rx_data_fifo_din <= {I_gmii_rdata[3:0], rx_data_fifo_din[7:4]};
else
rx_data_fifo_din <= I_gmii_rdata;
end
udp_pkg_buf #(
.DATA_WIDTH_W (8 ),
.DATA_WIDTH_R (8 ),
.ADDR_WIDTH_W (12 ),
.ADDR_WIDTH_R (12 ),
.SHOW_AHEAD_EN (1'b1 ),
.OUTREG_EN ("NOREG" )
)
rx_data_fifo
(
.rst (I_rst ),
.clkw (I_gmii_rclk ),
.we (rx_data_fifo_wren ),
.di (rx_data_fifo_din ),
.clkr (I_rx_clk ),
.re (rx_data_fifo_rden ),
.dout (rx_data_fifo_dout ),
.wrusedw (),
.rdusedw ()
);
udp_pkg_buf #(
.DATA_WIDTH_W (16 ),
.DATA_WIDTH_R (16 ),
.ADDR_WIDTH_W (6 ),
.ADDR_WIDTH_R (6 ),
.SHOW_AHEAD_EN (1'b1 ),
.OUTREG_EN ("NOREG" )
)
rx_len_fifo
(
.rst (I_rst ),
.clkw (I_gmii_rclk ),
.we (rx_len_fifo_wren ),
.di (rx_len_fifo_din ),
.clkr (I_rx_clk ),
.re (rx_len_fifo_rden ),
.dout (rx_len_fifo_dout ),
.wrusedw (),
.rdusedw (),
.empty_flag (rx_len_fifo_rdemtpy)
);
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
rx_lock <= 1'b0;
else if(!rx_lock && !rx_len_fifo_rdemtpy)
rx_lock <= 1'b1;
else if(rx_cnt == rx_len - 1'b1)
rx_lock <= 1'b0;
else
rx_lock <= rx_lock;
end
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
rx_len_fifo_rden <= 1'b0;
else if(!rx_lock && !rx_len_fifo_rdemtpy)
rx_len_fifo_rden <= 1'b1;
else
rx_len_fifo_rden <= 1'b0;
end
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
rx_len <= 16'd0;
else if(rx_len_fifo_rden)
rx_len <= rx_len_fifo_dout;
else
rx_len <= rx_len;
end
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
rx_data_fifo_rden <= 1'b0;
else if(rx_lock && rx_len_fifo_rden)
rx_data_fifo_rden <= 1'b1;
else if(rx_cnt == rx_len - 1'b1)
rx_data_fifo_rden <= 1'b0;
else
rx_data_fifo_rden <= rx_data_fifo_rden;
end
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
rx_cnt <= 16'd0;
else if(rx_cnt == rx_len - 1)
rx_cnt <= 16'd0;
else if(rx_data_fifo_rden)
rx_cnt <= rx_cnt + 1'b1;
else
rx_cnt <= rx_cnt;
end
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
O_rx_valid <= 1'b0;
else if(rx_data_fifo_rden)
O_rx_valid <= 1'b1;
else
O_rx_valid <= 1'b0;
end
always@(posedge I_rx_clk or posedge I_rst) begin
if(I_rst)
O_rx_data <= 8'd0;
else if(rx_data_fifo_rden)
O_rx_data <= rx_data_fifo_dout;
else
O_rx_data <= 8'd0;
end
endmodule 复制代码
4 三速以太网回环测试
4.1 100M 以太网测试 udp 协议栈的逻辑时钟为 12.5MHz , RGMII 接口的时钟为 25MHz ,将参数 speed_10_100 设置成 1 ’b1 。
打开设备管理器,在网络适配器选项中选择自己的以太网卡。
在高级选项中将网卡的连接速度和工作模式设置成 100Mbps 全双工。
连接好网线,并将程序下栽进开发板中。此时开发板网卡自协商的速度为 100Mbps 。使用网络调试助手发送数据帧, FPGA 将报文解析完成后,通过发送端传回 PC ,结果如下图所示。
4.2 10M 以太网测试 udp 协议栈的逻辑时钟为 1.25MHz , RGMII 接口的时钟为 2.5MHz ,时钟选择由分频逻辑生成。选择 125MHz 时钟进行计数分配,代码如下:
reg clk_1_25;
reg clk_2_5;
reg clk_2_5_90;
reg [7:0] clk_cnt;
always@(posedge clk_125 or negedge pll_locked) begin
if(!pll_locked)
clk_cnt <= 0;
else if(clk_cnt == 99)
clk_cnt <= 0;
else
clk_cnt <= clk_cnt + 1;
end
always@(posedge clk_125 or negedge pll_locked) begin
if(!pll_locked)
clk_1_25 <= 0;
else
case(clk_cnt)
49 :clk_1_25 <= 1;
99 :clk_1_25 <= 0;
default:clk_1_25 <= clk_1_25;
endcase
end
always@(posedge clk_125 or negedge pll_locked) begin
if(!pll_locked)
clk_2_5 <= 0;
else
case(clk_cnt)
24 :clk_2_5 <= 1;
49 :clk_2_5 <= 0;
74 :clk_2_5 <= 1;
99 :clk_2_5 <= 0;
default:clk_2_5 <= clk_2_5;
endcase
end
always@(posedge clk_125 or negedge pll_locked) begin
if(!pll_locked)
clk_2_5_90 <= 0;
else
case(clk_cnt)
12 :clk_2_5_90 <= 1;
37 :clk_2_5_90 <= 0;
62 :clk_2_5_90 <= 1;
87 :clk_2_5_90 <= 0;
default:clk_2_5_90 <= clk_2_5_90;
endcase
end 复制代码
将网卡的连接速度和工作模式设置成 10Mbps 全双工。上电开发板, PC 设置中显示网络连接速度为 10Mbps 。
将程序下栽进开发板中,通过网络调试助手进行数据回环,结果如下图所示。
ping 包测试结果如下图所示。