正点原子 发表于 2023-3-11 09:55:35

《ATK-DFPGL22G之FPGA开发指南_V1.0》第五十一章以太网UDP测试实验

本帖最后由 正点原子 于 2023-3-11 09:55 编辑

1)实验平台:正点原子紫光PGL22G开发板
2)购买链接:https://item.taobao.com/item.htm?&id=692368045899
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-340253-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子FPGA交流群:435699340





第五十一章以太网UDP测试实验
UDP是一种面向无连接的传输层协议,属于TCP/IP协议簇的一种。UDP具有消耗资源少、通信效率高等优点,通常用来传输音频、视频等对实时性要求高的场合。本章我们来学习如何通过
ATK-DFPGL22G开发板实现UDP通信的功能。
本章分为以下几个章节:
51.1简介
51.2实验任务
51.3硬件设计
51.4程序设计
51.5下载验证


51.1简介
UDP概述
UDP(User Datagram Protocol),即用户数据报协议,是一种面向无连接的传输层协议。无连接是指在传输数据时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输(如视频会议等)都会采用UDP协议进行传输,这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
UDP和TCP是传输层中非常重要的两个协议,位于OSI(Open System Interconnection,开放式系统互联)参考模型中的第四层(传输层),是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,位于IP协议层(网络层)之上。OSI将计算机网络体系结构分为七层:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,OSI参考模型如下图所示。
图 51.1.1 OSI参考模型
以太网UDP传输单包数据的格式由图 51.1.1所示。从图中可以看出,以太网的数据包就是对各层协议的逐层封装来实现数据的传输。用户数据打包在UDP协议中,UDP协议又是基于IP协议之上的,IP协议又是走MAC层发送的,即从包含关系来说:MAC帧中的数据段为IP数据报,IP报文中的数据段为UDP报文,UDP报文中的数据段为用户希望传输的数据内容。接下来我们逐个来向大家介绍不同层的数据格式。
图 51.1.2 以太网UDP传输数据包格式
其中以太网的帧格式在“以太网ARP测试实验”中已经向大家作了详细的介绍,如果对以太网帧格式不熟悉的话,可以参考“以太网ARP测试实验”。IP协议(互联网分组交换协议)是TCP/IP协议簇中非常重要的一个协议,下面我们来熟悉下IP协议。
IP协议
IP协议是TCP/IP协议簇中的核心协议,也是TCP/IP协议的载体,IP协议规定了数据传输时的基本单元和格式。从前面介绍的图 51.1.2中可以看出,IP协议位于以太网MAC帧格式的数据段,IP协议内容由IP首部和数据字段组成。所有的TCP、UDP及ICMP数据都以IP数据报格式传输,IP数据包格式如图 51.1.3所示。
图 51.1.3 IP数据报格式
前20个字节和紧跟其后的可选字段是IP数据报的首部,前20个字节是固定的,后面可选字段是可有可无的,首部的每一行以32位(4个字节)为单位。
版本:4位IP版本号(Version),这个值设置为二进制的0100时表示IPv4,设置为0110时表示IPv6,目前使用比较多的IP协议版本号是4。
首部长度:4位首部长度(IHL,Internet Header Length),表示IP首部一共有多少个32位(4个字节)。在没有可选字段时,IP首部长度为20个字节,因此首部长度的值为5。
服务类型:8位服务类型(TOS,Type of service),该字段被划分成两个子字段:3位优先级字段(现在已经基本忽略掉了)和4位TOS字段,最后一位固定为0。服务类型为0时表示一般服务。
总长度:16位IP数据报总长度(Total Length),包括IP首部和IP数据部分,以字节为单位。我们利用IP首部长度和IP数据报总长度,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长16bit,所以IP数据报最长可达65535字节。尽管理论上可以传输长达65535字节的IP数据报,但实际上还要考虑网络的最大承载能力等因素。
标识字段:16位标识(Identification)字段,用来标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。
标志字段:3位标志(Flags)字段,第1位为保留位;第2位表示禁止分片(1表示不分片 0:允许分片);第3位标识更多分片(除了数据报的最后一个分片外,其它分片都为1)。
片偏移:13位片偏移(Fragment Offset),在接收方进行数据报重组时用来标识分片的顺序。
生存时间:8位生存时间字段,TTL(Time To Live)域防止丢失的数据包在无休止的传播,一般被设置为64或者128。
协议:8位协议(Protocol)类型,表示此数据报所携带上层数据使用的协议类型,ICMP为1,TCP为6,UDP为17。
首部校验和:16位首部校验和(Header Checksum),该字段只校验数据报的首部,不包含数据部分;校验IP数据报头部是否被破坏、篡改和丢失等。
源IP地址:32位源IP地址(Source Address),即发送端的IP地址,如192.168.1.123。
目的IP地址:32位目的IP地址(Destination Address),即接收端的IP地址,如192.168.1.102。
可选字段:是数据报中的一个可变长度的可选信息,选项字段以32bit为界,不足时插入值为0的填充字节,保证IP首部始终是32bit的整数倍。
以上内容是对IP首部格式的详细阐述,还需要补充的内容是IP首部校验和的计算方法,其计算步骤如下:
将16位检验和字段置为0,然后将IP首部按照16位分成多个单元;
对各个单元采用反码加法运算(即高位溢出位会加到低位,通常的补码运算是直接丢掉溢出的高位);
此时仍然可能出现进位的情况,将得到的和再次分成高16位和低16位进行累加;
最后将得到的和的反码填入校验和字段。
例如,我们使用IP协议发送一个IP数据报总长度为50个字节(有效数据为30个字节)的数据包,发送端IP地址为192.168.1.123,接收端IP地址为192.168.102,则IP首部数据如下:
图 51.1.4 IP首部数据
按照上述提到的 IP 首部校验和的方法计算 IP 首部校验和,即:
0x4500 + 0x0032 + 0x0000 + 0x4000 + 0x4011 + 0x0000(计算时强制置0) + 0xc0a8 + 0x017b + 0xc0a8 + 0x0166 = 0x24974
0x0002 + 0x4974 = 0x4976
0x0000 + 0x4976 = 0x4976(此种情况并未出现进位)
check_sum = ~0x4976(按位取反) = 0xb689
到此为止IP协议内容已经介绍完了,我们从前面介绍的图 51.1.2可以知道,UDP的首部和数据位于IP协议的数据段。既然已经有IP协议了,为什么还需要UDP协议呢?为什么我们选择的是UDP还不是传输更可靠的TCP呢?带着这些疑问我们继续往下看。
UDP协议
首先回答为什么还需要UDP协议?事实上数据是可以直接封装在IP协议里而不使用TCP、UDP或者其它上层协议的。然而在网络传输中同一IP服务器需要提供各种不同的服务,各种不同的服务类型是使用端口号来区分的,例如用于浏览网页服务的80端口,用于FTP(文件传输协议)服务的21端口等。TCP和UDP都使用两个字节的端口号,理论上可以表示的范围为0~65535,足够满足各种不同的服务类型。
然后是为什么不选择传输更可靠的TCP协议,而是UDP协议呢?TCP协议与UDP协议作为传输层最常用的两种传输协议,这两种协议都是使用IP作为网络层协议进行传输。下面是TCP协议与UDP协议的区别:
1、TCP协议面向连接,是流传输协议,通过连接发送数据,而UDP协议传输不需要连接,是数据报协议;
2、TCP为可靠传输协议,而UDP为不可靠传输协议。即TCP协议可以保证数据的完整和有序,而UDP不能保证;
3、UDP由于不需要连接,故传输速度比TCP快,且占用资源比TCP少;
4、应用场合:TCP协议常用在对数据文件完整性较高的一些场景中,如文件传输等。UDP常用于对通讯速度有较高要求或者传输数据较少时,比如对速度要求较高的视频直播和传输数据较少的QQ等。
首先可以肯定的告诉大家,使用FPGA实现TCP协议是完全没有问题的,但是,FPGA发展到现在,却鲜有成功商用的RTL级的TCP协议设计,大部分以太网传输都是基于比较简单的UDP协议。TCP协议设计之初是根据软件灵活性设计的,如果使用硬件逻辑实现,工程量会十分巨大,而且功能和性能无法得到保证,因此,TCP协议设计并不适合使用硬件逻辑实现。UDP协议是一种不可靠传输,发送方只负责数据发送出去,而不管接收方是否正确的接收。在很多场合,是可以接受这种潜在的不可靠性的,例如视频实时传输显示等。
UDP数据格式如图 51.1.5所示:
图 51.1.5 UDP数据格式
UDP首部共8个字节,同IP首部一样,也是一行以32位(4个字节)为单位。
源端口号:16位发送端端口号,用于区分不同服务的端口,端口号的范围从0到65535。
目的端口号:16位接收端端口号。
UDP长度:16位UDP长度,包含UDP首部长度+数据长度,单位是字节(byte)。
UDP校验和:16位UDP校验和。UDP计算校验和的方法和计算IP数据报首部校验和的方法相似,但不同的是IP数据报的校验和只检验IP数据报的首部,而UDP校验和包含三个部分:UDP伪首部,UDP首部和UDP的数据部分。伪首部的数据是从IP数据报头和UDP数据报头获取的,包括源IP地址,目的IP地址,协议类型和UDP长度,其目的是让UDP两次检查数据是否已经正确到达目的地,只是单纯为了做校验用的。在大多数使用场景中接收端并不检测UDP校验和,因此这里不做过多介绍。
以太网的帧格式、IP数据报协议以及UDP协议到这里已经全部介绍完了,关于用户数据、UDP、IP、MAC四个报文的关系如下图所示:
图 51.1.6 以太网包数据格式
用户数据打包在UDP协议中,UDP协议又是基于IP协议之上的,IP协议又是走MAC层发送的,即从包含关系来说:MAC帧中的数据段为IP数据报,IP报文中的数据段为UDP报文,UDP报文中的数据段为用户希望传输的数据内容。现在再回过头看图 51.1.2的内容就非常容易理解了。
51.2实验任务
本节实验任务是上位机通过网口调试助手发送数据给FPGA,FPGA通过以太网接口接收数据并将接收到的数据发送给上位机,完成以太网UDP数据的环回。
51.3硬件设计
千兆以太网接口部分的硬件设计原理及本实验中各端口信号的管脚分配,和“以太网ARP测试实验”完全相同,请参考“以太网ARP读写测试实验”中的硬件设计部分。
51.4程序设计
图 51.4.1是根据本章实验任务画出的系统框图。和“以太网ARP测试实验”相比,将ARP控制模块替换成了以太网控制模块,并增加了一个同步FIFO和UDP顶层模块。本次实验虽然实现的是UDP通信,但保留了ARP顶层模块,这是由于上位机应用程序只知道接收端的目的IP地址和端口号,却不知道接收端的MAC地址,因此这里通过ARP协议来获取接收端的MAC地址,否则需要在发送端手动绑定接收端MAC地址,而手动绑定的方法较为繁琐,因此这里保留了ARP协议。
本次实验同时实现了ARP协议和UDP协议,GMII接收侧的引脚同时连接至ARP顶层模块和UDP顶层模块,这个两个模块会分别根据ARP协议和UDP协议解析数据。而GMII发送侧引脚只能和ARP顶层模块和UDP顶层模块的其中一个连接,因此以太网控制模块会根据当前接收到的协议类型,选择切换GMII发送侧引脚和ARP顶层模块或者UDP顶层模块连接。除此之外,以太网控制模块根据输入的ARP接收的类型,控制ARP顶层模块返回ARP应答信号。以太网单次会接收到大量数据,因此本次实验需要一个FIFO模块用来缓存数据,由于本次实验所使用的GMII接收时钟和GMII发送时钟实际上为同一个时钟,因此这里使用的是同步FIFO。
图 51.4.1 以太网UDP测试系统框图
GMII TO RGMII模块负责将双沿(DDR)数据和单沿(SDR)数据之间的转换;ARP顶层模块解析ARP请求命令,并返回开发板的MAC地址;以太网控制模块根据输入的ARP接收完成信号类型,控制ARP顶层模块返回ARP应答信号,并根据当前接收到的协议类型,选择切换ARP顶层模块和UDP顶层模块的GMII发送侧引脚;UDP顶层模块实现了以太网UDP数据包的接收、发送以及CRC校验的功能。同步FIFO模块是由Pango软件自带的FIFO IP核生成的,FIFO的大小为2048个32bit,为了能够满足单包数据量较大的情况(尽管通常情况下,以太网帧有效数据不超过1500个字节),所以FIFO的深度最好设置的大一点,这里把深度设置为2048,宽度为32位。
FPGA顶层模块例化了以下五个模块, GMII TO RGMII模块(gmii_to_rgmii)、ARP顶层模块(arp)、UDP顶层模块(udp)、同步FIFO模块(sync_fifo_2048x32b)和以太网控制模块(eth_ctrl),实现了各模块之间的数据交互。
其中GMII TO RGMII(gmii_to_rgmii)模块和ARP顶层模块(arp)在“以太网ARP测试实验”中已经向大家作了详细的介绍,如果大家对这部分内容不熟悉的话,可以参考“以太网ARP测试实验”。
本章我们重点介绍UDP顶层模块(udp),UDP顶层模块实现了整个以太网帧格式与UDP协议的功能。
UDP顶层模块例化了UDP接收模块(udp_rx)、UDP发送模块(udp_tx)和CRC校验模块(crc32_d8)。
UDP接收模块(udp_rx):UDP接收模块较为简单,因为我们不需要对数据做IP首部校验也不需要做CRC循环冗余校验,只需要判断目的MAC地址与开发板MAC地址、目的IP地址与开发板IP地址是否一致即可。接收模块的解析顺序是:前导码+帧起始界定符→以太网帧头→IP首部→UDP首部→UDP数据(有效数据)→接收结束。IP数据报一般以32bit为单位,为了和IP数据报格式保持一致,所以要把8位数据转成32位数据,因此接收模块实际上是完成了8位数据转32位数据的功能。
UDP发送模块(udp_tx):UDP发送模块和接收模块比较类似,但是多了IP首部校验和和CRC循环冗余校验的计算。CRC的校验并不是在发送模块完成,而是在CRC校验模块(crc32_d8)里完成的。发送模块的发送顺序是前导码+帧起始界定符→以太网帧头→IP首部→UDP首部→UDP数据(有效数据)→CRC校验。输入的有效数据为32位数据,GMII接口为8位数据接口,因此发送模块实际上完成的是32位数据转8位数据的功能。
CRC校验模块(crc32_d8):CRC校验模块是对UDP发送模块的数据(不包括前导码和帧起始界定符)做校验,把校验结果值拼在以太网帧格式的FCS字段,如果CRC校验值计算错误或者没有的话,那么电脑网卡会直接丢弃该帧导致收不到数据(有些网卡是可以设置不做校验的)。CRC32校验在FPGA实现的原理是LFSR(Linear Feedback Shift Register,线性反馈移位寄存器),其思想是各个寄存器储存着上一次CRC32运算的结果,寄存器的输出即为CRC32的值。
其中CRC校验模块和ARP模块例化的校验模块完全相同,这里我们重点介绍UDP接收模块和UDP发送模块。
UDP接收模块按照UDP的数据格式解析数据,并实现将8位用户数据转成32位数据的功能。由UDP的数据格式可知,解析UDP数据很适合使用状态机来实现,下图为UDP接收模块的状态跳转图。
图 51.4.2 UDP接收模块的状态跳转图
接收模块使用三段式状态机来解析以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。这里需要注意的一点是,在中间状态如前导码错误、MAC地址错误以及IP地址错误时跳转到st_rx_end状态而不是跳转到st_idle状态。因为中间状态在解析到数据错误时,单包数据的接收还没有结束,如果此时跳转到st_idle状态会误把有效数据当成前导码来解析,所以状态跳转到st_rx_end。而eth_rxdv信号为0时,单包数据才算接收结束,所以st_rx_end跳转到st_idle的条件是eth_rxdv=0,准备接收下一包数据。因为代码较长,只粘贴了第三段状态机的接收数据状态和接收结束状态源代码,代码如下:
249             st_rx_data : begin         
250               //接收数据,转换成32bit            
251               if(gmii_rx_dv) begin
252                     data_cnt <= data_cnt + 16'd1;
253                     rec_en_cnt <= rec_en_cnt + 2'd1;
254                     if(data_cnt == data_byte_num - 16'd1) begin
255                         skip_en <= 1'b1;                  //有效数据接收完成
256                         data_cnt <= 16'd0;
257                         rec_en_cnt <= 2'd0;
258                         rec_pkt_done <= 1'b1;               
259                         rec_en <= 1'b1;                     
260                         rec_byte_num <= data_byte_num;
261                     end   
262                     //先收到的数据放在了rec_data的高位,所以当数据不是4的倍数时,
263                     //低位数据为无效数据,可根据有效字节数来判断(rec_byte_num)
264                     if(rec_en_cnt == 2'd0)
265                         rec_data <= gmii_rxd;
266                     else if(rec_en_cnt == 2'd1)
267                         rec_data <= gmii_rxd;
268                     else if(rec_en_cnt == 2'd2)
269                         rec_data <= gmii_rxd;      
270                     else if(rec_en_cnt==2'd3) begin
271                         rec_en <= 1'b1;
272                         rec_data <= gmii_rxd;
273                     end   
274               end
275             end   
276             st_rx_end : begin                               //单包数据接收完成   
277               if(gmii_rx_dv == 1'b0 && skip_en == 1'b0)
278                     skip_en <= 1'b1;
279             end   
程序中的st_rx_data状态表示接收UDP的有效数据,在接收完有效数据后,拉高rec_pkt_done(单包有效数据接收完成)信号,如程序中第258行代码所示。
图 51.4.3为接收过程中在线调试采集的波形图,上位机通过网口调试助手发送 (十六进制为:68 74 74 70 3A 2F 2F 77 77 77 2E 6F 70 65 6E 65 64 76 2E 63 6F 6D),图中gmii_rx_dv和gmii_rxd为GMII接口的接收有效信号和数据,skip_en为状态机的跳转信号。每次单包数据接收完成都会产生rec_pkt_done信号,rec_en和rec_data为收到的数据有效信号和32位数据。
Udp的仿真代码如下所示:
1   moduletb_udp;
2   
3   //parameterdefine
4   parameterT = 8;                     //时钟周期为8ns
5   parameterOP_CYCLE = 100;            //操作周期(发送周期间隔)
6   
7   //开发板MAC地址 00-11-22-33-44-55
8   parameterBOARD_MAC = 48'h00_11_22_33_44_55;   
9   //开发板IP地址 192.168.1.10   
10parameterBOARD_IP= {8'd192,8'd168,8'd1,8'd10};
11//目的MAC地址 ff_ff_ff_ff_ff_ff
12parameterDES_MAC   = 48'hff_ff_ff_ff_ff_ff;
13//目的IP地址 192168.1.102
14parameterDES_IP    = {8'd192,8'd168,8'd1,8'd10};
15
16//reg define
17reg         gmii_clk;    //时钟信号
18reg         sys_rst_n;   //复位信号
19
20reg         tx_start_en;
21reg   tx_data    ;
22reg   tx_byte_num;
23reg   des_mac    ;
24reg   des_ip   ;
25
26reg      flow_cnt   ;
27reg   delay_cnt;
28
29wire          gmii_rx_clk; //GMII接收时钟
30wire          gmii_rx_dv ; //GMII接收数据有效信号
31wire   gmii_rxd   ; //GMII接收数据
32wire          gmii_tx_clk; //GMII发送时钟
33wire          gmii_tx_en ; //GMII发送数据使能信号
34wire   gmii_txd   ; //GMII发送数据
35               
36wire          tx_done    ;
37wire          tx_req   ;
38
39//*****************************************************
40//**                  main code
41//*****************************************************
42
43assign gmii_rx_clk = gmii_clk   ;
44assign gmii_tx_clk = gmii_clk   ;
45assign gmii_rx_dv= gmii_tx_en ;
46assign gmii_rxd    = gmii_txd   ;
47
48//给输入信号初始值
49initial begin
50      gmii_clk         = 1'b0;
51      sys_rst_n          = 1'b0;   //复位
52      #(T+1)sys_rst_n= 1'b1;   //在第(T+1)ns的时候复位信号信号拉高
53end
54
55//125Mhz的时钟,周期则为1/125Mhz=8ns,所以每4ns,电平取反一次
56always #(T/2) gmii_clk = ~gmii_clk;
57
58always @(posedge gmii_clk or negedge sys_rst_n) begin
59      if(!sys_rst_n) begin
60          tx_start_en <= 1'b0;
61          tx_data <= 32'h_00_11_22_33;
62          tx_byte_num <= 1'b0;
63          des_mac <= 1'b0;
64          des_ip <= 1'b0;
65          delay_cnt <= 1'b0;
66          flow_cnt <= 1'b0;
67      end
68      else begin
69          case(flow_cnt)
70            'd0 : flow_cnt <= flow_cnt + 1'b1;
71            'd1 : begin
72                  tx_start_en <= 1'b1;//拉高开始发送使能信号
73                  tx_byte_num <= 16'd10;//设置发送的字节数
74                  flow_cnt <= flow_cnt + 1'b1;
75            end
76            'd2 : begin
77                  tx_start_en <= 1'b0;
78                  flow_cnt <= flow_cnt + 1'b1;
79            end   
80            'd3 : begin
81                  if(tx_req)
82                      tx_data <= tx_data + 32'h11_11_11_11;
83                  if(tx_done) begin
84                      flow_cnt <= flow_cnt + 1'b1;
85                      tx_data <= 32'h_00_11_22_33;
86                  end   
87            end
88            'd4 : begin
89                  delay_cnt <= delay_cnt + 1'b1;
90                  if(delay_cnt == OP_CYCLE - 1'b1)
91                      flow_cnt <= flow_cnt + 1'b1;
92            end
93            'd5 : begin
94                  tx_start_en <= 1'b1;//拉高开始发送使能信号
95                  tx_byte_num <= 16'd30;//设置发送的字节数
96                  flow_cnt <= flow_cnt + 1'b1;               
97            end
98            'd6 : begin
99                  tx_start_en <= 1'b0;
100               flow_cnt <= flow_cnt + 1'b1;
101             end
102             'd7 : begin
103               if(tx_req)
104                     tx_data <= tx_data + 32'h11_11_11_11;
105               if(tx_done) begin
106                     flow_cnt <= flow_cnt + 1'b1;
107                     tx_data <= 32'h_00_11_22_33;
108               end
109             end
110             default:;
111         endcase   
112   end
113 end
114
115 //例化UDP模块
116 udp                                             
117    #(
118   .BOARD_MAC   (BOARD_MAC),      //参数例化
119   .BOARD_IP      (BOARD_IP ),
120   .DES_MAC       (DES_MAC),
121   .DES_IP      (DES_IP   )
122   )
123    u_udp(
124   .rst_n         (sys_rst_n   ),
125   
126   .gmii_rx_clk   (gmii_rx_clk ),         
127   .gmii_rx_dv    (gmii_rx_dv),         
128   .gmii_rxd      (gmii_rxd    ),                  
129   .gmii_tx_clk   (gmii_tx_clk ),
130   .gmii_tx_en    (gmii_tx_en),         
131   .gmii_txd      (gmii_txd),
132
133   .rec_pkt_done(),   
134   .rec_en      (),   
135   .rec_data      (),         
136   .rec_byte_num(),      
137   .tx_start_en   (tx_start_en),      
138   .tx_data       (tx_data    ),         
139   .tx_byte_num   (tx_byte_num),
140   .des_mac       (des_mac    ),
141   .des_ip      (des_ip   ),   
142   .tx_done       (tx_done    ),      
143   .tx_req      (tx_req)         
144   );
145
146 endmodule
第10行代码将目的ip地址改写成与开发板的ip地址一样,因为仿真的原理是开发板的udp的环回,既将udp的发送数据直接发送给udp的接收数据,如果两个ip地址不一样error_en信号会拉高报错。
图 51.4.3 UDP接收仿真的波形图
UDP发送模块按照UDP的数据格式发送数据,并将32位用户数据转成8位数据的功能,也就是接收模块的逆过程。同样也非常适合使用状态机来完成发送数据的功能,状态跳转图如下图所示:
图 51.4.4 UDP发送模块的状态跳转图
发送模块和接收模块有很多相似之处,同样使用三段式状态机来发送以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。
发送模块的代码中定义了数组来存储以太网的帧头、IP首部以及UDP的首部,在复位时初始化数组的值,部分源代码如下。
70reg   preamble; //前导码
71reg   eth_head ; //以太网首部
72regip_head   ; //IP首部 + UDP首部
省略部分代码……
210         //初始化数组   
211         //前导码 7个8'h55 + 1个8'hd5
212         preamble <= 8'h55;               
213         preamble <= 8'h55;
214         preamble <= 8'h55;
215         preamble <= 8'h55;
216         preamble <= 8'h55;
217         preamble <= 8'h55;
218         preamble <= 8'h55;
219         preamble <= 8'hd5;
220         //目的MAC地址
221         eth_head <= DES_MAC;
222         eth_head <= DES_MAC;
223         eth_head <= DES_MAC;
224         eth_head <= DES_MAC;
225         eth_head <= DES_MAC;
226         eth_head <= DES_MAC;
227         //源MAC地址
228         eth_head <= BOARD_MAC;
229         eth_head <= BOARD_MAC;
230         eth_head <= BOARD_MAC;
231         eth_head <= BOARD_MAC;
232         eth_head <= BOARD_MAC;
233         eth_head <= BOARD_MAC;
234         //以太网类型
235         eth_head <= ETH_TYPE;
236         eth_head <= ETH_TYPE;      
237   end
以上代码在复位时对数组进行初始化。
245             st_idle   : begin
246               if(trig_tx_en) begin
247                     skip_en <= 1'b1;
248                     //版本号:4 首部长度:5(单位:32bit,20byte/4=5)
249                     ip_head <= {8'h45,8'h00,total_num};   
250                     //16位标识,每次发送累加1      
251                     ip_head <= ip_head + 1'b1;
252                     //bit: 010表示不分片
253                     ip_head <= 16'h4000;   
254                     //协议:17(udp)                  
255                     ip_head <= {8'h40,UDP_TYPE,16'h0};   
256                     //源IP地址               
257                     ip_head <= BOARD_IP;
258                     //目的IP地址   
259                     if(des_ip != 32'd0)
260                         ip_head <= des_ip;
261                     else
262                         ip_head <= DES_IP;      
263                     //16位源端口号:123416位目的端口号:1234                     
264                     ip_head <= {16'd1234,16'd1234};
265                     //16位udp长度,16位udp校验和            
266                     ip_head <= {udp_num,16'h0000};
267                     //更新MAC地址
268                     if(des_mac != 48'b0) begin
269                         //目的MAC地址
270                         eth_head <= des_mac;
271                         eth_head <= des_mac;
272                         eth_head <= des_mac;
273                         eth_head <= des_mac;
274                         eth_head <= des_mac;
275                         eth_head <= des_mac;
276                     end
277               end   
278             end      
在程序的第249行至266行代码,为IP首部数组进行赋值。
345            st_tx_data: begin                           //发送数据
346               crc_en <= 1'b1;
347               gmii_tx_en <= 1'b1;
348               tx_byte_sel <= tx_byte_sel + 2'd1;
349               if(tx_byte_sel == 1'b0)
350                     gmii_txd <= tx_data;
351               else if(tx_byte_sel == 2'd1)
352                     gmii_txd <= tx_data;                  
353               else if(tx_byte_sel == 2'd2) begin
354                     gmii_txd <= tx_data;   
355                     if(data_cnt != tx_data_num - 16'd2)
356                         tx_req <= 1'b1;
357               end
358               else if(tx_byte_sel == 2'd3)
359                     gmii_txd <= tx_data;   
360               if(data_cnt < tx_data_num - 16'd1)
361                     data_cnt <= data_cnt + 16'd1;                        
362               else if(data_cnt == tx_data_num - 16'd1)begin
363                     //如果发送的有效数据少于18个字节,在后面填补充位
364                     //补充的值为最后一次发送的有效数据
365                     tx_req <= 1'b0;
366                     if(data_cnt + real_add_cnt < real_tx_data_num - 16'd1)
367                         real_add_cnt <= real_add_cnt + 5'd1;
368                     else begin
369                         skip_en <= 1'b1;
370                         data_cnt <= 16'd0;
371                         real_add_cnt <= 5'd0;
372                         tx_byte_sel <= 2'd0;                        
373                     end   
374                     if(real_add_cnt > 0) begin
375                         gmii_txd <= 8'd0;
376                     end   
377               end   
378             end
程序第345行至378行代码为发送UDP数据段的状态。我们前面讲过以太网帧格式的数据部分最少是46个字节,去掉IP首部字节和UDP首部字节后,有效数据至少为18个字节,程序设计中已经考虑到这种情况,当发送的有效数据少于18个字节时,会在有效数据后面发送补充位,填充的数据为0。
379             st_crc      : begin                        //发送CRC校验值
380               gmii_tx_en <= 1'b1;
381               tx_byte_sel <= tx_byte_sel + 2'd1;
382               if(tx_byte_sel == 2'd0)
383                     gmii_txd <= {~crc_next, ~crc_next, ~crc_next,~crc_next,
384                                  ~crc_next, ~crc_next, ~crc_next,~crc_next};
385               else if(tx_byte_sel == 2'd1)
386                     gmii_txd <= {~crc_data,~crc_data, ~crc_data,~crc_data,
387                              ~crc_data,~crc_data, ~crc_data,~crc_data};
388               else if(tx_byte_sel == 2'd2) begin
389                     gmii_txd <= {~crc_data, ~crc_data, ~crc_data,~crc_data,
390                              ~crc_data,~crc_data, ~crc_data,~crc_data};                              
391               end
392               else if(tx_byte_sel == 2'd3) begin
393                     gmii_txd <= {~crc_data, ~crc_data, ~crc_data,~crc_data,
394                                  ~crc_data, ~crc_data, ~crc_data,~crc_data};
395                     tx_done_t <= 1'b1;
396                     skip_en <= 1'b1;
397               end                                                                                                                                          
398             end
程序的第379行至398行代码为发送CRC校验值状态,发送模块的CRC校验是由crc32_d4模块完成的,发送模块将输入的crc的计算结果每4位高低位互换,按位取反发送出去。
图 51.4.5为发送过程中在线抓取的波形图,图中tx_start_en作为开始发送的启动信号,eth_tx_en和eth_tx_data即为GMII接口的发送接口。在开始发送以太网帧头时crc_en拉高,开始CRC校验的计算,在将要发送有效数据时拉高tx_req(发送数据请求)信号,tx_data即为待发送的有效数据,在所有数据发送完成后输出tx_done(发送完成)信号和crc_clr(CRC校验值复位)信号。
图 51.4.5 UDP发送仿真的波形图
以太网控制模块的代码如下:
1module eth_ctrl(
2      input            clk       ,   //系统时钟
3      input            rst_n   ,   //系统复位信号,低电平有效
4      //ARP相关端口信号                                 
5      input            arp_rx_done,    //ARP接收完成信号
6      input            arp_rx_type,    //ARP接收类型 0:请求1:应答
7      outputreg      arp_tx_en,      //ARP发送使能信号
8      output             arp_tx_type,    //ARP发送类型 0:请求1:应答
9      input            arp_tx_done,    //ARP发送完成信号
10   input            arp_gmii_tx_en, //ARP GMII输出数据有效信号
11   input       arp_gmii_txd,   //ARP GMII输出数据
12   //UDP相关端口信号
13   input            udp_tx_start_en,//UDP开始发送信号
14   input            udp_tx_done,    //UDP发送完成信号
15   input            udp_gmii_tx_en, //UDP GMII输出数据有效信号
16   input       udp_gmii_txd,   //UDP GMII输出数据   
17   //GMII发送引脚                     
18   output             gmii_tx_en,   //GMII输出数据有效信号
19   output        gmii_txd      //UDP GMII输出数据
20   );
21
22 //reg define
23 reg      protocol_sw; //协议切换信号
24 reg      udp_tx_busy; //UDP正在发送数据标志信号
25 reg      arp_rx_flag; //接收到ARP请求信号的标志
26
27 //*****************************************************
28 //**                  main code
29 //*****************************************************
30
31 assign arp_tx_type = 1'b1;   //ARP发送类型固定为ARP应答                                 
32 assign gmii_tx_en = protocol_sw ? udp_gmii_tx_en : arp_gmii_tx_en;
33 assign gmii_txd = protocol_sw ? udp_gmii_txd : arp_gmii_txd;
34
35 //控制UDP发送忙信号
36 always @(posedge clk or negedge rst_n) begin
37   if(!rst_n)
38         udp_tx_busy <= 1'b0;
39   else if(udp_tx_start_en)   
40         udp_tx_busy <= 1'b1;
41   else if(udp_tx_done)
42         udp_tx_busy <= 1'b0;
43 end
44
45 //控制接收到ARP请求信号的标志
46 always @(posedge clk or negedge rst_n) begin
47   if(!rst_n)
48         arp_rx_flag <= 1'b0;
49   else if(arp_rx_done && (arp_rx_type == 1'b0))   
50         arp_rx_flag <= 1'b1;
51   else if(protocol_sw == 1'b0)
52         arp_rx_flag <= 1'b0;
53 end
54
55 //控制protocol_sw和arp_tx_en信号
56 always @(posedge clk or negedge rst_n) begin
57   if(!rst_n) begin
58         protocol_sw <= 1'b0;
59         arp_tx_en <= 1'b0;
60   end
61   else begin
62         arp_tx_en <= 1'b0;
63         if(udp_tx_start_en)
64             protocol_sw <= 1'b1;
65         else if(arp_rx_flag && (udp_tx_busy == 1'b0)) begin
66             protocol_sw <= 1'b0;
67             arp_tx_en <= 1'b1;
68         end   
69   end      
70 end
71
72
73 endmodule
以太网控制模块的代码较简单,如果输入的arp_rx_done(ARP接收完成信号)为高电平,且arp_rx_type为低电平(ARP接收类型为请求)时,表示接收到ARP请求数据包,此时拉高arp_rx_flag信号;当arp_rx_flag为高电平,且udp_tx_busy(当前UDP发送模块处于空闲状态)信号为低电平时,此时拉高arp_tx_en信号,开始控制ARP顶层模块发送ARP应答数据包,并拉低protocol_sw信号,此时GMII发送端口信号和ARP顶层模块的发送端口信号相连。
当protocol_sw等于1时,GMII发送引脚和UDP GMII发送引脚相连,否则和ARP GMII发送引脚相连,如程序中第32行第33行代码所示。
51.5下载验证
编译工程并生成比特流.sbit文件后,此时将下载器一端连接电脑,另一端与开发板上的JTAG下载口连接,将网线一端连接开发板的网口,另一端连接电脑的网口或者路由器,接下来连接电源线,并打开开发板的电源开关,网口的位置如下图所示。
图 51.5.1 网口位置
点击PDS工具栏的下载按钮,在弹出的Fabric Configuration界面中双击“Boundary Scan”,我们将生成好的sbit流文件下载到开发板中去。
程序下载完成后,PHY芯片会和电脑网卡进行通信(自协商),如果程序下载正确并且硬件连接无误的话,我们点击电脑右下角的网络图标,会看到本地连接刚开始显示的是正在识别,一段时间之后显示未识别的网络,打开方式如下图所示(WIN7和WIN10操作可能存在差异,但基本相同)。
图 51.5.2 点击网络图标
接下来就可以使用网口调试助手Center进行通信了,该工具位于开发板所随附的资料“6_软件资料/1_软件/网口调试助手”目录下(打开网口调试助手前,开发板必须硬件连接正确并且程序下载完成)。网口调试助手打开界面如下图所示:
图 51.5.3 网口调试助手界面
打开网口调试助手后,协议类型选择:UDP;本地主机地址选择:本地连接的IP地址(在这里是192.168.1.102);本地主机端口号:1234;设置完成后点击【打开】按钮。如下图所示:
图 51.5.4 网口调试助手打开界面
远程主机选择:192.168.1.10 : 1234 (开发板的IP地址和端口号),在这里本机主端口号和远程主机端口号都为1234,见udp_tx模块,源代码如下所示:
262 //16位源端口号:123416位目的端口号:1234                     
263 ip_head <= {16'd1234,16'd1234};
网口调试助手打开后,在发送文本框中输入数据“http://www.openedv.com/forum.php”并点击发送,如下图所示:
图 51.5.5 网口调试助手收发数据界面
可以看到网口调试助手中接收到数据“http://www.openedv.com/forum.php”,接收到的数据与发送的数据一致。
接下来通过Wireshark软件抓取网口的数据包,界面如下图所示:
图 51.5.6 wireshark打开界面
双击上图所示的以太网或者先选中以太网,再点击上方红框选中的蓝色按钮,即可开始抓取本地连接的数据包,抓取界面如下图所示:
图 51.5.7 wireshark以太网打开界面
从上图可以看到,已经抓取到其它应用程序使用以太网发送的数据包,但是这些数据包并不是开发板发送的数据包,我们这个时候重新在网口调试助手中点击“发送”按钮,可以看到Wireshark软件中抓取的数据,如下图所示。
图 51.5.8 wireshark以太网抓取到的数据包
上图中第17行是上位机发送的ARP请求数据包,第18行是开发板返回的ARP应答数据包,第19行是上位机发送的UDP数据包,第20行是开发板返回的UDP数据包。双击开发板返回的数据包,可以看到开发板发送的详细数据,如下图所示:
图 51.5.9 Wireshark抓取到的详细数据
由上图可知,源IP地址(开发板IP地址)为192.168.1.10,目的IP地址(电脑IP地址)为192.168.1.102,源端口号和目的端口号都是1234。上图中下方红框为开发板发送的16进制数据(去掉前导码、SFD和CRC值),可以看到,UDP的用户数据段对应的ASIC码为“http://www.openedv.com/forum.php”。
页: [1]
查看完整版本: 《ATK-DFPGL22G之FPGA开发指南_V1.0》第五十一章以太网UDP测试实验