首页 > 代码库 > 用Verilog语言实现一个简单的MII模块

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

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

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

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

技术分享

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

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

技术分享图1 模块关系

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

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

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

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

技术分享

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

  

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

技术分享

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

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

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

技术分享

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

  下面的代码给出了一个简单的利用SMI管理物理芯片的模块:

  1 module PHY_Ctrl(
  2     input                         clk_i,// <= 2.5MHz
  3     input                         rst_i,
  4     
  5     //management signals
  6     input                [4:0]    phy_addr_i,
  7     input                [4:0]    reg_addr_i,
  8     input                         wr_en_i,
  9     input                         rd_en_i,
 10     input                [15:0]   din_i,
 11     output    reg        [15:0]   dout_o,
 12     
 13     //PHY signals
 14     output                        phy_nrst_o,
 15     inout                         mdio_io,
 16     output                        mdc_o
 17     //input                         col_i,
 18     //input                         crs_i,
 19     );
 20             
 21     assign mdc_o = clk_i;
 22     assign mdc_n = ~clk_i;
 23     assign phy_nrst_o = ~rst_i;
 24     
 25     reg                 mdo_en;
 26     reg                 mdo;
 27     assign mdio_io = mdo_en ? mdo : 1bz;
 28     
 29     reg     [6:0]       bit_cnt;
 30     wire    [3:0]       byte_sel;
 31     reg     [7:0]       current_byte;
 32     reg                 op_is_write;
 33     
 34     assign byte_sel[0] = (bit_cnt == 32);
 35     assign byte_sel[1] = (bit_cnt == 40);
 36     assign byte_sel[2] = (bit_cnt == 48);
 37     assign byte_sel[3] = (bit_cnt == 56);
 38     
 39     always @(posedge mdc_n, posedge rst_i)
 40         if (rst_i)
 41             current_byte <= 0;
 42         else
 43             if (byte_sel)//current byte‘s value must be maintained
 44                 case (byte_sel)
 45                     4b0001: current_byte <= {1b0,1b1,~op_is_write,op_is_write,phy_addr_i[4:1]};
 46                     4b0010: current_byte <= {phy_addr_i[0],reg_addr_i[4:0],1b0,1b0};
 47                     4b0100: current_byte <= din_i[15:8];
 48                     4b1000: current_byte <= din_i[7:0];
 49                 endcase
 50     
 51     always@(posedge mdc_n, posedge rst_i)
 52         if (rst_i)
 53             begin
 54                 bit_cnt <= 0;
 55                 op_is_write <= 0;
 56                 mdo_en <= 1;
 57             end
 58         else
 59             case (bit_cnt)
 60                 0:
 61                     if (wr_en_i | rd_en_i)
 62                         begin
 63                             bit_cnt <= 1;
 64                             mdo_en <= 1;
 65                             op_is_write <= wr_en_i;
 66                         end
 67                 47:
 68                     begin
 69                         mdo_en <= op_is_write;
 70                         bit_cnt <= 48;
 71                     end
 72                 65:
 73                     begin
 74                         mdo_en <= 1;
 75                         bit_cnt <= 0;
 76                     end
 77                 default:
 78                     bit_cnt <= bit_cnt + 1b1;
 79             endcase
 80             
 81     always @(posedge mdc_n, posedge rst_i)
 82         if (rst_i)
 83             begin
 84                 mdo <= 1;
 85                 dout_o <= 0;
 86             end
 87         else
 88             if (1 <= bit_cnt && bit_cnt <= 32)//preamble
 89                 mdo <= 1;
 90             else if (33 <= bit_cnt && bit_cnt <= 40)
 91                 mdo <= current_byte[40 - bit_cnt];
 92             else if (41 <= bit_cnt && bit_cnt <= 48)
 93                 mdo <= current_byte[48 - bit_cnt];
 94             else if (49 <= bit_cnt && bit_cnt <= 56)
 95                 if (op_is_write)
 96                     mdo <= current_byte[56 - bit_cnt];
 97                 else
 98                     dout_o[64 - bit_cnt] <= mdio_io;
 99             else if (57 <= bit_cnt && bit_cnt <= 64)
100                 if (op_is_write)
101                     mdo <= current_byte[64 - bit_cnt];
102                 else
103                     dout_o[64 - bit_cnt] <= mdio_io;
104         
105 endmodule

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

 1 module PHY_Conf(
 2     input                 clk_100m_i,
 3     input                 rst_i,
 4     
 5     output                phy_nrst_o,
 6     inout                 mdio_io,
 7     output                mdc_o,
 8     
 9     input                 loopback_en_i
10     );
11      
12     reg clk_1m;
13     reg [5:0] cnt;
14     
15     always @(posedge clk_100m_i, posedge rst_i)
16         if (rst_i)
17             begin
18                 clk_1m <= 0;
19                 cnt <= 0;
20             end
21         else
22             if (cnt >= 49)
23                 begin
24                     clk_1m <= ~clk_1m;
25                     cnt <= 0;
26                 end
27             else
28                 cnt <= cnt + 1b1;
29     
30     reg loopback_en_s1;
31     reg loopback_en_s2;
32     
33     always @(posedge clk_1m, posedge rst_i)
34         if (rst_i)
35             begin
36                 loopback_en_s1 <= 0;
37                 loopback_en_s2 <= 0;
38             end
39         else
40             begin
41                 loopback_en_s1 <= loopback_en_s2;
42                 loopback_en_s2 <= loopback_en_i;
43             end
44             
45     always @(posedge clk_1m, posedge rst_i)
46         if (rst_i)
47             begin
48             end
49         else
50             if (loopback_en_s2 != loopback_en_s1)
51                 begin
52                     phy_wr_en <= 1;
53                     phy_din <= {1b0, loopback_en_s2, 2b11, 12b0};
54                 end
55             else
56                 phy_wr_en <= 0;
57     
58     reg             phy_wr_en;
59     reg [15:0]      phy_din;
60     wire [15:0]     phy_dout;
61 
62     PHY_Ctrl phy_ctrl(
63         .clk_i(clk_1m),
64         .rst_i(rst_i),
65         .phy_addr_i(5b0),
66         .reg_addr_i(5b0),
67         .din_i(phy_din),
68         .wr_en_i(phy_wr_en),
69         //.rd_en_i(phy_rd_en_i),
70         //.dout_o(phy_dout_o),
71         .phy_nrst_o(phy_nrst_o),
72         .mdio_io(mdio_io),
73         .mdc_o(mdc_o)
74         );
75         
76 endmodule

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

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

技术分享

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

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

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

 1 module TX_Module(
 2     input                     txclk_i,
 3     input                     rst_i,
 4     
 5     output                    txen_o,
 6     output                    txer_o,
 7     output reg    [3:0]       txd_o,
 8 
 9     input         [15:0]      tx_din_i,
10     input                     tx_en_i,
11     output                    tx_busy_o
12     );
13     
14     wire txclk_n;
15     assign txclk_n = ~txclk_i;
16     assign txer_o = 0;//required!
17     
18     reg [3:0] cnt;
19     
20     always @(posedge txclk_i, posedge rst_i)
21         if(rst_i)
22             cnt <= 0;
23         else
24             case(cnt)
25             0:
26                 if(tx_en_i)
27                     cnt <= 1;
28             1,2,3,4,5,6,7,8:
29                 cnt <= cnt + 1b1;
30             9:
31                 cnt <= 0;
32         endcase
33     
34     assign txen_o = (0 < cnt && cnt <= 9);
35     assign tx_busy_o = (cnt != 0);
36     
37     always @(posedge txclk_i, posedge rst_i)
38         if(rst_i)
39             txd_o <= 0;
40         else
41             case(cnt)
42                 1: txd_o <= 4h5;//preamble
43                 2: txd_o <= 4h5;//preamble
44                 3: txd_o <= 4h5;//preamble
45                 4: txd_o <= 4hd;//preamble
46                 5: txd_o <= tx_din_i[3:0];
47                 6: txd_o <= tx_din_i[7:4];
48                 7: txd_o <= tx_din_i[11:8];
49                 8: txd_o <= tx_din_i[15:12];
50             endcase
51     
52 endmodule

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

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

 1 module RX_Module(
 2     input                 rxclk_i,
 3     input                 rst_i,
 4     input                 rxdv_i,
 5     input       [3:0]     rxd_i,
 6 
 7     output                rx_busy_o,
 8     output reg            rxd_ready_o,
 9     output      [15:0]    rx_dout_o
10     );
11     
12     reg         [3:0]     cnt;
13     wire                  rxclk_n;
14     
15     reg         [15:0]    rxd;
16     reg         [15:0]    rxd_buffer;
17     
18     assign rxclk_n = ~rxclk_i;
19     assign rx_dout_o = rxd_buffer;
20     assign rx_busy_o = (cnt != 0);
21     
22     always@(posedge rxclk_n, posedge rst_i)
23         if(rst_i)
24             cnt <= 0;
25         else
26             case(cnt)
27                 0:
28                     if(rxdv_i)
29                         cnt <= 1;
30                 1,2,3,4:
31                     cnt <= cnt + 1b1;
32                 5:
33                     if (rxdv_i)
34                         cnt <= 6;
35                     else
36                         cnt <= 0;
37                 6:
38                     cnt <= 7;
39                 7:
40                     cnt <= 8;
41                 8:
42                     cnt <= 5;
43             endcase
44             
45     always @(posedge rxclk_i, posedge rst_i)
46         if (rst_i)
47             begin
48                 rxd <= 0;
49                 rxd_ready_o <= 0;
50             end
51         else
52             case(cnt)
53                 5: begin rxd[3:0] <= rxd_i; rxd_ready_o <= 0; end
54                 6: rxd[7:4] <= rxd_i;
55                 7: rxd[11:8] <= rxd_i;
56                 8: begin rxd[15:12] <= rxd_i; rxd_ready_o <= 1; end
57             endcase
58 
59     always @(posedge rxclk_i, posedge rst_i)
60         if (rst_i)
61             rxd_buffer <= 0;
62         else
63             if (cnt == 5 && rxd_ready_o)
64                 rxd_buffer <= rxd;
65 
66 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维持高电平),通过简单的发送数据和接收数据来验证模块功能的正确性。

 1 module MII_Lite(
 2     input                 clk_100m_i,//100Mhz
 3     input                 rst_i,
 4     
 5     //PHY serial management interface signals
 6     output                phy_nrst_o,
 7     inout                 mdio_io,
 8     output                mdc_o,
 9     
10     //PHY configuration signals, to be extended...
11     input                 loopback_en_i,
12     
13     input                 txclk_i,
14     output     [3:0]      txd_o,
15     output                txen_o,
16     output                txer_o,
17     
18     input     [15:0]      tx_din_i,
19     input                 tx_en_i,
20     output                tx_busy_o,
21     
22     input                 rxclk_i,
23     input                 rxdv_i,
24     //input                 rxer_i,
25     input     [3:0]       rxd_i,
26     
27     output                rx_busy_o,
28     output                rxd_ready_o,
29     output    [15:0]      rx_dout_o
30     );
31     
32     PHY_Conf phy_conf (
33         .clk_100m_i(clk_100m_i),
34         .rst_i(rst_i),
35         
36         .phy_nrst_o(phy_nrst_o),
37         .mdio_io(mdio_io),
38         .mdc_o(mdc_o),
39         
40         .loopback_en_i(loopback_en_i)
41         );
42         
43     reg [23:0] cnt1;
44     always @(posedge txclk_i, posedge rst_i)
45         if(rst_i)
46             cnt1 <= 0;
47         else
48            case(cnt1)
49             0:
50                 if (tx_en_i)
51                     begin
52                         tx_en <= 1;
53                         cnt1 <= 1;
54                     end
55             1:
56                 begin
57                     tx_en <= 0;
58                     cnt1 <= 2;
59                 end
60             124999:
61                 cnt1 <= 0;
62             default:
63                 cnt1 <= cnt1 + 1b1;
64             endcase
65                 
66     reg tx_en;
67         
68     TX_Module tx_unit(
69         .txclk_i(txclk_i),
70         .rst_i(rst_i),
71         
72         .txen_o(txen_o),
73         .txer_o(txer_o),//required!
74         .txd_o(txd_o),
75         
76         .tx_din_i(tx_din_i),
77         .tx_en_i(tx_en),
78         .tx_busy_o(tx_busy_o)
79         );
80 
81     RX_Module rx_unit(
82         .rxclk_i(rxclk_i),
83         .rst_i(rst_i),
84         
85         .rxdv_i(rxdv_i),
86           //.rxer_i(rxer_i),
87         .rxd_i(rxd_i),
88         
89         .rx_busy_o(rx_busy_o),
90         .rxd_ready_o(rxd_ready_o),
91         .rx_dout_o(rx_dout_o)
92         );
93         
94 endmodule

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

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