用Verilog语言实现一个简单的MII模块

时间:2023-03-06 09:40:02

  项目中要求简单地测试一下基于FPGA的模拟平台的RJ45网口,也就是需要实现一个MII或者RMII模块。看了一下官方网口PHY芯片的官方文档,还是感觉上手有点障碍,想在网络上找些参考代码看看,最后只在opencores找到了一些MAC层控制模块,代码庞大且复杂,对于初学者来说阅读起来很困难。

  于是在此以一个初学者的角度记录一下我实现一个简单的MII模块的过程,并且指出一些实现过程中要注意的问题。希望可以帮助有需要的朋友。

  为了便于测试,我选择了和我们平台使用相同物理芯片的FPGA开发板NEXYS 3,物理芯片为MICROCHIP出品的LAN8710A芯片。在NEXYS 3的内部,PHY芯片的管脚连接如图0所示:

用Verilog语言实现一个简单的MII模块

  图0 NEXYS3内部LAN8710A芯片管脚连接(图片来自NEXYS 3官方文档截图)

  在这个简单的MII模块中,主要有一下几个子模块:PHY配置模块,发送模块,接收模块;其中PHY配置模块有一个PHY控制模块,用来读写PHY中的寄存器。这些模块如图1所示:

用Verilog语言实现一个简单的MII模块图1 模块关系

  首先,我们需要编写一个控制PHY的子模块。按照官方文档,管理PHY芯片的方式是通过SMI(Serial Management Interface)进行的;SMI用于控制芯片状态或者读取芯片状态。SMI包括两个信号:MDC和MDIO。

  MDC为一个非周期性的时钟信号,由使用SMI的模块指定。LAN8710A的官方文档指出:MDC时钟信号的相邻上升沿和下降沿之间的最小间隔是160ns,最大间隔没有限制,而其周期最小为400ns,同样的,最大周期也没有限制;因此MDC的最大频率为2.5MHz。

  MDIO,是一个双向端口信号,用于串行数据的传输。双向端口信号的控制方式在后面的代码中可以参考。 

  在PHY芯片中,根据IEEE802.3规范中的22号条款要求的0号寄存器到6号寄存器和厂商自定义的16号寄存器到31号寄存器大致如图2所示(第一列即为它们的地址):

用Verilog语言实现一个简单的MII模块

图2 PHY芯片中的部分寄存器

  在上面这些寄存器中,寄存器0位基本的控制寄存器,对PHY芯片的控制一般通过写这个寄存器来完成,控制寄存器的各位的功能如图3所示:

用Verilog语言实现一个简单的MII模块

图3 控制寄存器(PHY芯片中地址为0)

  通过SMI读物理芯片的寄存器的时钟顺序如图4所示:

用Verilog语言实现一个简单的MII模块

图4 SMI读物理芯片寄存器时序图(图片来自LAN8710A芯片官方文档截图)

  通过SMI写物理芯片的寄存器的时钟顺序如图5所示:

用Verilog语言实现一个简单的MII模块

图5 SMI写物理芯片寄存器时序图(图片来自LAN8710A芯片官方文档截图)

  在读写过程中传输的串行数据一定不能忘记了最前面的那32个1,最初我忽略了这一点走了很多弯路。

  下面的代码给出了一个简单的利用SMI管理物理芯片的模块;该模块的功能为读写PHY芯片中的寄存器,phy_addr_i为PHY芯片的地址,默认为0,一般不需要关注,但是如果涉及到多个PHY芯片,则要选择正确的芯片地址;reg_addr_i为PHY芯片中寄存器的地址,din_i为写寄存器时的16位数据;dout_o为读寄存器时获取的16位数据;wr_en_i有效时读取寄存器数据,rd_en_i有效时将din_i上的数据写入到相应的寄存器。

 module PHY_Ctrl(
input clk_i,// <= 2.5MHz
input rst_i, //management signals
input [:] phy_addr_i,
input [:] reg_addr_i,
input wr_en_i,
input rd_en_i,
input [:] din_i,
output reg [:] dout_o, //PHY signals
output phy_nrst_o,
inout mdio_io,
output mdc_o
//input col_i,
//input crs_i,
); assign mdc_o = clk_i;
assign mdc_n = ~clk_i;
assign phy_nrst_o = ~rst_i; reg mdo_en;
reg mdo;
assign mdio_io = mdo_en ? mdo : 'bz; reg [:] bit_cnt;
wire [:] byte_sel;
reg [:] current_byte;
reg op_is_write; assign byte_sel[] = (bit_cnt == );
assign byte_sel[] = (bit_cnt == );
assign byte_sel[] = (bit_cnt == );
assign byte_sel[] = (bit_cnt == ); always @(posedge mdc_n, posedge rst_i)
if (rst_i)
current_byte <= ;
else
if (byte_sel)//current byte's value must be maintained
case (byte_sel)
'b0001: current_byte <= {1'b0,'b1,~op_is_write,op_is_write,phy_addr_i[4:1]};
'b0010: current_byte <= {phy_addr_i[0],reg_addr_i[4:0],1'b1,'b0};
'b0100: current_byte <= din_i[15:8];
'b1000: current_byte <= din_i[7:0];
endcase always@(posedge mdc_n, posedge rst_i)
if (rst_i)
begin
bit_cnt <= ;
op_is_write <= ;
mdo_en <= ;
end
else
case (bit_cnt)
:
if (wr_en_i | rd_en_i)
begin
bit_cnt <= ;
mdo_en <= ;
op_is_write <= wr_en_i;
end
:
begin
mdo_en <= op_is_write;
bit_cnt <= ;
end
:
begin
mdo_en <= ;
bit_cnt <= ;
end
default:
bit_cnt <= bit_cnt + 'b1;
endcase always @(posedge mdc_n, posedge rst_i)
if (rst_i)
begin
mdo <= ;
dout_o <= ;
end
else
if ( <= bit_cnt && bit_cnt <= )//preamble
mdo <= ;
else if ( <= bit_cnt && bit_cnt <= )
mdo <= current_byte[ - bit_cnt];
else if ( <= bit_cnt && bit_cnt <= )
mdo <= current_byte[ - bit_cnt];
else if ( <= bit_cnt && bit_cnt <= )
if (op_is_write)
mdo <= current_byte[ - bit_cnt];
else
dout_o[ - bit_cnt] <= mdio_io;
else if ( <= bit_cnt && bit_cnt <= )
if (op_is_write)
mdo <= current_byte[ - bit_cnt];
else
dout_o[ - bit_cnt] <= mdio_io; endmodule

  在顶层模块中要这样直接的读写寄存器还是过于麻烦,于是我将这个模块再次封装,使得在顶层模块中用一些电平信号来控制PHY的状态。目前我自己用到的只有“环回”这一个状态量,模块按照下面的代码进行了封装:

 module PHY_Conf(
input clk_100m_i,
input rst_i, output phy_nrst_o,
inout mdio_io,
output mdc_o, input loopback_en_i
); reg clk_1m;
reg [:] cnt; always @(posedge clk_100m_i, posedge rst_i)
if (rst_i)
begin
clk_1m <= ;
cnt <= ;
end
else
if (cnt >= )
begin
clk_1m <= ~clk_1m;
cnt <= ;
end
else
cnt <= cnt + 'b1; reg loopback_en_s1;
reg loopback_en_s2; always @(posedge clk_1m, posedge rst_i)
if (rst_i)
begin
loopback_en_s1 <= ;
loopback_en_s2 <= ;
end
else
begin
loopback_en_s1 <= loopback_en_s2;
loopback_en_s2 <= loopback_en_i;
end always @(posedge clk_1m, posedge rst_i)
if (rst_i)
begin
end
else
if (loopback_en_s2 != loopback_en_s1)
begin
phy_wr_en <= ;
phy_din <= {'b0, loopback_en_s2, 2'b11, 'b0};
end
else
phy_wr_en <= ; reg phy_wr_en;
reg [:] phy_din;
wire [:] phy_dout; PHY_Ctrl phy_ctrl(
.clk_i(clk_1m),
.rst_i(rst_i),
.phy_addr_i('b0),
.reg_addr_i('b0),
.din_i(phy_din),
.wr_en_i(phy_wr_en),
//.rd_en_i(phy_rd_en_i),
//.dout_o(phy_dout_o),
.phy_nrst_o(phy_nrst_o),
.mdio_io(mdio_io),
.mdc_o(mdc_o)
); endmodule

  在顶层模块中,我们如果需要把PHY配置为环回状态,只需要维持loopback_en_i为高电平即可,否则维持其为低电平。

  在配置好PHY之后,我们要考虑发送数据和接收数据。首先为了简单起见,我们先给出最简单的发送数据的模块和接收数据的模块,然后再考虑数据的缓冲等细节。图6给出了发送数据和接收数据的时序关系以及在发送过程或接收过程中的一些特殊的标志数据:

用Verilog语言实现一个简单的MII模块

图6 接收数据的时序以及发送过程和接收过程中的一些特殊标志数据(图片来自LAN8710A官方文档截图)

  在图5中,出现了一些标志数据,其中"JK"为发送过程中的4B5B编码,不需要我们在发送模块中发送,紧随其后的"555D"则是发送数据时必须的前缀。因此,在发送数据之前要先发送"555D";在接收数据时,真正的数据之前也有固定的前缀"55555D",我们在接收时需要这个前缀丢弃。

  在下面的代码中,我们的目标是在一个脉冲信号tx_en_i的激发下将tx_din_i上的16位数据发送出去。如上所述,在发送tx_din_i上的16位数据之前,我们发送了前缀"555d"。

 module TX_Module(
input txclk_i,
input rst_i, output txen_o,
output txer_o,
output reg [:] txd_o, input [:] tx_din_i,
input tx_en_i,
output tx_busy_o
); wire txclk_n;
assign txclk_n = ~txclk_i;
assign txer_o = ;//required! reg [:] cnt; always @(posedge txclk_i, posedge rst_i)
if(rst_i)
cnt <= ;
else
case(cnt)
:
if(tx_en_i)
cnt <= ;
,,,,,,,:
cnt <= cnt + 'b1;
:
cnt <= ;
endcase assign txen_o = ( < cnt && cnt <= );
assign tx_busy_o = (cnt != ); always @(posedge txclk_i, posedge rst_i)
if(rst_i)
txd_o <= ;
else
case(cnt)
: txd_o <= 'h5;//preamble
: txd_o <= 'h5;//preamble
: txd_o <= 'h5;//preamble
: txd_o <= 'hd;//preamble
: txd_o <= tx_din_i[15:12];
: txd_o <= tx_din_i[11:8];
: txd_o <= tx_din_i[7:4];
: txd_o <= tx_din_i[3:0];
endcase endmodule

在上面的发送模块的代码中,txen_o信号的值是值得注意的地方,txen_o在输出数据时必须维持高电平,在数据传输完毕时立即变为低电平;另一个很重要的地方是如果暂时没有其它需求,一定要将txer_o置为低电平状态,并且连接到芯片的相应引脚,否则会导致发送模块不能正常发送数据。在这个发送模块中,每次只能发送16位数据,每次发送的前缀也占了16位,这样看来效率比较低,是一个需要改进的地方;在后续的工作中,我们在发送模块引入了FIFO,通过将模块中的cnt状态量在5、6、7、8这4个状态循环,每次会将FIFO中存在的数据全部一起发送出去,并且由于FIFO与发送模块是异步的,我们可以连续地向FIFO中写数据,发送模块连续地从FIFO取数据然后发送。

  接下来的代码给出了一个简单的接收模块,该模块假设接收到的数据的大小是以16位为基本单位的,在每接收到一个完整的16位的数据后,接收模块同过将rxd_ready置位来通知上层的模块,具体代码如下:

 module RX_Module(
input rxclk_i,
input rst_i,
input rxdv_i,
input [:] rxd_i, output rx_busy_o,
output reg rxd_ready_o,
output [:] rx_dout_o
); reg [:] cnt;
wire rxclk_n; reg [:] rxd;
reg [:] rxd_buffer; assign rxclk_n = ~rxclk_i;
assign rx_dout_o = rxd_buffer;
assign rx_busy_o = (cnt != ); always@(posedge rxclk_n, posedge rst_i)
if(rst_i)
cnt <= ;
else
case(cnt)
:
if(rxdv_i)
cnt <= ;
,,,:
cnt <= cnt + 'b1;
:
if (rxdv_i)
cnt <= ;
else
cnt <= ;
:
cnt <= ;
:
cnt <= ;
:
cnt <= ;
endcase always @(posedge rxclk_i, posedge rst_i)
if (rst_i)
begin
rxd <= ;
rxd_ready_o <= ;
end
else
case(cnt)
: begin rxd[15:12] <= rxd_i; rxd_ready_o <= ; end
: rxd[11:8] <= rxd_i;
: rxd[7:4] <= rxd_i;
: begin rxd[3:0] <= rxd_i; rxd_ready_o <= ; end
endcase always @(posedge rxclk_i, posedge rst_i)
if (rst_i)
rxd_buffer <= ;
else
if (cnt == && rxd_ready_o)
rxd_buffer <= rxd; endmodule

在上面的接收模块中,考虑到接收到的数据可能不止16位,因此在利用状态量cnt在5、6、7、8这几个状态循环直到接收完最后一个16位的数据,我们使用了一个16位的缓冲区,在rxd_ready有效时可以通过rx_dout信号从该缓冲区内读取上一次接收到的16位数据。

  现在,我们已经实现了最基本的发送模块和接收模块,发送模块TX_Module在tx_en_i有效时将tx_din_i上的16位数据发送出去,接收模块在每次接收到16位数据后将rxd_ready置位一个时钟周期,此时上层模块可以从rx_dout_o读取这16位数据。

  下面的代码给出了一个简单的顶层模块,在这个模块中,我们可以通过将tx_en_i连接到一个按键上,将tx_din_i连接到一系列的switch滑动按钮上,将rx_dout_o连接到一系列的LED灯上或者连接到七段数码管显示模块,然后使能PHY的环回功能(使loopback_en_i维持高电平),通过简单的发送数据和接收数据来验证模块功能的正确性。

 module MII_Lite(
input clk_100m_i,//100Mhz
input rst_i, //PHY serial management interface signals
output phy_nrst_o,
inout mdio_io,
output mdc_o, //PHY configuration signals, to be extended...
input loopback_en_i, input txclk_i,
output [:] txd_o,
output txen_o,
output txer_o, input [:] tx_din_i,
input tx_en_i,
output tx_busy_o, input rxclk_i,
input rxdv_i,
//input rxer_i,
input [:] rxd_i, output rx_busy_o,
output rxd_ready_o,
output [:] rx_dout_o
); PHY_Conf phy_conf (
.clk_100m_i(clk_100m_i),
.rst_i(rst_i), .phy_nrst_o(phy_nrst_o),
.mdio_io(mdio_io),
.mdc_o(mdc_o), .loopback_en_i(loopback_en_i)
); reg [:] cnt1;
always @(posedge txclk_i, posedge rst_i)
if(rst_i)
cnt1 <= ;
else
case(cnt1)
:
if (tx_en_i)
begin
tx_en <= ;
cnt1 <= ;
end
:
begin
tx_en <= ;
cnt1 <= ;
end
:
cnt1 <= ;
default:
cnt1 <= cnt1 + 'b1;
endcase reg tx_en; TX_Module tx_unit(
.txclk_i(txclk_i),
.rst_i(rst_i), .txen_o(txen_o),
.txer_o(txer_o),//required!
.txd_o(txd_o), .tx_din_i(tx_din_i),
.tx_en_i(tx_en),
.tx_busy_o(tx_busy_o)
); RX_Module rx_unit(
.rxclk_i(rxclk_i),
.rst_i(rst_i), .rxdv_i(rxdv_i),
//.rxer_i(rxer_i),
.rxd_i(rxd_i), .rx_busy_o(rx_busy_o),
.rxd_ready_o(rxd_ready_o),
.rx_dout_o(rx_dout_o)
); endmodule

  在后续的工作中,我们在发送模块和接收模块中都加入了一个FIFO缓冲区,并且将顶层模块更仔细的封装,以提供给上层模块调用;在发送模块中,FIFO由上层模块提供的时钟信号驱动,上层模块只需要监测发送模块中的FIFO的full信号,如果full信号为低电平,则可以向FIFO中写数据,当tx_en_i有效时,发送模块周期性地检查FIFO,如果FIFO不空,则一次性地将所有数据发送出去,如果在发送过程中有数据写入FIFO,发送模块可以持续的读取并发送这些数据;在接收模块中,FIFO的时钟与接收模块的时钟相同,每当接收模块接收到一个单位(单位为FIFO的宽度)的数据后,就将这个单位的数据写入FIFO,上层模块只需要监测接收模块的FIFO的empty信号,如果empty信号为低电平,则表示接收到数据了,这是就可以将数据读取出来。

本文更新地址:http://www.cnblogs.com/0x4863/p/6703805.html