用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,是一个双向端口信号,用于串行数据的传输。双向端口信号的控制方式在后面的代码中可以参考。
在PHY芯片中,根据IEEE802.3规范中的22号条款要求的0号寄存器到6号寄存器和厂商自定义的16号寄存器到31号寄存器大致如图2所示(第一列即为它们的地址):
图2 PHY芯片中的部分寄存器
在上面这些寄存器中,寄存器0位基本的控制寄存器,对PHY芯片的控制一般通过写这个寄存器来完成,控制寄存器的各位的功能如图3所示:
图3 控制寄存器(PHY芯片中地址为0)
通过SMI读物理芯片的寄存器的时钟顺序如图4所示:
图4 SMI读物理芯片寄存器时序图(图片来自LAN8710A芯片官方文档截图)
通过SMI写物理芯片的寄存器的时钟顺序如图5所示:
图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给出了发送数据和接收数据的时序关系以及在发送过程或接收过程中的一些特殊的标志数据:
图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
用Verilog语言实现一个简单的MII模块的更多相关文章
- 用C语言编写一个简单的词法分析程序
问题描述: 用C或C++语言编写一个简单的词法分析程序,扫描C语言小子集的源程序,根据给定的词法规则,识别单词,填写相应的表.如果产生词法错误,则显示错误信息.位置,并试图从错误中恢复.简单的恢复方法 ...
- 用Go语言实现一个简单的聊天机器人
一.介绍 目的:使用Go语言写一个简单的聊天机器人,复习整合Go语言的语法和基础知识. 软件环境:Go1.9,Goland 2018.1.5. 二.回顾 Go语言基本构成要素:标识符.关键字.字面量. ...
- 一个简单的AMD模块加载器
一个简单的AMD模块加载器 参考 https://github.com/JsAaron/NodeJs-Demo/tree/master/require PS Aaron大大的比我的完整 PS 这不是一 ...
- 用 C 语言编写一个简单的垃圾回收器
人们似乎觉得编写垃圾回收机制是非常难的,是一种仅仅有少数智者和Hans Boehm(et al)才干理解的高深魔法.我觉得编写垃圾回收最难的地方就是内存分配,这和阅读K&R所写的malloc例 ...
- socket实例C语言:一个简单的聊天程序
我们老师让写一个简单的聊天软件,并且实现不同机子之间的通信,我用的是SOCKET编程.不废话多说了,先附上代码: 服务器端server.c #include <stdio.h> #incl ...
- 【阿菜做实践】利用go语言写一个简单的Pow样例
本篇博客的主要内容是用go写一个简单的Proof-of-Work共识机制,不涉及到网络通信环节,只是一个本地的简单demo.开发IDE用的是JB Golang. 整个项目的文件结构如下: PoWdem ...
- 手把手教你编写一个简单的PHP模块形态的后门
看到Freebuf 小编发表的用这个隐藏于PHP模块中的rootkit,就能持久接管服务器文章,很感兴趣,苦无作者没留下PoC,自己研究一番,有了此文 0×00. 引言 PHP是一个非常流行的web ...
- go 语言实现一个简单的 web 服务器
学习Go语言的一些感受,不一定准确. 假如发生战争,JAVA一般都是充当航母战斗群的角色.一旦出动,就是护卫舰.巡洋舰.航母舰载机.预警机.电子战飞机.潜艇等等浩浩荡荡,杀将过去.(JVM,数十个JA ...
- go语言实现一个简单的登录注册web小程序
最近学习golang也有一段时间了,基础差不多学了个大概,因为本人是java程序员,所以对web更感兴趣.根据<go web编程>中的例子改编一个更简单的例子,供新手参考,废话不多说,上菜 ...
随机推荐
- DBCP 连接池
DBCP数据源 DBCP 是 Apache 软件基金组织下的开源连接池实现 导入maven包: <!-- dbcp连接池 --> <dependency> <groupI ...
- 浅析redis缓存 在spring中的配置 及其简单的使用
一:如果你需要在你的本地项目中配置redis.那么你首先得需要在你的本地安装redis 参考链接[http://www.runoob.com/redis/redis-install.html] 下载r ...
- django 1.开发接口环境搭建
首先需要的环境: pycharm Python 3.6.0 django 2.1.3 安装命令: pip3 install django 查看版本号和安装的路径: pip show ...
- linux通过expect工具来实现自动登录服务器,并执行相关操作
参考地址:https://www.cnblogs.com/liyuanhong/articles/7728034.html EOF的使用参考:https://www.cnblogs.com/liyua ...
- c++ boost库配置
1.官方下载地址 https://www.boost.org/ 2.下载解压 3.配置VS 4.配置目录
- Mysql查询库、表存储量(Size)
Mysql查询库.表存储量(Size) 1.要查询表所占的容量,就是把表的数据和索引加起来就可以了. SELECT SUM(DATA_LENGTH) + SUM(INDEX_LENGTH) FROM ...
- 配置rpm包安装的jdk环境变量
最近在搭建james邮件服务的时候,由于这个服务是用Java开发的,之前这台服务器跑过tomcat服务,故有Java环境,就没在意有无配置环境变量,但在启动james的时候报没有配置环境变量: 那么问 ...
- mysql设置某列不重复
5.1版本以上:ALTER TABLE dbname.table ADD UNIQUE (fieldname); 数据库名为dbname,表名table,字段名fieldname.
- 10、jstl标签库
一.JSTL是SUN公司制定的一个JSP标签库,由apache jakarta组织负责维护的开源项目,目的是用标签代替java代码片段.JSTL 1.0 由四个定制标记库(core.format.xm ...
- java实现单链接的几种常用操作
public class ListNode { public int value; public ListNode next; public ListNode(int value) { this.va ...