UDP是一种面向无连接的传输层协议,属于TCP/IP协议族的一种,UDP具有消耗资源少、通信效率高等优点,一般性地用来传输音频或者视频等对实时性要求高的场合。
ICMP是TCP/IP协议族的一个IP层子协议,包含在IP数据报里,主要用于IP主机、路由器之间传递控制消息,控制消息是指网络是否连通,主机是否可达等功能,其中被大家所熟知的ping功能就是基于ICMP协议进行工作的。
ping主要是用来探测主机和主机之间是否可以进行通信,如果不能ping到某台主机,表示不能与这台主机建立连接。ping使用的是ICMP协议,发送ICMP回送请求消息给目的主机。ICMP协议规定:目的主机必须返回ICMP回送应答消息给源主机,如果源主机在一定时间内收到应答,则代表主机可达。
在这个例程,我们会编码实现千兆以太网的ARP、ICMP、UDP三个常用的通信协议,当然在此之前笔者会和大家一起整理整理以太网OSI七层参考模型的知识,回顾回顾大学上的谢希仁版计算机网络课,OSI将计算机网络体系结构分为七层即:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,OSI分层结构如图1所示,工作实践以后再去思考学校里学到的理论知识,理论结合实践,更加加深对知识要点的理解。
图1 OSI分层结构
也许在本科读书的时候,课本上会给出固定的结论,告诉大家在以太网的每一层上具体去实现什么协议类型,但当时也迷迷糊糊,可能突击考试的时候会记住过后又还给老师了,实际上透过现象看本质,不妨把以太网上的层层数据传输看成是一个数据组封装、解封装的过程。
这里以TCP/IP的五层模型为例,如图2所示是以太网上数据发报组封装的示意图,可以看到上层数据需要层层加报头才能最后形成一个完整的帧并通过物理层发送出去,而如图3所示是以太网上数据收报解封装的示意图,同样的在收到帧以后,需要层层解报头才能最后获取上层数据。
图2 以太网上数据发报组封装
图3 以太网上数据收报解封装
如图4所示是以太网上各层间的通信示意图,形象地描绘了整个以太网的通信各个主机之间通过不同的路由和网关,组报数据和报头发送以太网帧,解报数据和报头接收以太网帧的动态过程。
图4 以太网上各层间的通信示意图
如图5所示是以太网UDP报文格式,大家可以清楚地看到,UDP报文由:前导码、以太网首部、IP首部、UDP首部、用户数据、CRC32共同组成,其实就是层层组报和解报的过程,所以对于软件设计上来说我们也采用模块划分的思想,把报文解析模块层层细化,然后通过一些使能信号触发对应报文的发送,一方面使得代码的整体逻辑更加简化明确,另一方面在调试定位问题上也更加清晰直白。
图5 以太网UDP报文格式
对于以太网首部解析模块的代码设计,大家直接把上个例程的mac_receive_analy模块拿过来用即可,但需要简单改进一下,因为上个例程中只针对了ARP帧的解析,但在这个例程中我们还需要解析ICMP和UDP帧,而这两种帧类型都属于TCP/IP协议族,即需要在以太网首部14字节的最后2字节的类型中判断其为IP协议还是ARP协议,如果是IP协议那么继续解析IP首部,再进一步判断是ICMP还是UDP帧,如果是ARP协议那么继续解析ARP数据内容。
图6 IP数据报格式
如图6所示是IP数据报格式,IP协议是TCP/IP协议族中的核心协议,也是TCP/IP 协议的载体,IP协议规定了数据传输时的基本单元和格式。IP协议位于以太网MAC帧格式的数据段,同时IP协议内容由IP首部和数据字段组成,其中IP数据报各个字段的说明如下:
版本:4位IP版本号,这个值设置为二进制的0100时表示IPv4,设置为0110时表示 IPv6,目前使用比较多的IP协议版本号是4。
首部长度:4位首部长度,表示IP首部一共有多少个32位即4个字节,默认在没有可选字段时,IP首部长度为20个字节,因此首部长度的值为5。
区分服务:8位服务类型,该字段被划分成两个子字段:3 位优先级字段和4位TOS字段最后一位固定为 0,一般默认设置服务类型为0代表一般服务。
总长度:16 位IP数据报总长度,包括IP首部和IP数据部分以字节为单位。所以利用IP首部长度和IP数据报总长度,就可以知道IP数据报中数据内容的起始位置和长度。因为该字段长16位,所以理论上IP数据报最长可达65535字节的IP数据报,但实际还要考虑网络的最大承载能力等因素。
标识字段:16位标识字段,用来标识主机发送的每一份数据报,通常每发送一份报文 其值就会累加一。
标志字段:3位标志字段,第1位为保留位;第2位表示禁止分片其中1表示不分片0:允许分片;第3位标识更多分片,除了数据报的最后一个分片外,其它分片都为1。
片偏移:13 位片偏移,在接收方进行数据报重组时用来标识分片的顺序。
生存时间:8 位生存时间字段,TTL域防止丢失的数据包在无休止的传播,一般被设置为64或者128。
协议:8位协议类型,表示此数据报所携带上层数据使用的协议类型,常用协议类型:ICMP为1,TCP为6,UDP为17。
首部校验和:16位首部校验和,该字段只校验数据报的首部,不包含数据部分,校验 IP 数据报头部是否正确。
源IP地址:32 位源IP地址,即发送端的IP地址。
目的IP地址:32 位目的IP地址,即接收端的IP地址。
可选字段:是数据报中的一个可变长度的可选信息,选项字段以32位为界,不足时则插入0的填充字节,从而确保IP首部始终是32位的整数倍。
细心的同学不难发现,对于IP首部解析最难的地方其实就在首部校验和的计算上,事实上IP首部计算并不复杂,但我们需要把这个计算过程用FPGA时序逻辑加以实现。
对于发送方来说计算IP首部检验和的过程如下:初始计算校验和字段时该字段全部用0填充,IP头部以16位为一个单位,逐个进行二进制反码求和运算,最后把得到结果取反,再放入校验和字段即可。
对于接收方来说验证IP首部检验和的过程如下:把接收的IP报文头部以16位为单位逐个求和,如果最后结果全为1,那么校验正确,否则校验错误。
为了方便大家,这里举个IP首部计算和验证的例子,对于发送方IP首部起始为:45 00 00 31 89 f5 00 00 6e 06 00 00 de b7 45 5d c0 a8 00 dc,其中IP首部校验和计算过程如下:
4500 + 0031 + 89f5 + 0000 + 6e06 + 0000 + deb7 + 455d + c0a8 + 00dc = 322c4,结果出现了进位,则继续迭代计算:22c4 + 0003 = 22c7,22c7取反即得dd38,故将dd38填充到IP首部检验和的位置。
对于接收方验证IP首部时,直接相加即:4500 + 0031 + 89f5 + 0000 + 6e06 + dd38 + deb7 + 455d + c0a8 + 00dc = 3fffc,结果出现了进位,则继续迭代计算:fffc + 0003 = ffff,得到的结果全为1那么校验正确。
图7 IP首部检验和计算示意图
如上图7所示是一种简洁的IP首部检验和计算时序逻辑,用check_head_sum信号作为IP首部检验和,并人为的将GMII总线上的gmii_rxd打一拍得到gmii_rxd_ff,这里主要是为了将gmii_rxd和gmii_rxd_ff两个8位数据,通过数据拼接的方法整合成一个16位数据方便校验计算,同时也将check_head_sum打一拍得到check_head_sum_ff,这里主要是为了方便相加操作求IP首部校验和,大家不妨对照上面的时序逻辑示意图再仔细思考下整个计算流程。
如表1所示是ip_receive_analy模块信号列表,在这个模块里我们主要去实现如下3个功能:1.从IP首部20个字节的第5到第8个字节中获取IP报文的总长度;2.判断该帧是ICMP帧还是UDP帧,因为这两种帧类型都属于TCP/IP协议族,都在IP首部中标明,所以我们就用udp_analy_en和icmp_analy_en两个使能信号去激活下游udp_receive_analy 和icmp_receive_analy两个模块;3.计算IP首部校验和并验证其是否正确,如图8所示是IP首部解析模块的代码设计。
信号列表 |
||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
ip_analy_en |
I |
1 |
gmii_rxd |
I |
8 |
gmii_rx_dv |
I |
1 |
local_ip_addr |
I |
32 |
pc_ip_addr |
I |
32 |
get_ip_total_length |
O |
32 |
udp_analy_en |
O |
1 |
icmp_analy_en |
O |
1 |
ip_checksum_err |
O |
1 |
表1 ip_receive_analy模块信号列表
如下图9所示是UDP数据报格式,其中UDP首部共8个字节,和IP首部一样也是一行以32位为单位。
源端口号:16 位发送端的端口号,用于区分不同服务的端口,其中端口号的范围从 0 到 65535;
目的端口号:16位接收端的端口号,端口号的范围同样也是从0到65535;
UDP长度:16位UDP长度,包含UDP首部长度+数据长度,并以字节为单位;
UDP校验和:16位UDP校验和。
图9 UDP数据报格式
UDP计算校验和的方法和计算IP数据报首部校验和的方法相似,但不同的地方是IP首部校验和只检验IP数据报首部。但UDP校验和包含三部分:UDP伪首部、UDP首部、UDP数据部分。其中伪首部又是从IP数据报头和UDP数据报头中获取的,内容包括源IP地址、目的 IP地址、协议类型、UDP长度共计12字节,因为只是单纯为了做校验用并不在传输报文中发送,所以我们需要人为构造UDP伪首部。
如图10所示,是笔者从Wireshark下抓取的一帧UDP报文,举个实例来对比说明IP和UDP首部校验和的计算的区别,通过对这样一帧报文进行数据的分解即可清楚地看到如下部分的有效信息,其中IP和UDP首部校验和笔者都用红色标注:
以太网首部:50 7b 9d 6a ce 22 00 0a 35 01 fe c0 08 00
IP首部 :45 00 00 30 00 11 40 00 80 11 79 56 c0 a8 00 02 c0 a8 00 03
UDP伪首部:c0 a8 00 02 c0 a8 00 03 00 11 00 1c
UDP首部 :1f 90 1f 90 00 1c 90 eb
UDP数据 :48 45 4c 4c 4f 20 41 4c 49 4e 58 20 48 45 49 4a 49 4e 0d 0a
图10 Wireshark下抓取的一帧UDP报文
我们结合上面的数据来计算验证IP和UDP首部校验和,具体计算过程如下:
IP首部校验和ip_check_buffer = 4500 + 0030 + 0011 + 4000 + 8011 + 7956 + c0a8 + 0002 + c0a8 + 0003 = 2fffd,fffd + 0002 = ffff;
UDP校验和udp_check_buffer = c0a8 + 0002 + c0a8 + 0003 + 0011 + 001c + 1f90 + 1f90 + 001c + 90eb + 4845 + 4c4c + 4f20 + 414c + 494e + 5820 +4845 + 494a + 494e + 0d0a = 4fffb,fffb+ 0004 = ffff;(UDP伪首部红色标注,UDP首部蓝色标注,UDP数据绿色标志)
如表2所示是udp_receive_analy模块信号列表,在这个模块里我们主要去实现如下3个功能:1.从UDP首部8个字节的第7、8个字节中获取UDP报文的总长度;2.向udpram的伪双口RAM中顺序写入UDP帧中接收到的数据;3.构造伪首部,计算UDP首部校验和并验证其是否正确并用udp_tx_en使能信号去激活下游udp_transfer模块向PC端发送同样数据内容的UDP帧,如图11所示是UDP首部解析模块的代码设计。
信号列表 |
||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
udp_analy_en |
I |
1 |
ip_checksum_err |
I |
1 |
local_mac_addr |
I |
48 |
pc_mac_addr |
I |
48 |
local_ip_addr |
I |
32 |
pc_ip_addr |
I |
32 |
local_port_addr |
I |
16 |
pc_port_addr |
I |
16 |
gmii_rxd |
I |
8 |
gmii_rx_dv |
I |
1 |
udpram_wr_addr |
O |
16 |
udpram_wr_din |
O |
8 |
udpram_wea |
O |
1 |
get_udp_total_length |
O |
16 |
udp_tx_en |
O |
1 |
表2 udp_receive_analy模块信号列表
如下图12所示是FPGA流水线数据相加示意图,这里充分体现FPGA设计中以面积换速度的思想,其中ABCD四个输入,A与B相加结果为E,C与D相加结果为F,再将E与F相加结果为G,在每个相加结果之后都有寄存器。在UDP数据解析模块的代码设计中,针对UDP首部校验和中的三部分:UDP伪首部、UDP首部、UDP数据,笔者均采用了这种流水线方式,感兴趣的同学可以对照代码再深入思考理解一下。
这里也是用了伪双口RAM进行数据缓存,主要是考虑UDP帧需要首部校验,如果用FIFO做缓存校验错误,那么写入FIFO中的错误数据必然要读出清理出去,才能使得正确的数据再被读出,当然FPGA设计当中非常灵活,有同学考虑可以采用拉低vld数据输入输出指示信号的方式,把FIFO中的错误数据需要读出而不发送到下游模块,也是一种很好的思路,但会造成设计上更加复杂,而用伪双口RAM即使写入错误的数据,下条数据到了直接写入相同的地址覆盖即可。
图12 FPGA流水线数据相加示意图
在依次设计完以太网首部解析模块、ARP数据解析模块、IP首部解析模块、UDP数据解析模块后,只剩下ICMP数据解析模块了,在这个模块中我们主要实现PC端发起的ping功能,也是ICMP帧中最常用和最关键的功能,ICMP是TCP/IP协议族的一个IP层子协议,包含在IP数据报里,用于IP主机、路由器之间传递控制消息,如图13所示是ICMP数据报格式,其中回送请求报文类型为8'h08,回答报文类型为8'h00。
注意到ICMP数据报中也包含了校验和,其校验和的计算方式和UDP首部校验类似,但ICMP首部校验和的计算不需要再构造伪首部,只需要代入ICMP首部和ICMP数据部分计算即可。
图13 ICMP数据报格式
如图14所示是Wireshark下抓取的一帧ICMP报文,同样地举例说明如何计算ICMP首部校验和,同样地通过对这样一帧报文进行数据的分解即可清楚地看到如下部分的有效信息,其中IP和UDP首部校验和笔者都用红色标注:
以太网首部:50 7b 9d 6a ce 22 00 0a 35 01 fe c0 08 00
IP首部 :45 00 00 3c 00 01 40 00 ff 01 fa 69 c0 a8 00 02 c0 a8 00 03
ICMP首部 :00 00 54 33
ICMP数据 :01 00 00 29 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 61 62 63 64 65 66 67 68 69
我们结合上面的数据来计算验证IP和UDP首部校验和,具体计算过程如下:
IP首部校验和ip_check_buffer = 4500 + 003c + 0001 + 4000 + ff01 + fa69 + c0a8 + 0002 + c0a8 + 0003 = 3fffc,fffc + 0003 = ffff;
ICMP校验和icmp_check_buffer = 0000 + 5433 + 0100 + 0029 + 6162 + 6364 + 6566 + 6768 + 696a + 6b6c + 6d6e + 6f70 + 7172 + 7374 + 7576 + 7761 + 6263 + 6465 + 6667 + 6869 = 6fff9,fff9+ 0006 = ffff;(ICMP首部蓝色标注,ICMP数据绿色标志)
图14 Wireshark下抓取的一帧ICMP报文
如表3所示是icmp_receive_analy模块信号列表,本模块主要去实现响应ICMP帧下的回送请求命令即实现ping操作下的回送应答。
在这个模块里我们主要去实现如下3个功能:1.从ICMP首部4个字节的第1个字节中判断报文类型是否是“回送请求”,如果是则计算ICMP的首部校验和是否正确,如果不是则忽略这条帧;2.向icmpram的伪双口RAM中顺序写入ICMP帧中接收到的数据;3.计算ICMP首部校验和并验证其是否正确并用icmp_reply_en使能信号去激活下游icmp_transfer模块向PC端发送同样数据内容的ICMP帧,但这时候需要把ICMP首部中的报文类型改成“回送应答”,PC端则可以在cmd命令行下ping通开发板,如图15所示是UDP首部解析模块的代码设计。
信号列表 |
||
信号名 |
I/O |
位宽 |
clk |
I |
1 |
rst_n |
I |
1 |
icmp_analy_en |
I |
1 |
ip_checksum_err |
I |
1 |
gmii_rxd |
I |
8 |
gmii_rx_dv |
I |
1 |
ip_total_length |
I |
16 |
icmpram_wr_addr |
O |
16 |
icmpram_wr_din |
O |
8 |
icmpram_wea |
O |
1 |
icmp_reply_en |
O |
1 |
表3 icmpram_receive_analy模块信号列表
图15 ICMP数据解析模块的代码设计
如图16所示,在eth_receive_analy_top模块中把太网首部、IP首部、ARP数据、UDP数据、ICMP数据解析各模块都例化到一起即可,再在整个设计中例化eth_receive_analy_top顶层解析模块,实现代码的模块化复用。
图16 千兆网口实现太网首部、IP首部、ARP数据、UDP数据、ICMP数据解析各模块顶层文件的例化