最近迷上了FPGA的网络通信和GTP光通信,个人感觉光通信简单一些,那就从难得网络通信开始吧,先搞个最简单的,使用MDIO配置和读取网络PHY的信息。
板子:米联客的MA703FA(A7-35T板子);
参考例程:正点原子达芬奇开发板例程;
IDE:vivado2020.2;
具体的原理啥的建议去看正点原子的文档吧,讲得很好,但原子的例程感觉不贴近实际项目,所以我改了一下,使之适合真是项目。
先来看看这块芯片RTL8211FD的数据手册。
这是官方给的应用架构,很简单,RTL8211FD与MAC通信,通过MDC和MDIO配置。
芯片BD,没啥好说的,典型的rgmii接口,内部模块电路感觉没必要深究,反正也不懂,能用就行了。
硬件复位这里要注意,官方说应至少保持10ms低电平,最好还是按官方说的做,菜就要多听话。
芯片地址这里注意了,RTL8211FD器件地址由5位构成,高两位固定为2’b00,第三位后这三个引脚的上下拉电平决定,所以看看米联客板子的原理图:
由此可得:此RTL8211FD器件地址为:5’b00001;
这块芯片是不需要配置也能使用的,参考官方给的寄存器默认值可知,默认状态下芯片开启自协商,速率1000M,这就行了,已经不需要配置了,但为了学习,我们可以读状态寄存器,从而获得连接状态和通信速率。需要读两个状态寄存器:
第一是这个,需要读他的第2和第5位,
第2位:芯片和MAC连接成功;
第5位:芯片和MAC自协商成功;
还有这个寄存器,主要读芯片的通信速率;
第4到5位是通信速率;
好了,在看代码:
MDIO控制器直接用原子的,写法和他们家的iic控制器类似,够骚,也够繁琐,但能用,菜就别讲究了吧。
//****************************************************************************************//
module mdio_dri #(
parameter PHY_ADDR = 5'b00001,//PHY地址
parameter CLK_DIV = 6'd10 //分频系数
)
(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input i_op_exec , //触发开始信号
input i_op_rh_wl , //低电平写,高电平读
input [4:0] i_op_addr , //寄存器地址
input [15:0] i_op_wr_data, //写入寄存器的数据
output reg o_op_done , //读写完成
output reg [15:0] o_op_rd_data, //读出的数据
output reg o_op_rd_ack , //读应答信号 0:应答 1:未应答
output reg o_dri_clk , //驱动时钟
output reg o_eth_mdc , //PHY管理接口的时钟信号
inout i_eth_mdio //PHY管理接口的双向数据信号
);
//parameter define
localparam st_idle = 6'b00_0001; //空闲状态
localparam st_pre = 6'b00_0010; //发送PRE(前导码)
localparam st_start = 6'b00_0100; //开始状态,发送ST(开始)+OP(操作码)
localparam st_addr = 6'b00_1000; //写地址,发送PHY地址+寄存器地址
localparam st_wr_data = 6'b01_0000; //TA+写数据
localparam st_rd_data = 6'b10_0000; //TA+读数据
//reg define
reg [5:0] cur_state ;
reg [5:0] next_state;
reg [5:0] clk_cnt ; //分频计数
reg [15:0] wr_data_t ; //缓存写寄存器的数据
reg [4:0] addr_t ; //缓存寄存器地址
reg [6:0] cnt ; //计数器
reg st_done ; //状态开始跳转信号
reg [1:0] op_code ; //操作码 2'b01(写) 2'b10(读)
reg mdio_dir ; //MDIO数据(SDA)方向控制
reg mdio_out ; //MDIO输出信号
reg [15:0] rd_data_t ; //缓存读寄存器数据
//wire define
wire mdio_in ; //MDIO数据输入
wire [5:0] clk_divide ; //PHY_CLK的分频系数
assign i_eth_mdio = mdio_dir ? mdio_out : 1'bz; //控制双向io方向
assign mdio_in = i_eth_mdio; //MDIO数据输入
//将PHY_CLK的分频系数除以2,得到dri_clk的分频系数,方便对MDC和MDIO信号操作
assign clk_divide = CLK_DIV >> 1;
//分频得到dri_clk时钟
always @(posedge clk) begin
if(!rst_n) begin
o_dri_clk <= 1'b0;
clk_cnt <= 1'b0;
end
else if(clk_cnt == clk_divide[5:1] - 1'd1) begin
clk_cnt <= 1'b0;
o_dri_clk <= ~o_dri_clk;
end
else clk_cnt <= clk_cnt + 1'b1;
end
//产生PHY_MDC时钟
always @(posedge o_dri_clk) begin
if(!rst_n) o_eth_mdc <= 1'b1;
else if(cnt[0] == 1'b0) o_eth_mdc <= 1'b1;
else o_eth_mdc <= 1'b0;
end
//(三段式状态机)同步时序描述状态转移
always @(posedge o_dri_clk) begin
if(!rst_n) cur_state <= st_idle;
else cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always @(*) begin
next_state = st_idle;
case(cur_state)
st_idle : begin
if(i_op_exec) next_state = st_pre;
else next_state = st_idle;
end
st_pre : begin //发送前导码,32'hffffffff
if(st_done) next_state = st_start;
else next_state = st_pre;
end
st_start : begin //ST+OP=2'b01+2'bxx
if(st_done) next_state = st_addr;
else next_state = st_start;
end
st_addr : begin //PHYAD+REGAD
if(st_done) begin
if(op_code == 2'b01) next_state = st_wr_data; //MDIO接口写操作
else next_state = st_rd_data; //MDIO接口读操作
end
else next_state = st_addr;
end
st_wr_data : begin
if(st_done) next_state = st_idle;
else next_state = st_wr_data;
end
st_rd_data : begin
if(st_done) next_state = st_idle;
else next_state = st_rd_data;
end
default : next_state = st_idle;
endcase
end
//时序电路描述状态输出
always @(posedge o_dri_clk) begin
if(!rst_n) begin
cnt <= 5'd0;
op_code <= 1'b0;
addr_t <= 1'b0;
wr_data_t <= 1'b0;
rd_data_t <= 1'b0;
o_op_done <= 1'b0;
st_done <= 1'b0;
o_op_rd_data <= 1'b0;
o_op_rd_ack <= 1'b1;
mdio_dir <= 1'b0;
mdio_out <= 1'b1;
end
else begin
st_done <= 1'b0 ;
cnt <= cnt +1'b1 ;
case(cur_state)
st_idle : begin
mdio_out <= 1'b1;
mdio_dir <= 1'b0;
o_op_done <= 1'b0;
cnt <= 7'b0;
if(i_op_exec) begin
op_code <= {i_op_rh_wl,~i_op_rh_wl}; //OP_CODE: 2'b01(写) 2'b10(读)
addr_t <= i_op_addr;
wr_data_t <= i_op_wr_data;
o_op_rd_ack <= 1'b1;
end
end
st_pre : begin //发送前导码:32个1bit
mdio_dir <= 1'b1; //切换MDIO引脚方向:输出
mdio_out <= 1'b1; //MDIO引脚输出高电平
if(cnt == 7'd62) st_done <= 1'b1;
else if(cnt == 7'd63) cnt <= 7'b0;
end
st_start : begin
case(cnt)
7'd1 : mdio_out <= 1'b0; //发送开始信号 2'b01
7'd3 : mdio_out <= 1'b1;
7'd5 : mdio_out <= op_code[1]; //发送操作码
7'd6 : st_done <= 1'b1;
7'd7 : begin
mdio_out <= op_code[0];
cnt <= 7'b0;
end
default : ;
endcase
end
st_addr : begin
case(cnt)
7'd1 : mdio_out <= PHY_ADDR[4]; //发送PHY地址
7'd3 : mdio_out <= PHY_ADDR[3];
7'd5 : mdio_out <= PHY_ADDR[2];
7'd7 : mdio_out <= PHY_ADDR[1];
7'd9 : mdio_out <= PHY_ADDR[0];
7'd11: mdio_out <= addr_t[4]; //发送寄存器地址
7'd13: mdio_out <= addr_t[3];
7'd15: mdio_out <= addr_t[2];
7'd17: mdio_out <= addr_t[1];
7'd18: st_done <= 1'b1;
7'd19: begin
mdio_out <= addr_t[0];
cnt <= 7'd0;
end
default : ;
endcase
end
st_wr_data : begin
case(cnt)
7'd1 : mdio_out <= 1'b1; //发送TA,写操作(2'b10)
7'd3 : mdio_out <= 1'b0;
7'd5 : mdio_out <= wr_data_t[15];//发送写寄存器数据
7'd7 : mdio_out <= wr_data_t[14];
7'd9 : mdio_out <= wr_data_t[13];
7'd11: mdio_out <= wr_data_t[12];
7'd13: mdio_out <= wr_data_t[11];
7'd15: mdio_out <= wr_data_t[10];
7'd17: mdio_out <= wr_data_t[9];
7'd19: mdio_out <= wr_data_t[8];
7'd21: mdio_out <= wr_data_t[7];
7'd23: mdio_out <= wr_data_t[6];
7'd25: mdio_out <= wr_data_t[5];
7'd27: mdio_out <= wr_data_t[4];
7'd29: mdio_out <= wr_data_t[3];
7'd31: mdio_out <= wr_data_t[2];
7'd33: mdio_out <= wr_data_t[1];
7'd35: mdio_out <= wr_data_t[0];
7'd37: begin
mdio_dir <= 1'b0;
mdio_out <= 1'b1;
end
7'd39: st_done <= 1'b1;
7'd40: begin
cnt <= 7'b0;
o_op_done <= 1'b1; //写操作完成,拉高op_done信号
end
default : ;
endcase
end
st_rd_data : begin
case(cnt)
7'd1 : begin
mdio_dir <= 1'b0; //MDIO引脚切换至输入状态
mdio_out <= 1'b1;
end
7'd2 : ; //TA[1]位,该位为高阻状态,不操作
7'd4 : o_op_rd_ack <= mdio_in; //TA[0]位,0(应答) 1(未应答)
7'd6 : rd_data_t[15] <= mdio_in; //接收寄存器数据
7'd8 : rd_data_t[14] <= mdio_in;
7'd10: rd_data_t[13] <= mdio_in;
7'd12: rd_data_t[12] <= mdio_in;
7'd14: rd_data_t[11] <= mdio_in;
7'd16: rd_data_t[10] <= mdio_in;
7'd18: rd_data_t[9] <= mdio_in;
7'd20: rd_data_t[8] <= mdio_in;
7'd22: rd_data_t[7] <= mdio_in;
7'd24: rd_data_t[6] <= mdio_in;
7'd26: rd_data_t[5] <= mdio_in;
7'd28: rd_data_t[4] <= mdio_in;
7'd30: rd_data_t[3] <= mdio_in;
7'd32: rd_data_t[2] <= mdio_in;
7'd34: rd_data_t[1] <= mdio_in;
7'd36: rd_data_t[0] <= mdio_in;
7'd39: st_done <= 1'b1;
7'd40: begin
o_op_done <= 1'b1; //读操作完成,拉高op_done信号
o_op_rd_data <= rd_data_t;
rd_data_t <= 16'd0;
cnt <= 7'd0;
end
default : ;
endcase
end
default : ;
endcase
end
end
endmodule
然后是控制逻辑,原子的例程是通过触摸按键软复位RTL8211FD,然后再读那两个寄存器,感觉没啥用,自己重写了一个,功能是硬件复位完成后直接循环读那两个寄存器,如果自协商完成且连接成功,led亮,否则灭,再用另个led来只是当前的通信速率,直接上代码:
//****************************************************************************************//
module mdio_ctrl(
(* mark_debug ="true" *) input clk ,
(* mark_debug ="true" *) input rst_n ,
(* mark_debug ="true" *) input i_op_done , //读写完成
(* mark_debug ="true" *) input [15:0] i_op_rd_data , //读出的数据
(* mark_debug ="true" *) input i_op_rd_ack , //读应答信号 0:应答 1:未应答
(* mark_debug ="true" *) output reg o_op_exec , //触发开始信号
(* mark_debug ="true" *) output reg o_op_rh_wl , //低电平写,高电平读
(* mark_debug ="true" *) output reg [4:0] o_op_addr , //寄存器地址
(* mark_debug ="true" *) input i_eth_rst_n ,
(* mark_debug ="true" *) output reg o_link_led ,
(* mark_debug ="true" *) output reg [1:0] o_speed_led
);
(* mark_debug ="true" *) reg [2:0] flow_cnt; //流程控制计数器
always @(posedge clk) begin
if(!rst_n) begin
flow_cnt <= 3'd0;
// link_error <= 1'b0;
o_op_exec <= 1'b0;
o_op_rh_wl <= 1'b0;
o_op_addr <= 1'b0;
// op_wr_data <= 1'b0;
o_link_led <= 1'b0;
o_speed_led<= 2'b0;
end
else begin
case(flow_cnt)
3'd0:begin
if(i_eth_rst_n) flow_cnt <= 3'd1; //reste_n is ok
end
3'd1: begin
o_op_exec <= 1'b1; //开始操作MDIO
o_op_rh_wl<= 1'b1; //读操作
o_op_addr <= 5'h01; //读BMSR状态寄存器
flow_cnt<= 3'd2;
end
3'd2: begin
if(i_op_done) begin //读操作完成
if(!i_op_rd_ack) flow_cnt<= 3'd3; //phy应答,做后续判断
else flow_cnt<= 3'd1; //phy未应答,重新发起读BMSR状态寄存器
end
end
3'd3: begin
if(i_op_rd_data[5] == 1'b1 && i_op_rd_data[2] == 1'b1) begin
o_link_led<=1'b1; //自协商完成,link完成
flow_cnt<=3'd4;
end
else o_link_led<=1'b0;
end
3'd4: begin //读PHYSR特定状态寄存器
o_op_addr <= 5'h1A;
flow_cnt<= 3'd5;
end
3'd5: begin
if(i_op_done) begin //读操作完成
if(!i_op_rd_ack) flow_cnt<= 3'd6; //phy应答,做后续判断
else flow_cnt<= 3'd4; //phy未应答,重新发起读PHYSR特定状态寄存器
end
end
3'd6: begin
flow_cnt<=3'd1;
if(i_op_rd_data[5:4] == 2'b10) o_speed_led<=2'b11; //1000Mbps
else if(i_op_rd_data[5:4] == 2'b01) o_speed_led<=2'b01; //100Mbps
else if(i_op_rd_data[5:4] == 2'b00) o_speed_led<=2'b10; //100Mbps
else o_speed_led<= 2'b0;
end
default: flow_cnt <= 3'd0;
endcase
end
end
endmodule
最后是顶层,这里加了硬件复位的10ms,之前说了,菜就别嫌麻烦,跟官方走:
module mdio_rw_test(
input sys_clk ,
output eth_mdc , //PHY管理接口的时钟信号
inout eth_mdio , //PHY管理接口的双向数据信号
output eth_rst_n , //以太网复位信号
output o_link_led ,
output [1:0] o_speed_led //LED连接速率指示
);
//wire define
wire op_exec ; //触发开始信号
wire op_rh_wl ; //低电平写,高电平读
wire [4:0] op_addr ; //寄存器地址
wire [15:0] op_wr_data ; //写入寄存器的数据
wire op_done ; //读写完成
wire [15:0] op_rd_data ; //读出的数据
wire op_rd_ack ; //读应答信号 0:应答 1:未应答
wire dri_clk ; //驱动时钟
wire rsn_n;
wire clk_50m;
clk_wiz_0 u_clk_wiz_0
(
// Clock out ports
.clk_out1(clk_50m), // output clk_out1
// Status and control signals
.locked(rsn_n), // output locked
// Clock in ports
.clk_in1(sys_clk)); // input clk_in1
localparam T_10MS = 10_000_000; //10ms=10_000_000ns
localparam T_P = 20 ; //50M周期=20ns
localparam RST_CNT= T_10MS/T_P+10;
reg [18:0] eth_rsrn_cnt;
reg o_eth_rst_n;
//根据数据手册,硬件复位至少10ms的低电平
always @(posedge clk_50m) begin
if(!rsn_n) eth_rsrn_cnt<='d0;
else if(eth_rsrn_cnt==RST_CNT) eth_rsrn_cnt<=RST_CNT;
else eth_rsrn_cnt<=eth_rsrn_cnt+19'd1;
end
always @(posedge clk_50m) begin
if(!rsn_n) o_eth_rst_n<='d0;
else if(eth_rsrn_cnt==RST_CNT) o_eth_rst_n<=1'd1;
else o_eth_rst_n<=1'd0;
end
//硬件复位
assign eth_rst_n = o_eth_rst_n;
//MDIO接口驱动
mdio_dri #(
.PHY_ADDR (5'h01), //PHY地址
.CLK_DIV (6'd10) //分频系数
)
u_mdio_dri(
.clk (clk_50m ),
.rst_n (rsn_n ),
.i_op_exec (op_exec ),
.i_op_rh_wl (op_rh_wl ),
.i_op_addr (op_addr ),
.i_op_wr_data(op_wr_data),
.o_op_done (op_done ),
.o_op_rd_data(op_rd_data),
.o_op_rd_ack (op_rd_ack ),
.o_dri_clk (dri_clk ),
.o_eth_mdc (eth_mdc ),
.i_eth_mdio (eth_mdio )
);
mdio_ctrl u_mdio_ctrl(
.clk (dri_clk ),
.rst_n (rsn_n ),
.i_op_done (op_done ), //读写完成
.i_op_rd_data (op_rd_data ), //读出的数据
.i_op_rd_ack (op_rd_ack ), //读应答信号 0:应答 1:未应答
.o_op_exec (op_exec ), //触发开始信号
.o_op_rh_wl (op_rh_wl ), //低电平写,高电平读
.o_op_addr (op_addr ), //寄存器地址
.i_eth_rst_n (o_eth_rst_n),
.o_link_led (o_link_led ),
.o_speed_led (o_speed_led)
);
endmodule
结果:灯亮,成功