基于FPGA的实用UDP设计(包含源工程文件)

这篇具有很好参考价值的文章主要介绍了基于FPGA的实用UDP设计(包含源工程文件)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、概述

  前文对ARP协议、ICMP协议、UDP协议分别做了讲解,并且通过FPGA实现了三种协议,最终实现的UDP协议工程中也包含了ARP和ICMP协议,对应的总体框架如图所示。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图1 基于FPGA的UDP协议实现

  尽管上述模块包含3种协议的接收和发送,但实际上都是通过一个网口收发数据,所以三部分的接收模块和发送模块均只有一个在工作,其余模块均处于空闲状态,造成资源浪费。

  所以本文将对这部分内容进行重新设计,最终只会有一个接收数据的模块,能够识别协议类型,进行对应协议的数据解析。也只会存在一个发送模块,通过协议类型指示信号确定具体发送哪种协议。当接收到PC的ARP请求时,依旧会向PC端回复ARP应答指令,不需要用户接口进行干预。FPGA接收到回显请求时,也会自动向PC端发出回显应答指令。当接收到PC端的UDP数据报文时,会将拆包后的数据段输出到用户接口,并且将数据的长度一起输出。当用户需要通过UDP发送数据到PC端时,只需要在发送模块处于空闲时,将UDP_tx_en拉高,并且把需要发送数据的字节数传输给以太网模块即可,当UDP_tx_req请求数据信号为高电平时,用户在下个时钟周期将需要发送的数据输入以太网模块即可。可以通过拉高用户接口的ARP_req信号,向目的IP地址发出ARP请求。注意该模块的ARP应答和回显应答是不需要外部信号干预的,在模块内部自动完成。

  这种设计方式会节省4个CRC校验模块,以及很多计数器和移位寄存器资源,但是控制会稍微复杂一点,主要是涉及的信号比较多,但是最后也是实现了,能够达到上述要求,后续使用比较方便。

  当UDP_rx_data_vld有效时,表示接收到UDP数据,并且此时可以根据数据长度信号得知这帧数据的长度。需要发送数据时,也只需要把数据个数输入,将发送使能拉高一个时钟周期,然后等待数据请求信号拉高,之后输入需要发送的数据即可。ARP应答和回显请求用户都不需要关心,所以比较方便。

2、工程设计

  本文依旧使用UDP回环进行测试,实现功能与前文一致,主要是对以太网接收和发送模块进行修改,顶层模块连线图如下所示。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图2 工程顶层模块

  工程的顶层模块连接相比图1的工程会简单很多,eth模块可以实现ARP、ICMP、UDP协议的接收和发送,比图1中ARP、ICMP、UDP三个模块实现的功能更复杂,但是开放给用户的接口更加简单。

  顶层模块就是将按键消抖模块key、锁相环模块、rgmii转gmii模块、UDP数据暂存FIFO模块、以太网接收发送模块eth的端口进行连线,所以此处就不把其代码贴出来了,需要了解的可以打开工程自行查看。

  注意按键模块的输出直接接在ARP_req模块上,按下该按键后,FPGA向目的IP地址发送ARP请求数据报文,获取目的IP地址对应的目的MAC地址,然后作为以太网发送模块的目的MAC地址和目的IP地址。

  这里的FIFO用来暂存UDP接收的数据,作为UDP发送模块的数据来源,从而实现UDP回环。

3、以太网模块eth

  该模块的设计稍显复杂,对应的框图如下所示,包含以太网接收模块eth_rx、以太网发送模块eth_tx、以太网控制模块eth_ctrl、ICMP回显数据暂存FIFO、两个CRC校验模块。

  其中以太网接收模块eth_rx,能够接收ARP、ICMP、UDP的数据报文,将接收到的报文类型输出,如果接收的报文是ARP报文,需要将源MAC地址、源IP地址输出。如果接收的报文是ICMP报文,需要把报文的类型、代码、标识符、序列号以及数据段输出。如果接收的报文是UDP报文,则需要把接收的数据输出到控制模块。由于此处ICMP和UDP都有数据段,但是同一时间又只有会存在一种报文,所以共用同一个数据信号iUDP_rx_data,该数据的具体含义根据此时接收报文的类型eth_rx_type的值确定。

  如果接收的报文是ICMP回显请求报文,则将接收的数据存入ICMP FIFO中,便于发送回显应答报文时取用。

  以太网发送模块eth_tx,当接收到发送数据使能信号时,根据发送协议类型开始产生对应数据报文。ICMP和UDP均需要从外部取数据,同一时刻只可能发送一种报文,所以也可以共用同一个请求数据输入信号和数据输入信号。这里还需要考虑一个问题,在前文讲解ARP协议时,讲到过帧间隙,也就是两帧数据之间的最小间隔,发送96位(12字节)数据的时间,为了便于用户使用,所以设计时应该考虑帧间隙问题。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图3 以太网顶层模块

  以太网控制模块eth_ctrl:当接收到ARP请求数据报文时,向PC端回复ARP应答数据报文。当用户端口的ARP请求(arq_req)信号拉高时,向PC端发出ARP请求数据报文。接收到ICMP的回显请求数据报文时,将数据段存入ICMP FIFO中,然后向PC端发送回显应答数据报文。如果接收的ICMP指令不是回显请求,目前不做处理,但是会接收该数据报文,不会把数据段存入FIFO中,后续如果需要处理其他ICMP协议,可以添加对应代码即可。当接收到UDP报文时,将数据段输出到用户接口,当用户需要发送UDP报文时,向以太网发送模块产生发送使能信号。该模块还具有仲裁功能,当同时需要发送ARP、ICMP、UDP报文时,依次发送,因为ARP和ICMP报文一般比较短,所以先发送。

  顶层模块仅对6个子模块端口连线,所以代码此处就不给出,此处给出顶层模块的两个TestBench文件,一个用于ARP和UDP的仿真,另一个用于ICMP协议的仿真,因为ICMP只对回显请求进行应答,用户接口并没有引出ICMP协议,所以无法开发板无法主动向PC端发出回显请求指令,这也是这个设计的缺陷吧。但是开发板一般不需要发出回显请求,所以对使用不会有影响。

  用于对ARP和UDP进行仿真的TestBench文件:

`timescale 1 ns/1 ns
module test();
    localparam	CYCLE		=   8                           ;//系统时钟周期,单位ns,默认8ns;
    localparam	RST_TIME	=   10                          ;//系统复位持续时间,默认10个系统时钟周期;
    localparam	STOP_TIME	=   1000                        ;//仿真运行时间,复位完成后运行1000个系统时钟后停止;
    localparam  BOARD_MAC   =   48'h00_11_22_33_44_55       ;
    localparam  BOARD_IP    =   {8'd192,8'd168,8'd1,8'd10}  ;
    localparam  BOARD_PORT  =   16'd1234                    ;//开发板的UDP端口号;
    localparam  DES_PORT    =   16'd5678                    ;//UDP目的端口号;
    localparam  DES_MAC     =   48'h23_45_67_89_0a_bc       ;
    localparam  DES_IP      =   {8'd192,8'd168,8'd1,8'd23}  ;
    localparam  IP_TYPE     =   16'h0800                    ;//16'h0800表示IP协议;
    localparam  ARP_TYPE    =   16'h0806                    ;//16'h0806表示ARP协议;

    reg			                clk                         ;//系统时钟,默认100MHz;
    reg			                rst_n                       ;//系统复位,默认低电平有效;

    wire         [7 : 0]        gmii_rxd                    ;
    wire                        gmii_rx_dv                  ;
    wire                        gmii_tx_en                  ;
    wire        [7 : 0]         gmii_txd                    ;
    wire                        udp_rx_done                 ;
    wire        [15 : 0]        udp_rx_byte_num             ;
    wire        [7 : 0]         udp_rx_data                 ;
    wire        [15 : 0]        udp_rx_data_num             ;
    wire                        udp_rx_data_vld             ;
    wire                        tx_rdy                      ;

    wire                        udp_tx_req                  ;
    reg         [7 : 0]         udp_tx_data                 ;
    reg         [15 : 0]        udp_tx_data_num             ;
    reg                         arp_req                     ;
    reg                         udp_tx_en                   ;

    assign gmii_rx_dv = gmii_tx_en;
    assign gmii_rxd = gmii_txd;

    eth #(
        .BOARD_MAC      ( BOARD_MAC     ),
        .BOARD_IP       ( BOARD_IP      ),
        .DES_MAC        ( BOARD_MAC     ),//仿真的时候目的地址也使用开发板地址,不然接收模块不会接收数据;
        .DES_IP         ( BOARD_IP      ),//仿真的时候目的地址也使用开发板地址,不然接收模块不会接收数据;
        .BOARD_PORT     ( BOARD_PORT    ),
        .DES_PORT       ( DES_PORT      ),
        .IP_TYPE        ( IP_TYPE       ),
        .ARP_TYPE       ( ARP_TYPE      )
    )
    u_eth (
        .rst_n              ( rst_n             ),
        .gmii_rx_clk        ( clk               ),
        .gmii_rx_dv         ( gmii_rx_dv        ),
        .gmii_rxd           ( gmii_rxd          ),
        .gmii_tx_clk        ( clk               ),
        .arp_req            ( arp_req           ),
        .udp_tx_en          ( udp_tx_en         ),
        .udp_tx_data        ( udp_tx_data       ),
        .udp_tx_data_num    ( udp_tx_data_num   ),
        .gmii_tx_en         ( gmii_tx_en        ),
        .gmii_txd           ( gmii_txd          ),
        .tx_rdy             ( tx_rdy            ),
        .udp_tx_req         ( udp_tx_req        ),
        .udp_rx_done        ( udp_rx_done       ),
        .udp_rx_data        ( udp_rx_data       ),
        .udp_rx_data_num    ( udp_rx_data_num   ),
        .udp_rx_data_vld    ( udp_rx_data_vld   )
    );
    
    //生成周期为CYCLE数值的系统时钟;
    initial begin
        clk = 0;
        forever #(CYCLE/2) clk = ~clk;
    end

    //生成复位信号;
    initial begin
        udp_tx_en <= 0; udp_tx_data_num <= 19;udp_tx_data <= 0;arp_req <= 0;
        rst_n <= 1;
        #2;
        rst_n <= 0;//开始时复位10个时钟;
        #(RST_TIME*CYCLE);
        rst_n <= 1;
        #(20*CYCLE);
        repeat(1)begin
            @(posedge clk);
            arp_req <= 1'b1;
            @(posedge clk);
            arp_req <= 1'b0;
            repeat(10)@(posedge clk);
            arp_req <= 1'b1;
            @(posedge clk);
            arp_req <= 1'b0;
        end
        @(posedge tx_rdy);
        repeat(10)@(posedge clk);
        repeat(7)begin
            udp_tx_en <= 1'b1;
            udp_tx_data_num <= {$random} % 64;//只产生64以内随机数,便于测试,不把数据报发的太长了;
            @(posedge clk);
            udp_tx_en <= 1'b0;
            @(posedge tx_rdy);
            repeat(30)@(posedge clk);
        end
        #(20*CYCLE);
        $stop;//停止仿真;
    end

    always@(posedge clk)begin
        if(udp_tx_req)begin//产生0~255随机数作为测试;
            udp_tx_data <= {$random} % 256;
        end
    end

endmodule

  用于对ICMP仿真的TestBench文件:

`timescale 1 ns/1 ns
module test();
    localparam	CYCLE		=   8                           ;//系统时钟周期,单位ns,默认8ns;
    localparam	RST_TIME	=   10                          ;//系统复位持续时间,默认10个系统时钟周期;
    localparam	STOP_TIME	=   1000                        ;//仿真运行时间,复位完成后运行1000个系统时钟后停止;
    localparam  BOARD_MAC   =   48'h00_11_22_33_44_55       ;
    localparam  BOARD_IP    =   {8'd192,8'd168,8'd1,8'd10}  ;
    localparam  BOARD_PORT  =   16'd1234                    ;//开发板的UDP端口号;
    localparam  DES_PORT    =   16'd5678                    ;//UDP目的端口号;
    localparam  DES_MAC     =   48'h23_45_67_89_0a_bc       ;
    localparam  DES_IP      =   {8'd192,8'd168,8'd1,8'd23}  ;
    localparam  IP_TYPE     =   16'h0800                    ;//16'h0800表示IP协议;
    localparam  ARP_TYPE    =   16'h0806                    ;//16'h0806表示ARP协议;

    reg			                clk                         ;//系统时钟,默认100MHz;
    reg			                rst_n                       ;//系统复位,默认低电平有效;

    reg          [7 : 0]        gmii_rxd                    ;
    reg                         gmii_rx_dv                  ;
    wire                        gmii_tx_en                  ;
    wire        [7 : 0]         gmii_txd                    ;
    wire                        udp_rx_done                 ;
    wire        [15 : 0]        udp_rx_byte_num             ;
    wire        [7 : 0]         udp_rx_data                 ;
    wire        [15 : 0]        udp_rx_data_num             ;
    wire                        udp_rx_data_vld             ;
    wire                        tx_rdy                      ;

    wire                        udp_tx_req                  ;
    reg         [7 : 0]         udp_tx_data                 ;
    wire        [15 : 0]        udp_tx_data_num             ;
    wire                        udp_tx_en                   ;
    reg         [7 : 0]         rx_data [255 : 0]           ;//申请256个数据的存储器

    assign udp_tx_data_num = udp_rx_data_num;
    assign udp_tx_en = udp_rx_done;

    eth #(
        .BOARD_MAC      ( BOARD_MAC     ),
        .BOARD_IP       ( BOARD_IP      ),
        .DES_MAC        ( BOARD_MAC     ),//仿真的时候目的地址也使用开发板地址,不然接收模块不会接收数据;
        .DES_IP         ( BOARD_IP      ),//仿真的时候目的地址也使用开发板地址,不然接收模块不会接收数据;
        .BOARD_PORT     ( BOARD_PORT    ),
        .DES_PORT       ( DES_PORT      ),
        .IP_TYPE        ( IP_TYPE       ),
        .ARP_TYPE       ( ARP_TYPE      )
    )
    u_eth (
        .rst_n              ( rst_n             ),
        .gmii_rx_clk        ( clk               ),
        .gmii_rx_dv         ( gmii_rx_dv        ),
        .gmii_rxd           ( gmii_rxd          ),
        .gmii_tx_clk        ( clk               ),
        .arp_req            ( 1'b0              ),
        .udp_tx_en          ( udp_tx_en         ),
        .udp_tx_data        ( udp_tx_data       ),
        .udp_tx_data_num    ( udp_tx_data_num   ),
        .gmii_tx_en         ( gmii_tx_en        ),
        .gmii_txd           ( gmii_txd          ),
        .tx_rdy             ( tx_rdy            ),
        .udp_tx_req         ( udp_tx_req        ),
        .udp_rx_done        ( udp_rx_done       ),
        .udp_rx_data        ( udp_rx_data       ),
        .udp_rx_data_num    ( udp_rx_data_num   ),
        .udp_rx_data_vld    ( udp_rx_data_vld   )
    );

    reg                 crc_clr         ;
    reg                 gmii_crc_vld    ;
    reg     [7 : 0]     gmii_rxd_r      ;
    reg                 gmii_rx_dv_r    ;
    reg                 crc_data_vld    ;
    reg     [9 : 0]     i               ;
    reg     [15 : 0]    num             ;
    wire    [31 : 0]    crc_out         ;

    //生成周期为CYCLE数值的系统时钟;
    initial begin
        clk = 0;
        forever #(CYCLE/2) clk = ~clk;
    end

    //生成复位信号;
    initial begin
        num <= 0;
        crc_clr <= 0;
        gmii_rxd <= 0;
        gmii_rx_dv <= 0;
        gmii_rxd_r <= 0;
        gmii_rx_dv_r <= 0;
        gmii_crc_vld <= 1'b0;
        for(i = 0 ; i < 256 ; i = i + 1)begin
            rx_data[i] <= {$random} % 256;//初始化存储体;
            #1;
        end
        rst_n <= 1;
        #2;
        rst_n <= 0;//开始时复位10个时钟;
        repeat(RST_TIME) @(posedge clk);
        rst_n <= 1;
        repeat(20) @(posedge clk);
        repeat(4)begin//发送2帧数据;
            gmii_tx_test({$random} % 64 + 18);
            #1;
            gmii_crc_vld <= 1'b1;
            gmii_rxd_r <= crc_out[7 : 0];
            @(posedge clk);
            gmii_rxd_r <= crc_out[15 : 8];
            @(posedge clk);
            gmii_rxd_r <= crc_out[23 : 16];
            @(posedge clk);
            gmii_rxd_r <= crc_out[31 : 24];
            @(posedge clk);
            gmii_crc_vld <= 1'b0;
            crc_clr <= 1'b1;
            @(posedge clk);
            crc_clr <= 1'b0;
            repeat(50) @(posedge clk);
        end
        repeat(20) @(posedge clk);
        $stop;//停止仿真;
    end
    
    task gmii_tx_test(
        input [15 : 0]  data_num   //需要把多少个存储体中的数据进行发送,取值范围[18,255];
    );
        reg [31 : 0] ip_check;
        reg [15 : 0] total_num;
        reg [31 : 0] icmp_check;
        begin
            total_num <= data_num + 28;
            #1;
            icmp_check <=  16'h1 + 16'h8;//ICMP首部相加;
            ip_check <= DES_IP[15:0] + BOARD_IP[15:0] + DES_IP[31:16] + BOARD_IP[31:16] + 16'h4500 + total_num + 16'h4000 + num + 16'h8001;
            if(~data_num[0])begin//ICMP数据段个数为偶数;
                for(i=0 ; 2*i < data_num ; i= i+1)begin
                    #1;//计算ICMP数据段的校验和。
                    icmp_check <= icmp_check + {rx_data[i][7:0],rx_data[i+1][7:0]};
                end
            end
            else begin//ICMP数据段个数为奇数;
                for(i=0 ; 2*i < data_num+1 ; i = i+1)begin
                    //计算ICMP数据段的校验和。
                    if(2*i + 1 == data_num)
                        icmp_check <= icmp_check + {rx_data[i][7:0]};
                    else
                        icmp_check <= icmp_check + {rx_data[i][7:0],rx_data[i+1][7:0]};
                end
            end
            crc_data_vld <= 1'b0;
            @(posedge clk);
            repeat(7)begin//发送前导码7个8'H55;
                gmii_rxd_r <= 8'h55;
                gmii_rx_dv_r <= 1'b1;
                @(posedge clk);
            end
            gmii_rxd_r <= 8'hd5;//发送SFD,一个字节的8'hd5;
            @(posedge clk);
            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];
                @(posedge clk);
            end
            for(i=0 ; i<6 ; i=i+1)begin//发送6个字节的源MAC地址;
                gmii_rxd_r <= DES_MAC[47-8*i -: 8];
                @(posedge clk);
            end
            for(i=0 ; i<2 ; i=i+1)begin//发送2个字节的以太网类型;
                gmii_rxd_r <= IP_TYPE[15-8*i -: 8];
                @(posedge clk);
            end
            //发送IP帧头数据;
            gmii_rxd_r <= 8'H45;
            @(posedge clk);
            gmii_rxd_r <= 8'd00;
            ip_check <= ip_check[15 : 0] + ip_check[31:16];
            icmp_check <= icmp_check[15 : 0] + icmp_check[31:16];
            @(posedge clk);
            gmii_rxd_r <= total_num[15:8];
            ip_check <= ip_check[15 : 0] + ip_check[31:16];
            icmp_check <= icmp_check[15 : 0] + icmp_check[31:16];
            @(posedge clk);
            gmii_rxd_r <= total_num[7:0];
            ip_check[15 : 0] <= ~ip_check[15 : 0];
            icmp_check <= ~icmp_check[15 : 0];
            @(posedge clk);
            gmii_rxd_r <= num[15:8];
            @(posedge clk);
            gmii_rxd_r <= num[7:0];
            @(posedge clk);
            gmii_rxd_r <= 8'h40;
            @(posedge clk);
            gmii_rxd_r <= 8'h00;
            @(posedge clk);
            gmii_rxd_r <= 8'h80;
            @(posedge clk);
            gmii_rxd_r <= 8'h01;
            @(posedge clk);
            gmii_rxd_r <= ip_check[15:8];
            @(posedge clk);
            gmii_rxd_r <= ip_check[7:0];
            @(posedge clk);
            for(i=0 ; i<4 ; i=i+1)begin//发送6个字节的源IP地址;
                gmii_rxd_r <= DES_IP[31-8*i -: 8];
                @(posedge clk);
            end
            for(i=0 ; i<4 ; i=i+1)begin//发送4个字节的目的IP地址;
                gmii_rxd_r <= BOARD_IP[31-8*i -: 8];
                @(posedge clk);
            end
            //发送ICMP帧头及数据包;
            gmii_rxd_r <= 8'h08;//发送回显请求。
            @(posedge clk);
            gmii_rxd_r <= 8'h00;
            @(posedge clk);
            gmii_rxd_r <= icmp_check[31:16];
            @(posedge clk);
            gmii_rxd_r <= icmp_check[15:0];
            @(posedge clk);
            gmii_rxd_r <= 8'h00;
            @(posedge clk);
            gmii_rxd_r <= 8'h01;
            @(posedge clk);
            gmii_rxd_r <= 8'h00;
            @(posedge clk);
            gmii_rxd_r <= 8'h08;
            @(posedge clk);
            for(i=0 ; i<data_num ; i=i+1)begin
                gmii_rxd_r <= rx_data[i];
                @(posedge clk);
            end
            crc_data_vld <= 1'b0;
            gmii_rx_dv_r <= 1'b0;
            num = num + 1;
        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

4、以太网接收模块

  首先查看ARP、ICMP、UDP协议的数据帧格式,如下图所示:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图4 ARP数据帧格式

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图5 ICMP数据帧格式

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图6 UDP数据帧格式

  通过对比上面三图可知,前导码、帧起始符、以太网帧头、CRC校验码均一致,所以这几部分还是可以根据前文一样设计,不做改变。当以太网帧头的类型为16’h0806时表示ARP,等于16’h0800时表示报文是IP协议。此时还是可以通过状态机进行实现,对应的状态转换图如下所示:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图7 以太网接收模块状态转换图

  状态机初始位于空闲状态IDLE,当移位寄存器检测到前导码和帧起始符之后start信号为高电平,此时状态机跳转到接收以太网帧头状态ETH_HEAD。在ETH_HEAD状态下,如果接收的报文目的MAC地址不是FPGA的MAC地址,也不是广播地址,直接丢弃该报文,error_flag拉高,状态机跳转到RX_END状态。否则继续接收,如果是ARP报文,则跳转到接收ARP数据的状态ARP_DATA。如果接收的报文是IP报文,则跳转到接收IP首部的状态IP_HEAD,如果既不是ARP报文也不是IP报文,则error_flag拉高,直接丢弃该报文,状态机跳转到RX_END,等待这帧报文传输完毕,之后跳转到空闲状态,对一帧报文进行检测。

  在接收ARP数据过程中,需要判断目的IP地址与开发板IP地址是否相同,如果不同要把报文丢弃。当ARP数据段接收完毕后,跳转到接收CRC校验的状态,如果接收的CRC数据与CRC校验模块的数据相同,那么接收的数据无误,把接收完成信号拉高,并且把接收的源MAC地址,源IP地址,ARP数据报文类型输出。状态机跳转到RX_END状态,然后回到空闲状态。

  在接收IP首部数据过程中,需要检测报文类型,如果是ICMP或者UDP报文,则继续接收,否则将error_flag拉高,丢弃该报文。

  当IP首部接收完成后,跳转到接收ICMP或者UDP的首部数据状态IUDP_HEAD,因为ICMP和UDP首部的长度是一样的,所以共用同一个状态,根据IP首部接收到的数据报文类型来判断接收的是ICMP首部数据还是UDP首部数据。

  如果是ICMP首部,需要把代码、类型、标识符、序列号输出,如果是UDP首部,只需要把目的端口地址与开发板地址进行对比即可,如果相同则接收后面的数据段内容,否则丢弃报文。当首部接收完毕后,则跳转到接收数据段内容的状态IUDP_DATA。

  在接收数据段时,如果接收的ICMP数据,需要计算出数据段的校验和。由于UDP协议可以不发校验和,则接收UDP数据时,需要计算校验和。数据的长度根据IP首部的数据报文总长度计算得到。UDP和ICMP的数据输出使用同一个信号iUDP_rx_data,具体传输的数据类型根据报文的类型确定,在以太网控制模块进一步进行存储和输出。

  关键代码如下所示,代码比较多,此处只贴重要内容,完整的查看工程。

    localparam      IDLE        =   8'b0000_0001                ;//初始状态,检测前导码。
    localparam      ETH_HEAD    =   8'b0000_0010                ;//接收以太网帧头。
    localparam      IP_HEAD     =   8'b0000_0100                ;//接收IP帧头。
    localparam      IUDP_HEAD   =   8'b0000_1000                ;//接收ICMP或者UDP帧头。
    localparam      IUDP_DATA   =   8'b0001_0000                ;//接收ICMP或者UDP数据。
    localparam      ARP_DATA    =   8'b0010_0000                ;//接收ARP数据。
    localparam      CRC         =   8'b0100_0000                ;//接收CRC校验码。
    localparam      RX_END      =   8'b1000_0000                ;//接收一帧数据结束。
    //以太网类型定义
    localparam      IP_TPYE     =   16'h0800                    ;//以太网帧类型 IP。
    localparam      ARP_TPYE    =   16'h0806                    ;//以太网帧类型 ARP。
    localparam      ICMP_TYPE   =   8'd01                       ;//ICMP协议类型。
    localparam      UDP_TYPE    =   8'd17                       ;//UDP协议类型。
    
    reg                             start                       ;//检测到前导码和SFD信号后的开始接收数据信号。
    reg                             error_flag                  ;//检测到接收数据包不是发给该开发板或者接收到的不是ARP、ICMP、UDP数据包时拉高。
    reg             [7 : 0]	        state_n                     ;//状态机次态。
    reg             [7 : 0]	        state_c                     ;//状态机现态。
    reg             [15 : 0]        cnt                         ;//计数器,辅助状态机的跳转。
    reg             [15 : 0]        cnt_num                     ;//计数器的状态机每个状态下接收数据的个数。
    reg             [5 : 0]         ip_head_byte_num            ;//IP首部数据的字节数。
    reg             [15 : 0]        ip_total_length             ;//IP报文总长度。
    reg             [15 : 0]        des_ip                      ;//目的IP地址。
    reg             [7 : 0]         gmii_rxd_r      [6 : 0]     ;//接收信号的移位寄存器;
    reg             [6 : 0]         gmii_rx_dv_r                ;
    reg             [23 : 0]        des_crc                     ;//接收的CRC校验数值;
    reg             [47 : 0]        des_mac                     ;
    reg             [15 : 0]        opcode                      ;
    reg             [47 : 0]        src_mac_t                   ;
    reg             [31 : 0]        src_ip_t                    ;
    reg             [31 : 0]        reply_checksum_add          ;

    wire       		                add_cnt                     ;
    wire       		                end_cnt                     ;
    
    //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//接收完以太网帧头数据,且没有出现错误。
                    if(eth_rx_type == 2'd1)//如果该数据报是ARP类型,则跳转到ARP接收数据状态;
                        state_n = ARP_DATA;
                    else//否则跳转到接收IP报头的状态;
                        state_n = IP_HEAD;
                end
                else begin
                    state_n = state_c;
                end
            end
            IP_HEAD:begin
                if(error_flag)begin//在接收IP帧头过程中检测到错误。
                    state_n = RX_END;
                end
                else if(end_cnt)begin//接收完以IP帧头数据,且没有出现错误。
                    state_n = IUDP_HEAD;//跳转到接收ICMP或UDP报头状态;
                end
                else begin
                    state_n = state_c;
                end
            end
            IUDP_HEAD:begin
                if(end_cnt)begin//接收完以ICMP帧头或UDP帧头数据,则继续接收ICMP数据或UDP数据。
                    state_n = IUDP_DATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            IUDP_DATA:begin
                if(end_cnt)begin//接收完ICMP数据或UDP数据,跳转到CRC校验状态。
                    state_n = CRC;
                end
                else begin
                    state_n = state_c;
                end
            end
            ARP_DATA:begin
                if(error_flag)begin//接收数据出现错误。
                    state_n = RX_END;
                end
                else if(end_cnt)begin//接收完所有数据。
                    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) && gmii_rx_dv_r[0];
    assign end_cnt = add_cnt && cnt == cnt_num - 1;

    //状态机在不同状态,需要接收不同的数据个数,在接收以太网帧头时,需要接收14byte数据。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为20;
            cnt_num <= 16'd20;
        end
        else begin
            case(state_c)
                ETH_HEAD : cnt_num <= 16'd14;//以太网帧头长度位14字节。
                IP_HEAD  : cnt_num <= ip_head_byte_num;//IP帧头为20字节数据。
                IUDP_HEAD : cnt_num <= 16'd8;//UDP和ICMP帧头为8字节数据。
                IUDP_DATA : cnt_num <= iudp_data_length;//UDP数据段需要根据数据长度进行变化。
                ARP_DATA  : cnt_num <= 16'd46;//ARP数据段46字节。
                CRC      : cnt_num <= 16'd4;//CRC校验为4字节数据。
                default: cnt_num <= 16'd20;
            endcase
        end
    end

    //接收目的MAC地址,需要判断这个包是不是发给开发板的,目的MAC地址是不是开发板的MAC地址或广播地址。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            des_mac <= 48'd0;
        end
        else if((state_c == ETH_HEAD) && add_cnt && cnt < 16'd6)begin
            des_mac <= {des_mac[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 if(add_cnt)begin
            case(state_c)
                ETH_HEAD : begin
                    if(cnt == 6)//判断接收的数据是不是发送给开发板或者广播数据。
                        error_flag <= ((des_mac != BOARD_MAC) && (des_mac != 48'HFF_FF_FF_FF_FF_FF));
                    else if(cnt == 12)//接收的数据报不是IP协议且不是ARP协议。
                        error_flag <= ({gmii_rxd_r[0],gmii_rxd} != IP_TPYE) && ({gmii_rxd_r[0],gmii_rxd} != ARP_TPYE);
                end
                IP_HEAD : begin
                    if(cnt == 9)//如果当前接收的数据不是UDP协议,且不是ICMP协议;
                        error_flag <= (gmii_rxd_r[0] != UDP_TYPE) && (gmii_rxd_r[0] != ICMP_TYPE);
                    else if(cnt == 16'd18)//判断目的IP地址是否为开发板的IP地址。
                        error_flag <= ({des_ip,gmii_rxd_r[0],gmii_rxd} != BOARD_IP);
                end
                ARP_DATA : begin
                    if(cnt == 27)begin//判断接收的目的IP地址是否正确,操作码是否为ARP的请求或应答指令。
                        error_flag <= ((opcode != 16'd1) && (opcode != 16'd2)) || ({des_ip,gmii_rxd_r[1],gmii_rxd_r[0]} != BOARD_IP);
                    end
                end
                IUDP_DATA : begin
                    if((cnt == 3) && (eth_rx_type == 2'd3))begin//UDP的目的端口地址不等于开发板的目的端口地址。
                        error_flag <= ({gmii_rxd_r[1],gmii_rxd_r[0]} != BOARD_PORT);
                    end
                end
                default: error_flag <= 1'b0;
            endcase
        end
        else begin
            error_flag <= 1'b0;
        end
    end

    //根据接收的数据判断该数据报的类型。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            eth_rx_type <= 2'd0;
        end//接收的协议是ARP协议;
        else if(state_c == ETH_HEAD && add_cnt && cnt == 12)begin
            if({gmii_rxd_r[0],gmii_rxd} == ARP_TPYE)begin
                eth_rx_type <= 1;
            end
            else begin
                eth_rx_type <= 0;
            end
        end
        else if(state_c == IP_HEAD && add_cnt && cnt == 9)begin
            if(gmii_rxd_r[0] == UDP_TYPE)//接收的数据包是UDP协议;
                eth_rx_type <= 3;
            else if(gmii_rxd_r[0] == ICMP_TYPE)//接收的协议是ICMP协议;
                eth_rx_type <= 2;
        end
    end
    
    //接收IP首部和ARP数据段的数据。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            ip_head_byte_num <= 6'd20;
            ip_total_length <= 16'd28;
            des_ip <= 16'd0;
            iudp_data_length <= 16'd0;
            opcode <= 16'd0;//ARP的OP编码。
            src_mac_t <= 48'd0;//ARP传输的源MAC地址;
            src_ip_t <= 32'd0;//ARP传输的源IP地址;
        end
        else if(state_c == IP_HEAD && add_cnt)begin
            case(cnt)
                16'd0 : ip_head_byte_num <= {gmii_rxd_r[0][3:0],2'd0};//接收IP首部的字节个数。
                16'd3 : ip_total_length <= {gmii_rxd_r[1],gmii_rxd_r[0]};//接收IP报文总长度的低八位数据。
                16'd4 : iudp_data_length <= ip_total_length - ip_head_byte_num - 8;//计算UDP报文数据段的长度,UDP帧头为8字节数据。
                16'd17: des_ip <= {gmii_rxd_r[1],gmii_rxd_r[0]};//接收目的IP地址。
                default: ;
            endcase
        end
        else if(state_c == ARP_DATA && add_cnt)begin
            case(cnt)
                16'd7 : opcode <= {gmii_rxd_r[1],gmii_rxd_r[0]};//操作码;
                16'd13 : src_mac_t <= {gmii_rxd_r[5],gmii_rxd_r[4],gmii_rxd_r[3],gmii_rxd_r[2],gmii_rxd_r[1],gmii_rxd_r[0]};//源MAC地址;
                16'd17 : src_ip_t <= {gmii_rxd_r[3],gmii_rxd_r[2],gmii_rxd_r[1],gmii_rxd_r[0]};//源IP地址;
                16'd25 : des_ip <= {gmii_rxd_r[1],gmii_rxd_r[0]};//接收目的IP地址高16位。
                default: ;
            endcase
        end
    end
    
    //接收ICMP首部相关数据,UDP首部数据不需要保存。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            icmp_rx_type <= 8'd0;//ICMP类型;
            icmp_rx_code <= 8'd0;//ICMP代码;
            icmp_rx_id <= 16'd0;//ICMP标识符
            icmp_rx_seq <= 16'd0;//ICMP请求;
        end
        else if(state_c == IUDP_HEAD && add_cnt)begin
            if(eth_rx_type == 2'd2)//如果是ICMP协议。
                case(cnt)
                    16'd0 : icmp_rx_type <= gmii_rxd_r[0];//接收ICMP报文类型。
                    16'd1 : icmp_rx_code <= gmii_rxd_r[0];//接收ICMP报文代码。
                    16'd5 : icmp_rx_id <= {gmii_rxd_r[1],gmii_rxd_r[0]};//接收ICMP的ID。
                    16'd7 : icmp_rx_seq <= {gmii_rxd_r[1],gmii_rxd_r[0]};//接收ICMP报文的序列号。
                    default: ;
                endcase
        end
    end

    //接收ICMP或者UDP的数据段,并输出使能信号。
    always@(posedge clk)begin
        iudp_rx_data <= (state_c == IUDP_DATA) ? gmii_rxd_r[0] : iudp_rx_data;//在接收UDP数据阶段时,接收数据。
        iudp_rx_data_vld <= (state_c == IUDP_DATA);//在接收数据阶段时,将数据输出。
    end
    
    //生产CRC校验相关的数据和控制信号。
    always@(posedge clk)begin
        crc_data <= gmii_rxd_r[0];//将移位寄存器最低位存储的数据作为CRC输入模块的数据。
        crc_clr <= (state_c == IDLE);//当状态机处于空闲状态时,清除CRC校验模块计算。
        crc_en <= (state_c != IDLE) && (state_c != RX_END) && (state_c != CRC);//CRC校验使能信号。
    end

    //接收PC端发送来的CRC数据。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            des_crc <= 24'hff_ff_ff;
        end
        else if(add_cnt && state_c == CRC)begin//先接收的是低位数据;
            des_crc <= {gmii_rxd_r[0],des_crc[23:8]};
        end
    end

    //计算接收到的ICMP数据段校验和。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            reply_checksum_add <= 32'd0;
        end
        else if(state_c == RX_END)begin//累加器清零。
            reply_checksum_add <= 32'd0;
        end
        else if(state_c == IUDP_DATA && add_cnt && eth_rx_type == 2'd2)begin
            if(end_cnt && iudp_data_length[0])begin//如果计数器计数结束且数据个数为奇数个(最低位为1),那么直接将当前数据与累加器相加。
                reply_checksum_add <= reply_checksum_add + {8'd0,gmii_rxd_r[0]};
            end
            else if(cnt[0])//计数器计数到奇数时,将前后两字节数据拼接相加。
                reply_checksum_add <= reply_checksum_add + {gmii_rxd_r[1],gmii_rxd_r[0]};
        end
    end

    //生成相应的输出数据。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            rx_done <= 1'b0;//接收一帧数据完成信号,高电平有效;
            src_mac <= 48'd0;//ARP接收的源MAC地址;
            src_ip <= 32'd0;//ARP接收的源IP地址;
            arp_rx_type <= 1'b0;
            data_checksum <= 32'd0;//ICMP数据段校验和;
        end//如果CRC校验成功,把UDP协议接收完成信号拉高,把接收到UDP数据个数和数据段的校验和输出。
        else if(state_c == CRC && end_cnt && ({gmii_rxd_r[0],des_crc[23:0]} == crc_out))begin//CRC校验无误。
            if(eth_rx_type == 2'd1)begin//如果接收的是ARP协议;
                src_mac <= src_mac_t;//将接收的源MAC地址输出;
                src_ip <= src_ip_t;//将接收的源IP地址输出;
                arp_rx_type <= (opcode == 16'd1) ? 1'b0 : 1'b1;//接收ARP数据报的类型;
            end
            else begin//如果接收的协议是IP协议;
                data_checksum <= (eth_rx_type == 2'd2) ? reply_checksum_add : data_checksum;//如果是ICMP,需要计算数据段的校验和。
            end
            rx_done <= 1'b1;//将接收一帧数据完成信号拉高一个时钟周期;
        end
        else begin
            rx_done <= 1'b0;
        end
    end

  该模块仿真结果如下所示,接收ARP协议:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图8 接收ARP报文

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图9 ARP报文放大

  接收ICMP协议:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图10 接收ICMP报文

  把ICMP的数据段放大,如下图所示:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图11 ICMP报文数据段放大

  接收UDP协议的仿真如下图所示,将接收的数据段输出到,蓝色信号就是输出的数据信号。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图12 ARP报文放大

5、以太网发送模块

  发送模块依旧可以使用状态机嵌套计数器的形式实现,状态机对应的状态转换图如下所示。

  状态机包括初始状态、发送前导码帧起始符状态、发送以太网帧头状态、发送IP帧头状态、发送ICMP或UDP帧头状态、发送ICMP数据或UDP数据状态、发送ARP数据状态、发送CRC校验状态、帧间隙等待状态。总共9个状态,由于ICMP帧头和UDP帧头长度基本一样,且数据段都需要从外部输入数据,所以ICMP和UDP的帧头、数据段共用一个状态。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图13 以太网发送模块状态转换图

  该模块设计还是比较简单的,在合并ARP发送模块、ICMP发送模块、UDP发送模块的基础上,增加了一个帧间隙的状态,一帧数据发完后,等待12字节的时间后回到空闲状态,那么上游模块可以马上调用该模块发送下一帧数据,上游模块不必做什么延时,方便使用。

  注意该模块的数据请求信号需要提前数据输入信号三个时钟周期产生,这是因为请求信号和数据输入信号都是ICMP和UDP复用的,以太网控制模块需要根据发送协议类型,生成对应的请求信号,到ICMP的FIFO或者UDP的用户端口去请求数据输入,输入的数据还要整合成输入数据,所以需要消耗三个时钟周期。

  参考代码的主要部分如下所示:

    localparam      IDLE            =   9'b00000_0001               ;//初始状态,等待开始发送信号;
    localparam      PREAMBLE        =   9'b00000_0010               ;//发送前导码+帧起始界定符;
    localparam      ETH_HEAD        =   9'b00000_0100               ;//发送以太网帧头;
    localparam      IP_HEAD         =   9'b00000_1000               ;//发送IP帧头;
    localparam      IUDP_HEAD       =   9'b00001_0000               ;//发送ICMP或UDP帧头;
    localparam      IUDP_DATA       =   9'b00010_0000               ;//发送ICMP或UDP协议数据;
    localparam      ARP_DATA        =   9'b00100_0000               ;//发送ARP数据段;
    localparam      CRC             =   9'b01000_0000               ;//发送CRC校验值;
    localparam      IFG             =   9'b10000_0000               ;//帧间隙,也就是传输96bit的时间,对应12Byte数据。

    localparam      MIN_DATA_NUM    =   16'd18                      ;//以太网数据最小46个字节,IP首部20个字节+UDP首部8个字节,所以数据至少46-20-8=18个字节。

    reg                                 gmii_tx_en_r                ;//
    reg             [47 : 0]            des_mac_r                   ;//
    reg             [31 : 0]            des_ip_r                    ;
    reg             [8 : 0]	            state_n                     ;
    reg             [8 : 0]	            state_c                     ;
    reg             [15 : 0] 	        cnt                         ;//
    reg             [15 : 0]            cnt_num                     ;//
    reg             [15 : 0]            iudp_tx_byte_num_r          ;
    reg             [31 : 0]            ip_head             [4 : 0] ;
    reg             [31 : 0]            iudp_head           [1 : 0] ;//
    reg             [7 : 0]             arp_data            [17 : 0];
    reg             [15 : 0]            ip_total_num                ;
    reg             [31 : 0]            ip_head_check               ;//IP头部校验码;
    reg             [31 : 0]            icmp_check                  ;//ICMP校验;
    
    wire       		                    add_cnt                     ;
    wire       		                    end_cnt                     ;
    
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            ip_head[0] <= 32'd0;
            ip_head[1] <= {16'd0,16'h4000};//高16位表示标识,每次发送数据后会加1,低16位表示不分片。
            ip_head[2] <= 32'd0;
            ip_head[3] <= 32'd0;
            ip_head[4] <= 32'd0;
            iudp_head[0] <= 32'd0;
            iudp_head[1] <= 32'd0;
            arp_data[0] <= 8'd0;
            arp_data[1] <= 8'd0;
            arp_data[2] <= 8'd0;
            arp_data[3] <= 8'd0;
            arp_data[4] <= 8'd0;
            arp_data[5] <= 8'd0;
            arp_data[6] <= 8'd0;
            arp_data[7] <= 8'd0;
            arp_data[8] <= 8'd0;
            arp_data[9] <= 8'd0;
            arp_data[10] <= 8'd0;
            arp_data[11] <= 8'd0;
            arp_data[12] <= 8'd0;
            arp_data[13] <= 8'd0;
            arp_data[14] <= 8'd0;
            arp_data[15] <= 8'd0;
            arp_data[16] <= 8'd0;
            arp_data[17] <= 8'd0;
            icmp_check  <= 32'd0;
            ip_head_check <= 32'd0;//IP头部校验和;
            des_mac_r <= DES_MAC;
            des_ip_r <= DES_IP;
            iudp_tx_byte_num_r <= MIN_DATA_NUM;
            ip_total_num <= MIN_DATA_NUM + 28;
            eth_tx_type_r <= 0;
        end
        //在状态机空闲状态下,上游发送使能信号时,将目的MAC地址和目的IP的数据进行暂存。
        else if(state_c == IDLE && eth_tx_start)begin
            if(eth_tx_type == 2'd1)begin//如果需要发送ARP报文;
                arp_data[0] <= 8'h00;//ARP硬件类型;
                arp_data[1] <= 8'h01;
                arp_data[2] <= 8'h08;//发送协议类型;
                arp_data[3] <= 8'h00;
                arp_data[4] <= 8'h06;//硬件地址长度;
                arp_data[5] <= 8'h04;//协议地址长度;
                arp_data[6] <= 8'h00;//发送ARP操作类型;
                arp_data[7] <= arp_tx_type ? 8'h02 : 8'h01;
                arp_data[8] <= BOARD_MAC[47 : 40];//源MAC地址;
                arp_data[9] <= BOARD_MAC[39 : 32];
                arp_data[10] <= BOARD_MAC[31 : 24];
                arp_data[11] <= BOARD_MAC[23 : 16];
                arp_data[12] <= BOARD_MAC[15 : 8];
                arp_data[13] <= BOARD_MAC[7 : 0];
                arp_data[14] <= BOARD_IP[31 : 24];//源IP地址;
                arp_data[15] <= BOARD_IP[23 : 16];
                arp_data[16] <= BOARD_IP[15 : 8];
                arp_data[17] <= BOARD_IP[7 : 0];
            end
            else if(eth_tx_type == 2'd2)begin//发送ICMP协议数据报;
                iudp_head[0][31 : 16] <= {icmp_tx_type,icmp_tx_code};//存储ICMP的类型和代码。
                iudp_head[1] <= {icmp_tx_id,icmp_tx_seq};//存储ICMP的标识符和ID;
                ip_head[2] <= {8'h80,8'd1,16'd0};//分别表示生存时间,协议类型,1表示ICMP,6表示TCP,17表示UDP协议,低16位校验和先默认为0;
                iudp_tx_byte_num_r <= iudp_tx_byte_num;//把数据段的长度暂存;
                icmp_check <= icmp_data_checksum;//ICMP的校验和初始值为数据端的校验和。
            end
            else if(eth_tx_type == 2'd3)begin//发送UDP协议数据报;
                iudp_head[0] <= {BOARD_PORT,DES_PORT};//16位源端口和目的端口地址。
                iudp_head[1][31 : 16] <= (((iudp_tx_byte_num >= MIN_DATA_NUM) ? iudp_tx_byte_num : MIN_DATA_NUM) + 8);//计算UDP需要发送报文的长度。
                iudp_head[1][15 : 0] <= 16'd0;//UDP的校验和设置为0。
                ip_head[2] <= {8'h80,8'd17,16'd0};//分别表示生存时间,协议类型,1表示ICMP,6表示TCP,17表示UDP协议,低16位校验和先默认为0;
                iudp_tx_byte_num_r <= iudp_tx_byte_num;//把数据段的长度暂存;
            end
            eth_tx_type_r <= eth_tx_type;//把以太网数据报的类型暂存;
            //如果需要发送的数据多余最小长度要求,则发送的总数居等于需要发送的数据加上UDP和IP帧头数据。
            ip_total_num <= (((iudp_tx_byte_num >= MIN_DATA_NUM) ? iudp_tx_byte_num : MIN_DATA_NUM) + 28);
            if((des_mac != 48'd0) && (des_ip != 32'd0))begin//当接收到目的MAC地址和目的IP地址时更新。
                des_ip_r <= des_ip;
                des_mac_r <= des_mac;
            end
            else begin
                des_ip_r <= DES_IP;
                des_mac_r <= DES_MAC;
            end
        end
        //在发送以太网帧头时,就开始计算IP帧头和ICMP的校验码,并将计算结果存储,便于后续直接发送。
        else if(state_c == ETH_HEAD && add_cnt)begin
            case (cnt)
                16'd0 : begin//初始化需要发送的IP头部数据。
                    ip_head[0] <= {8'h45,8'h00,ip_total_num[15 : 0]};//依次表示IP版本号,IP头部长度,IP服务类型,IP包的总长度。
                    ip_head[3] <= BOARD_IP;//源IP地址。
                    ip_head[4] <= des_ip_r;//目的IP地址。
                end
                16'd1 : begin//开始计算IP头部校验和数据,并且将计算结果存储到对应位置。
                    ip_head_check <= ip_head[0][31 : 16] + ip_head[0][15 : 0];
                    if(eth_tx_type == 2'd2)
                        icmp_check <= icmp_check + iudp_head[0][31 : 16];
                end
                16'd2 : begin
                    ip_head_check <= ip_head_check + ip_head[1][31 : 16];
                    if(eth_tx_type == 2'd2)
                        icmp_check <= icmp_check + iudp_head[1][31 : 16];
                end
                16'd3 : begin
                    ip_head_check <= ip_head_check + ip_head[1][15 : 0];
                    if(eth_tx_type == 2'd2)
                        icmp_check <= icmp_check + iudp_head[1][15 : 0];
                end
                16'd4 : begin
                    ip_head_check <= ip_head_check + ip_head[2][31 : 16];
                    if(eth_tx_type == 2'd2)
                        icmp_check <= icmp_check[31 : 16] + icmp_check[15 : 0];//可能出现进位,累加一次。
                end
                16'd5 : begin
                    ip_head_check <= ip_head_check + ip_head[3][31 : 16];
                    if(eth_tx_type == 2'd2)
                        icmp_check <= icmp_check[31 : 16] + icmp_check[15 : 0];//可能出现进位,累加一次。
                end
                16'd6 : begin
                    ip_head_check <= ip_head_check + ip_head[3][15 : 0];
                    if(eth_tx_type == 2'd2)
                        iudp_head[0][15 : 0] <= ~icmp_check[15 : 0];//按位取反得到校验和。
                end
                16'd7 : begin
                    ip_head_check <= ip_head_check + ip_head[4][31 : 16];
                end
                16'd8 : begin
                    ip_head_check <= ip_head_check + ip_head[4][15 : 0];
                end
                16'd9,16'd10 : begin
                    ip_head_check <= ip_head_check[31 : 16] + ip_head_check[15 : 0];
                end
                16'd11 : begin
                    ip_head[2][15:0] <= ~ip_head_check[15 : 0];
                end
                default: begin
                    ip_head_check <= 32'd0;//校验和清零,用于下次计算。
                end
            endcase
        end
        else if(state_c == IP_HEAD && end_cnt)
            ip_head[1] <= {ip_head[1][31 : 16]+1,16'h4000};//高16位表示标识,每次发送数据后会加1,低16位表示不分片。
    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(eth_tx_start && (eth_tx_type != 2'd0))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//发送完以太网帧头数据;
                    if(~eth_tx_type_r[1])//如果发送ARP数据,则跳转到发送ARP数据状态;
                        state_n = ARP_DATA;
                    else//否则跳转到发送IP首部状态;
                        state_n = IP_HEAD;
                end
                else begin
                    state_n = state_c;
                end
            end
            IP_HEAD:begin
                if(end_cnt)begin//发送完IP帧头数据;
                    state_n = IUDP_HEAD;
                end
                else begin
                    state_n = state_c;
                end
            end
            IUDP_HEAD:begin
                if(end_cnt)begin//发送完UDP帧头数据;
                    state_n = IUDP_DATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            IUDP_DATA:begin
                if(end_cnt)begin//发送完udp协议数据;
                    state_n = CRC;
                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 = IFG;
                end
                else begin
                    state_n = state_c;
                end
            end
            IFG:begin
                if(end_cnt)begin//延时帧间隙对应时间。
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default:begin
                state_n = IDLE;
            end
        endcase
    end

    //计数器,用于记录每个状态机每个状态需要发送的数据个数,每个时钟周期发送1byte数据。
    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//初始值为20;
            cnt_num <= 16'd20;
        end
        else begin
            case (state_c)
                PREAMBLE : cnt_num <= 16'd8;//发送7个前导码和1个8'hd5。
                ETH_HEAD : cnt_num <= 16'd14;//发送14字节的以太网帧头数据。
                IP_HEAD : cnt_num <= 16'd20;//发送20个字节是IP帧头数据。
                IUDP_HEAD : cnt_num <= 16'd8;//发送8字节的UDP帧头数据。
                IUDP_DATA : if(iudp_tx_byte_num_r >= MIN_DATA_NUM)//如果需要发送的数据多余以太网最短数据要求,则发送指定个数数据。
                                cnt_num <= iudp_tx_byte_num_r;
                            else//否则需要将指定个数数据发送完成,不足长度补零,达到最短的以太网帧要求。
                                cnt_num <= MIN_DATA_NUM;
                ARP_DATA : cnt_num <= 16'd46;//ARP数据阶段,发送46字节数据;
                CRC : cnt_num <= 16'd5;//CRC在时钟1时才开始发送数据,这是因为CRC计算模块输出的数据会延后一个时钟周期。
                IFG : cnt_num <= 16'd12;//帧间隙对应时间为12Byte数据传输时间。
                default: cnt_num <= 16'd20;
            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 if(cnt == 12)
                                crc_data <= 8'h08;//发送源以太网协议类型,先发高字节;
                            else
                                crc_data <= eth_tx_type_r[1] ? 8'h00 : 8'h06;//如果高位有效,表示发送IP协议,否则ARP协议。
                ARP_DATA : if(cnt < 18)
                                crc_data <= arp_data[cnt];
                            else if(cnt < 24)
                                crc_data <= des_mac_r[47 - 8*(cnt - 18) -: 8];//发送目的MAC地址,先发高字节;
                            else if(cnt < 28)
                                crc_data <= des_ip_r[31 - 8*(cnt - 24) -: 8];//发送目的IP地址,先发高字节;
                            else//后面18位数据补0;
                                crc_data <= 8'd0;
                IP_HEAD : if(cnt < 4)//发送IP帧头。
                                crc_data <= ip_head[0][31 - 8*cnt -: 8];
                            else if(cnt < 8)
                                crc_data <= ip_head[1][31 - 8*(cnt-4) -: 8];
                            else if(cnt < 12)
                                crc_data <= ip_head[2][31 - 8*(cnt-8) -: 8];
                            else if(cnt < 16)
                                crc_data <= ip_head[3][31 - 8*(cnt-12) -: 8];
                            else 
                                crc_data <= ip_head[4][31 - 8*(cnt-16) -: 8];
                IUDP_HEAD : if(cnt < 4)//发送UDP帧头数据。
                                crc_data <= iudp_head[0][31 - 8*cnt -: 8];
                            else
                                crc_data <= iudp_head[1][31 - 8*(cnt-4) -: 8];
                IUDP_DATA : if(iudp_tx_byte_num_r >= MIN_DATA_NUM)//需要判断发送的数据是否满足以太网最小数据要求。
                                crc_data <= iudp_tx_data;//如果满足最小要求,将需要配发送的数据输出。
                            else if(cnt < iudp_tx_byte_num_r)//不满足最小要求时,先将需要发送的数据发送完。
                                crc_data <= iudp_tx_data;//将需要发送的数据输出即可。
                            else//剩余数据补充0。
                                crc_data <= 8'd0;
                default : ;
            endcase
        end
    end

    //生成数据请求输入信号,外部输入数据延后该信号三个时钟周期,所以需要提前产生三个时钟周期产生请求信号;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            iudp_tx_data_req <= 1'b0;
        end
        //在数据段的前三个时钟周期拉高;
        else if(state_c == IUDP_HEAD && add_cnt && (cnt == cnt_num - 4))begin
            iudp_tx_data_req <= 1'b1;
        end//在ICMP或者UDP数据段时,当发送完数据的前三个时钟拉低;
        else if(iudp_tx_byte_num_r >= MIN_DATA_NUM)begin//发送的数据段长度大于等于18.
            if(state_c == IUDP_DATA && add_cnt && (cnt == cnt_num - 4))begin
                iudp_tx_data_req <= 1'b0;
            end
        end
        else begin//发送的数据段长度小于4;
            if(state_c == IUDP_HEAD && (iudp_tx_byte_num_r <= 3) && add_cnt && (cnt == cnt_num + iudp_tx_byte_num_r - 4))begin
                iudp_tx_data_req <= 1'b0;
            end//发送的数据段有效长度大于等于4,小于18时;
            else if(state_c == IUDP_DATA && (iudp_tx_byte_num_r > 3) && add_cnt && (cnt == iudp_tx_byte_num_r - 4))begin
                iudp_tx_data_req <= 1'b0;
            end
        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(eth_tx_start || state_c != IDLE)
            rdy = 1'b0;
        else
            rdy = 1'b1;
    end

  该模块发送ARP报文仿真如下所示:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图14 ARP发送报文仿真

  发送ICMP报文仿真如下所示:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图15 ICMP发送报文仿真

  发送UDP报文仿真如下所示:

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图16 UDP发送报文仿真

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图17 整体仿真

6、以太网控制模块

  该模块的难度在于相关信号比较多,会涉及以太网发送模块信号、以太网接模块信号、UDP用户接口信号、ICMP的FIFO控制信号。

  当接收到ARP请求报文后,需要将ARP发送报文使能拉高,等待以太网发送模块空闲时,开始发送ARP应答报文。当接收到用户端口的ARP请求时,驱动以太网发送模块向目的IP地址发送ARP请求报文。

  当接收到ICMP回显请求报文后,需要把ICMP数据段内容存入ICMP FIFO中,并且把ICMP发送报文使能信号拉高,等待以太网发送模块空闲时,开始发送ICMP回显应答报文。当发送模块的数据请求信号为高电平时,如果发送的报文是ICMP数据报文,则从ICMP FIFO中读取数据输入以太网发送模块。

  当以太网接收模块接收到UDP报文后,把接收的UDP数据段输出到UDP用户端口。当用户端口的UDP开始发送信号有效时,把UDP发送使能信号拉高,等待以太网发送模块空闲时,驱动以太网发送模块发送UDP数据报文。当以太网发送模块发送UDP报文请求数据输入时,向用户端口产生数据输入使能,并且把UDP用户端口输入的数据输出到以太网发送模块作为UDP数据段的数据。

  该模块的核心参考代码如下所示:

    //高电平表示接收的数据报文是ICMP回显请求;
    assign icmp_echo_request = (eth_rx_type == 2'd2) && (icmp_rx_type == 8) && (icmp_rx_code == 0);

    //把UDP发送使能信号暂存,可能当前发送模块处于工作状态;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            udp_tx_flag <= 1'b0;
        end
        else if(udp_tx_en)begin
            udp_tx_flag <= 1'b1;
        end
        else if(eth_tx_start && (&eth_tx_type))begin
            udp_tx_flag <= 1'b0;
        end
    end

    //把arp发送使能信号暂存,可能当前发送模块处于工作状态;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            arp_tx_flag <= 1'b0;
            arp_req_r <= 1'b0;
        end
        //当接受到ARP请求数据包,或者需要发出ARP请求时拉高;
        else if((rx_done && (eth_rx_type == 2'd1) && ~arp_rx_type) || arp_req)begin
            arp_tx_flag <= 1'b1;
            arp_req_r <= arp_req;
        end//当ARP指令发送出去后拉低。
        else if(eth_tx_start && (eth_tx_type == 2'd1))begin
            arp_tx_flag <= 1'b0;
            arp_req_r <= 1'b0;
        end
    end

    //把icmp发送使能信号暂存,可能当前发送模块处于工作状态;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            icmp_tx_flag <= 1'b0;
        end
        //当接受到ICMP回显请求时拉高;
        else if(rx_done && icmp_echo_request)begin
            icmp_tx_flag <= 1'b1;
        end//当ICMP指令发送出去后拉低。
        else if(eth_tx_start && (eth_tx_type == 2'd2))begin
            icmp_tx_flag <= 1'b0;
        end
    end

    //开始发送以太网帧;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            eth_tx_start <= 1'b0;
            eth_tx_type <= 2'd0;
            arp_tx_type <= 1'b0;
            icmp_tx_type <= 8'd0;
            icmp_tx_code <= 8'd0;
            icmp_tx_id <= 16'd0;
            icmp_tx_seq <= 16'd0;
            iudp_tx_byte_num <= 16'd0;
        end
        //接收到ARP的请求数据报时,把开始发送信号拉高;
        else if(arp_tx_flag && tx_rdy)begin
            eth_tx_start <= 1'b1;
            eth_tx_type <= 2'd1;
            arp_tx_type <= arp_req_r ? 1'b0 : 1'b1;//发送ARP应答报文;
        end//当接收到ICMP回显请求时,把开始发送信号拉高;
        else if(icmp_tx_flag && tx_rdy)begin
            eth_tx_start <= 1'b1;
            eth_tx_type <= 2'd2;
            icmp_tx_type <= 8'd0;//发送ICMP回显应答数据报文。
            icmp_tx_code <= 8'd0;
            icmp_tx_id  <= icmp_rx_id;//将回显请求的的ID传回去。
            icmp_tx_seq <= icmp_rx_seq;
            iudp_tx_byte_num <= iudp_rx_byte_num;
        end//当需要发送udp数据时,把开始发送信号拉高;
        else if(udp_tx_flag && tx_rdy)begin
            eth_tx_start <= 1'b1;
            eth_tx_type <= 2'd3;
            iudp_tx_byte_num <= udp_tx_data_num;
        end//如果检测到模块处于空闲状态,则将开始信号拉低。
        else begin
            eth_tx_start <= 1'b0;
        end
    end
    
    //将接收的ICMP数据存入FIFO中。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            icmp_fifo_wr_en <= 1'b0;
            icmp_fifo_wdata <= 8'd0;
        end//如果接收的数据是ICMP数据段的数据,把ICMP的数据存储到FIFO中。
        else if(iudp_rx_data_vld && icmp_echo_request)begin
            icmp_fifo_wr_en <= 1'b1;
            icmp_fifo_wdata <= iudp_rx_data;
        end
        else begin
            icmp_fifo_wr_en <= 1'b0;
        end
    end

    //通过数据请求信号产生从ICMP的FIFO中读取数据或者向用户接口发送UDP数据请求信号;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            udp_tx_req <= 1'b0;
            icmp_fifo_rd_en <= 1'b0;
        end
        else if(iudp_tx_data_req)begin
            if(eth_tx_type_r == 2'd2)begin//如果发送的是ICMP数据报,则从FIFO中读取数据;
                udp_tx_req <= 1'b0;
                icmp_fifo_rd_en <= 1'b1;
            end
            else begin//否则表示发送的UDP数据报,则从外部获取UDP数据。
                udp_tx_req <= 1'b1;
                icmp_fifo_rd_en <= 1'b0;
            end
        end
        else begin
            udp_tx_req <= 1'b0;
            icmp_fifo_rd_en <= 1'b0;
        end
    end

    //将ICMP FIFO或者外部UDP获取的数据发送给以太网发送模块;
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            iudp_tx_data <= 8'd0;
        end
        else if(eth_tx_type_r == 2'd2)begin
            iudp_tx_data <= icmp_fifo_rdata;
        end
        else begin
            iudp_tx_data <= udp_tx_data;
        end
    end

    //将接收的UDP数据输出。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            udp_rx_data_vld <= 1'b0;
            udp_rx_data <= 8'd0;
        end//如果接收到UDP数据段信号,将UDP的数据输出。
        else if(iudp_rx_data_vld && eth_rx_type == 2'd3)begin
            udp_rx_data_vld <= 1'b1;
            udp_rx_data <= iudp_rx_data;
        end
        else begin
            udp_rx_data_vld <= 1'b0;
        end
    end

    //一帧UDP数据接收完成。
    always@(posedge clk)begin
        if(rst_n==1'b0)begin//初始值为0;
            udp_rx_done <= 1'b0;
            udp_rx_data_num <= 16'd0;
        end
        else if(&eth_rx_type)begin//如果接收的是UDP数据报;
            udp_rx_done <= rx_done;//将输出完成信号输出;
            udp_rx_data_num <= iudp_rx_byte_num;//把UDP数据长度输出;
        end
    end

  该模块不贴仿真结果了,需要的打开工程自行查看。

7、上板测试

  CRC校验模块、FIFO的设置在前文都已经详细讲解了,所以本文不在赘述。

  最后把顶层模块的ILA注释取消,综合工程,查看工程的使用量,如下所示,使用了1195个查找表,而以太网模块使用了1141个查找表,1131个触发器资源。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图18 本工程消耗资源占比

  前文实现udp回环,ARP应,ICMP应答的工程资源消耗如下图所示,工程消耗1979个查找表,2073个触发器。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图19 前一工程消耗资源占比

  对比图17、18可知,本文实现相同功能后,本文工程能够节约785查找表,节约大概百分之四十的查找表资源。节约了871触发器资源,大概节约原工程的42%触发器资源。

  将程序下载到开发板中,然后打开网络调试助手,wireshark软件,发送UDP数据,网络调试助手抓取结果如下所示。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图20 网络调试助手抓取数据

  在命令提示符中使用ping指令,结果如下所示。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图21 ping验证

  通过使用wireshark抓取UDP数据报文,如下图所示,PC先向FPGA发出ARP请求报文获取FPGA的MAC地址,然后再发送UDP报文,FPGA接收到UDP报文后,将数据传回PC端。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图22 wireshark抓取UDP数据报

  下图粉红色报文是wireshark抓取的ICMP报文,FPGA接收PC端发出的回显请求报文后,向PC端发出回显应答报文。

基于FPGA的实用UDP设计(包含源工程文件),FPGA,以太网,FPGA基础模块,fpga开发,udp,单片机

图23 wireshark抓取ICMP数据报

  关于报文细节内容,本文就不再赘述了,前文讲解ARP、ICMP、UDP时已经经过详细分析,本文分析原理一致,不再对比ILA抓取数据和wireshark工具抓取的数据了。

  本文对前文学到的几种协议进行了总结、简化设计,使用一个模块发送和接收三种协议数据,这三种协议往往一起使用,后续可以直接使用该模块。

  可以在公众号后台回复“基于FPGA实用UDP设计”(不包括引号)获取本文工程。文章来源地址https://www.toymoban.com/news/detail-834541.html

到了这里,关于基于FPGA的实用UDP设计(包含源工程文件)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • FPGA高端项目:图像缩放+GTX+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持

    FPGA高端项目:图像缩放+GTX+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持 没玩过图像处理、GT高速接口、UDP网络通信,都不好意思说自己玩儿过FPGA,这是CSDN某大佬说过的一句话,鄙人深信不疑。。。GT资源是Xilinx系列FPGA的重要卖点,也是做

    2024年03月14日
    浏览(89)
  • FPGA高端项目:图像缩放+GTP+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持

    FPGA高端项目:图像缩放+GTP+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持 没玩过图像处理、GT高速接口、UDP网络通信,都不好意思说自己玩儿过FPGA,这是CSDN某大佬说过的一句话,鄙人深信不疑。。。GT资源是Xilinx系列FPGA的重要卖点,也是做

    2024年02月05日
    浏览(57)
  • FPGA高端项目:图像采集+GTX+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持

    FPGA高端项目:图像采集+GTX+UDP架构,高速接口以太网视频传输,提供2套工程源码加QT上位机源码和技术支持 没玩过图像处理、GT高速接口、UDP网络通信,都不好意思说自己玩儿过FPGA,这是CSDN某大佬说过的一句话,鄙人深信不疑。。。GT资源是Xilinx系列FPGA的重要卖点,也是做

    2024年02月05日
    浏览(62)
  • FPGA - 以太网UDP通信(一)

    ​以太网是一种计算机局域网技术。IEEE组织的IEEE 802.3标准制定了以太网的技术标准,它规定了包括物理层的连线、电子信号和介质访问层协议的内容。 ​ 以太网是现实世界中最普遍的一种计算机网络。以太网有两类:第一类是经典以太网,第二类是交换式以太网,使用了一

    2024年04月18日
    浏览(54)
  • 千兆以太网传输层 UDP 协议原理与 FPGA 实现(UDP接收)

    相关文章: (1)千兆以太网网络层 ARP 协议的原理与 FPGA 实现 (2)千兆以太网硬件设计及链路层 MAC 协议格式 (3)CRC校验原理及实现 (4)RGMII 与 GMII 转换电路设计 (5)千兆以太网网络层 IP 协议介绍与 IP 校 验和算法实现 (6)千兆以太网传输层 UDP 协议原理与 FPGA 实现(

    2024年02月04日
    浏览(123)
  • FPGA优质开源项目 – UDP万兆光纤以太网通信

    本文开源一个FPGA项目:UDP万兆光通信。该项目实现了万兆光纤以太网数据回环传输功能。Vivado工程代码结构和之前开源的《UDP RGMII千兆以太网》类似,只不过万兆以太网是调用了Xilinx的10G Ethernet Subsystem IP核实现。 下面围绕该IP核的使用、用户接口,以及数据传输方案展开介

    2024年02月10日
    浏览(57)
  • FPGA实现千兆/百兆自适应以太网UDP传输

    笔者最近在项目中需要使用到ZYNQ中PL端做以太网UDP传输并且需要支持100M/1000M自适应切换。使用的PHY型号为RTL8211。以下分享的主要为利用已有的1000M协议栈修改为100M并且实现二者自适应切换,IP核主要实现以下功能 1、实现100M/1000M自适应 2、回环测试 PS:完整的IP核文件下载地

    2024年01月21日
    浏览(54)
  • FPGA优质开源项目 - UDP RGMII千兆以太网

    本文介绍一个FPGA开源项目:UDP RGMII千兆以太网通信。该项目在我之前的工作中主要是用于FPGA和电脑端之间进行图像数据传输。本文简要介绍一下该项目的千兆以太网通信方案、以太网IP核的使用以及Vivado工程源代码结构。 Vivado 的 Tri Mode Ethernet MAC IP核需要付费才能使用,因

    2024年02月14日
    浏览(77)
  • 基于FPGA的数据采集、编码、通讯和存储系统设计(即FPGA+RTL8211千兆以太网+SD卡存储+RTC+Uart+AD7606数模转换+电流放大采集等硬件设计与程序验证)

    介绍一个小项目,加强对FPGA相关接口的整体把握。 硬件及软件代码梳理: 硬件系统的主要功能框图,其中FPGA作为处理单元,实现了包括电流和电压的采集、千兆以太网通讯、SD卡本地数据存储和串口通讯等。已经过板级测试,测试包含:千兆网通讯收发测试、AD采集的数据

    2024年04月13日
    浏览(52)
  • FPGA 高端项目:基于 SGMII 接口的 UDP 协议栈,提供2套工程源码和技术支持

    FPGA 高端项目:基于 SGMII 接口的 UDP 协议栈,提供2套工程源码和技术支持 目前网上的fpga实现udp基本生态如下: 1:verilog编写的udp收发器,但中间的FIFO或者RAM等调用了IP,或者不带ping功能,这样的代码功能正常也能用,但不带ping功能基本就是废物,在实际项目中不会用这样的

    2024年01月21日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包