【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)

1、系统概括

  本文主要实现通过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接口信号转换模块,在前文已经做了详细讲解,本文不再赘述。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第1张图片

图1 顶层模块信号流向图

  每个模块的左侧信号为该模块输入信号,右侧为该模块输出信号。本文主要讲解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发送数据;
    );

2、ARP模块设计

  下图是ARP模块的内部信号走向,主要包含三个模块,ARP接收模块arp_rx,ARP发送模块arp_tx,CRC校验模块。本工程对ARP接收和发送均做了CRC校验,来确保数据的正确性,接收其实可以不做CRC校验的。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第2张图片

图2 ARP模块内部信号走向

  下文分别对这几个模块的设计进行讲解,​该模块核心参考代码如下所示:

    //例化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

2.1、CRC校验模块

  CRC校验如果要从原理开始讲解,占用的篇幅会很大,所以本文先不讲解其原理,直接使用工具生成CRC校验的代码,对生成的代码进行修改,得到本文使用的模块即可,具体原理之后用一篇文章对其进行详细讲解。

  能够生成CRC校验码的工具很多,我使用的网络链接Generator for CRC HDL code (bues.ch),设置方式如下图示。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第3张图片

图3 生成CRC代码

  生成的代码如下所示:

`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接收和发送模块中一起仿真,并且验证该模块功能是否正确。

2.2、ARP接收模块

  前文对以太网帧格式和ARP协议的帧格式做了详细讲解,如图4所示,首先包括7个字节的前导码8’h55,然后帧起始符8’hd5,之后就是14字节的以太网帧头,后跟28字节的ARP数据,18字节的数据0,最后4字节的CRC校验码。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第4张图片

图4 以太网ARP协议数据报

  本模块以状态机为主结构,内部通过一个计数器cnt的计数值完成状态之间的跳转,状态转换图如下所示。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第5张图片

图5 状态转换图

  本设计使用了一个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]的数据对齐。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第6张图片

图6 检测到前导码和帧起始符

  然后就是接收以太网帧头和ARP数据报了,如下所示,都比较简单,不做赘述,需要细节的可以打开工程自行仿真查看。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第7张图片

图7 接收以太网帧头和ARP数据

  下图为接收CRC校验,天蓝色信号des_crc是接收到的CRC校验码的低24位,红色信号crc_out是该模块通过接收数据计算出的CRC校验码,在计数器等于3时,{gmii_rxd_r[0],crc_out}==crc_out则说明接收的数据正确,将接收完成信号arp_rx_done拉高一个时钟周期。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第8张图片

图8 接收CRC校验

  注意32位CRC校验码先发低字节数据,后发高字节数据。

  如何验证CRC计算正确呢?可以通过CRC计算器,计算出发送这些数据的CRC校验码是多少,与仿真结果对比,进而确定模块输出的CRC校验码是否正常。

  如下图所示,将以太网帧头和ARP数据(包括18字节的填充数据)写入1处,选择CRC-32,点击3处进行计算,4就是计算结果,32’h30D25366,与图8中红色信号计算结果一致,证明CRC校验模块设计也没有问题。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第9张图片

图9 CRC校验计算器

  以上就是ARP接收模块的设计,对该模块进行了仿真,仿真也处于正常状态,后续上板可以通过ILA抓包查看上板时序。

2.3、ARP发送模块

  ARP发送模块相比接收模块会更加简单,最简单的设计方法其实是通过一个计数器记录发送数据个数,然后根据计数器译码输出数据即可,但是这种方式不方便查看。所以本文还是通过一个状态机嵌套计数器的方式实现。

  状态机处于空闲状态时,如果gmii_tx_start开始信号位高电平时,此时如果检测模块接收的目的MAC和目的IP地址是否为0,不为0就更新目的MAC地址和目的IP地址。开始产生数据,状态机跳转到发送前导码和帧起始符状态。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第10张图片

图10 ARP发送模块状态转换图

  前导码和帧起始符发送完成后,状态机跳转到发送以太网帧头状态,然后发送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有效时,状态机跳转到发送前导码和帧起始符状态,开始产生数据。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第11张图片

图11 ARP发送模块仿真

  下图为发送CRC校验字段的仿真,红色信号是最终输出的数据,而橙色crc_out是CRC校验模块计算得到的数据。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第12张图片

图12 ARP发送模块仿真

  ARP发送和接收模块的设计就到此结束了,后续上板时通过ILA抓取,查看代码运行。

3、ARP_CTRL模块设计

  如果按键按下,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

  由于模块过于简单,就不进行仿真了。

4、上板测试

  将顶层模块综合,加入ILA,然后上板测试,综合工程后下载程序到板子,然后做一些准备,顶层将目的IP地址设置为192.168.1.102,所以我们需要先把电脑的IP地址设置为192.168.1.102。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第13张图片

图13 顶层模块

  如下图所示,在电脑的搜索框中输入网络连接,然后点击查看网络连接,开发板网口与电脑通过网线连接后,图中3处就会出现以太网。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第14张图片

图14 查看网络连接

  然后选中以太网鼠标右键,点击属性。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第15张图片

图15 查看属性

  然后进行下图所示操作,首先双击协议版本,然后手动设置IP,将IP地址设置成图13中目的IP地址一致,其余设置与图16保持一致,之后点击确定。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第16张图片

图16 PC的IP设置

  然后再电脑的搜索栏中输入dos,鼠标右键命令提示符,然后以管理员身份运,如下图所示。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第17张图片

图17 运行命令提示符

  输入arp -a指令,查看arp列表,如下图所示,可以看到PC中存在192.168.1.102的IP地址,但是并没有FPGA的MAC地址和IP地址,这是因为开发板和PC还没有进行通信。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第18张图片

图18 查看arp列表

  然后通过下载器给FPGA下载代码,然后按下开发板的按键,FPGA向PC发出ARP请求。通过ILA抓取gmii_tx_en或者gmii_rx_dv的结果如下所示,检测到按键信号有效后,发出arp请求数据帧,目的MAC地址为广播地址。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第19张图片

图19 FPGA发出ARP请求

  当PC端接收到FPGA发送的ARP请求数据报后,PC端回复FPGA的ARP应答数据报(下图天蓝色信号)。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第20张图片

图20 PC端发出ARP应答数据报

  将应答数据报放大后,如下图所示,此时目的MAC地址就是开发板的目的MAC地址,因为PC已经知道FPGA的MAC地址了。arp_rx_done信号拉高表示该数据报接收完成且CRC校验通过。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第21张图片

图21 PC端发出的ARP应答数据报

  此时在命令提示符中输入arp -a,就可以查到开发板的MAC地址和IP地址了,如下图所示。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第22张图片

图22 命令提示符查询arp连接

  此时我们还可以打开wireshark软件抓取以太网数据报,打开wireshark软件,如下图所示,点击以太网。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第23张图片

图23 打开wireshark软件

  然后按下开发板的按键,wireshark软件就能够抓到FPGA发送给PC的ARP请求数据报和PC回复FPGA的ARP应答包,如下图所示。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第24张图片

图24 wireshark软件抓取ARP数据报

  1是FPGA发送的ARP请求数据报,双击该数据报,得到如下图所示,可以看到下图wireshark抓到的数据与图20中ILA抓取的数据是一致的。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第25张图片

图25 wireshark抓取的ARP请求包

  2是PC回复FPGA的ARP应答数据报,双击打开该数据报,如下图所示,报文类型为2,证明是ARP应答包,该数据报发送的数据与图21中ILA抓取的数据一致。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第26张图片

图26 wireshark抓取的ARP应答包

  上述对FPGA发送ARP请求,PC回复ARP应答进行了验证,后面需要对PC端发出ARP请求,FPGA是否能够回复PC端进行验证。首先通过arp -d指令,删除PC端arp连接,然后再使用arp -a查询arp列表,得知开发板的MAC地址和IP地址已经被PC清除。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第27张图片

图27 清除PC端arp列表

  然后开发板重新下载程序,ILA准备抓取数据,wireshark清除抓取的数据后重新抓取数据,然后再命令提示符窗口输入ping 192.168.1.10,因为开发板没有实现ICMP协议,所以并不能响应,但是该指令PC端也会向开发板发送ARP请求指令,所以本文可以用该指令进行测试。

  当该指令运行结束后,输入arp -a指令,就可以查看到FPGA的MAC地址和IP地址已经存在PC端的ARP列表里了,验证成功。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第28张图片

图28 PC端发起ARP请求

  接下来查看ILA抓取的数据报吧,ILA深度设置的比较大,并且可以触发32次,所以基本上不会漏掉数据报。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第29张图片

图29 ILA抓取的数据报

  如下图所示,ILA抓取PC端发送给FPGA的ARP请求数据报后,FPGA马上向PC端回复了ARP应答数据报。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第30张图片

图30 ILA抓取的ARP数据报

  下图为ILA抓取的ARP请求数据报,PC端发出的ARP请求数据报的以太网帧头的目的MAC地址为广播地址,但是再ARP数据段中,发送的目的MAC地址全为0,并不是广播地址,这也许是因为接收端不会解析这部分数据,所以就没有做处理吧。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第31张图片

图31 ILA抓取PC端发出的ARP请求包

  同时wireshark抓取的数据报问如下所示,不知道为什么,我的wireshark每次都会显示发送了2次的ARP请求,但是ILA和FPGA都只能收到一个数据报。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第32张图片

图32 wireshark抓取数据报

  打开ARP请求数据报,如下图所示,两个目的MAC地址的数值也不一致,应该跟猜想一样吧。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第33张图片

图33 wireshark抓取ARP请求数据报

  然后查看FPGA发送的ARP应答数据报,ILA抓取的结果如下图所示,紫红色信号就是以太网发送信号。目的MAC地址就是图33中PC端的MAC地址,目的IP也是电脑的IP地址。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第34张图片

图34 ILA抓取FPGA发送的ARP应答数据报

  对应的wireshark抓取该包的数据如下图所示,与上图保持一致。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第35张图片

图35 wireshark抓取ARP应答数据报

  从图34可知发送的CRC校验码为32’hcdc83243,使用CRC计算器对图35的数据进行计算,如下图所示,与模块计算一致,没有错误。

【实战干货】FPGA实现ARP协议,细节全解析!(包含源工程文件)_第36张图片

图36 验证CRC计算

  至此,本文的验证就结束了,主要通过FPGA解析ARP数据报,然后向PC端发送ARP数据报,也简单介绍了wireshark软件的使用,利用该软件,结合ILA再调试时可以事半功倍。后续的ICMP和UDP其实都离不开ARP协议,学会设计ARP协议后,ICMP和UDP的实现也就比较简单了。

  在接收数据时充分使用移位寄存器还可以将代码简化,有些地方接收数据根本不需要进行移位操作,在计数器某个状态时,直接对移位寄存器中某段数据保存即可,也可以简化判断电路。

  本工程可以在公众号后台回复“基于FPGA的ARP实现”(不包含引号)获取。

你可能感兴趣的:(FPGA,以太网,fpga开发)