上一篇教程介绍了NEXYS4 开发板中DDR2的使用方式,这一篇介绍不可或缺的网络接口RJ45在FPGA开发板中的使用
NEXYS 4上的局域网接口RJ45使用常见的LAN8720A物理层芯片,支持10兆网和100兆网,使用RMII(Reduced Media Independent Interface)。它的文档在此:LAN8720A
NEXYS 4文档中介绍说,使用EDK(Embedded Development Kit)的工程可以用axi_ethernetlit或者axi_ethernet IP访问物理层芯片。使用EDK的意思是利用FPGA内自带的ARM核,放入一个小型的Linux核心,比如Microblaze或Zynq,然后用软件编程的形式收发数据。
网络传输TCP/IP五层模型如下,最顶层的用户产生数据,经过层层包装到最底层的物理层传输出去,到接收端在经过层层拆包验收后传到接收端的用户,各层负责把数据包往下传(实线),但实际上是在和另一端的相对应层互相握手沟通(虚线)。这个过程包含很多的细节,除了物理层是由LAN8720A芯片和RJ45接口完成的以外,其他都是由FPGA内部逻辑或者ARM核中的软件逻辑完成的。
这个系列的教程还没有到将各种接口综合到Linux核心的阶段,EDK先向后放,我们优先用PL(programmable logic)部分测试其基础功能,以此学习加强理解
LAN8720A的外部连接应该如下,我们需要做的是写一个简单的10/100 Ethernet MAC,其他部分在开发板上已经存在
芯片内部结构如下,下面慢慢介绍
在右侧的信号是在RESET#信号拉低,也就是复位状态是有效的,当复位结束,RESET#拉高,相应信号就被读入芯片内部。这种模式被称为configuration strap,不知道该怎么翻译
在configuration strap时MODE引脚的配置就可以调配寄存器中相应位数的默认值,这样常见的配置模式就不需要写寄存器来配置,还是很方便的
数据信号在芯片内部流通过程我们并不关心,不过还是简单介绍一下。
发送端如下
LAN8720A的芯片复位分为硬件复位和软件复位,硬件复位是把RESETn引脚拉低至少100us;软件复位是给控制寄存器0的最高位[15]写入1,并等待0.5s,具体操作之后就介绍。
这篇教程会在逻辑开始时先后把硬件复位和软件复位都进行一遍。
SMI串行管理接口读写数据的时序图如下,MDC的频率不固定,只要周期在400ns以上就好,也就是频率在2.5MHz以下,保险起见我们选择1MHz。并且我们选择在时钟下降沿输出或者读入数据,避免上升沿的冲突
PHY Address物理层地址默认是0,在复位状态时把引脚PHYAD1拉高可以修改物理层地址到1,如果你需要更高的地址就需要配置寄存器了。当你有多个物理层芯片连在一起时就需要配置不同的物理层地址来分别访问
Register Address寄存器地址和不同寄存器有关,寄存器表如下
在这之中最重要的寄存器是前5个
基础控制寄存器,地址0,可读写:
基础状态寄存器,地址1,仅可读
这个寄存器只要看后面几位就好,看自动速度识别是否完成,连接是否完成
物理层ID1寄存器,地址2,可读写
作为寄存器读取测试使用,看其读出默认数据是否正确
物理层ID2寄存器,地址3,可读写
同样可以作为寄存器读取测试使用
自动识别广播寄存器,地址4,可读写
这个寄存器在复位模式配置MODE时需要注意,具体可以参考上面的模式选择表
在所有寄存器都配置完成后,我们就可以用下面这个时序图来收发数据了。可以看出LAN8720A的收发数据是在时钟下降沿发生的
基础思路是,先硬件复位,配置configuration strap,再进行软件复位,读取几个寄存器以确保一切正常后等待CRS_DV引脚来读取数据。这里不打算涉及负责的网络协议,随便找个网口连接后,看到有数据即可,不涉及数据包内部内容。
根据上面的设计,做出如下状态机,当中加入了为ChipScope准备的辅助开关
module ethernet(
input clk,
input rst,
input switch_continue,
output reg led,
// LAN8720A PHY chip port
inout MDIO,
output wire MDC,
output reg RESETn,
inout RXD1_MODE1,
inout RXD0_MODE0,
inout RXERR_PHYAD0,
output reg TXD0,
output reg TXD1,
output reg TXEN,
inout CRS_DV_MODE2,
inout INT_REFCLKO,
output reg CLKIN
);
引脚定义。里面加入了一个叫switch_continue的引脚,它被连接到第二个拨动开关上,用来把状态机停在读取寄存器之前,不然我们很难在ChipScope上抓取相应数据
这里我们第一次用到了inout这种端口定义,顾名思义是出入复用的引脚,它的用法如下(不包含在最终代码中,只是示例)
双向引脚一般有一个输出使能引脚,输出时连接到相应的寄存器输出上,输入则转为高阻态1’bz,而输入信号则赋值给另一个wire,传递出去或者到另一个寄存器
module bidirec (oe, clk, inp, outp, bidir);
// Port Declaration
input oe;
input clk;
input [7:0] inp;
output [7:0] outp;
inout [7:0] bidir;
reg [7:0] a;
reg [7:0] b;
assign bidir = oe ? a : 8'bZ ;
assign outp = b;
// Always Construct
always @ (posedge clk)
begin
b <= bidir;
a <= inp;
end
endmodule
输出50MHz到LAN8720A的CLKIN引脚
// Clock to LAN8720A is 50MHz, need to be lowered down
always @(posedge clk or posedge rst) begin
if(rst) begin
CLKIN <= 1'b0;
end
else begin
CLKIN <= ~CLKIN;
end
end
双向引脚配置
// Control of the bi-directional data
reg [2:0] MODE;
reg PHYAD0;
reg INTSEL;
reg strap_oe;
wire [1:0] RXD;
(* dont_touch = "true" *)reg [1:0] rxd_d;
wire RXERR;
(* dont_touch = "true" *)reg rxerr_d;
wire CRS_DV;
(* dont_touch = "true" *)reg crs_dv_d;
wire INT;
assign RXD1_MODE1 = (strap_oe) ? MODE[1] : 1'bz;
assign RXD0_MODE0 = (strap_oe) ? MODE[0] : 1'bz;
assign RXERR_PHYAD0 = (strap_oe) ? PHYAD0 : 1'bz;
assign CRS_DV_MODE2 = (strap_oe) ? MODE[2] : 1'bz;
assign INT_REFCLKO = (strap_oe) ? INTSEL : 1'bz;
assign RXD = {RXD1_MODE1, RXD0_MODE0};
assign RXERR = RXERR_PHYAD0;
assign CRS_DV = CRS_DV_MODE2;
assign INT = INT_REFCLKO;
状态机定义
// State machine
parameter IDLE = 4'd0;
parameter RESET = 4'd1;
parameter RDPHYID1 = 4'd2;
parameter RDPHYID2 = 4'd3;
parameter RESET_SOFT = 4'd4;
parameter UNRESET_SOFT = 4'd5;
parameter SETMODE = 4'd6;
parameter UNRESET = 4'd7;
parameter RD_BC0 = 4'd8;
parameter RD_BS1 = 4'd9;
parameter RX_TX = 4'd10;
(* dont_touch = "true" *)reg [3:0] state;
(* dont_touch = "true" *)reg [3:0] next_state;
(* dont_touch = "true" *)reg [15:0] data_from_SMI;
SMI读写模块,在后面加入子模块内容
// SMI management
reg wrh_rdl;
reg [4:0] reg_addr;
reg [15:0] wr_data;
wire [15:0] rd_data;
reg SMI_start;
wire SMI_complete;
wire MDI;
wire MDO;
wire MD_OE;
assign MDIO = (MD_OE) ? MDO : 1'bz;
assign MDI = MDIO;
SMI_manage SMI_manage(
.clk(clk),
.rst(rst),
.mdc(MDC),
.mdo(MDO),
.mdi(MDI),
.md_oe(MD_OE),
.wrh_rdl(wrh_rdl),
.reg_addr(reg_addr),
.wr_data(wr_data),
.rd_data(rd_data),
.start(SMI_start),
.complete(SMI_complete)
);
状态机具体实现
always @(posedge clk) begin
led <= INT;
end
// State machine
always @(posedge clk or posedge rst) begin
if(rst) begin
state <= IDLE;
end
else begin
state <= next_state;
end
end
reg [25:0] wait_count;
(* dont_touch = "true" *)reg read_phase;
(* dont_touch = "true" *)reg [3:0] read_data;
always @(posedge clk) begin
case(state)
IDLE: begin
next_state <= RESET;
RESETn <= 1'b0;
reg_addr <= 5'd0;
wrh_rdl <= 1'b0;
SMI_start <= 1'b0;
data_from_SMI <= 16'h0000;
wait_count <= 26'd0;
PHYAD0 <= 1'b0; // Set PHY address to 0
INTSEL <= 1'b1; // REF_CLK In Mode
MODE <= 3'b111;
strap_oe <= 1'b1;
read_phase <= 1'b0;
read_data <= 4'd0;
end
RESET: begin
next_state <= SETMODE;
RESETn <= 1'b0;
end
// Need to wait for 200us, which is 20000 clock cycles in 100MHz
SETMODE: begin
MODE <= 3'b111;
PHYAD0 <= 1'b0;
INTSEL <= 1'b1;
if(wait_count < 26'd20000) begin
wait_count <= wait_count + 26'd1;
end
else begin
next_state <= UNRESET;
end
end
UNRESET: begin
strap_oe <= 1'b0;
RESETn <= 1'b1;
wait_count <= 26'd0;
if(switch_continue) begin
next_state <= RESET_SOFT;
end
end
RESET_SOFT: begin
if(SMI_complete) begin
next_state <= UNRESET_SOFT;
SMI_start <= 1'b0;
end
else begin
SMI_start <= 1'b1;
wrh_rdl <= 1'b1;
reg_addr <= 5'd0;
wr_data <= 16'h8000;
end
end
// Need to be kept in software reset for about 0.5s
UNRESET_SOFT: begin
if(wait_count < 26'd50000000) begin
wait_count <= wait_count + 26'd1;
end
else begin
next_state <= RDPHYID1;
end
end
RDPHYID1: begin
if(SMI_complete) begin
data_from_SMI <= rd_data;
next_state <= RDPHYID2;
SMI_start <= 1'b0;
end
else begin
SMI_start <= 1'b1;
wrh_rdl <= 1'b0;
reg_addr <= 5'd2;
end
end
RDPHYID2: begin
if(SMI_complete) begin
data_from_SMI <= rd_data;
next_state <= RD_BC0;
SMI_start <= 1'b0;
end
else begin
SMI_start <= 1'b1;
wrh_rdl <= 1'b0;
reg_addr <= 5'd3;
end
end
RD_BC0: begin
if(SMI_complete) begin
data_from_SMI <= rd_data;
next_state <= RD_BS1;
SMI_start <= 1'b0;
end
else begin
SMI_start <= 1'b1;
wrh_rdl <= 1'b0;
reg_addr <= 5'd0;
end
end
RD_BS1: begin
if(SMI_complete) begin
data_from_SMI <= rd_data;
next_state <= RX_TX;
SMI_start <= 1'b0;
end
else begin
SMI_start <= 1'b1;
wrh_rdl <= 1'b0;
reg_addr <= 5'd1;
end
end
RX_TX: begin
SMI_start <= 1'b0;
rxd_d <= RXD;
rxerr_d <= RXERR;
crs_dv_d <= CRS_DV;
if(crs_dv_d) begin
read_phase <= ~read_phase; // invert every time a new signal detected
if(read_phase) begin
read_data[1:0] <= rxd_d;
end
else begin
read_data[3:2] <= rxd_d;
end
end
end
default: begin
next_state <= IDLE;
end
endcase
end
endmodule
状态机SMI接口控制子模块
module SMI_manage(
input clk,
input rst,
output reg mdc,
output reg mdo,
input mdi,
output reg md_oe,
input wrh_rdl,
input [4:0] reg_addr,
input [15:0] wr_data,
input start,
output reg [15:0] rd_data,
output reg complete
);
接口定义
// MDIO input and output control
reg mdi_d;
always @(posedge clk) begin
mdi_d <= mdi;
end
// MDC generator, count to 50 and invert, 100MHz => 1MHz
reg [5:0] mdc_count;
reg mdc_en;
reg [1:0] mdc_d;
wire mdc_negedge;
wire mdc_posedge;
always @(posedge clk or posedge rst) begin
if(rst) begin
mdc_count <= 6'd0;
mdc <= 1'b0;
end
else if(mdc_en) begin
if(mdc_count < 6'd50) begin
mdc_count <= mdc_count + 6'd1;
end
else begin
mdc_count <= 6'd0;
mdc <= ~mdc;
end
end
end
由使能信号控制的mdc时钟输出,每延迟50个时钟反一次,把100MHz降频到1MHz
// negative edge detection, MDIO read and write only happen at negative edge
always @(posedge clk) begin
mdc_d <= {mdc_d[0], mdc};
end
assign mdc_negedge = (mdc_d == 2'b10) ? 1'b1 : 1'b0;
assign mdc_posedge = (mdc_d == 2'b01) ? 1'b1 : 1'b0;
// Detect the rising edge of input signal start
reg [1:0] start_d;
wire start_posedge;
always @(posedge clk) begin
start_d <= {start_d[0],start};
end
assign start_posedge = (start_d == 2'b01) ? 1'b1 : 1'b0;
侦测mdc的下降沿和start信号的上升沿,避免信号持续时间长短导致的逻辑错误
// State machine with three parts
reg [1:0] state;
reg [5:0] md_count;
reg [45:0] data1;
reg [15:0] data2;
reg complete_d;
always @(posedge clk or posedge rst) begin
if(rst) begin
state <= 2'b00;
complete <= 1'b0;
mdc_en <= 1'b0;
md_count <= 6'd0;
mdo <= 1'b1;
md_oe <= 1'b0;
data1 <= 46'd0;
data2 <= 16'd0;
rd_data <= 16'd0;
end
else begin
case(state)
// Wait for start signal
2'b00: begin
md_count <= 6'd0;
md_oe <= 1'b0;
mdo <= 1'b1;
complete <= complete_d;
complete_d <= 1'b0;
if(start_posedge) begin
state <= 2'b01;
mdc_en <= 1'b1;
data1 <= {32'hFFFFFFFF, 2'b01, (wrh_rdl)?2'b01:2'b10, 5'h00, reg_addr};
data2 <= wr_data;
end
end
// Preamble, Start of Frame, OP Code, PHY addr, reg addr
// length of 46
2'b01: begin
md_oe <= 1'b1;
if(mdc_negedge) begin
{mdo,data1} <= {data1,1'b0};
end
if(mdc_negedge & (md_count < 6'd45)) begin
md_count <= md_count + 6'd1;
end
else if(mdc_negedge) begin
md_count <= 6'd0;
state <= 2'b10;
end
end
// Turn around
// length of 2
2'b10: begin
if(mdc_negedge && (md_count == 6'd0)) begin
md_count <= 6'd1;
end
else if(mdc_negedge) begin
md_count <= 6'd0;
md_oe <= wrh_rdl;
state <= 2'b11;
end
end
// Data to or from PHY
// length of 16
2'b11: begin
if(mdc_negedge) begin
{mdo,data2} <= {data2,1'b0};
rd_data <= {rd_data[14:0], mdi_d};
end
if(mdc_negedge & (md_count < 6'd15)) begin
md_count <= md_count + 6'd1;
end
else if(mdc_negedge) begin
md_count <= 6'd0;
state <= 2'b00;
complete <= 1'b1;
complete_d <= 1'b1;
end
end
endcase
end
end
endmodule
SMI控制子模块内部也有一个小的状态机,用来控制SMI指令不同部分:前序、读写模式、物理层地址、寄存器地址、读写翻转、读/写数据。
大致可以分为46位的输入、翻转、读/写数据和等待四部分,分别对应这个状态机的各个部分
仿真用的testbench和前面的教程比较相似,没有包括LAN8720A的仿真模块(其实是没找到),主要以后期的ChipScope为主。代码如下
`timescale 1ns/1ns
module tb_ethernet;
reg clock;
reg reset;
wire led;
initial begin
clock = 1'b0;
reset = 1'b0;
// Reset for 1us
#100
reset = 1'b1;
#1000
reset = 1'b0;
end
// Generate 100MHz clock signal
always #5 clock <= ~clock;
ethernet ethernet_top(
.clk (clock),
.rst (reset),
.switch_continue (1'b1),
.led (led),
// LAN8720A PHY chip port
.MDIO (),
.MDC (),
.RESETn (),
.RXD1_MODE1 (),
.RXD0_MODE0 (),
.RXERR_PHYAD0 (),
.TXD0 (),
.TXD1 (),
.TXEN (),
.CRS_DV_MODE2 (),
.INT_REFCLKO (),
.CLKIN ()
);
endmodule
写一个简单的仿真脚本sim.do,由于没有调用Xilinx的IP,不需要包含库文件和glbl.v:
vlib work
vlog ../src/ethernet.v ../src/SMI_manage.v ./tb_ethernet.v
vsim work.tb_ethernet -voptargs=+acc +notimingchecks
log -depth 7 /tb_ethernet/*
do wave.do
run 1ms
调用仿真脚本do sim.do后,得到如上方结果,放大可以看到SMI控制的具体细节,不过由于读取引脚悬空,没有连接任何信号,读出来的是蓝色代表的高阻态
新建一个叫ethernet的project,初始配置可以参考之前的教程。添加代码文件ethernet.v和SMI_manage.v。
下一步加入约束constraint文件ethernet.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):
## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project
## Clock signal
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports clk]
create_clock -period 10.000 -name sys_clk_pin -waveform {0.000 5.000} -add [get_ports clk]
##Switches
set_property -dict {PACKAGE_PIN J15 IOSTANDARD LVCMOS33} [get_ports rst]
set_property -dict {PACKAGE_PIN L16 IOSTANDARD LVCMOS33} [get_ports switch_continue]
## LEDs
set_property -dict {PACKAGE_PIN H17 IOSTANDARD LVCMOS33} [get_ports led]
##SMSC Ethernet PHY
set_property -dict {PACKAGE_PIN C9 IOSTANDARD LVCMOS33} [get_ports MDC]
set_property -dict {PACKAGE_PIN A9 IOSTANDARD LVCMOS33} [get_ports MDIO]
set_property -dict {PACKAGE_PIN B3 IOSTANDARD LVCMOS33} [get_ports RESETn]
set_property -dict {PACKAGE_PIN D9 IOSTANDARD LVCMOS33} [get_ports CRS_DV_MODE2]
set_property -dict {PACKAGE_PIN C10 IOSTANDARD LVCMOS33} [get_ports RXERR_PHYAD0]
set_property -dict {PACKAGE_PIN C11 IOSTANDARD LVCMOS33} [get_ports RXD0_MODE0]
set_property -dict {PACKAGE_PIN D10 IOSTANDARD LVCMOS33} [get_ports RXD1_MODE1]
set_property -dict {PACKAGE_PIN B9 IOSTANDARD LVCMOS33} [get_ports TXEN]
set_property -dict {PACKAGE_PIN A10 IOSTANDARD LVCMOS33} [get_ports TXD0]
set_property -dict {PACKAGE_PIN A8 IOSTANDARD LVCMOS33} [get_ports TXD1]
set_property -dict {PACKAGE_PIN D5 IOSTANDARD LVCMOS33} [get_ports CLKIN]
set_property -dict {PACKAGE_PIN B8 IOSTANDARD LVCMOS33} [get_ports INT_REFCLKO]
到这里可以点击 Run Synthesis做综合,几秒钟完成后用Set Up Debug配置ChipScope:
设置观察长度为8192,因为持续时间会比较长。下面就可以Run Implementation和Generate Bitstream生成配置文件了。
和前面的教程一样,USB线连接NEXYS4板子,开启Hardware Manager,然后auto连接上板子,Program Device烧写进程序,注意Debug probes file有对应的ltx文件,完成后用网线连接任意一台主机或者猫到开发板上的RJ45接口,只要把RESET拨回到0,就可以看到它旁边的两个LED灯亮起:
下面看ChipScope抓取的结果,观察前面的代码,我们有加入一个辅助ChipScope的开关,用的是第二个开关,而复位是第一个开关。如此设计来观察不同的寄存器读取:
我们读取了四个寄存器,它们在ChipScope里的结果分别如下
PHYID1读出的结果是0x0007:
PHYID2读出的结果是0xc0f1:
Basic Control基础控制寄存器的结果是0x3000
Basic Status基础状态寄存器的结果是0x7809
查阅文档可以看出读出的结果和预期是相符的。
在这之后我们就可以看看是否从网线上读出了什么数据,修改ChipScope的trigger为CRS_DV上升沿R,抓取到的数据如下:
可以清楚的看到LAN8720A接收到了一串数据包,放大可以看清具体数据,如果仔细分析甚至可以看到IP地址、MAC地址之类的信息,这个就不公开了,可以自己尝试后自己分析
这篇教程介绍的是NEXYS 4开发板上局域网物理层芯片LAN8720A的用法,这块芯片因为配置简单而非常常用,市面上可以买到的嵌入式局域网模块很多都是基于这款芯片,因此这篇教程不只是针对FPGA的开发者,还可以让其他嵌入式系统的学习者借鉴,比如想用Arduino控制类似局域网模块的开发者。
NEXYS 4上已经介绍了一部分接口,剩下的有UART串口通信、USB接口、麦克风、Pmod通用接口、温度传感器、模数转换ADC、音频接口、视频接口VGA。当全部介绍过之后,一个熟练的FPGA开发者就可以综合利用板上的几乎全部资源,再加上FPGA强大的并行计算能力,能做出很多ARM架构嵌入式系统无法完成的效果。
下一篇介绍UART串口通信,利用平时烧写芯片的USB线和开发板通信