本文主要实现通过FPGA实现ARP协议的接收和发送,按键按下后,FPGA会向PC端发送ARP请求指令,PC会对FPGA发送ARP应答。同时当FPGA接收到PC端的ARP请求时,需要把FPGA的IP和MAC地址通过ARP应答发送给PC端。
由于画各个模块的信号流向图比较费时间,所以直接使用vivado的RTL图替代,如下图所示,工程包括5个模块。
key是按键消抖和检测模块,arp_ctrl是ARP控制模块,控制ARP模块向PC端发送ARP请求还是ARP应答,ARP模块实现ARP协议的接收和发送。锁相环模块是将输入100MHz时钟信号转换为200MHz作为IDELAYCTRL的参考时钟信号,rgmii_to_gmii是RGMII接口信号与GMII接口信号转换模块,在前文已经做了详细讲解,本文不再赘述。
每个模块的左侧信号为该模块输入信号,右侧为该模块输出信号。本文主要讲解arp模块和arp_ctrl模块的设计,按键消抖模块,GMII与RGMII接口信号转换模块在前文均已讲解过,此处直接使用即可。
顶层模块的核心参考代码如下所示:
assign des_mac = src_mac;
assign des_ip = src_ip;
//例化锁相环,输出200MHZ时钟,作为IDELAYECTRL的参考时钟。
clk_wiz_0 u_clk_wiz_0 (
.clk_out1 ( idelay_clk),//output clk_out1;
.resetn ( rst_n ),//input resetn;
.clk_in1 ( clk ) //input clk_in1;
);
//例化按键消抖模块。
key #(
.TIME_20MS ( TIME_20MS ),//按键抖动持续的最长时间,默认最长持续时间为20ms。
.TIME_CLK ( TIME_CLK ) //系统时钟周期,默认8ns。
)
u_key (
.clk ( gmii_rx_clk ),//系统时钟,125MHz。
.rst_n ( rst_n ),//系统复位,低电平有效。
.key_in ( key_in ),//待输入的按键输入信号,默认低电平有效;
.key_out ( key_out ) //按键消抖后输出信号,当按键按下一次时,输出一个时钟宽度的高电平;
);
//例化ARP控制模块;
arp_ctrl u_arp_ctrl (
.clk ( gmii_rx_clk ),//输入时钟;
.rst_n ( rst_n ),//复位信号,低电平有效;
.key_in ( key_out ),//按键按下,高电平有效;
.arp_rx_done ( arp_rx_done ),//ARP接收完成信号;
.arp_rx_type ( arp_rx_type ),//ARP接收类型 0:请求 1:应答;
.arp_tx_rdy ( arp_tx_rdy ),//ARP发送模块忙闲指示信号。
.arp_tx_en ( arp_tx_en ),//ARP发送使能信号;
.arp_tx_type ( arp_tx_type ) //ARP发送类型 0:请求 1:应答;
);
//例化ARP模块;
arp #(
.BOARD_MAC ( BOARD_MAC ),//开发板MAC地址 00-11-22-33-44-55;
.BOARD_IP ( BOARD_IP ),//开发板IP地址 192.168.1.10;
.DES_MAC ( DES_MAC ),//目的MAC地址 ff_ff_ff_ff_ff_ff;
.DES_IP ( DES_IP ) //目的IP地址 192.168.1.102;
)
u_arp (
.rst_n ( rst_n ),//复位信号,低电平有效。
.gmii_rx_clk ( gmii_rx_clk ),//GMII接收数据时钟。
.gmii_rx_dv ( gmii_rx_dv ),//GMII输入数据有效信号。
.gmii_rxd ( gmii_rxd ),//GMII输入数据。
.gmii_tx_clk ( gmii_tx_clk ),//GMII发送数据时钟。
.arp_tx_en ( arp_tx_en ),//ARP发送使能信号。
.arp_tx_type ( arp_tx_type ),//ARP发送类型 0:请求 1:应答。
.des_mac ( des_mac ),//发送的目标MAC地址。
.des_ip ( des_ip ),//发送的目标IP地址。
.gmii_tx_en ( gmii_tx_en ),//GMII输出数据有效信号。
.gmii_txd ( gmii_txd ),//GMII输出数据。
.arp_rx_done ( arp_rx_done ),//ARP接收完成信号。
.arp_rx_type ( arp_rx_type ),//ARP接收类型 0:请求 1:应答。
.src_mac ( src_mac ),//接收到目的MAC地址。
.src_ip ( src_ip ),//接收到目的IP地址。
.arp_tx_rdy ( arp_tx_rdy ) //ARP发送模块忙闲指示指示信号,高电平表示该模块空闲。
);
//例化gmii转RGMII模块。
rgmii_to_gmii u_rgmii_to_gmii (
.idelay_clk ( idelay_clk ),//IDELAY时钟;
.rst_n ( rst_n ),
.gmii_tx_en ( gmii_tx_en ),//GMII发送数据使能信号;
.gmii_txd ( gmii_txd ),//GMII发送数据;
.gmii_rx_clk ( gmii_rx_clk ),//GMII接收时钟;
.gmii_rx_dv ( gmii_rx_dv ),//GMII接收数据有效信号;
.gmii_rxd ( gmii_rxd ),//GMII接收数据;
.gmii_tx_clk ( gmii_tx_clk ),//GMII发送时钟;
.rgmii_rxc ( rgmii_rxc ),//RGMII接收时钟;
.rgmii_rx_ctl ( rgmii_rx_ctl ),//RGMII接收数据控制信号;
.rgmii_rxd ( rgmii_rxd ),//RGMII接收数据;
.rgmii_txc ( rgmii_txc ),//RGMII发送时钟;
.rgmii_tx_ctl ( rgmii_tx_ctl ),//RGMII发送数据控制信号;
.rgmii_txd ( rgmii_txd ) //RGMII发送数据;
);
下图是ARP模块的内部信号走向,主要包含三个模块,ARP接收模块arp_rx,ARP发送模块arp_tx,CRC校验模块。本工程对ARP接收和发送均做了CRC校验,来确保数据的正确性,接收其实可以不做CRC校验的。
下文分别对这几个模块的设计进行讲解,该模块核心参考代码如下所示:
//例化arp接收模块;
arp_rx #(
.BOARD_MAC ( BOARD_MAC ),//开发板MAC地址 00-11-22-33-44-55;
.BOARD_IP ( BOARD_IP ) //开发板IP地址 192.168.1.10;
)
u_arp_rx (
.clk ( gmii_rx_clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.gmii_rx_dv ( gmii_rx_dv ),//GMII输入数据有效信号;
.gmii_rxd ( gmii_rxd ),//GMII输入数据;
.crc_out ( rx_crc_out ),//CRC校验模块输出的数据;
.arp_rx_done ( arp_rx_done ),//ARP接收完成信号;
.arp_rx_type ( arp_rx_type ),//ARP接收类型 0:请求 1:应答;
.src_mac ( src_mac ),//接收到的源MAC地址;
.src_ip ( src_ip ),//接收到的源IP地址;
.crc_data ( rx_crc_data ),//需要CRC模块校验的数据;
.crc_en ( rx_crc_en ),//CRC开始校验使能;
.crc_clr ( rx_crc_clr ) //CRC数据复位信号;
);
//例化接收数据时需要的CRC校验模块;
crc32_d8 u_crc32_d8_rx (
.clk ( gmii_rx_clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.data ( rx_crc_data ),//需要CRC模块校验的数据;
.crc_en ( rx_crc_en ),//CRC开始校验使能;
.crc_clr ( rx_crc_clr ),//CRC数据复位信号;
.crc_out ( rx_crc_out ) //CRC校验模块输出的数据;
);
//例化CRC发送模块;
arp_tx #(
.BOARD_MAC ( BOARD_MAC ),//开发板MAC地址 00-11-22-33-44-55;
.BOARD_IP ( BOARD_IP ),//开发板IP地址 192.168.1.10;
.DES_MAC ( DES_MAC ),//目的MAC地址 ff_ff_ff_ff_ff_ff;
.DES_IP ( DES_IP ),//目的IP地址 192.168.1.102;
.ETH_TYPE ( ETH_TYPE ),//以太网帧类型,16'h0806表示ARP协议,16'h0800表示IP协议;
.HD_TYPE ( HD_TYPE ),//硬件类型 以太网;
.PROTOCOL_TYPE ( PROTOCOL_TYPE ),//上层协议为IP协议;
.HD_LEN ( HD_LEN ),//硬件地址长度。
.PROTOCOL_LEN ( PROTOCOL_LEN ),//协议地址长度。
.OPCODE ( OPCODE ) //操作码,1表示请求,2表示应答。
)
u_arp_tx (
.clk ( gmii_tx_clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.arp_tx_en ( arp_tx_en ),//ARP发送使能信号;
.arp_tx_type ( arp_tx_type ),//ARP发送类型 0:请求 1:应答;
.des_mac ( des_mac ),//发送的目标MAC地址;
.des_ip ( des_ip ),//发送的目标IP地址;
.crc_out ( tx_crc_out ),//CRC校验数据;
.gmii_tx_en ( gmii_tx_en ),//GMII输出数据有效信号;
.gmii_txd ( gmii_txd ),//GMII输出数据;
.crc_en ( tx_crc_en ),//CRC开始校验使能;
.crc_clr ( tx_crc_clr ),//CRC数据复位信号;
.crc_data ( tx_crc_data ),//输出给CRC校验模块进行计算的数据;
.rdy ( arp_tx_rdy ) //模块忙闲指示信号,高电平表示该模块处于空闲状态;
);
//例化发送数据时需要的CRC校验模块;
crc32_d8 u_crc32_d8_tx (
.clk ( gmii_tx_clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.data ( tx_crc_data ),//需要CRC模块校验的数据;
.crc_en ( tx_crc_en ),//CRC开始校验使能;
.crc_clr ( tx_crc_clr ),//CRC数据复位信号;
.crc_out ( tx_crc_out ) //CRC校验模块输出的数据;
);
arp顶层模块对应的TestBench代码如下所示:
`timescale 1 ns/1 ns
module test();
localparam CYCLE = 8 ;//系统时钟周期,单位ns,默认10ns;
localparam RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
localparam BOARD_MAC = 48'h00_11_22_33_44_55 ;
localparam BOARD_IP = {8'd192,8'd168,8'd1,8'd10} ;
localparam DES_MAC = 48'h23_45_67_89_0a_bc ;
localparam DES_IP = {8'd192,8'd168,8'd1,8'd23} ;
localparam ETH_TPYE = 16'h0806 ;//以太网帧类型 ARP
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg gmii_rx_dv ;
reg [7 : 0] gmii_rxd ;
reg arp_tx_en ;
reg arp_tx_type ;
reg [47 : 0] des_mac ;
reg [31 : 0] des_ip ;
wire gmii_tx_en ;
wire [7 : 0] gmii_txd ;
wire arp_rx_done ;
wire arp_rx_type ;
wire [47 : 0] src_mac ;
wire [31 : 0] src_ip ;
wire arp_tx_rdy ;
always@(*)begin
gmii_rxd = gmii_txd;
gmii_rx_dv = gmii_tx_en;
end
//例化ARP模块;
arp #(
.BOARD_MAC ( BOARD_MAC ),
.BOARD_IP ( BOARD_IP ),
.DES_MAC ( DES_MAC ),
.DES_IP ( DES_IP )
)
u_arp (
.rst_n ( rst_n ),//系统复位,默认低电平有效;
.gmii_rx_clk ( clk ),//系统时钟,默认125MHz;
.gmii_rx_dv ( gmii_rx_dv ),
.gmii_rxd ( gmii_rxd ),
.gmii_tx_clk ( clk ),//系统时钟,默认125MHz;
.arp_tx_en ( arp_tx_en ),
.arp_tx_type ( arp_tx_type ),
.des_mac ( des_mac ),
.des_ip ( des_ip ),
.gmii_tx_en ( gmii_tx_en ),
.gmii_txd ( gmii_txd ),
.arp_rx_done ( arp_rx_done ),
.arp_rx_type ( arp_rx_type ),
.src_mac ( src_mac ),
.src_ip ( src_ip ),
.arp_tx_rdy ( arp_tx_rdy )
);
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
#1;arp_tx_en = 0;arp_tx_type = 0;des_mac = 0; des_ip = 0;
rst_n = 1;
#2;
rst_n = 0;//开始时复位10个时钟;
#(RST_TIME*CYCLE);
rst_n = 1;
#(20*CYCLE);
des_mac = BOARD_MAC;
des_ip = BOARD_IP;
arp_tx_en = 1'b1;
arp_tx_type = 1'b0;
#(CYCLE);
arp_tx_en = 1'b0;
@(posedge arp_tx_rdy);
#(20*CYCLE);
des_mac = src_mac;
des_ip = src_ip;
arp_tx_en = 1'b1;
arp_tx_type = 1'b1;
#(CYCLE);
arp_tx_en = 1'b0;
#(10*CYCLE);
@(posedge arp_tx_rdy);
#(20*CYCLE);
$stop;//停止仿真;
end
endmodule
CRC校验如果要从原理开始讲解,占用的篇幅会很大,所以本文先不讲解其原理,直接使用工具生成CRC校验的代码,对生成的代码进行修改,得到本文使用的模块即可,具体原理之后用一篇文章对其进行详细讲解。
能够生成CRC校验码的工具很多,我使用的网络链接Generator for CRC HDL code (bues.ch),设置方式如下图示。
生成的代码如下所示:
`ifndef CRC_V_
`define CRC_V_
// CRC polynomial coefficients: x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1
// 0xEDB88320 (hex)
// CRC width: 32 bits
// CRC shift direction: right (little endian)
// Input word width: 8 bits
module crc (
input [31:0] crcIn,
input [7:0] data,
output [31:0] crcOut
);
assign crcOut[0] = crcIn[2] ^ crcIn[8] ^ data[2];
assign crcOut[1] = crcIn[0] ^ crcIn[3] ^ crcIn[9] ^ data[0] ^ data[3];
assign crcOut[2] = crcIn[0] ^ crcIn[1] ^ crcIn[4] ^ crcIn[10] ^ data[0] ^ data[1] ^ data[4];
assign crcOut[3] = crcIn[1] ^ crcIn[2] ^ crcIn[5] ^ crcIn[11] ^ data[1] ^ data[2] ^ data[5];
assign crcOut[4] = crcIn[0] ^ crcIn[2] ^ crcIn[3] ^ crcIn[6] ^ crcIn[12] ^ data[0] ^ data[2] ^ data[3] ^ data[6];
assign crcOut[5] = crcIn[1] ^ crcIn[3] ^ crcIn[4] ^ crcIn[7] ^ crcIn[13] ^ data[1] ^ data[3] ^ data[4] ^ data[7];
assign crcOut[6] = crcIn[4] ^ crcIn[5] ^ crcIn[14] ^ data[4] ^ data[5];
assign crcOut[7] = crcIn[0] ^ crcIn[5] ^ crcIn[6] ^ crcIn[15] ^ data[0] ^ data[5] ^ data[6];
assign crcOut[8] = crcIn[1] ^ crcIn[6] ^ crcIn[7] ^ crcIn[16] ^ data[1] ^ data[6] ^ data[7];
assign crcOut[9] = crcIn[7] ^ crcIn[17] ^ data[7];
assign crcOut[10] = crcIn[2] ^ crcIn[18] ^ data[2];
assign crcOut[11] = crcIn[3] ^ crcIn[19] ^ data[3];
assign crcOut[12] = crcIn[0] ^ crcIn[4] ^ crcIn[20] ^ data[0] ^ data[4];
assign crcOut[13] = crcIn[0] ^ crcIn[1] ^ crcIn[5] ^ crcIn[21] ^ data[0] ^ data[1] ^ data[5];
assign crcOut[14] = crcIn[1] ^ crcIn[2] ^ crcIn[6] ^ crcIn[22] ^ data[1] ^ data[2] ^ data[6];
assign crcOut[15] = crcIn[2] ^ crcIn[3] ^ crcIn[7] ^ crcIn[23] ^ data[2] ^ data[3] ^ data[7];
assign crcOut[16] = crcIn[0] ^ crcIn[2] ^ crcIn[3] ^ crcIn[4] ^ crcIn[24] ^ data[0] ^ data[2] ^ data[3] ^ data[4];
assign crcOut[17] = crcIn[0] ^ crcIn[1] ^ crcIn[3] ^ crcIn[4] ^ crcIn[5] ^ crcIn[25] ^ data[0] ^ data[1] ^ data[3] ^ data[4] ^ data[5];
assign crcOut[18] = crcIn[0] ^ crcIn[1] ^ crcIn[2] ^ crcIn[4] ^ crcIn[5] ^ crcIn[6] ^ crcIn[26] ^ data[0] ^ data[1] ^ data[2] ^ data[4] ^ data[5] ^ data[6];
assign crcOut[19] = crcIn[1] ^ crcIn[2] ^ crcIn[3] ^ crcIn[5] ^ crcIn[6] ^ crcIn[7] ^ crcIn[27] ^ data[1] ^ data[2] ^ data[3] ^ data[5] ^ data[6] ^ data[7];
assign crcOut[20] = crcIn[3] ^ crcIn[4] ^ crcIn[6] ^ crcIn[7] ^ crcIn[28] ^ data[3] ^ data[4] ^ data[6] ^ data[7];
assign crcOut[21] = crcIn[2] ^ crcIn[4] ^ crcIn[5] ^ crcIn[7] ^ crcIn[29] ^ data[2] ^ data[4] ^ data[5] ^ data[7];
assign crcOut[22] = crcIn[2] ^ crcIn[3] ^ crcIn[5] ^ crcIn[6] ^ crcIn[30] ^ data[2] ^ data[3] ^ data[5] ^ data[6];
assign crcOut[23] = crcIn[3] ^ crcIn[4] ^ crcIn[6] ^ crcIn[7] ^ crcIn[31] ^ data[3] ^ data[4] ^ data[6] ^ data[7];
assign crcOut[24] = crcIn[0] ^ crcIn[2] ^ crcIn[4] ^ crcIn[5] ^ crcIn[7] ^ data[0] ^ data[2] ^ data[4] ^ data[5] ^ data[7];
assign crcOut[25] = crcIn[0] ^ crcIn[1] ^ crcIn[2] ^ crcIn[3] ^ crcIn[5] ^ crcIn[6] ^ data[0] ^ data[1] ^ data[2] ^ data[3] ^ data[5] ^ data[6];
assign crcOut[26] = crcIn[0] ^ crcIn[1] ^ crcIn[2] ^ crcIn[3] ^ crcIn[4] ^ crcIn[6] ^ crcIn[7] ^ data[0] ^ data[1] ^ data[2] ^ data[3] ^ data[4] ^ data[6] ^ data[7];
assign crcOut[27] = crcIn[1] ^ crcIn[3] ^ crcIn[4] ^ crcIn[5] ^ crcIn[7] ^ data[1] ^ data[3] ^ data[4] ^ data[5] ^ data[7];
assign crcOut[28] = crcIn[0] ^ crcIn[4] ^ crcIn[5] ^ crcIn[6] ^ data[0] ^ data[4] ^ data[5] ^ data[6];
assign crcOut[29] = crcIn[0] ^ crcIn[1] ^ crcIn[5] ^ crcIn[6] ^ crcIn[7] ^ data[0] ^ data[1] ^ data[5] ^ data[6] ^ data[7];
assign crcOut[30] = crcIn[0] ^ crcIn[1] ^ crcIn[6] ^ crcIn[7] ^ data[0] ^ data[1] ^ data[6] ^ data[7];
assign crcOut[31] = crcIn[1] ^ crcIn[7] ^ data[1] ^ data[7];
endmodule
`endif // CRC_V_
上述代码不能直接使用,需要稍微修改,代码中的crcIn[31:0]其实就是crcOut[0:31]打一拍的结果,最终crcOut[31:0]输出也不能直接使用,~crcOut[0:31]才是真正的CRC校验码,为了方便使用,下面是修改后的CRC校验模块,每次输入8个数据,32位CRC校验码在下个时钟周期输出,输出的CRC校验码可以直接使用,不需要做任何处理,那么这个模块的实用性就比较高了。
module crc32_d8(
input clk ,//时钟信号
input rst_n ,//复位信号,低电平有效
input [7:0] data ,//输入待校验8位数据
input crc_en ,//crc使能,开始校验标志
input crc_clr ,//crc数据复位信号
output [31:0] crc_out //CRC校验数据
);
reg [31 : 0] crc_data ;
//CRC32的生成多项式为:G(x)= x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x^1 + 1
always@(posedge clk)begin
if(!rst_n)
crc_data <= 32'hff_ff_ff_ff;
else if(crc_clr)//CRC校验值复位
crc_data <= 32'hff_ff_ff_ff;
else if(crc_en)begin
crc_data[0] <= crc_data[24] ^ crc_data[30] ^ data[7] ^ data[1];
crc_data[1] <= crc_data[24] ^ crc_data[25] ^ crc_data[30] ^ crc_data[31] ^ data[7] ^ data[6] ^ data[1] ^ data[0];
crc_data[2] <= crc_data[24] ^ crc_data[25] ^ crc_data[26] ^ crc_data[30] ^ crc_data[31] ^ data[7] ^ data[6] ^ data[5] ^ data[1] ^ data[0];
crc_data[3] <= crc_data[25] ^ crc_data[26] ^ crc_data[27] ^ crc_data[31] ^ data[6] ^ data[5] ^ data[4] ^ data[0];
crc_data[4] <= crc_data[24] ^ crc_data[26] ^ crc_data[27] ^ crc_data[28] ^ crc_data[30] ^ data[7] ^ data[5] ^ data[4] ^ data[3] ^ data[1];
crc_data[5] <= crc_data[24] ^ crc_data[25] ^ crc_data[27] ^ crc_data[28] ^ crc_data[29] ^ crc_data[30] ^ crc_data[31] ^ data[7] ^ data[6] ^ data[4] ^ data[3] ^ data[2] ^ data[1] ^ data[0];
crc_data[6] <= crc_data[25] ^ crc_data[26] ^ crc_data[28] ^ crc_data[29] ^ crc_data[30] ^ crc_data[31] ^ data[6] ^ data[5] ^ data[3] ^ data[2] ^ data[1] ^ data[0];
crc_data[7] <= crc_data[24] ^ crc_data[26] ^ crc_data[27] ^ crc_data[29] ^ crc_data[31] ^ data[7] ^ data[5] ^ data[4] ^ data[2] ^ data[0];
crc_data[8] <= crc_data[0] ^ crc_data[24] ^ crc_data[25] ^ crc_data[27] ^ crc_data[28] ^ data[7] ^ data[6] ^ data[4] ^ data[3];
crc_data[9] <= crc_data[1] ^ crc_data[25] ^ crc_data[26] ^ crc_data[28] ^ crc_data[29] ^ data[6] ^ data[5] ^ data[3] ^ data[2];
crc_data[10] <= crc_data[2] ^ crc_data[24] ^ crc_data[26] ^ crc_data[27] ^ crc_data[29] ^ data[7] ^ data[5] ^ data[4] ^ data[2];
crc_data[11] <= crc_data[3] ^ crc_data[24] ^ crc_data[25] ^ crc_data[27] ^ crc_data[28] ^ data[7] ^ data[6] ^ data[4] ^ data[3];
crc_data[12] <= crc_data[4] ^ crc_data[24] ^ crc_data[25] ^ crc_data[26] ^ crc_data[28] ^ crc_data[29] ^ crc_data[30] ^ data[7] ^ data[6] ^ data[5] ^ data[3] ^ data[2] ^ data[1];
crc_data[13] <= crc_data[5] ^ crc_data[25] ^ crc_data[26] ^ crc_data[27] ^ crc_data[29] ^ crc_data[30] ^ crc_data[31] ^ data[6] ^ data[5] ^ data[4] ^ data[2] ^ data[1] ^ data[0];
crc_data[14] <= crc_data[6] ^ crc_data[26] ^ crc_data[27] ^ crc_data[28] ^ crc_data[30] ^ crc_data[31] ^ data[5] ^ data[3] ^ data[4] ^ data[1] ^ data[0];
crc_data[15] <= crc_data[7] ^ crc_data[27] ^ crc_data[28] ^ crc_data[29] ^ crc_data[31] ^ data[3] ^ data[4] ^ data[2] ^ data[0];
crc_data[16] <= crc_data[8] ^ crc_data[24] ^ crc_data[28] ^ crc_data[29] ^ data[7] ^ data[3] ^ data[2];
crc_data[17] <= crc_data[9] ^ crc_data[25] ^ crc_data[29] ^ crc_data[30] ^ data[6] ^ data[2] ^ data[1];
crc_data[18] <= crc_data[10] ^ crc_data[26] ^ crc_data[30] ^ crc_data[31] ^ data[5] ^ data[1] ^ data[0];
crc_data[19] <= crc_data[11] ^ crc_data[27] ^ crc_data[31] ^ data[4] ^ data[0];
crc_data[20] <= crc_data[12] ^ crc_data[28] ^ data[3];
crc_data[21] <= crc_data[13] ^ crc_data[29] ^ data[2];
crc_data[22] <= crc_data[14] ^ crc_data[24] ^ data[7];
crc_data[23] <= crc_data[15] ^ crc_data[24] ^ crc_data[25] ^ crc_data[30] ^ data[7] ^ data[6] ^ data[1];
crc_data[24] <= crc_data[16] ^ crc_data[25] ^ crc_data[26] ^ crc_data[31] ^ data[6] ^ data[5] ^ data[0];
crc_data[25] <= crc_data[17] ^ crc_data[26] ^ crc_data[27] ^ data[5] ^ data[4];
crc_data[26] <= crc_data[18] ^ crc_data[24] ^ crc_data[27] ^ crc_data[28] ^ crc_data[30] ^ data[7] ^ data[3] ^ data[4] ^ data[1];
crc_data[27] <= crc_data[19] ^ crc_data[25] ^ crc_data[28] ^ crc_data[29] ^ crc_data[31] ^ data[6] ^ data[3] ^ data[2] ^ data[0];
crc_data[28] <= crc_data[20] ^ crc_data[26] ^ crc_data[29] ^ crc_data[30] ^ data[5] ^ data[2] ^ data[1];
crc_data[29] <= crc_data[21] ^ crc_data[27] ^ crc_data[30] ^ crc_data[31] ^ data[4] ^ data[1] ^ data[0];
crc_data[30] <= crc_data[22] ^ crc_data[28] ^ crc_data[31] ^ data[3] ^ data[0];
crc_data[31] <= crc_data[23] ^ crc_data[29] ^ data[2];
end
end
//将计算的数据各位取反倒序赋值后输出。
assign crc_out[31:0] = ~{crc_data[0],crc_data[1],crc_data[2],crc_data[3],crc_data[4],crc_data[5],crc_data[6],crc_data[7],
crc_data[8],crc_data[9],crc_data[10],crc_data[11],crc_data[12],crc_data[13],crc_data[14],crc_data[15],
crc_data[16],crc_data[17],crc_data[18],crc_data[19],crc_data[20],crc_data[21],crc_data[22],crc_data[23],
crc_data[24],crc_data[25],crc_data[26],crc_data[27],crc_data[28],crc_data[29],crc_data[30],crc_data[31]};
endmodule
此处不对CRC校验模块进行仿真,在后文ARP接收和发送模块中一起仿真,并且验证该模块功能是否正确。
前文对以太网帧格式和ARP协议的帧格式做了详细讲解,如图4所示,首先包括7个字节的前导码8’h55,然后帧起始符8’hd5,之后就是14字节的以太网帧头,后跟28字节的ARP数据,18字节的数据0,最后4字节的CRC校验码。
本模块以状态机为主结构,内部通过一个计数器cnt的计数值完成状态之间的跳转,状态转换图如下所示。
本设计使用了一个7个字节的移位寄存器,把接收的gmii_rxd信号暂存,后续设计也可以使用移位寄存器中暂存的数据。状态机处于空闲状态时,会去检测移位寄存器中的数据是否全为8’h55且gmii_rxd为8’hd5,且gmii_rx_dv均为高电平,则检测到前导码和帧起始符,那么状态机跳转到接收以太网帧头状态。
在以太网帧头状态,接收目的MAC地址,如果接收到的目的MAC地址不是开发板目的MAC地址或者广播地址,则此数据报不是发给开发板的,状态机直接跳转到RX_END状态,丢弃该数据报。如果接收到的目的MAC地址满足要求则继续接收,需要检测以太网帧头的类型字节是否为ARP对应的16’h0806,如果是则跳转到ARP_DATA状态接收ARP数据段,如果不是则跳转到RX_END,丢弃该数据报。
在接收ARP数据状态时,需要把OP码保存,将源MAC地址和源IP地址保存,如果目的IP地址不是开发板IP地址,则将数据报丢弃,否则继续接收数据,当接收完ARP数据和18字节的填充数据后,跳转到接收CRC校验码状态。
状态机处于接收以太网帧头和接收ARP数据状态时,将CRC模块的使能信号拉高,且把移位寄存器第一个字节输出给CRC校验模块,进行CRC校验。当状态机跳转到接收CRC校验状态时,CRC校验模块的使能信号拉低。当状态机回到空闲状态时,CRC校验模块的清零信号拉高。
接收完PC端发出的CRC校验码之后,与CRC校验模块输出的CRC校验码对比,如果相同,则将接收到的源MAC地址,源IP地址输出,如果OP码等于1,则将ARP请求应答指示信号输出低电平,如果OP码为2,则ARP请求应答指示信号输出高电平。
状态机处于RX_END状态时,只有检测到gmii_rx_dv为低电平时,才会回到空闲状态,继续检测下一帧数据,这是为了防止该帧数据报中可能出现与帧头和前导码一样的数据,从而解析错误。
需要注意计数器cnt在状态机不处于空闲状态和RX_END状态时就会对gmii_rxc时钟计数,状态机回到空闲状态时需要清零。计数器在状态机每个状态的最大值不一样,状态机在接收以太网帧头需要接收14字节数据,那么计数器最大值为14-1,在接收ARP数据状态,因为需要接收46字节数据,故计数器最大值为46-1。
状态机与移位寄存器gmii_rxd_r[0]的数据对齐,所以后文对数据的截取大多截取的gmii_rxd_r[0]信号。
大概含义就这么多,其余细节查看代码即可,参考代码如下所示:
//The first section: synchronous timing always module, formatted to describe the transfer of the secondary register to the live register ?
always@(posedge clk)begin
if(!rst_n)begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//The second paragraph: The combinational logic always module describes the state transition condition judgment.
always@(*)begin
case(state_c)
IDLE:begin
if(start)begin//检测到前导码和SFD后跳转到接收以太网帧头数据的状态。
state_n = ETH_HEAD;
end
else begin
state_n = state_c;
end
end
ETH_HEAD:begin
if(error_flag)begin//在接收帧头数据时,检测到错误。
state_n = RX_END;
end
else if(end_cnt)begin//接收完以太网帧头数据,且没有出现错误,则继续接收ARP协议数据。
state_n = ARP_DATA;
end
else begin
state_n = state_c;
end
end
ARP_DATA:begin
if(error_flag)begin//在接收ARP协议过程中检测到错误。
state_n = RX_END;
end
else if(end_cnt)begin//接收完ARP协议数据且未检测到数据错误。
state_n = CRC;
end
else begin
state_n = state_c;
end
end
CRC:begin
if(end_cnt)begin//接收完CRC校验数据。
state_n = RX_END;
end
else begin
state_n = state_c;
end
end
RX_END:begin
if(~gmii_rx_dv)begin//检测到数据线上数据无效。
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default:begin
state_n = IDLE;
end
endcase
end
//将输入数据保存6个时钟周期,用于检测前导码和SFD。
//注意后文的state_c与gmii_rxd_r[0]对齐。
always@(posedge clk)begin
gmii_rxd_r[6] <= gmii_rxd_r[5];
gmii_rxd_r[5] <= gmii_rxd_r[4];
gmii_rxd_r[4] <= gmii_rxd_r[3];
gmii_rxd_r[3] <= gmii_rxd_r[2];
gmii_rxd_r[2] <= gmii_rxd_r[1];
gmii_rxd_r[1] <= gmii_rxd_r[0];
gmii_rxd_r[0] <= gmii_rxd;
gmii_rx_dv_r <= {gmii_rx_dv_r[5 : 0],gmii_rx_dv};
end
//在状态机处于空闲状态下,检测到连续7个8'h55后又检测到一个8'hd5后表示检测到帧头,此时将介绍数据的开始信号拉高,其余时间保持为低电平。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
start <= 1'b0;
end
else if(state_c == IDLE)begin
start <= ({gmii_rx_dv_r,gmii_rx_dv} == 8'hFF) && ({gmii_rxd,gmii_rxd_r[0],gmii_rxd_r[1],gmii_rxd_r[2],gmii_rxd_r[3],gmii_rxd_r[4],gmii_rxd_r[5],gmii_rxd_r[6]} == 64'hD5_55_55_55_55_55_55_55);
end
end
//计数器,状态机在不同状态需要接收的数据个数不一样,使用一个可变进制的计数器。
always@(posedge clk)begin
if(rst_n==1'b0)begin//
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + 1;
end
else begin
cnt <= 0;
end
end
//当状态机不在空闲状态或接收数据结束阶段时计数,计数到该状态需要接收数据个数时清零。
assign add_cnt = (state_c != IDLE) && (state_c != RX_END);
assign end_cnt = add_cnt && cnt == cnt_num - 1;
//状态机在不同状态,需要接收不同的数据个数,在接收以太网帧头时,需要接收14byte数据。
//在接收ARP数据时,需要接收46byte数据,在CRC阶段需要接收4字节CRC校验码。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为46;
cnt_num <= 6'd46;
end
else begin
case(state_c)
ETH_HEAD : cnt_num <= 6'd14;
CRC : cnt_num <= 6'd4;
default: cnt_num <= 6'd46;
endcase
end
end
//接收目的MAC地址,需要判断这个包是不是发给开发板的,目的MAC地址是不是开发板的MAC地址或广播地址。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
des_mac_t <= 48'd0;
end
else if((state_c == ETH_HEAD) && add_cnt && cnt < 5'd6)begin
des_mac_t <= {des_mac_t[39:0],gmii_rxd_r[0]};
end
end
//判断接收的数据是否正确,以此来生成错误指示信号,判断状态机跳转。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
error_flag <= 1'b0;
end
else begin
case(state_c)
ETH_HEAD : begin
if(add_cnt)
if(cnt == 6)//判断接收的数据是不是发送给开发板或者广播数据。
error_flag <= ((des_mac_t != BOARD_MAC) && (des_mac_t != 48'HFF_FF_FF_FF_FF_FF));
else if(cnt ==12)//判断接收的数据是不是ARP协议。
error_flag <= ({gmii_rxd_r[0],gmii_rxd} != ETH_TPYE);
end
ARP_DATA : begin
if(add_cnt && cnt == 28)begin//判断接收的目的IP地址是否正确,操作码是否为ARP的请求或应答指令。
error_flag <= ((opcode != 16'd1) && (opcode != 16'd2)) || (des_ip_t != BOARD_IP);
end
end
default: error_flag <= 1'b0;
endcase
end
end
//接收OP操作码,源MAC地址,源IP地址,目的IP地址。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
opcode <= 16'd0;
src_mac_t <= 48'd0;
src_ip_t <= 32'd0;
des_ip_t <= 32'd0;
end
else if(state_c == ARP_DATA && add_cnt)begin
case(cnt)
5'd6 : opcode[15:8] <= gmii_rxd_r[0];//操作码;
5'd7 : opcode[7:0] <= gmii_rxd_r[0];//操作码;
5'd8,5'd9,5'd10,5'd11,5'd12,5'd13 : src_mac_t <= {src_mac_t[39:0],gmii_rxd_r[0]};//源MAC地址;
5'd14,5'd15,5'd16,5'd17 : src_ip_t <= {src_ip_t[23:0],gmii_rxd_r[0]};//源IP地址;
5'd24,5'd25,5'd26,5'd27 : des_ip_t <= {des_ip_t[23:0],gmii_rxd_r[0]};//目标IP地址;
default: ;
endcase
end
end
//生产CRC校验相关的数据和控制信号。
always@(posedge clk)begin
crc_data <= gmii_rxd_r[0];//将移位寄存器最低位存储的数据作为CRC输入模块的数据。
crc_clr <= (state_c == IDLE);//当状态机处于空闲状态时,清除CRC校验模块计算。
crc_en <= (state_c == ETH_HEAD) || (state_c == ARP_DATA);//CRC校验使能信号。
end
//接收PC端发送来的CRC数据。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
des_crc <= 32'hff_ff_ff_ff;
end
else if(add_cnt && state_c == CRC)begin//先接收的是低位数据;
des_crc <= {gmii_rxd_r[0],des_crc[23:8]};
end
end
//生成相应的输出数据。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
arp_rx_type <= 1'b0;
arp_rx_done <= 1'b0;
src_mac <= 48'd0;
src_ip <= 32'd0;
end//如果CRC校验成功,把ARP协议接收完成信号拉高,把接收到的源MAC和IP地址输出,并且将ARP协议类型输出。
else if(state_c == CRC && end_cnt && ({gmii_rxd_r[0],des_crc[23:0]} == crc_out))begin
arp_rx_done <= 1'b1;
src_mac <= src_mac_t;
src_ip <= src_ip_t;
if(opcode == 16'd1)
arp_rx_type <= 1'b0;//ARP请求;
else
arp_rx_type <= 1'b1;//ARP应答;
end
else begin
arp_rx_done <= 1'b0;
end
end
对应的TestBench如下所示:
`timescale 1 ns/1 ns
module test();
parameter CYCLE = 8 ;//系统时钟周期,单位ns,默认10ns;
parameter RST_TIME = 10 ;//系统复位持续时间,默认10个系统时钟周期;
parameter STOP_TIME = 1000 ;//仿真运行时间,复位完成后运行1000个系统时钟后停止;
parameter BOARD_MAC = 48'h00_11_22_33_44_55 ;
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10} ;
localparam SOURCE_MAC = 48'h23_45_67_89_0a_bc ;
localparam SOURCE_IP = {8'd192,8'd168,8'd1,8'd23} ;
localparam ETH_TPYE = 16'h0806 ;//以太网帧类型 ARP
reg clk ;//系统时钟,默认100MHz;
reg rst_n ;//系统复位,默认低电平有效;
reg [7 : 0] gmii_rxd ;
reg gmii_rx_dv ;
wire crc_clr_r ;
wire crc_en ;
wire [7 : 0] crc_data ;
wire [31 : 0] crc_out_r ;
wire [31 : 0] src_ip ;
wire [47 : 0] src_mac ;
wire arp_rx_done ;
wire arp_rx_type ;
arp_rx #(
.BOARD_MAC ( BOARD_MAC ),
.BOARD_IP ( BOARD_IP )
)
u_arp_rx (
.clk ( clk ),
.rst_n ( rst_n ),
.gmii_rxd ( gmii_rxd ),
.gmii_rx_dv ( gmii_rx_dv ),
.crc_out ( crc_out_r ),
.crc_en ( crc_en ),
.crc_clr ( crc_clr_r ),
.crc_data ( crc_data ),
.arp_rx_done( arp_rx_done ),
.arp_rx_type( arp_rx_type ),
.src_mac ( src_mac ),
.src_ip ( src_ip )
);
//例化CRC校验模块
crc32_d8 u_crc32_d8_2 (
.clk ( clk ),
.rst_n ( rst_n ),
.data ( crc_data ),
.crc_en ( crc_en ),
.crc_clr ( crc_clr_r ),
.crc_out ( crc_out_r )
);
reg crc_clr ;
reg gmii_crc_vld ;
reg [7 : 0] gmii_rxd_r ;
reg gmii_rx_dv_r ;
reg crc_data_vld ;
wire [31 : 0] crc_out ;
//生成周期为CYCLE数值的系统时钟;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
#1;gmii_rxd = 0; gmii_rx_dv = 0;gmii_crc_vld = 1'b0;
gmii_rxd_r=0;gmii_rx_dv_r=0;crc_clr=0;
rst_n = 1;
#2;
rst_n = 0;//开始时复位10个时钟;
#(RST_TIME*CYCLE);
rst_n = 1;
#(20*CYCLE);
repeat(4)begin//发送4帧ARP指令;
gmii_tx_test();
gmii_crc_vld = 1'b1;
gmii_rxd_r = crc_out[7 : 0];
#(CYCLE);
gmii_rxd_r = crc_out[15 : 8];
#(CYCLE);
gmii_rxd_r = crc_out[23 : 16];
#(CYCLE);
gmii_rxd_r = crc_out[31 : 24];
#(CYCLE);
gmii_crc_vld = 1'b0;
crc_clr = 1'b1;
#(CYCLE);
crc_clr = 1'b0;
#(20*CYCLE);
end
$stop;//停止仿真;
end
reg [5:0] i;
task gmii_tx_test;
begin
crc_data_vld = 1'b0;
#(CYCLE);
repeat(7)begin//发送前导码7个8'H55;
gmii_rxd_r = 8'h55;
gmii_rx_dv_r = 1'b1;
#(CYCLE);
end
gmii_rxd_r = 8'hd5;//发送SFD,一个字节的8'hd5;
#(CYCLE);
crc_data_vld = 1'b1;
for(i=0 ; i<6 ; i=i+1)begin//发送6个字节的目的MAC地址;
gmii_rxd_r = BOARD_MAC[47-8*i -: 8];
#(CYCLE);
end
for(i=0 ; i<6 ; i=i+1)begin//发送6个字节的源MAC地址;
gmii_rxd_r = SOURCE_MAC[47-8*i -: 8];
#(CYCLE);
end
for(i=0 ; i<2 ; i=i+1)begin//发送2个字节的以太网类型;
gmii_rxd_r = ETH_TPYE[15-8*i -: 8];
#(CYCLE);
end
gmii_rxd_r = 8'd0;//发送2字节的硬件地址类型。
#(CYCLE);
gmii_rxd_r = 8'd1;
#(CYCLE);
gmii_rxd_r = 8'h08;//发送2字节的协议类型,0X0800表示上层IP协议。
#(CYCLE);
gmii_rxd_r = 8'h00;
#(CYCLE);
gmii_rxd_r = 8'h06;//发送1字节的硬件地址长度。
#(CYCLE);
gmii_rxd_r = 8'h04;//发送1字节的IP地址长度。
#(CYCLE);
gmii_rxd_r = 8'h00;//发送2字节的OP编码,1表示ARP请求,2表示ARP应答。
#(CYCLE);
gmii_rxd_r = 8'h02;
#(CYCLE);
for(i=0 ; i<6 ; i=i+1)begin//发送6个字节的源MAC地址;
gmii_rxd_r = SOURCE_MAC[47-8*i -: 8];
#(CYCLE);
end
for(i=0 ; i<4 ; i=i+1)begin//发送4个字节的源IP地址;
gmii_rxd_r = SOURCE_IP[31-8*i -: 8];
#(CYCLE);
end
for(i=0 ; i<6 ; i=i+1)begin//发送6个字节目的MAC地址;
gmii_rxd_r = BOARD_MAC[47-8*i -: 8];
#(CYCLE);
end
for(i=0 ; i<4 ; i=i+1)begin//发送4个字节的目的IP地址;
gmii_rxd_r = BOARD_IP[31-8*i -: 8];
#(CYCLE);
end
for(i=0 ; i<18 ; i=i+1)begin//补发18个字节的0;
gmii_rxd_r = 8'd0;
#(CYCLE);
end
crc_data_vld = 1'b0;
gmii_rx_dv_r = 1'b0;
end
endtask
crc32_d8 u_crc32_d8_1 (
.clk ( clk ),
.rst_n ( rst_n ),
.data ( gmii_rxd_r ),
.crc_en ( crc_data_vld ),
.crc_clr ( crc_clr ),
.crc_out ( crc_out )
);
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
gmii_rxd <= 8'd0;
gmii_rx_dv <= 1'b0;
end
else if(gmii_rx_dv_r || gmii_crc_vld)begin
gmii_rxd <= gmii_rxd_r;
gmii_rx_dv <= 1'b1;
end
else begin
gmii_rx_dv <= 1'b0;
end
end
endmodule
仿真截图如下所示,检测到前导码和帧起始符,start信号为高电平,状态机从空闲转台跳转到接收以太网帧头状态,且状态机与gmii_rxd_r[0]的数据对齐。
然后就是接收以太网帧头和ARP数据报了,如下所示,都比较简单,不做赘述,需要细节的可以打开工程自行仿真查看。
下图为接收CRC校验,天蓝色信号des_crc是接收到的CRC校验码的低24位,红色信号crc_out是该模块通过接收数据计算出的CRC校验码,在计数器等于3时,{gmii_rxd_r[0],crc_out}==crc_out则说明接收的数据正确,将接收完成信号arp_rx_done拉高一个时钟周期。
注意32位CRC校验码先发低字节数据,后发高字节数据。
如何验证CRC计算正确呢?可以通过CRC计算器,计算出发送这些数据的CRC校验码是多少,与仿真结果对比,进而确定模块输出的CRC校验码是否正常。
如下图所示,将以太网帧头和ARP数据(包括18字节的填充数据)写入1处,选择CRC-32,点击3处进行计算,4就是计算结果,32’h30D25366,与图8中红色信号计算结果一致,证明CRC校验模块设计也没有问题。
以上就是ARP接收模块的设计,对该模块进行了仿真,仿真也处于正常状态,后续上板可以通过ILA抓包查看上板时序。
ARP发送模块相比接收模块会更加简单,最简单的设计方法其实是通过一个计数器记录发送数据个数,然后根据计数器译码输出数据即可,但是这种方式不方便查看。所以本文还是通过一个状态机嵌套计数器的方式实现。
状态机处于空闲状态时,如果gmii_tx_start开始信号位高电平时,此时如果检测模块接收的目的MAC和目的IP地址是否为0,不为0就更新目的MAC地址和目的IP地址。开始产生数据,状态机跳转到发送前导码和帧起始符状态。
前导码和帧起始符发送完成后,状态机跳转到发送以太网帧头状态,然后发送ARP数据,最后发送CRC校验码,由于CRC校验码的计算会滞后输入信号一个时钟周期,所以将前面的数据打一拍在输出,就能刚好接上CRC校验码,数据发送完成后状态机回到初始状态。因此该模块其实比较简单。
对应的参考代码如下所示:
localparam IDLE = 5'b0_0001 ;//初始状态,等待开始发送信号;
localparam PREAMBLE = 5'b0_0010 ;//发送前导码+帧起始界定符;
localparam ETH_HEAD = 5'b0_0100 ;//发送以太网帧头;
localparam ARP_DATA = 5'b0_1000 ;//发送ARP协议数据;
localparam CRC = 5'b1_0000 ;//发送CRC校验值;
localparam MIN_DATA_NUM = 16'd46 ;//以太网数据最小为46个字节,不足部分填充数据0。
reg gmii_tx_en_r ;//
reg [47 : 0] des_mac_r ;//
reg [31 : 0] des_ip_r ;
reg [1 : 0] arp_tx_type_r ;
reg [4 : 0] state_n ;
reg [4 : 0] state_c ;
reg [5 : 0] cnt ;//
reg [5 : 0] cnt_num ;//
wire add_cnt ;
wire end_cnt ;
//在状态机空闲状态下,上游发送使能信号时,将目的MAC地址和目的IP以及ARP的操作类型进行暂存。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
des_ip_r <= DES_IP;
des_mac_r <= DES_MAC;
arp_tx_type_r <= OPCODE;
end
else if(arp_tx_en && state_c == IDLE)begin
arp_tx_type_r <= arp_tx_type ? 2'd2 : 2'd1;
if((des_mac != 48'd0) && (des_ip != 48'd0))begin//当接收到目的MAC地址和目的IP地址时更新。
des_ip_r <= des_ip;
des_mac_r <= des_mac;
end
end
end
//The first section: synchronous timing always module, formatted to describe the transfer of the secondary register to the live register ?
always@(posedge clk)begin
if(!rst_n)begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//The second paragraph: The combinational logic always module describes the state transition condition judgment.
always@(*)begin
case(state_c)
IDLE:begin
if(arp_tx_en)begin//在空闲状态接收到上游发出的使能信号;
state_n = PREAMBLE;
end
else begin
state_n = state_c;
end
end
PREAMBLE:begin
if(end_cnt)begin//发送完前导码和SFD;
state_n = ETH_HEAD;
end
else begin
state_n = state_c;
end
end
ETH_HEAD:begin
if(end_cnt)begin//发送完以太网帧头数据;
state_n = ARP_DATA;
end
else begin
state_n = state_c;
end
end
ARP_DATA:begin
if(end_cnt)begin//发送完ARP协议数据;
state_n = CRC;
end
else begin
state_n = state_c;
end
end
CRC:begin
if(end_cnt)begin//发送完CRC校验码;
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default:begin
state_n = IDLE;
end
endcase
end
//计数器cnt,记录状态机每个状态持续的时钟个数。
always@(posedge clk)begin
if(rst_n==1'b0)begin//
cnt <= 0;
end
else if(add_cnt)begin
if(end_cnt)
cnt <= 0;
else
cnt <= cnt + 1;
end
end
assign add_cnt = (state_c != IDLE);//状态机不处于空闲状态时对时钟进行计数。
assign end_cnt = add_cnt && cnt == cnt_num - 1;
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
cnt_num <= 6'd46;
end
else begin
case (state_c)
PREAMBLE : cnt_num <= 6'd8;
ETH_HEAD : cnt_num <= 6'd14;
CRC : cnt_num <= 6'd5;//CRC在时钟1时才开始发送数据,这是因为CRC计算模块输出的数据会延后一个时钟周期。
default: cnt_num <= 6'd46;
endcase
end
end
//根据状态机和计数器的值产生输出数据,只不过这不是真正的输出,还需要延迟一个时钟周期。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
crc_data <= 8'd0;
end
else if(add_cnt)begin
case (state_c)
PREAMBLE : if(end_cnt)
crc_data <= 8'hd5;//发送1字节SFD编码;
else
crc_data <= 8'h55;//发送7字节前导码;
ETH_HEAD : if(cnt < 6)
crc_data <= des_mac_r[47 - 8*cnt -: 8];//发送目的MAC地址,先发高字节;
else if(cnt < 12)
crc_data <= BOARD_MAC[47 - 8*(cnt-6) -: 8];//发送源MAC地址,先发高字节;
else
crc_data <= ETH_TYPE[15 - 8*(cnt-12) -: 8];//发送源以太网协议类型,先发高字节;
ARP_DATA : begin
case (cnt)
6'd0 , 6'd1 : crc_data <= HD_TYPE[15 - 8*cnt -: 8];//发送硬件类型,先发高字节;
6'd2 , 6'd3 : crc_data <= PROTOCOL_TYPE[15 - 8*(cnt-2) -: 8];//发送协议类型,先发高字节;
6'd4 : crc_data <= HD_LEN;//发送硬件地址长度;
6'd5 : crc_data <= PROTOCOL_LEN;//发送协议地址长度;
6'd6 : crc_data <= 8'd0;//发送ARP操作类型;
6'd7 : crc_data <= {6'd0,arp_tx_type_r};//发送ARP操作类型;
6'd8 , 6'd9 , 6'd10 , 6'd11 , 6'd12 , 6'd13 : crc_data <= BOARD_MAC[47 - 8*(cnt-8) -: 8];//发送源MAC地址;
6'd14 , 6'd15 , 6'd16 , 6'd17 : crc_data <= BOARD_IP[31 - 8*(cnt-14) -: 8];//发送源IP地址;
6'd18 , 6'd19 , 6'd20 , 6'd21 , 6'd22 , 6'd23 : crc_data <= des_mac_r[47 - 8*(cnt-18) -: 8];//发送目的MAC地址;
6'd24 , 6'd25 , 6'd26 , 6'd27 : crc_data <= des_ip_r[31 - 8*(cnt-24) -: 8];//发送目的IP地址;
default : crc_data <= 8'd0;//其余时间补0;
endcase
end
endcase
end
end
//生成一个crc_data指示信号,用于生成gmii_txd信号。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
gmii_tx_en_r <= 1'b0;
end
else if(state_c == CRC)begin
gmii_tx_en_r <= 1'b0;
end
else if(state_c == PREAMBLE)begin
gmii_tx_en_r <= 1'b1;
end
end
//生产CRC校验模块使能信号,初始值为0,当开始输出以太网帧头时拉高,当ARP和以太网帧头数据全部输出后拉低。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
crc_en <= 1'b0;
end
else if(state_c == CRC)begin//当ARP和以太网帧头数据全部输出后拉低.
crc_en <= 1'b0;
end//当开始输出以太网帧头时拉高。
else if(state_c == ETH_HEAD && add_cnt)begin
crc_en <= 1'b1;
end
end
//生产CRC校验模块清零信号,状态机处于空闲时清零。
always@(posedge clk)begin
crc_clr <= (state_c == IDLE);
end
//生成gmii_txd信号,默认输出0。
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
gmii_txd <= 8'd0;
end//在输出CRC状态时,输出CRC校验码,先发送低位数据。
else if(state_c == CRC && add_cnt && cnt>0)begin
gmii_txd <= crc_out[8*cnt-1 -: 8];
end//其余时间如果crc_data有效,则输出对应数据。
else if(gmii_tx_en_r)begin
gmii_txd <= crc_data;
end
end
//生成gmii_txd有效指示信号。
always@(posedge clk)begin
gmii_tx_en <= gmii_tx_en_r || (state_c == CRC);
end
//模块忙闲指示信号,当接收到上游模块的使能信号或者状态机不处于空闲状态时拉低,其余时间拉高。
//该信号必须使用组合逻辑产生,上游模块必须使用时序逻辑检测该信号。
always@(*)begin
if(arp_tx_en || state_c != IDLE)
rdy = 1'b0;
else
rdy = 1'b1;
end
ARP发送模块仿真如下图所示,当arp_tx_en有效时,状态机跳转到发送前导码和帧起始符状态,开始产生数据。
下图为发送CRC校验字段的仿真,红色信号是最终输出的数据,而橙色crc_out是CRC校验模块计算得到的数据。
ARP发送和接收模块的设计就到此结束了,后续上板时通过ILA抓取,查看代码运行。
如果按键按下,FPGA向PC发送ARP请求,如果FPGA接收到PC发出的ARP请求,则FPGA向PC端发送ARP应答。
ARP控制模块检测到按键按下且ARP发送模块处于空闲时,将ARP发送模块的开始发送信号拉高,向PC端发出ARP请求。如果检测到PC端发给FPGA的ARP请求,则将ARP发送模块的开始信号拉高。
参考代码如下所示:
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
start <= 1'b0;
end
else if(key_in || (arp_rx_done && ~arp_rx_type))begin
start <= 1'b1;
end
else if(arp_tx_rdy)begin//当下游模块处于空闲时拉低。
start <= 1'b0;
end
end
//当需要发送ARP指令且发送模块空闲时有效,其余时间均为低电平。
always@(posedge clk)begin
arp_tx_en <= start && arp_tx_rdy;
end
always@(posedge clk)begin
if(rst_n==1'b0)begin//初始值为0;
arp_tx_type <= 1'b0;
end
else if(arp_rx_done && ~arp_rx_type)begin//接收到PC的ARP请求时,应该回发应答信号。
arp_tx_type <= 1'b1;
end
else if(key_in || (arp_rx_done && arp_rx_type))begin//其余时间发送请求指令。
arp_tx_type <= 1'b0;
end
end
由于模块过于简单,就不进行仿真了。
将顶层模块综合,加入ILA,然后上板测试,综合工程后下载程序到板子,然后做一些准备,顶层将目的IP地址设置为192.168.1.102,所以我们需要先把电脑的IP地址设置为192.168.1.102。
如下图所示,在电脑的搜索框中输入网络连接,然后点击查看网络连接,开发板网口与电脑通过网线连接后,图中3处就会出现以太网。
然后选中以太网鼠标右键,点击属性。
然后进行下图所示操作,首先双击协议版本,然后手动设置IP,将IP地址设置成图13中目的IP地址一致,其余设置与图16保持一致,之后点击确定。
然后再电脑的搜索栏中输入dos,鼠标右键命令提示符,然后以管理员身份运,如下图所示。
输入arp -a指令,查看arp列表,如下图所示,可以看到PC中存在192.168.1.102的IP地址,但是并没有FPGA的MAC地址和IP地址,这是因为开发板和PC还没有进行通信。
然后通过下载器给FPGA下载代码,然后按下开发板的按键,FPGA向PC发出ARP请求。通过ILA抓取gmii_tx_en或者gmii_rx_dv的结果如下所示,检测到按键信号有效后,发出arp请求数据帧,目的MAC地址为广播地址。
当PC端接收到FPGA发送的ARP请求数据报后,PC端回复FPGA的ARP应答数据报(下图天蓝色信号)。
将应答数据报放大后,如下图所示,此时目的MAC地址就是开发板的目的MAC地址,因为PC已经知道FPGA的MAC地址了。arp_rx_done信号拉高表示该数据报接收完成且CRC校验通过。
此时在命令提示符中输入arp -a,就可以查到开发板的MAC地址和IP地址了,如下图所示。
此时我们还可以打开wireshark软件抓取以太网数据报,打开wireshark软件,如下图所示,点击以太网。
然后按下开发板的按键,wireshark软件就能够抓到FPGA发送给PC的ARP请求数据报和PC回复FPGA的ARP应答包,如下图所示。
1是FPGA发送的ARP请求数据报,双击该数据报,得到如下图所示,可以看到下图wireshark抓到的数据与图20中ILA抓取的数据是一致的。
2是PC回复FPGA的ARP应答数据报,双击打开该数据报,如下图所示,报文类型为2,证明是ARP应答包,该数据报发送的数据与图21中ILA抓取的数据一致。
上述对FPGA发送ARP请求,PC回复ARP应答进行了验证,后面需要对PC端发出ARP请求,FPGA是否能够回复PC端进行验证。首先通过arp -d指令,删除PC端arp连接,然后再使用arp -a查询arp列表,得知开发板的MAC地址和IP地址已经被PC清除。
然后开发板重新下载程序,ILA准备抓取数据,wireshark清除抓取的数据后重新抓取数据,然后再命令提示符窗口输入ping 192.168.1.10,因为开发板没有实现ICMP协议,所以并不能响应,但是该指令PC端也会向开发板发送ARP请求指令,所以本文可以用该指令进行测试。
当该指令运行结束后,输入arp -a指令,就可以查看到FPGA的MAC地址和IP地址已经存在PC端的ARP列表里了,验证成功。
接下来查看ILA抓取的数据报吧,ILA深度设置的比较大,并且可以触发32次,所以基本上不会漏掉数据报。
如下图所示,ILA抓取PC端发送给FPGA的ARP请求数据报后,FPGA马上向PC端回复了ARP应答数据报。
下图为ILA抓取的ARP请求数据报,PC端发出的ARP请求数据报的以太网帧头的目的MAC地址为广播地址,但是再ARP数据段中,发送的目的MAC地址全为0,并不是广播地址,这也许是因为接收端不会解析这部分数据,所以就没有做处理吧。
同时wireshark抓取的数据报问如下所示,不知道为什么,我的wireshark每次都会显示发送了2次的ARP请求,但是ILA和FPGA都只能收到一个数据报。
打开ARP请求数据报,如下图所示,两个目的MAC地址的数值也不一致,应该跟猜想一样吧。
然后查看FPGA发送的ARP应答数据报,ILA抓取的结果如下图所示,紫红色信号就是以太网发送信号。目的MAC地址就是图33中PC端的MAC地址,目的IP也是电脑的IP地址。
对应的wireshark抓取该包的数据如下图所示,与上图保持一致。
从图34可知发送的CRC校验码为32’hcdc83243,使用CRC计算器对图35的数据进行计算,如下图所示,与模块计算一致,没有错误。
至此,本文的验证就结束了,主要通过FPGA解析ARP数据报,然后向PC端发送ARP数据报,也简单介绍了wireshark软件的使用,利用该软件,结合ILA再调试时可以事半功倍。后续的ICMP和UDP其实都离不开ARP协议,学会设计ARP协议后,ICMP和UDP的实现也就比较简单了。
在接收数据时充分使用移位寄存器还可以将代码简化,有些地方接收数据根本不需要进行移位操作,在计数器某个状态时,直接对移位寄存器中某段数据保存即可,也可以简化判断电路。
本工程可以在公众号后台回复“基于FPGA的ARP实现”(不包含引号)获取。