本帖最后由 正点原子运营 于 2024-1-3 17:34 编辑
第五十一章 以太网UDP测试实验
1)实验平台:正点原子 ATK-DFPGL22G开发板
2) 章节摘自【正点原子】ATK-DFPGL22G之FPGA开发指南_V1.0
6)FPGA技术交流QQ群:435699340
UDP是一种面向无连接的传输层协议,属于TCP/IP协议簇的一种。UDP具有消耗资源少、通信效率高等优点,通常用来传输音频、视频等对实时性要求高的场合。本章我们来学习如何通过 ATK-DFPGL22G开发板实现UDP通信的功能。 本章分为以下几个章节: 1.1 简介 1.2 实验任务 1.3 硬件设计 1.4 程序设计 1.5 下载验证
1.1 简介UDP概述 UDP(User Datagram Protocol),即用户数据报协议,是一种面向无连接的传输层协议。无连接是指在传输数据时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输(如视频会议等)都会采用UDP协议进行传输,这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。 UDP和TCP是传输层中非常重要的两个协议,位于OSI(Open System Interconnection,开放式系统互联)参考模型中的第四层(传输层),是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,位于IP协议层(网络层)之上。OSI将计算机网络体系结构分为七层:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,OSI参考模型如下图所示。 以太网UDP传输单包数据的格式由图 51.1.1所示。从图中可以看出,以太网的数据包就是对各层协议的逐层封装来实现数据的传输。用户数据打包在UDP协议中,UDP协议又是基于IP协议之上的,IP协议又是走MAC层发送的,即从包含关系来说:MAC帧中的数据段为IP数据报,IP报文中的数据段为UDP报文,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所示。 前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首部数据如下: 按照上述提到的 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所示: 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四个报文的关系如下图所示: 用户数据打包在UDP协议中,UDP协议又是基于IP协议之上的,IP协议又是走MAC层发送的,即从包含关系来说:MAC帧中的数据段为IP数据报,IP报文中的数据段为UDP报文,UDP报文中的数据段为用户希望传输的数据内容。现在再回过头看图 51.1.2的内容就非常容易理解了。
1.2 实验任务本节实验任务是上位机通过网口调试助手发送数据给FPGA,FPGA通过以太网接口接收数据并将接收到的数据发送给上位机,完成以太网UDP数据的环回。
1.3 硬件设计千兆以太网接口部分的硬件设计原理及本实验中各端口信号的管脚分配,和“以太网ARP测试实验”完全相同,请参考“以太网ARP读写测试实验”中的硬件设计部分。
1.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。 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 TORGMII(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接收模块的状态跳转图。 接收模块使用三段式状态机来解析以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。这里需要注意的一点是,在中间状态如前导码错误、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[31:24] <= gmii_rxd;
- 266 else if(rec_en_cnt == 2'd1)
- 267 rec_data[23:16] <= gmii_rxd;
- 268 else if(rec_en_cnt == 2'd2)
- 269 rec_data[15:8] <= gmii_rxd;
- 270 else if(rec_en_cnt==2'd3) begin
- 271 rec_en <= 1'b1;
- 272 rec_data[7:0] <= 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为接收过程中在线调试采集的波形图,上位机通过网口调试助手发送www.openedv.com/forum.php(十六进制为: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 module tb_udp;
- 2
- 3 //parameter define
- 4 parameter T = 8; //时钟周期为8ns
- 5 parameter OP_CYCLE = 100; //操作周期(发送周期间隔)
- 6
- 7 //开发板MAC地址 00-11-22-33-44-55
- 8 parameter BOARD_MAC = 48'h00_11_22_33_44_55;
- 9 //开发板IP地址192.168.1.10
- 10 parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10};
- 11 //目的MAC地址ff_ff_ff_ff_ff_ff
- 12 parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff;
- 13 //目的IP地址192168.1.102
- 14 parameter DES_IP = {8'd192,8'd168,8'd1,8'd10};
- 15
- 16 //reg define
- 17 reg gmii_clk; //时钟信号
- 18 reg sys_rst_n; //复位信号
- 19
- 20 reg tx_start_en;
- 21 reg [31:0] tx_data ;
- 22 reg [15:0] tx_byte_num;
- 23 reg [47:0] des_mac ;
- 24 reg [31:0] des_ip ;
- 25
- 26 reg [3:0] flow_cnt ;
- 27 reg [13:0] delay_cnt ;
- 28
- 29 wire gmii_rx_clk; //GMII接收时钟
- 30 wire gmii_rx_dv ; //GMII接收数据有效信号
- 31 wire [7:0] gmii_rxd ; //GMII接收数据
- 32 wire gmii_tx_clk; //GMII发送时钟
- 33 wire gmii_tx_en ; //GMII发送数据使能信号
- 34 wire [7:0] gmii_txd ; //GMII发送数据
- 35
- 36 wire tx_done ;
- 37 wire tx_req ;
- 38
- 39 //*****************************************************
- 40 //** main code
- 41 //*****************************************************
- 42
- 43 assign gmii_rx_clk = gmii_clk ;
- 44 assign gmii_tx_clk = gmii_clk ;
- 45 assign gmii_rx_dv = gmii_tx_en ;
- 46 assign gmii_rxd = gmii_txd ;
- 47
- 48 //给输入信号初始值
- 49 initial begin
- 50 gmii_clk = 1'b0;
- 51 sys_rst_n = 1'b0; //复位
- 52 #(T+1) sys_rst_n = 1'b1; //在第(T+1)ns的时候复位信号信号拉高
- 53 end
- 54
- 55 //125Mhz的时钟,周期则为1/125Mhz=8ns,所以每4ns,电平取反一次
- 56 always #(T/2) gmii_clk = ~gmii_clk;
- 57
- 58 always @(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信号会拉高报错。 UDP发送模块按照UDP的数据格式发送数据,并将32位用户数据转成8位数据的功能,也就是接收模块的逆过程。同样也非常适合使用状态机来完成发送数据的功能,状态跳转图如下图所示: 发送模块和接收模块有很多相似之处,同样使用三段式状态机来发送以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。 发送模块的代码中定义了数组来存储以太网的帧头、IP首部以及UDP的首部,在复位时初始化数组的值,部分源代码如下。 - 70 reg [7:0] preamble[7:0] ; //前导码
- 71 reg [7:0] eth_head[13:0] ; //以太网首部
- 72 reg [31:0] ip_head[6:0] ; //IP首部 + UDP首部
复制代码省略部分代码…… - 210 //初始化数组
- 211 //前导码 7个8'h55 + 1个8'hd5
- 212 preamble[0] <= 8'h55;
- 213 preamble[1] <= 8'h55;
- 214 preamble[2] <= 8'h55;
- 215 preamble[3] <= 8'h55;
- 216 preamble[4] <= 8'h55;
- 217 preamble[5] <= 8'h55;
- 218 preamble[6] <= 8'h55;
- 219 preamble[7] <= 8'hd5;
- 220 //目的MAC地址
- 221 eth_head[0] <= DES_MAC[47:40];
- 222 eth_head[1] <= DES_MAC[39:32];
- 223 eth_head[2] <= DES_MAC[31:24];
- 224 eth_head[3] <= DES_MAC[23:16];
- 225 eth_head[4] <= DES_MAC[15:8];
- 226 eth_head[5] <= DES_MAC[7:0];
- 227 //源MAC地址
- 228 eth_head[6] <= BOARD_MAC[47:40];
- 229 eth_head[7] <= BOARD_MAC[39:32];
- 230 eth_head[8] <= BOARD_MAC[31:24];
- 231 eth_head[9] <= BOARD_MAC[23:16];
- 232 eth_head[10] <= BOARD_MAC[15:8];
- 233 eth_head[11] <= BOARD_MAC[7:0];
- 234 //以太网类型
- 235 eth_head[12] <= ETH_TYPE[15:8];
- 236 eth_head[13] <= ETH_TYPE[7:0];
- 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[0] <= {8'h45,8'h00,total_num};
- 250 //16位标识,每次发送累加1
- 251 ip_head[1][31:16] <= ip_head[1][31:16] + 1'b1;
- 252 //bit[15:13]: 010表示不分片
- 253 ip_head[1][15:0] <= 16'h4000;
- 254 //协议:17(udp)
- 255 ip_head[2] <= {8'h40,UDP_TYPE,16'h0};
- 256 //源IP地址
- 257 ip_head[3] <= BOARD_IP;
- 258 //目的IP地址
- 259 if(des_ip != 32'd0)
- 260 ip_head[4] <= des_ip;
- 261 else
- 262 ip_head[4] <= DES_IP;
- 263 //16位源端口号:1234 16位目的端口号:1234
- 264 ip_head[5] <= {16'd1234,16'd1234};
- 265 //16位udp长度,16位udp校验和
- 266 ip_head[6] <= {udp_num,16'h0000};
- 267 //更新MAC地址
- 268 if(des_mac != 48'b0) begin
- 269 //目的MAC地址
- 270 eth_head[0] <= des_mac[47:40];
- 271 eth_head[1] <= des_mac[39:32];
- 272 eth_head[2] <= des_mac[31:24];
- 273 eth_head[3] <= des_mac[23:16];
- 274 eth_head[4] <= des_mac[15:8];
- 275 eth_head[5] <= des_mac[7:0];
- 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[31:24];
- 351 else if(tx_byte_sel == 2'd1)
- 352 gmii_txd <= tx_data[23:16];
- 353 else if(tx_byte_sel == 2'd2) begin
- 354 gmii_txd <= tx_data[15:8];
- 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[7:0];
- 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[0], ~crc_next[1], ~crc_next[2],~crc_next[3],
- 384 ~crc_next[4], ~crc_next[5], ~crc_next[6],~crc_next[7]};
- 385 else if(tx_byte_sel == 2'd1)
- 386 gmii_txd <= {~crc_data[16],~crc_data[17], ~crc_data[18],~crc_data[19],
- 387 ~crc_data[20],~crc_data[21], ~crc_data[22],~crc_data[23]};
- 388 else if(tx_byte_sel == 2'd2) begin
- 389 gmii_txd <= {~crc_data[8], ~crc_data[9], ~crc_data[10],~crc_data[11],
- 390 ~crc_data[12],~crc_data[13], ~crc_data[14],~crc_data[15]};
- 391 end
- 392 else if(tx_byte_sel == 2'd3) begin
- 393 gmii_txd <= {~crc_data[0], ~crc_data[1], ~crc_data[2],~crc_data[3],
- 394 ~crc_data[4], ~crc_data[5], ~crc_data[6],~crc_data[7]};
- 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校验值复位)信号。 以太网控制模块的代码如下: - 1 module 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 output reg 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 [7:0] 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 [7:0] udp_gmii_txd, //UDP GMII输出数据
- 17 //GMII发送引脚
- 18 output gmii_tx_en, //GMII输出数据有效信号
- 19 output [7:0] 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行代码所示。
1.5 下载验证编译工程并生成比特流.sbit文件后,此时将下载器一端连接电脑,另一端与开发板上的JTAG下载口连接,将网线一端连接开发板的网口,另一端连接电脑的网口或者路由器,接下来连接电源线,并打开开发板的电源开关,网口的位置如下图所示。 点击PDS工具栏的下载按钮,在弹出的Fabric Configuration界面中双击“Boundary Scan”,我们将生成好的sbit流文件下载到开发板中去。 程序下载完成后,PHY芯片会和电脑网卡进行通信(自协商),如果程序下载正确并且硬件连接无误的话,我们点击电脑右下角的网络图标,会看到本地连接刚开始显示的是正在识别,一段时间之后显示未识别的网络,打开方式如下图所示(WIN7和WIN10操作可能存在差异,但基本相同)。 接下来就可以使用网口调试助手进行通信了,该工具位于开发板所随附的资料“6_软件资料/1_软件/网口调试助手”目录下(打开网口调试助手前,开发板必须硬件连接正确并且程序下载完成)。网口调试助手打开界面如下图所示: 打开网口调试助手后,协议类型选择:UDP;本地主机地址选择:本地连接的IP地址(在这里是192.168.1.102);本地主机端口号:1234;设置完成后点击【打开】按钮。如下图所示: 远程主机选择:192.168.1.10: 1234 (开发板的IP地址和端口号),在这里本机主端口号和远程主机端口号都为1234,见udp_tx模块,源代码如下所示: - 262 //16位源端口号:1234 16位目的端口号:1234
- 263 ip_head[5] <= {16'd1234,16'd1234};
复制代码接下来通过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”。 |