首先定义一些寄存器,用于存储接收到的头部信息。然后进行寄存器的初始化。由于数据是顺序接收和判断的,这里采用了case语句,并嵌入if判断语句控制case语句的跳转。只要图中的判断语句任意处出现否,则跳回数据接收模块的PRE_R状态。
接下来再来看一下代码部分。结合前面的学习,理解起来就比较容易了。首先来看一下数据接收模块,其功能是判断数据包是否符合协议规范,并解析出数据包中的数据。数据接收部分首先是对状态机的状态跳转控进行赋值,其次是对寄存器的定义,如代码注释所示:
//-------------------接收数据-------------------//
parameter
PRE_R = 4'd0,
SFD_R = 4'd1,
DA_R = 4'd2,
SA_R = 4'd3,
TYPE_R = 4'd4,
ARP_R = 4'd5,
IP_R = 4'd6,
UDP_R = 4'd7,
APP_R = 4'd8,
END_R = 4'd9;
reg [3:0] STATE_R;//接收状态
reg [7:0] cnt_R;//接收使用的计数器
reg [47:0]dest_mac;//目标MAC地址
reg [47:0]source_mac;//源MAC地址
reg [15:0]type;//接收数据的协议类型
reg [223:0]arp_data;//ARP中的数据
reg [159:0]ip_head;//IP报头
reg [63:0]udp_head;//UDP数据报头
reg [7:0]buffer[63:0];//数据缓存区
reg [15:0]data_lenght;//数据长度
reg [11:0] i;//数据计数器
reg [7:0]data1;
reg UDP_SD_EN;
这里有个寄存器变量data1,它的作用就像我们过安检时用的托盘。将物品放入托盘中,经过安检,取走物品后的托盘再次放入物品,过安检。于此类似,依次接收的数据,先放到变量data1中,然后对变量data1的值进行判断,是否符合帧格式,符合的话则用下一个输入值覆盖当前data1中的值,继续进行判断。
数据接收模块的主要部分是一个case语句,在case语句的不同分支中,实现对帧结构的解析和数据的读取。首先是对寄存器变量的初始化,使其在复位之后处于一个确定的状态。然后是对帧的起始标志的判断,就在PRE_R的状态判断七次存入data1中的值,如果是7个0x55,那么说明输入的前七个数值符合帧起始符,就跳转到下一个状态,判断存入data1中的值是否为帧起始符0xd5。如果是0xd5,继续判断,否则调出判断,返回初始状态。如果发送过来的是一个完整的帧数据包,则可以解析出帧数据的MAC地址,协议类型,IP地址,端口类型以及数据长度,校验码等信息。
UDP协议中,在UDP数据包里定义了数据部分的长度。那么,当我们解析出UDP数据包后,也就知道了这次发送的有效数据的长度。有了这个数据长度值,就可以在缓存接收到的数据时只存输入进来的有效数据了。
在解析UDP包头的时候,也对目标端口进行了判断。要保证TCP&UDP测试软件设置的时候的端口值和此处代码中的端口值保持一致,否则会导致数据接收失败。
UDP_R:// 接收UDP包头 8 byte
begin
if(cnt_R == 8'd7)
begin
udp_head <= {udp_head[55:0],data1};
cnt_R <= 8'd0;
if(udp_head[39:24] == 16'h7530)//16进制的7530是十进制的30000,即设定的端口号
begin
data_lenght <= udp_head[23:8] - 16'd8;//提取数据长度,udp_head的[23:8]定义了UDP包的数据长度,包括了UDP头部,所以这里要减去UDP头部的数据长度8
STATE_R <= APP_R;
end
else
begin
STATE_R <= PRE_R; //如果端口不匹配,则返回PRE_R状态
end
end
else
begin
udp_head <= {udp_head[55:0],data1};
cnt_R <= cnt_R +1'd1;
end
end
reg flag;//连发模式标志位
reg flag_h;//连发开始位标志
reg flag_l;//连发停止位标志
always@(posedge RX_CLK or negedge rst_N )
if(rst_N==1'b0)
flag_h<=1'b0;
else if((buffer[0]==8'h33)&&(STATE_UT==APP_R))//连发开始位标志3
flag_h<=1'b1;
else
flag_h<=1'b0;
always@(posedge RX_CLK or negedge rst_N )
if(rst_N==1'b0)
flag_l<=1'b0;
else if((buffer[0]==8'h73)&&(STATE_UT==APP_R))//连发停止位标志s
flag_l<=1'b1;
else
flag_l<=1'b0;
always@(posedge RX_CLK or negedge rst_N )
if(rst_N==1'b0)
flag<=1'b0;
else if(flag_l) //连发状态停止
flag<=1'b0;
else if(flag_h==1'b1)//连发状态开始
flag<=1'b1;
看这段代码,定义了另外两个寄存器变量,用于控制flag信号,那么控制flag变量的flag_h和flag_l又是如何产生的呢?在代码中,通过对数据接收模块状态和接收的第一个字符的值进行综合判断,当接收模块处于接收有效数据,且第一个接受到的数值为“3”的时候,拉高一个时钟周期的flag_h信号。flag_l则根据接收数据的第一个字符为“s”的时候拉高一个时钟周期。这里大家应该有注意到,当我们对数据接收缓存器的第一个值buffer[0]进行判断时,对比值是8’h33和8’h73。这里的8’h33和8’h73分别是ASCII码“3”和“s”对应的16进制数。这是因为我们使用TCP&UDP测试工具给FPGA发送数据的时候选择的是ASCII格式。
那什么时候开始进入数据发送的流程呢?首先要对帧结构头部寄存器进行初始化,设定MCA地址,协议类型,版本号和端口号等信息。然后进入IDEL状态,等待SD_EN信号为高,则进行数据长度的初始化并进入发送状态。
那么这里控制IDEL跳转到下一状态的SD_EN信号是什么作用呢?看下面这段代码就能知道,这个信号相当于一个选择器,当接收到的第一个字符为“3”的时候,flag信号为高,则SD_EN就一直为高电平了。SD_EN为高的时候,数据发送模就一直处于发送使能状态。而flag为低的时候,它的状态和UDP_SD_EN的状态就一样了,也就是FPGA每接收完一帧数据,便发送出一帧数据,并等待下一帧数据发送进来。
reg SD_EN;
always@(posedge clk_125M or negedge rst_N)
if(!rst_N)
SD_EN<=1'b0;
else if(flag)
SD_EN<=1'b1;
else if(!flag)
SD_EN<=UDP_SD_EN;
check1:
begin
if(cnt == 8'd6)
begin//IP计算首部校验和:以16位相加方式加到32位校验和中
ip_check_sum <= {ip_send[0],ip_send[1]} + {ip_send[2],ip_send[3]}+{ip_send[4],ip_send[5]} + {ip_send[6],ip_send[7]} + {ip_send[8],ip_send[9]}+{ip_send[12],ip_send[13]}+{ip_send[14],ip_send[15]}+{ip_send[16],ip_send[17]}+{ip_send[18],ip_send[19]};
crc_reset <= 1'd1;
cnt <= 8'd0;
STATE_UT <= check2;
end
else
begin
cnt <= cnt + 1'd1;
end
end
check2:
begin//IP计算首部校验和:16位相加取反
{ip_send[10],ip_send[11]} = ~(ip_check_sum[15:0] + ip_check_sum[31:16]);
STATE_UT <= send55;
end
然后正式进入数据发送阶段。首先是发送7个0x55和一个0xd5。作为帧数据开始标志,告诉接收设备,做好接收准备。之后就是发送源端和目标端MAC地址,以及协议类型,构成完整的数据帧头部。这里也要注意MAC地址的值和我们软件设置里相互对应。然后是IP数据包头和UDP数据包头部。
在发送完UDP头部之后,将要发送的就是帧内的有效数据了,这里也用flag信号控制了一下状态的跳转。当flag为高的时候,跳转到send_data1状态,否则跳转到send_data状态。
send_udp://发送UDP包头
begin
if(cnt == 8'd7)
begin
data_out <= udp_send[cnt];
cnt <= 8'd0;
data_counter <= 16'd0;
if(flag) //控制发送状态跳转至send_data1
STATE_UT<=send_data1;
else
STATE_UT <= send_data;
end
else
begin
data_out <= udp_send[cnt];
cnt <= cnt +1'd1;
end
end
send_data://发送数据
begin
if(data_lenght < 16'd19)
begin
if(data_counter == 16'd20)
begin
data_out <= buffer[data_counter];
data_counter <= 16'd0;
STATE_UT <= send_crc;
end
else
begin
data_out <= buffer[data_counter]; data_counter <= data_counter + 1'd1;
end
end
else
begin
if(data_counter == data_lenght - 16'd1)
begin
data_out <= buffer[data_counter];
data_counter <= 16'd0;
STATE_UT <= send_crc;
end
else
begin
data_out <= buffer[data_counter]; data_counter <= data_counter + 1'd1;
end
end
end
send_data1:
begin
if(cnt_l==1200)
begin
STATE_UT <=send_crc;
cnt_l<=12'd0;
end
else
begin
data_out<=8'h38;//输出数值随便定义,这里设定的“8”
cnt_l<=cnt_l+1'b1;
end
end
通过对比这两段代码可以知道,send_data和send_data1均是实现数据发送的。
send_data是按照协议流程,进行最小帧长度的判断,并处理,然后将接收数据缓冲寄存器buffer中存储的接收数据依次发送出去。
前面我们有提到,帧数据的长度为46字节到1500字节,这个长度是包括IP协议头部和UDP协议头部的,因此,一帧数据除了IP协议头部和UDP协议头部,携带的有效数据长度在19字节至1400多个字节之间。所以在发送数据的时候我们这里对需要发送的数据的长度做个判断,当发送的数据的长度不够19个字节的时候,就对其进行填充,避免被当成非法帧而丢包。
send_data1则是直接将ASCII码“8”的16进制数值连续1200次发送出去。这里的data_out的值大家给定什么,在TCP&UDP的接收区就可以收到什么。连续发送的次数只要在帧数据长度的最大值和最小值以内都可以。
回过头看一下,大概流程就是,如果接收数据的第一个字符是“3”,则flag信号为高电平。相应的SD_EN信号也为高电平,一直处于发送使能状态。同时由于flag为高,case语句中有效数据发送一直进入send_data1,即连续发送内容为1200个“8”的帧到PC端。数据发送完之后接着发送CRC校验的值,就完成了一帧数据的发送。