本文主要是介绍“FPGA+MDIO总线+UART串口=高效读写PHY芯片寄存器!“(含源代码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1、概述
前文对88E1518芯片的端口芯片及原理图进行了讲解,对MDIO的时序也做了简单的讲解。本文通过Verilog HDL去实现MDIO,但是88E1518芯片对不同页的寄存器读写需要切换页,无法直接访问寄存器,如果通过代码读写某些固定寄存器的话会比较麻烦。
为了简化调试,所以采用UART串口来控制MDIO的读写,PC端通过UART向FPGA发送读写PHY芯片寄存器的指令,FPGA通过MDIO总线从PHY芯片读取指定寄存器地址的数据后,通过UART将读取的数据发送到PC端的串口助手进行显示。
使用这种方式,以后就可以通过串口读写各种MDIO接口的寄存器了,而不再只是对88E1518单个芯片的调试有效了。
顶层模块的框图如图1所示:
对应的端口信号如表1所示(位宽均为1位):
信号名 | I/O | 含义 |
---|---|---|
clk | I | 系统时钟,100MHz |
rst_n | I | 系统复位,低电平有效 |
uart_rx | I | 串口接收信号 |
uart_tx | O | 串口发送引脚 |
mdc | O | MDIO的时钟信号,最大不能超过12MHz。 |
mdio | IO | MDIO接口双向数据线 |
参考代码如下所示:
module top #(parameter MDIO_DATA_W = 16 ,//MDIO数据位宽;parameter FCLK = 100_000_000 ,//系统时钟频率,默认100MHz;parameter FCLKMDC = 10_000_000 ,//mdc时钟频率,88e1518最大不能超过12MHz;parameter PHY_ADDR = 5'b0_0000 ,//PHY芯片的地址,88E1518芯片高四位默认为0,最低位由config引脚状态决定。parameter BPS = 115200 ,//串口波特率;parameter UART_DATA_W = 8 ,//串口数据位宽;parameter CHECK_W = 2'b00 ,//校验位,2'b00代表无校验位,2'b01表示奇校验,2'b10表示偶校验,2'b11按无校验处理。parameter STOP_W = 2'b01 //停止位,2'b01表示1位停止位,2'b10表示2位停止位,2'b11表示1.5位停止位;
)(input clk ,//系统时钟信号;input rst_n ,//系统复位信号,高电平有效;output mdc ,//mdio的时钟信号;inout mdio ,//mdio双向数据信号;output mdio_out_en ,input uart_rx ,//串口输入信号;output uart_tx //串口输出信号;
);wire [UART_DATA_W - 1 : 0] rx_out ;wire rx_out_vld ;wire [UART_DATA_W - 1 : 0] tx_data ;wire tx_data_vld ;wire tx_rdy ;wire mdio_rdy ;wire mdio_start ;wire mdio_rw_en ;wire [4 : 0] mdio_addr ;wire [MDIO_DATA_W - 1 : 0] mdio_wdata ;wire [MDIO_DATA_W - 1 : 0] mdio_rdata ;wire mdio_rdata_vld ;//例化串口接收模块;uart_rx #(.FCLK ( FCLK ),//系统时钟频率,默认100MHZ;.BPS ( BPS ),//串口波特率;.DATA_W ( UART_DATA_W ),//接收数据位数以及输出数据位宽;.CHECK_W ( CHECK_W ),//校验位,0代表无校验位;.STOP_W ( STOP_W ) //1位停止位;)u_uart_rx (.clk ( clk ),//系统工作时钟100MHZ;.rst_n ( rst_n ),//系统复位信号,低电平有效;.uart_rx ( uart_rx ),//UART接口输入信号;.rx_out ( rx_out ),//数据输出信号;.rx_out_vld ( rx_out_vld ) //数据有效指示信号;);//例化串口发送模块;uart_tx #(.FCLK ( FCLK ),//系统时钟频率,默认100MHZ;.BPS ( BPS ),//串口波特率;.DATA_W ( UART_DATA_W ),//接收数据位数以及输出数据位宽;.CHECK_W ( CHECK_W ),//校验位,0代表无校验位;.STOP_W ( STOP_W ) //1位停止位;)u_uart_tx (.clk ( clk ),//系统工作时钟100MHZ;.rst_n ( rst_n ),//系统复位信号,低电平有效;.tx_data ( tx_data ),//数据输入信号。.tx_data_vld ( tx_data_vld ),//数据有效指示信号,高电平有效。.uart_tx ( uart_tx ),//uart接口数据输出信号。.tx_rdy ( tx_rdy ) //模块忙闲指示信号;);//例化串口数据处理模块;uart_data_treat #(.UART_DATA_W ( UART_DATA_W ),//串口数据位宽;.MDIO_DATA_W ( MDIO_DATA_W ) //MDIO数据位宽;)u_uart_data_treat (.clk ( clk ),//系统工作时钟100MHZ;.rst_n ( rst_n ),//系统复位信号,低电平有效;.rx_data ( rx_out ),.rx_data_vld ( rx_out_vld ),.tx_rdy ( tx_rdy ),.mdio_rdata ( mdio_rdata ),.mdio_rdata_vld ( mdio_rdata_vld ),.mdio_rdy ( mdio_rdy ),.tx_data ( tx_data ),.tx_data_vld ( tx_data_vld ),.mdio_start ( mdio_start ),.mdio_rw_en ( mdio_rw_en ),.mdio_wdata ( mdio_wdata ),.mdio_addr ( mdio_addr ));//例化mdio接口模块;mdio_drive #(.DATA_W ( MDIO_DATA_W ),//数据位宽;.FCLK ( FCLK ),//系统时钟频率,默认100MHz;.FCLKMDC ( FCLKMDC ),//mdc时钟频率,88e1518最大不能超过12MHz;.PHY_ADDR ( PHY_ADDR ) //PHY芯片的地址,88E1518芯片高四位默认为0,最低位由config引脚状态决定。)u_mdio_drive (.clk ( clk ),//系统时钟信号;.rst_n ( rst_n ),//系统复位信号,高电平有效;.start ( mdio_start ),//开始写入或者读取信号;.rw_en ( mdio_rw_en ),//读写使能,高电平表示读数据,低电平表示写数据;.addr_reg ( mdio_addr ),//读写寄存器地址;.wr_data ( mdio_wdata ),//需要写入的数据;.mdc ( mdc ),//mdio的时钟信号;.mdio_out_en ( mdio_out_en ),//mdio三态门使能信号,高电平有效;用于仿真;.rd_data ( mdio_rdata ),//读出数据;.rd_data_vld ( mdio_rdata_vld),//读出数据有效指示信号,高电平有效;.rdy ( mdio_rdy ),//模块忙闲指示信号,高电平表示模块空闲,可以接收上游数据;.mdio ( mdio ),//mdio双向数据信号;.rd_ack ( ));ila_0 u_ila_0 (.clk ( clk ),//input wire clk.probe0 ( mdc ),//input wire [0:0] probe0 .probe1 ( u_mdio_drive.mdio_in ),//input wire [0:0] probe1 .probe2 ( mdio_out_en ),//input wire [0:0] probe2 .probe3 ( u_mdio_drive.state_c ),//input wire [4:0] probe3 .probe4 ( u_mdio_drive.cnt_data ),//input wire [5:0] probe4 .probe5 ( mdio_rdata ),//input wire [15:0] probe5 .probe6 ( mdio_rdata_vld ),//input wire [0:0] probe6.probe7 ( u_mdio_drive.start ),//input wire [0:0] probe7.probe8 ( u_mdio_drive.rdy ),//input wire [0:0] probe8.probe9 ( mdio_wdata ),//input wire [15:0] probe9.probe10 ( mdio_addr ),//input wire [4:0] probe10.probe11 ( mdio_rw_en ),//input wire [0:0] probe11.probe12 ( uart_rx ),//input wire [0:0] probe12.probe13 ( rx_out ),//input wire [7:0] probe13.probe14 ( rx_out_vld ),//input wire [0:0] probe14.probe15 ( tx_data ),//input wire [7:0] probe15.probe16 ( tx_data_vld ) //input wire [0:0] probe16);endmodule
对应的TestBench代码如下:
`timescale 1 ns/1 ns
module test();parameter CYCLE = 10 ;//系统时钟周期,单位ns,默认10ns;parameter RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;parameter MDIO_DATA_W = 16 ;parameter FCLK = 100_000_000 ;parameter FCLKMDC = 10_000_000 ;parameter PHY_ADDR = 5'b0_0000 ;parameter BPS = 115200 ;parameter UART_DATA_W = 8 ;parameter CHECK_W = 2'b00 ;parameter STOP_W = 2'b01 ;localparam BPS_CNT = FCLK/BPS ;//波特率对应时钟数,不用手动修改该参数;reg clk ;//系统时钟,默认100MHz;reg rst_n ;//系统复位,默认高电平有效;reg uart_rx ;wire uart_tx ;wire mdc ;wire mdio ;wire mdio_out_en ;reg mdio_out;assign mdio = (~mdio_out_en) ? mdio_out : 1'bz;top #(.MDIO_DATA_W ( MDIO_DATA_W ),.FCLK ( FCLK ),.FCLKMDC ( FCLKMDC ),.PHY_ADDR ( PHY_ADDR ),.BPS ( BPS ),.UART_DATA_W ( UART_DATA_W ),.CHECK_W ( CHECK_W ),.STOP_W ( STOP_W ))u_top (.clk ( clk ),.rst_n ( rst_n ),.uart_rx ( uart_rx ),.mdc ( mdc ),.mdio_out_en ( mdio_out_en ),.uart_tx ( uart_tx ),.mdio ( mdio ));//生成周期为CYCLE数值的系统时钟;initial beginclk = 0;forever #(CYCLE/2) clk = ~clk;end//生成复位信号;initial beginrst_n = 1;uart_rx = 0;mdio_out =0;#1;rst_n = 0;//开始时复位10个时钟;#(RST_TIME*CYCLE);rst_n = 1;mdio_rw_task(1'b1,5'd17,0);//读17号寄存器数据;mdio_rw_task(1'b0,5'd22,8'd1);//向22号寄存器写入1repeat(20)beginmdio_rw_task(1'b1,({$random} % 32),0);//读寄存器数据;mdio_rw_task(1'b0,({$random} % 32),({$random} % 256));//写寄存器;end$stop;//停止仿真;endtask mdio_rw_task(input rw_flag ,//mdio读写标志,高电平表示读操作,低电平表示写操作;input [4 : 0] addr ,//mdio读写寄存器地址信号;input [MDIO_DATA_W - 1 : 0] wdata //mdio进行写操作时,需要写入的地址;);reg rw_flag_r;beginrw_flag_r = rw_flag;uart_rx_task(8'h5a);//首先发送帧头,0x5auart_rx_task({7'd0,rw_flag_r});//发送读写操作;uart_rx_task({3'd0,addr});//发送读写寄存器地址;if(~rw_flag_r)begin//mdio进行写操作时,需要写入的地址;uart_rx_task(wdata[15:8]);uart_rx_task(wdata[7:0]);endendendtask//模拟串口发送函数,1位起始位,1位停止位,无校验位,8位数据,先发低位;task uart_rx_task(input [UART_DATA_W-1:0] data //串口待发送数据;); integer i;//用于控制循环次数;begin@(posedge clk);//延迟一个时钟后发送起始位;uart_rx = 1'b0;repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;for(i=0 ; i<8 ; i=i+1)beginuart_rx = data[i];repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;endif(CHECK_W == 2'b01)beginuart_rx = ~(^data);//奇校验时,发送数据;repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;endelse if(CHECK_W == 2'b10)beginuart_rx = (^data);//偶校验时,发送数据;repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;end@(posedge clk);//延迟一个时钟后发送停止位;uart_rx = 1'b1;if(STOP_W == 2'b01)//1位停止位;repeat(BPS_CNT) @(posedge clk);//延迟BPS_CNT个时钟;else if(STOP_W == 2'b10)//2位停止位;repeat(2*BPS_CNT) @(posedge clk);//延迟2*BPS_CNT个时钟;else if(STOP_W == 2'b11)//1.5位停止位;repeat(BPS_CNT*3/2) @(posedge clk);//延迟1.5*BPS_CNT个时钟;endendtaskendmodule
2、串口数据处理模块
至于uart串口接收模块和uart串口发送模块,前文已经对UART全模式接收和发送的代码设计进行过详细讲解,不在赘述。
uart_rx模块接收数据后,需要对数据进行判断,确定对应的数据是对寄存器进行读操作还是写操作,以及是接收的数据是地址还是寄存器的数据?读操作只需要发送读指示信号和寄存器地址即可,而写还需要发送写入HPY芯片内部寄存器的16位数据,所以读写不同,串口发送的数据长度用过也不同。
因此此处规定一下PC端UART发送数据的格式,没发送一次完整的读写操作码及数据称为一帧数据。帧的起始位为8’h5A,FPGA检测到PC端发了8’h5A,表示后面的数据就是对PHY芯片进行读写的操作码和数据。
帧起始后面跟1字节的读写指示信号,第0位为高电平表示进行读操作,低电平表示对PHY内部寄存器进行写操作,其余7位数据无效。
之后PC端发送需要读写PHY芯片的寄存器地址,由于PHY芯片的寄存器地址为5位,所以发送的地址数据只有低5位有效,高3位无效。
如果是读取PHY芯片内部寄存器数据,那么到此结束,等待数据返回即可。如果是要写入数据到PHY芯片内部寄存器,那么还需要发送两字节的写入数据,先发送需要写入数据的高8位,后发送低8位数据。
上述就是我们自己为了方便设置的通信格式,实际上可以简化,可以把读写方式跟寄存器地址合并为1字节数。帧起始码也可以去掉,根据第一字节某些无效位进行判断,但是为了更加直观的查看数据,就不进行简化了。读写PHY芯片寄存器的串口助手指令格式如下所示:
上面都是对该协议的讲述,本模块就是来对该协议进行解析,并且把MDIO驱动模块从PHY芯片指定寄存器地址读取的数据输出到串口发送模块,将读取的数据最终发送到电脑的串口调试助手上。
该模块的端口信号如表2所示:
信号名 | 位宽 | IO | 含义 |
---|---|---|---|
clk | 1 | I | 系统时钟,100MHz。 |
rst_n | 1 | I | 系统复位,低电平有效。 |
Rx_out | 8 | I | 串口接收数据。 |
Rx_out_vld | 1 | I | 串口接收数据有效指示信号。 |
Tx_data | 8 | O | 串口需要发送的数据 |
Tx_data_vld | 1 | O | 串口发送数据有效指示信号。 |
Tx_rdy | 1 | I | 串口发送模块空闲指示信号。 |
Start | O | 1 | 开始读写PHY寄存器,高电平有效。 |
rw_en | O | 1 | 高电平表示读取PHY内部寄存器数据,低电平表示往PHY内部寄存器写入数据。 |
Addr | O | 5 | 读写寄存器地址。 |
Wdata | O | 16 | 需要写入PHY寄存器的数据。 |
Rdata | I | 16 | 从PHY内部寄存器读出的数据。 |
Rdata_vld | I | 1 | 读出的数据有效指示信号。 |
Mdio_rdy | I | 1 | MDIO驱动模块空闲指示信号,高电平有效。 |
该模块的代码比较简单,此处做简单介绍,模块内部代码分为两部分,一部分对接收到PC端发送的UART数据进行检测,检测到帧头8’h5A后,就启用一个计数器对后面接收的串口数据进行计数,第2字节的最低位能够表示此次进行读操作还是写操作,将最低位作为rw_en信号输出,计数器的长度也与该位数据取值有关,高电平表示读操作,那这一帧数据除去帧头就2字节,此时计数器最大值应该为2-1,低电平表示写操作,除去帧头应该有4字节数据,那么计数器的最大值应该是4-1。
然后就是接收第3字节数据的低5位数据作为PHY芯片寄存器地址,如果是读操作,此时就应该拉高start。如果是写操作,还需要接收2字节数据作为wdata输出,才能拉高start信号。但实际上start信号还与MDIO驱动模块是否空闲有关,如果此时MDIO处于工作状态,则等MDIO驱动模块空闲后在拉高start信号。
本模块的另一部分功能就是将MDIO驱动模块从PHY内部寄存器读出的16位数据发送转换成串口发送模块的8位数据,然后传输给电脑。这部分比较简单,因为串口模块需要发送指令,驱动模块才会进行读操作,所以MDIO驱动模块读出数据是有限的,而且PC发送指令到MDIO驱动模块读出数据所需要的时间大于把MDIO读出数据通过串口发送到PC的时间,所以就不会存在数据丢失,不需要使用FIFO、RAM等存储结构暂存读出的数据。
当接收到MDIO读取的数据,并且串口发送模块空闲时,先把读取数据的高8位发送,发送完成后在发送低8位数据。这里会用到一个计数器来记录本次发送的是高位数据还是低位数据。
总体思路就是这样,参考代码如下所示:
module uart_data_treat #(parameter UART_DATA_W = 8 ,//uart传输数据位宽;parameter MDIO_DATA_W = 16 //mdio读写数据位宽;
)(input clk ,//系统时钟信号;input rst_n ,//系统复位信号,低电平有效;input [UART_DATA_W - 1 : 0] rx_data ,//uart接收到的数据;input rx_data_vld ,//uart接收到的数据有效指示信号,高电平有效;input tx_rdy ,//uart发送模块空闲指示信号,高电平有效;output reg [UART_DATA_W - 1 : 0] tx_data ,//uart需要发送的数据;output reg tx_data_vld ,//uart需要发送数据有效指示信号,高电平有效;input [MDIO_DATA_W - 1 : 0] mdio_rdata ,//mdio读取的数据;input mdio_rdata_vld ,//mdio读取的数据有效指示信号,高电平有效;input mdio_rdy ,//mdio接口模块空闲指示信号,高电平有效;output reg mdio_start ,//mdio开始进行读写操作信号;output reg mdio_rw_en ,//mdio接口模块执行读写操作,高电平表示读操作,低电平表示写操作;output reg [MDIO_DATA_W - 1 : 0] mdio_wdata ,//mdio在写操作时写入寄存器的数据;output reg [4 : 0] mdio_addr //mdio读写寄存器的地址;
); reg rx_done ;//reg uart_rx_flag ;//reg [1 : 0] cnt_rx ;//reg [2 : 0] cnt_rx_num ;reg [MDIO_DATA_W - 1 : 0] mdio_rdata_r ;//reg uart_tx_flag ;//reg cnt_tx ;//wire add_cnt_tx ;wire end_cnt_tx ;wire add_cnt_rx ;wire end_cnt_rx ;/************* 处理FPGA通过接收到PC端产生的数据开始 ****************///规定一下串口数据的格式,首先得有个帧头8‘h5a,然后需要发送读写寄存器的地址和读写操作,//第二字节的最低位表示读写操作,高电平表示mdio读操作,低电平表示mdio进行写操作。//同时可以根据该位判断该帧数据长度,如果是读操作,则后面只有1字节的寄存器地址,如果是写操作,则地址后面应该还有2字节写数据;//第三字节的低5位表示读写寄存器的地址,高三位无效,可随意设置;//如果是读操作,则没有其余操作了,等待后文模块将读出数据通过串口发送到PC即可。//如果是写操作,还需要接收2字节的写数据,之后才能产生start信号。//标志信号,初始值为0,当检测到帧头8'h5a时拉高,当接收完一帧数据时拉低;//该操作表示,如果PC串口发送数据过快,则只接收最开时接收到的一帧数据,发送完成后在接收其余数据。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;uart_rx_flag <= 1'b0;endelse if(end_cnt_rx)begin//接收完一帧数据;uart_rx_flag <= 1'b0;end//当接收到数据帧头且没有已经接收但没有发出的数据时拉高;else if(rx_data_vld && (rx_data == 8'h5a) && (~rx_done))beginuart_rx_flag <= 1'b1;endend//接收数据寄存器,当uart_rx_flag信号拉高后接收到有效数据时加一。//注意帧头并不会被计数器计数,所以在计算计数器接收数据个数时不包括帧头。//当把读写操作、读写地址、写数据接收完成时清零。always@(posedge clk)beginif(rst_n==1'b0)begin//cnt_rx <= 0;endelse if(add_cnt_rx)beginif(end_cnt_rx)cnt_rx <= 0;elsecnt_rx <= cnt_rx + 1;endendassign add_cnt_rx = uart_rx_flag && rx_data_vld;//当uart_rx_flag信号有效且接收数据有效时拉高;assign end_cnt_rx = add_cnt_rx && cnt_rx == cnt_rx_num - 1;//当接收到指定个有效数据时拉高,表示接收数据完成。//根据读写操作判断计数器的长度,从而实现接收数据的长度变化。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为4;cnt_rx_num <= 3'd4;endelse if(cnt_rx==0 && add_cnt_rx)beginif(~rx_data[0])//当接收到的表示进行写操作时,表示总共需要接收4字节数据。cnt_rx_num <= 3'd4;else//否则表示接收的是读操作的数据,则只需要接收2字节数据;cnt_rx_num <= 3'd2;endend//mdio进行读写操作的指示信号,高电平表示读。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;mdio_rw_en <= 1'b0;end//接收到的第一字节最低位数据。else if(cnt_rx==0 && add_cnt_rx)beginmdio_rw_en <= rx_data[0];endend//mdio读写操作的寄存器地址。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;mdio_addr <= 5'd0;end//接收到的第二字节数据低5位是读写寄存器地址。else if(cnt_rx==1 && add_cnt_rx)beginmdio_addr <= rx_data[4:0];endend//将串口发送的2字节数据进行拼接,作为mdio的写数据。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;mdio_wdata <= 16'd0;endelse if(add_cnt_rx)beginif(cnt_rx==2)//串口先发高八位数据;mdio_wdata <= {rx_data , mdio_wdata[7:0]};else if(cnt_rx==3)//再发低八位数据;mdio_wdata <= {mdio_wdata[15:8] , rx_data};endend//接收串口一帧数据的指示信号。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;rx_done <= 1'b0;endelse if(mdio_start)begin//当mdio模块将接收到的数据发送,所以拉低;rx_done <= 1'b0;endelse if(end_cnt_rx)begin//当rx_done <= 1'b1;endend//mdio读写寄存器的开始信号,当mdio发送数据模块空闲且接收到串口发送的完整数据时拉高,其余时间拉低。always@(posedge clk)beginif(rx_done && mdio_rdy)beginmdio_start <= 1'b1;endelse beginmdio_start <= 1'b0;endend/************* 处理FPGA通过接收到PC端产生的数据结束 ****************//************** 把mdio读取数据通过串口发送给PC开始 *****************///由于mdio每次最多读取2字节数据,且每次读取数据都需要PC端先通过串口设置读写指令及读写寄存器地址,还有帧头数据。//所以串口发送和接收波特率相同的情况下,FPGA给PC端发送数据时比较空闲的,数据比较少,不需要用FIFO之类的做缓冲。//直接使用寄存器暂存即可,不会丢失mdio读出的数据。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;mdio_rdata_r <= {{MDIO_DATA_W}{1'b0}};end//当mdio读出数据有效且没有未发送数据时将数据暂存;else if(mdio_rdata_vld && ~uart_tx_flag)beginmdio_rdata_r <= mdio_rdata;endend//有未发送数据指示信号,初始值为0,当mdio读取有效数据时拉高。//当接收的数据发送完成时拉低。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;uart_tx_flag <= 1'b0;endelse if(end_cnt_tx)beginuart_tx_flag <= 1'b0;endelse if(mdio_rdata_vld)beginuart_tx_flag <= 1'b1;endend//发送数据字节数计数器,初始值为0,当有未发送数据且下游串口发送模块空闲时加一。//当发送完mdio读取的2字节数据时清零。always@(posedge clk)beginif(rst_n==1'b0)begin//cnt_tx <= 0;endelse if(add_cnt_tx)beginif(end_cnt_tx)cnt_tx <= 0;elsecnt_tx <= cnt_tx + 1;endendassign add_cnt_tx = uart_tx_flag && tx_rdy;//当有未发送数据且串口发送模块空闲时拉高。assign end_cnt_tx = add_cnt_tx && cnt_tx == 2 - 1;//当发送完2字节数据时拉高;//串口发送数据,先发送高八位数据,后发送低8位数据。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;tx_data <= {{UART_DATA_W}{1'b0}};endelse if(add_cnt_tx)beginif(cnt_tx==0)//先发送高八位数据;tx_data <= mdio_rdata_r[15:8];else//后发送低8位数据;tx_data <= mdio_rdata_r[7:0];endend//生成串口发送数据有效指示信号,当发送数据时拉高,其余时间拉低。always@(posedge clk)beginif(add_cnt_tx)begin//初始值为0;tx_data_vld <= 1'b1;endelse begintx_data_vld <= 1'b0;endend/************** 把mdio读取数据通过串口发送给PC结束 *****************/endmodule
该模块的仿真结果如下所示,当接收到PC端发送的帧头8’h5A后,计数器开始工作,对后续数据进行解析。
上图检测到帧头8’h5A,然后下一字节数据最低位为0表示写指令,此时需要接收4字节数据,cnt_rx_num则赋值为4,然后依次接收寄存器地址和数据,最后将接收数据结尾的仿真截图放大,得到图4。
下图是PC端发送读寄存器指令的时序,首先检测到帧头8’h5A,下一字节的最低位为1,表示进行读操作,除去帧头只有2字节数据,所以计数器的最大值cnt_rx_num为2。
将开始部分放大后如下图所示,
结束部分的时序如下图所示,计数器和标志信号这些都要清零处理,将start信号拉高一个时钟周期。
另一部分的仿真功能此处没有做过多处理,因为MDIO从机的程序并没有进行编写,所以仿真时返回的数据始终为0,所以这部分仿真看起来比较简单,如下图所示:
当从PHY芯片的寄存器读取到数据mdio_rdata_vld拉高,并且串口发送模块空闲(tx_rdy为高电平)时,将flag信号拉高,并且把mdio_rdata的高8位数据输出给串口发送模块进行发送,将tx_data_vld拉高一个时钟周期。开始传输数据的细节如下图所示:
由于发送的数据始终为0,所以上述仿真可能不是很直观,有兴趣的后续可以通过ILA直接抓取该模块的信号,那样更加简单,不必写MDIO的从机,或者可以对此模块单独仿真。
3、MDIO驱动模块
MDIO接口的时序在前文讲解88E1518芯片时已经讲解过了,读写时序如下图所示,所以本文就不再赘述。
本模块的端口信号如下表3所示:
信号名 | 位宽 | IO | 含义 |
---|---|---|---|
clk | 1 | I | 系统时钟,100MHz。 |
rst_n | 1 | I | 系统复位,低电平有效。 |
start | I | 1 | 开始读写PHY寄存器,高电平有效。 |
rw_en | I | 1 | 高电平表示读取PHY内部寄存器数据,低电平表示往PHY内部寄存器写入数据。 |
addr | I | 5 | 读写寄存器地址。 |
wdata | I | 16 | 需要写入PHY寄存器的数据。 |
rdata | O | 16 | 从PHY内部寄存器读出的数据。 |
rdata_vld | O | 1 | 读出的数据有效指示信号。 |
mdio_rdy | O | 1 | MDIO驱动模块空闲指示信号,高电平有效。 |
mdc | O | 1 | MDIO接口的时钟信号,最大不超过12MHz。 |
mdio | IO | 1 | MDIO双向数据线。 |
通过图5的读写时序图,使用状态机会比较简单,其实使用计数器位主架构会更加简单。本文使用状态机,总共划分为5个状态,如图12所示,空闲状态时没有读写操作,此时rdy信号为高电平。本模块需要生成MDC时钟信号,系统时钟100MHz,MDC采用10MHz,便于分频实现。
在检测到上游模块的开始读写寄存器信号start为高电平,并且MDC下降沿到来时,状态机由空闲状态跳转到START状态,该状态会发送32位前导码,2位起始位,2位读写指示信号,5位PHY地址,5位寄存器地址,不管进行读操作还是写操作,都需要进行发送这些数据,所以将这些数据全部归为START状态,简化状态机。
发送完寄存器地址后,跳转到TA状态,如果是写操作,发送2位数据10,如果是读操作,则释放总线。之后根据读写操作,分别跳转到读数据和写数据状态。数据在MDC下降沿进行输出或读取,此处通过分频计数器的值来判断MDC的下降沿和上升沿,最好不要把MDC作为触发器的时钟信号,FPGA尽量全部使用同步时钟信号。
在读数据或者写数据状态时,代码中并不只是停留了16个时钟,而是24个时钟,原因在于图5每次进行读写数据后,都会回到空闲状态,间隔了8个时钟周期。为了保险就将这段时间合并在读写寄存器数据的状态了,只不过后面8个时钟周期直接释放总线,达到相同效果。
上述就是状态机的跳转,当然还需要一个计数器用来计数MDC的时钟个数,用作状态机跳转的判断依据,以及记录发送的数据个数,当状态机不在空闲状态且分频计数器计数结束时,该计数器就加1,状态机处于不同状态,这个计数器的最大值不一样。所以需要另一个信号来记录计数器的最大值。
注意写数据时先写高位,读数据时也是先读取高位数据,代码的总体思路就是这样了,当然还有些暂存寄存器地址,读写指示信号等,这些可以自行研究。对应的参考代码如下所示:
module mdio_drive #(parameter DATA_W = 16 ,//数据位宽;parameter FCLK = 100_000_000 ,//系统时钟频率,默认100MHz;parameter FCLKMDC = 10_000_000 ,//mdc时钟频率,88e1518最大不能超过12MHz;parameter PHY_ADDR = 5'b0_0000 //PHY芯片的地址,88E1518芯片高四位默认为0,最低位由config引脚状态决定。
)(input clk ,//系统时钟信号;input rst_n ,//系统复位信号,高电平有效;output reg mdc ,//mdio的时钟信号;inout mdio ,//mdio双向数据信号;output reg mdio_out_en ,//mdio三态门使能信号,高电平有效;用于仿真;input start ,//开始写入或者读取信号;input rw_en ,//读写使能,高电平表示读数据,低电平表示写数据;input [4 : 0] addr_reg ,//读写寄存器地址;input [DATA_W - 1 : 0] wr_data ,//需要写入的数据;output reg [DATA_W - 1 : 0] rd_data ,//读出数据;output reg rd_data_vld ,//读出数据有效指示信号,高电平有效;output reg rdy ,//模块忙闲指示信号,高电平表示模块空闲,可以接收上游数据;output reg rd_ack //读应答,高电平表示PHY芯片应答了读操作,本设计并未使用。);//处理计数器的参数;localparam DIV_NUM = FCLK / FCLKMDC ;//分频系数;localparam DIV_NUM_W = clogb2(DIV_NUM-1) ;//分频计数器位宽计算,该计数器只会计数到最大值减一;//状态机的状态定义;localparam IDLE = 5'b00001 ;//空闲状态;localparam ADDR = 5'b00010 ;//发送前导码,地址,读写方式的状态;localparam TA = 5'b00100 ;//根据读写转换总线状态;localparam WR_DATA = 5'b01000 ;//写数据状态;localparam RD_DATA = 5'b10000 ;//读数据状态;reg mdio_out ;//mdio输出数据;//reg mdio_out_en ;//mdio三态门使能信号,高电平有效;reg start_r ;//将开始信号暂时保存,与mdc信号对齐;reg rw_en_r ;//将读写指示信号暂存;reg [15 : 0] wr_data_r ;//将写数据暂存;reg [4 : 0] state_n ;//状态机的次态;reg [4 : 0] state_c ;//状态机的现态;reg [45 : 0] start_addr ;//将32位前导码,2位起始位,2位读写指示位,5位PHY地址,5位寄存器地址拼接;reg [5 : 0] cnt_data_num ;//计数器cnt_data在不同状态下需要发送或读取数据的个数;reg [DIV_NUM_W - 1 : 0] cnt_div ;//分频计数器,用于生成mdc时钟;reg [5 : 0] cnt_data ;//用于计数状态机不再空闲状态下发送的数据位数;wire add_cnt_data ;wire end_cnt_data ;wire end_cnt_div ;wire mdio_in ;wire idl2addr_start ;wire addr2ta_start ;wire ta2wr_start ;wire ta2rd_start ;wire wr2idl_start ;wire rd2idl_start ;//mdio的三态接口;assign mdio = mdio_out_en ? mdio_out : 1'bz;assign mdio_in = mdio;//自动计算位宽函数;function integer clogb2(input integer depth);beginif(depth == 0)clogb2 = 1;else if(depth != 0)for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)depth=depth >> 1;endendfunction//分频计数器,计数到分频系数减一清零;always@(posedge clk)beginif(rst_n==1'b0)begin//cnt_div <= 0;endelse if(end_cnt_div)cnt_div <= 0;elsecnt_div <= cnt_div + 1;end//分频计数器结束条件,计数到分频系数减一时清零;assign end_cnt_div = cnt_div == DIV_NUM - 1;//MDIO的时钟信号,当分频计数器计数结束时拉低,计数到一半时拉高;always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;mdc <= 1'b0;endelse if(cnt_div == DIV_NUM - 1)beginmdc <= 1'b0;endelse if(cnt_div == DIV_NUM/2 - 1)beginmdc <= 1'b1;endend//把开始读写信号暂存,为了mdio的数据与mdc时钟对齐;always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;start_r <= 1'b0;endelse if(start)beginstart_r <= 1'b1;endelse if(end_cnt_div)beginstart_r <= 1'b0;endend//将读写指示信号、写数据暂存,状态机不在空闲时,外部输入数据无效;always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;rw_en_r <= 1'b0;wr_data_r <= 16'd0;endelse if(start && state_c == IDLE)beginrw_en_r <= rw_en;wr_data_r <= wr_data;endend//The first section: synchronous timing always module, formatted to describe the transfer of the secondary register to the live register ?always@(posedge clk)beginif(rst_n==1'b0)beginstate_c <= IDLE;endelse beginstate_c <= state_n;endend//The second paragraph: The combinational logic always module describes the state transition condition judgment.always@(*)begincase(state_c)IDLE:beginif(idl2addr_start)beginstate_n = ADDR;endelse beginstate_n = state_c;endendADDR:beginif(addr2ta_start)beginstate_n = TA;endelse beginstate_n = state_c;endendTA:beginif(ta2wr_start)beginstate_n = WR_DATA;endelse if(ta2rd_start)beginstate_n = RD_DATA;endelse beginstate_n = state_c;endendWR_DATA:beginif(wr2idl_start)beginstate_n = IDLE;endelse beginstate_n = state_c;endendRD_DATA:beginif(rd2idl_start)beginstate_n = IDLE;endelse beginstate_n = state_c;endenddefault:beginstate_n = IDLE;endendcaseend// Third paragraph: Design transfer conditions;assign idl2addr_start = state_c==IDLE && start_r && end_cnt_div;//状态机处于空闲状态,接收到上游模块发送的开始信号且分频计数器计数结束;assign addr2ta_start = state_c==ADDR && end_cnt_data;//处于发送地址,前导码状态,且数据发送完成(计数器cnt_data计数结束);assign ta2wr_start = state_c==TA && end_cnt_data && (~rw_en_r);//处于TA状态,且经过固定时钟周期后,如果进行写操作,则跳转到写数据阶段;assign ta2rd_start = state_c==TA && end_cnt_data && rw_en_r;//处于TA状态,且经过固定时钟周期后,如果进行读操作,则跳转到读数据阶段;assign wr2idl_start = state_c==WR_DATA && end_cnt_data;//处于写数据状态,且写完所有数据;assign rd2idl_start = state_c==RD_DATA && end_cnt_data;//处于读数据状态,且读完所有数据;//cnt_data计数器,用来计数状态机不处于空闲状态时,在各个状态下发送的数据个数;//初始值为0,当状态机不处于空闲状态且分频计数器计数结束时加1。//当计数器计数到cnt_data_num-1时表示该状态的数据已经写入或读取完成,此时计数器清零;always@(posedge clk)beginif(rst_n==1'b0)begin//cnt_data <= 0;endelse if(add_cnt_data)beginif(end_cnt_data)cnt_data <= 0;elsecnt_data <= cnt_data + 1;endendassign add_cnt_data = ((state_c != IDLE) && end_cnt_div);assign end_cnt_data = add_cnt_data && cnt_data == cnt_data_num - 1;//状态机各个状态需要发送数据或者读取数据的个数;always@(posedge clk)beginif(state_c == TA)begin//此处最多需要发送2位数据;cnt_data_num <= 6'd2;endelse if(state_c == WR_DATA || state_c == RD_DATA)begin//读取或者写入的数据都是16位,但是手册里让每次数据发送完成和接收完成后,需要释放总线一段时间才能进行下次读写,所以在这个阶段多加几个时钟周期。cnt_data_num <= 6'd25;endelse begin//在ADDR状态下,需要发送32位前导码,2位起始位,2位读写指示位,5位PHY地址,5位寄存器地址;cnt_data_num <= 32+2+2+5+5;endend//当状态机处于空闲状态且开始信号有效时,将状态机需要在ADDR状态发送的数据拼接;always@(posedge clk)beginif(rst_n==1'b0)begin//初始值全为高电平;start_addr <= 46'h3fff_ffff_ffff;endelse if(start && state_c == IDLE)begin//状态机在空闲状态下,检测到开始发送信号时,将前导码,起始位,读写状态,PHY地址,寄存器地址拼接。start_addr <= {32'hffff_ffff,2'b01,{rw_en,~rw_en},PHY_ADDR,addr_reg};endend//输出mdio的数据。always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;mdio_out <= 1'b0;endelse if(state_c == ADDR)begin//输出前导码,地址等数据;mdio_out <= start_addr[45 - cnt_data];endelse if(state_c == TA)begin//此过程输出2'b10,读的时候将三态使能信号拉低即可释放总线,与数据线状态无关,所以不会影响。if(cnt_data == 0)mdio_out <= 1'b1;elsemdio_out <= 1'b0;endelse if(state_c == WR_DATA && (cnt_data < 16))begin//写状态时,将16位数据输出,先输出高位数据;mdio_out <= wr_data_r[15 - cnt_data];endend//三态门使能信号;always@(posedge clk)begin//当状态机处于ADDR 或者 写数据 或者 TA状态且写有效时将三态门使能;if(state_c == ADDR || (state_c == WR_DATA && (cnt_data < 16)) || (state_c == TA && ~rw_en_r))beginmdio_out_en <= 1'b1;endelse begin//其余时间三态门使能关闭,释放总线;mdio_out_en <= 1'b0;endend//读应答,只有在TA阶段,MDC下降沿当数据线被PHY芯片拉低时,表示PHY芯片应答了,此时将应答标志拉高,其余时间均为低电平;always@(posedge clk)beginif(state_c == TA && rw_en_r && (cnt_div == DIV_NUM - 1))beginrd_ack <= ~mdio_in;endelse beginrd_ack <= 1'b0;endend//读取采集到的数据;always@(posedge clk)beginif(rst_n==1'b0)begin//初始值为0;rd_data <= 16'd0;end//状态机处于读取状态时,在分频时钟上升沿沿读取数据,先读取高位数据;else if(state_n == RD_DATA && (cnt_div == DIV_NUM/2 - 1) && (cnt_data < 16))beginrd_data[15 - cnt_data] <= mdio_in;endend//生成读取数据有效指示信号;always@(posedge clk)begin//当读取完所有数据时,输出有效指示信号拉高,其余时间拉低;rd_data_vld <= ((state_c == RD_DATA) && (cnt_data == 15) && (cnt_div == DIV_NUM/2 - 1));end//忙闲指示信号,always@(*)beginif(start || start_r || (state_c != IDLE))rdy = 1'd0;elserdy = 1'b1;endendmodule
该模块的仿真读寄存器的整体时序如下图所示:
放大后如图14所示,首先前导码输出32个高电平。
然后依次发送2为起始位,2为读指示信号,5位PHY地址(本次使用88E1518的PHY地址设置为5’d0和5’d1,由硬件电路决定),5位寄存器地址,然后释放总线,mdio_out_en是三态门的使能信号,低电平表示释放总线。之后经过在16个MDC时钟的下降沿读取mdio数据线上的信号,读取完毕后将rdata_vld拉高,表示读取数据有效。
写寄存器的总体仿真时序如下图所示:
起始时序如下图所示:
前导码发送之后的时序如下图所示,当数据发送完毕后把总线释放。
4、上板测试
为了查看MDIO接口时序,所以在顶层文件中加入了一个ILA模块,用来抓取需要查看的一些信号,便于调试。然后开发板插上千兆网线,串口数据线,下载器,最后打开电源下载程序,如下所示。
串口助手读写PHY寄存器
程序下载完成后,查看电脑上此时网口的传输速率,在搜索栏中搜索“查看网络连接”,如下图所示。
如下图所示,如果将电脑通过网线与开发板的网口连接,则会出现以太网字样,选中后鼠标右键,点击状态。
如果电脑网卡速率大于等于1Gbps,那么此时会如下图显示一致,使用1Gbps进行传输,因为此时PC和FPGA的PHY芯片的通信速率是通过自动协商完成的,所以会选择都支持的最高通信速率。
PC端的最大通信速率可以手动修改,如图所示,鼠标右击以太网,选中属性。
然后点击配置,如下图所示:
如下图所示,选择高级,然后下拉点击连接速度和双工模式,之后可以发现默认是自动侦测,此时我们手动修改位100Mbps 全双工。
之后在查看以太网的通信速率,结果如图所示,此时就是100Mbps 全双工通信模式了。
这是PC端进行修改,本文需要实现FPGA通过修改内部寄存器实现以太通信速率的修改,所以先将PC端修改回自动侦测,然后我们通过配置PHY芯片寄存器达到修改通信速率的效果。
首先打开串口调试助手,将波特率设置为115200(代码中默认设置的115200,使用其他波特率需要修改顶层文件的波特率数值),将数据位设置为8位,起始位1位,无校验位,1位停止位,接收和发送的数据均以16进制数据进行显示,设置如下图所示:
首先将ILA的start位高电平作为触发条件,然后通过串口调试助手发送读取指令,首先读取17_0号寄存器的数据,通过bit15和bit14判断当前PHY工作速率,如下图所示,发送帧头5A,然后读指示数据01,然后跟读取寄存器地址,17的16进制为11。发送指令后,上面会返回读取到该寄存器的数据为16‘hAC48,bit15:14为2’b10,则表示此时的通信速率为1000Mbps,bit13为高电平表示全双工模式。
此时可以通过PC端查看,也为1Gbps传输速率,然后查看ILA触发的波形时序,如下图所示,FPGA释放MDIO总线后,MDIO总线会被上拉电阻拉高,然后下个时钟会被PHY芯片拉低,然后就在MDC时钟上升沿输出对应寄存器的数据,FPGA接收到的数据也是16’hAC48,与串口助手返回的数据一致。
所以PHY芯片默认会使用1000Mbps全双工模式进行通信,然后我们要修改PHY芯片通信模式,则需要将PHY芯片的自动协商关闭(0_0.12写入0),此时将通信速率更改为10Mbps,则0_0.13和0_0.6均写入0。并且使用半双工模式,那么0_0.8写入0,想要0_0.13和0_0.6,0_0.8写入生效,就必须在0_0.15或者0_0.9写入1,此处在0_0.9写入1重启自动协商,所以需要写入的数据是16’h0200,串口助手发送写指令的格式如下所示。
向0号寄存器吸入16’h0200,ILA抓取的时序如下所示:
上述时序图可能太小,将图放大,如下面两图所示:
然后继续读取17_0.15:13寄存器,查看PHY芯片此时的通信速率,以及工作模式,读取的数据如下图所示,返回数据为16’h0C08,表明此时PHY芯片以10Mbps半双工模式进行通信,表示写入0_0寄存器的数据有效。
此时查看PC端以太网的通信速率如下图所示,也为10Mbps的通信速率,因此也可以判断数据的正确性。
注意在设置0_0寄存器时,需要重点关注自动协商是否被关闭,如果没有关闭自动协商,对PHY通信速率设置是不会起作用的。
综上所述,本文通过串口调试助手间接读写PHY芯片内部的寄存器,这种方式对于开始了解一个接口时非常有效,可以对你的猜想快速进行验证,可以对内部任何寄存器进行读写操作,只需要通过串口助手修改发送的数据和地址即可,不需要对代码做出任何调整。
本文也是对MDIO的时序以及实现做了充分验证,由兴趣的可以读取其他页寄存器数据验证换页的操作,只需要提前向22号寄存器写入对应页即可跳转到指定页。工程文件可以在公众号后台回复“FPGA实现MDIO控制器”(不含引号)即可。
对了,放一个我刚开始读写这些寄存器的视频,由于开始没注意自动协商,怎么改寄存器都达不到效果,浪费了一些时间。
串口助手调试PHY芯片
这篇关于“FPGA+MDIO总线+UART串口=高效读写PHY芯片寄存器!“(含源代码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!