1、概述
前文对ARP协议、ICMP协议、UDP协议分别做了讲解,并且通过FPGA实现了三种协议,最终实现的UDP协议工程中也包含了ARP和ICMP协议,对应的总体框架如图所示。
尽管上述模块包含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回环进行测试,实现功能与前文一致,主要是对以太网接收和发送模块进行修改,顶层模块连线图如下所示。
工程的顶层模块连接相比图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字节)数据的时间,为了便于用户使用,所以设计时应该考虑帧间隙问题。
以太网控制模块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协议的数据帧格式,如下图所示:
通过对比上面三图可知,前导码、帧起始符、以太网帧头、CRC校验码均一致,所以这几部分还是可以根据前文一样设计,不做改变。当以太网帧头的类型为16’h0806时表示ARP,等于16’h0800时表示报文是IP协议。此时还是可以通过状态机进行实现,对应的状态转换图如下所示:
状态机初始位于空闲状态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协议:
接收ICMP协议:
把ICMP的数据段放大,如下图所示:
接收UDP协议的仿真如下图所示,将接收的数据段输出到,蓝色信号就是输出的数据信号。
5、以太网发送模块
发送模块依旧可以使用状态机嵌套计数器的形式实现,状态机对应的状态转换图如下所示。
状态机包括初始状态、发送前导码帧起始符状态、发送以太网帧头状态、发送IP帧头状态、发送ICMP或UDP帧头状态、发送ICMP数据或UDP数据状态、发送ARP数据状态、发送CRC校验状态、帧间隙等待状态。总共9个状态,由于ICMP帧头和UDP帧头长度基本一样,且数据段都需要从外部输入数据,所以ICMP和UDP的帧头、数据段共用一个状态。
该模块设计还是比较简单的,在合并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报文仿真如下所示:
发送ICMP报文仿真如下所示:
发送UDP报文仿真如下所示:
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 && (ð_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(ð_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个触发器资源。
前文实现udp回环,ARP应,ICMP应答的工程资源消耗如下图所示,工程消耗1979个查找表,2073个触发器。
对比图17、18可知,本文实现相同功能后,本文工程能够节约785查找表,节约大概百分之四十的查找表资源。节约了871触发器资源,大概节约原工程的42%触发器资源。
将程序下载到开发板中,然后打开网络调试助手,wireshark软件,发送UDP数据,网络调试助手抓取结果如下所示。
在命令提示符中使用ping指令,结果如下所示。
通过使用wireshark抓取UDP数据报文,如下图所示,PC先向FPGA发出ARP请求报文获取FPGA的MAC地址,然后再发送UDP报文,FPGA接收到UDP报文后,将数据传回PC端。
下图粉红色报文是wireshark抓取的ICMP报文,FPGA接收PC端发出的回显请求报文后,向PC端发出回显应答报文。
关于报文细节内容,本文就不再赘述了,前文讲解ARP、ICMP、UDP时已经经过详细分析,本文分析原理一致,不再对比ILA抓取数据和wireshark工具抓取的数据了。
本文对前文学到的几种协议进行了总结、简化设计,使用一个模块发送和接收三种协议数据,这三种协议往往一起使用,后续可以直接使用该模块。文章来源:https://www.toymoban.com/news/detail-834541.html
可以在公众号后台回复“基于FPGA实用UDP设计”(不包括引号)获取本文工程。文章来源地址https://www.toymoban.com/news/detail-834541.html
到了这里,关于基于FPGA的实用UDP设计(包含源工程文件)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!