用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 : 1‘bz;
 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                     4‘b0001: current_byte <= {1‘b0,1‘b1,~op_is_write,op_is_write,phy_addr_i[4:1]};
 46                     4‘b0010: current_byte <= {phy_addr_i[0],reg_addr_i[4:0],1‘b0,1‘b0};
 47                     4‘b0100: current_byte <= din_i[15:8];
 48                     4‘b1000: 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 + 1‘b1;
 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 + 1‘b1;
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 <= {1‘b0, loopback_en_s2, 2‘b11, 12‘b0};
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(5‘b0),
66         .reg_addr_i(5‘b0),
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 + 1‘b1;
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 <= 4‘h5;//preamble
43                 2: txd_o <= 4‘h5;//preamble
44                 3: txd_o <= 4‘h5;//preamble
45                 4: txd_o <= 4‘hd;//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 + 1‘b1;
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 + 1‘b1;
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信号为低电平,则表示接收到数据了,这是就可以将数据读取出来。

时间: 2024-10-23 18:22:36

用Verilog语言实现一个简单的MII模块的相关文章

用GO语言实现一个简单的搜索引擎

用GO语言实现一个简单的搜索引擎 项目地址是:https://github.com/wyh267/FalconEngine 对搜索引擎感兴趣的可以去看看这本书,比较浅并且也比较完整的介绍了一个搜索引擎的全部机能. 我的这个搜索引擎原始数据是MySql数据库的,大家可以根据需要进行二次开发,用来支持其他数据库或者本地文件,Detail文件是存储在Redis数据库中,同样这部分也可以根据自己的需要二次开发,使用本地文件或者其他数据库,倒排索引和正排索引本地存储的时候使用的json格式,比较耗磁盘,第

Swift语言编写一个简单的条形码扫描APP

swift语言编写一个简单的条形码扫描APP 原文地址:appcoda 在处理职员在杂货店的收银台排了很长的队伍,在机场帮助检查背包和旅客,或者在主要的食品供应商,协助处理乏味的存货清单过程,条形码扫描是很简单的处理工具.实际上,他们已经用了这个办法来解决消费者在智能购物,图书分类,等其他目的.因此,让我们来制作一个iPhone版本的条形码扫描工具吧! 对我们来说幸运的是,苹果已经制作了条形码扫描的程序,实现它是一件很简单的事情.我们将要研究进入AV Foundation框架的世界,组建APP,

用Go语言实现一个简单的聊天机器人

一.介绍 目的:使用Go语言写一个简单的聊天机器人,复习整合Go语言的语法和基础知识. 软件环境:Go1.9,Goland 2018.1.5. 二.回顾 Go语言基本构成要素:标识符.关键字.字面量.分隔符.操作符.它们可以组成各种表达式和语句,而后者都无需以分号结尾. 标识符:程序实体,前者即为后者的名称. 关键字:被编程语言保留的字符序列,不能把它用作标识符. 字面量:值的一种标记法. 操作符==运算符:用于执行特定算术或逻辑操作的符号,操作的对象称为操作数. 数组:由若干相同类型的元素组成

nginx 学习五 filter模块简介和实现一个简单的filter模块

1 nginx过滤模块简介 过滤(filter)模块是过滤响应头和内容的模块,可以对回复的头和内容进行处理.它的处理时间在获取回复内容之后, 向用户发送响应之前.它的处理过程分为两个阶段,过滤HTTP回复的头部和主体,在这两个阶段可以分别对头部和主体 进行修改. 2 过滤模块执行顺序 2.1 ngx_http_output_(head, body)_filter_pt 先看一下nginx常用的过滤模块,在ngx_moudles.c中有一下代码: ngx_module_t *ngx_modules

【Nginx】开发一个简单的HTTP模块

首先来分析一下HTTP模块是如何介入Nginx的. 当master进程fork出若干个workr子进程后,每个worker子进程都会在自己的for死循环中不断调用事件模块: for ( ;; ) { .... ngx_process_events_and_timers(cycle); /* 调用事件模块 */ .... } 事件模块检测是否有TCP连接请求,当收到一个SYN包后,由事件模块建立一条TCP连接.连接建立成功后,交由HTTP框架处理,HTTP框架负责接收HTTP头部,并根据头部信息将

基于Servlet、JSP、JDBC、MySQL的一个简单的用户注册模块(附完整源码)

最近看老罗视频,做了一个简单的用户注册系统.用户通过网页(JSP)输入用户名.真名和密码,Servlet接收后通过JDBC将信息保存到MySQL中.虽然是个简单的不能再简单的东西,但麻雀虽小,五脏俱全,在此做一归纳和整理.下面先上源码: 一.index.jsp <%@ page language="java" import="java.util.*" pageEncoding="utf-8"%> <% String path =

用 C 语言编写一个简单的垃圾回收器

人们似乎认为编写垃圾回收机制是很难的,是一种只有少数智者和Hans Boehm(et al)才能理解的高深魔法.我认为编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc样例难度是相当的. 在开始之前有一些重要的事情需要说明一下:第一,我们所写的代码是基于Linux Kernel的,注意是Linux Kernel而不是GNU/Linux.第二,我们的代码是32bit的.第三,请不要直接使用这些代码.我并不保证这些代码完全正确,可能其中有一些我 还未发现的小的bug,但是整体思路仍

用L脚本语言开发一个简单的局域网聊天程序

#scp #这是一个简单的局域网聊天程序的例子 定义:字符串,string1 定义:字符串,string2 #addr1是对方的地址 #addr2是自己的地址 #如果addr1和addr2相同,就是自己和自己聊天 定义:地址,addr1,127.0.0.1,27015 定义:地址,addr2,127.0.0.1,27015 定义:整数,字节数,0 #在自己的UDP端口上监听 定义:网络连接,conn2,UDP 监听:conn2,addr2 #连接对方的UDP端口 定义:网络连接,conn1,UD

手把手教你编写一个简单的PHP模块形态的后门

看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web server端的script语言.目前很多web应用程序都基于php语言实现.由于php是个开源软件并易于扩展,所以我们可以通过编写一个PHP模块(module 或者叫扩展 extension)来实现一个Backdoor. 本文就简单介下如何一步步编写一个简单的php 动态扩展后门. 0×01. p