FPGA RGMII GMII MII接口以太网通讯之以太网简介
以太网是一种产生较早,使用相当广泛的局域网。其最初是由Xerox(施乐)公司创建并由Xerox、 Intel和DEC公司联合开发的基带局域网规范,后来被电气与电子工程师协会(IEEE)所采纳作为802.3的标准。
以太网的分类有标准以太网(10Mbit/s) ,快速以太网 (100Mbit/s)和千兆以太网(1000Mbit/s) 。随着以太网技术的飞速发展, 市场上也出现了万兆以太网(10Gbit/s) ,它扩展了IEEE802.3协议和MAC规范,使其技术支持10Gbit/s的传输速率。 然而在实际应用中, 标准以太网和快速以太网已经能够满足我们的日常需求,对通信速率要求较高的场合,才会用到千兆以太网。
以太网通信离不开连接端口的支持, 网络数据连接的端口就是以太网接口。 以太网接口类型有RJ45接口, RJ11接口(电话线接口), SC光纤接口等。其中RJ45接口是我们现在最常见的网络设备接口(如:电脑网口),我们开发板使用的就是这种接口。RJ45接口俗称“水晶头”,专业术语为RJ45连接器,由插头(接头、水晶头) 和插座(母座) 组成, 属于双绞线以太网接口类型。 RJ45插头只能沿固定方向插入,设有一个塑料弹片与RJ45插槽卡住以防止脱落。
RJ45接口样式如图 43.1.1所示:
RJ45接口定义以及各引脚功能说明如图 43.1.2所示,在以太网中只使用了1,2,3,6这四根线,其中1, 2这组负责传输数据(TX+、 TX-) , 而3, 6这组负责接收数据(RX+、 RX-) ,另外四根线是备用的。
以太网是目前应用最广泛的局域网通讯方式,同时也是一种协议。以太网协议定义了一系列软件和硬件标准,从而将不同的计算机设备连接在一起。 我们知道串口通信单次只传输一个字节, 而以太网通信是以数据包的形式传输, 其单包数据量达到几十, 甚至成百上千个字节。
下图为以太网通过UDP(User Datagram Protocol,用户数据报协议)传输单包数据的格式,
从图中可以看出, 以太网的数据包就是对各层协议的逐层封装来实现数据的传输。 这里只是让大家了解下以太网数据包的格式,后面会逐个展开来讲。
以太网MAC帧格式
以太网技术的正式标准是IEEE 802.3,它规定了以太网传输数据的帧结构,我们可以把以太网MAC层理解成高速公路, 我们必须遵循它的规则才能在上面通行, 以太网MAC层帧格式如图1.4所示。
以太网传输数据时按照上面的顺序从头到尾依次被发送和接收,我们下面进一步解释各个区域。
前导码(Preamble) : 为了实现底层数据的正确阐述,物理层使用7个字节同步码(0和1交替(55-55-55-55-55-55-55))实现数据的同步。
帧起始界定符(SFD, Start Frame Delimiter):使用1个字节的SFD(固定值为0xd5)来表示一帧的开始,即后面紧跟着传输的就是以太网的帧头。
目的MAC地址: 即接收端物理MAC地址,占用6个字节。 MAC地址从应用上可分为单播地址、组播地址和广播地址。单播地址:第一个字节的最低位为0,比如00-00-00-11-11-11,一般用于标志唯一的设备;组播地址:第一个字节的最低位为1,比如01-00-00-11-11-11,一般用于标志同属一组的多个设备;广播地址:所有48bit全为1,即FF-FF-FF-FF-FF-FF,它用于标志同一网段中的所有设备。
源MAC地址:即发送端物理MAC地址,占用6个字节。
长度/类型: 上图中的长度/类型具有两个意义,当这两个字节的值小于1536(十六进制为0x0600)时,代表该以太网中数据段的长度;如果这两个字节的值大于1536,则表示该以太网中的数据属于哪个上层协议,例如0x0800代表IP协议(网际协议) 、 0x0806代表ARP协议(地址解析协议)等。
数据:以太网中的数据段长度最小46个字节, 最大1500个字节。由于以太网的冲突检测特性,数据段长度至少是46个字节,当数据段长度不够46个字节时,要在后面补填充位。 最大值1500称为以太网的最大传输单元(MTU, Maximum Transmission Unit),之所以限制最大传输单元是因为在多个计算机的数据帧排队等待传输时,如果某个数据帧太大的话,那么其它数据帧等待的时间就会加长,导致体验变差,这就像一个十字路口的红绿灯,你可以让绿灯持续亮一小时,但是等红灯的人一定不愿意的。另外还要考虑网络I/O控制器缓存区资源以及网络最大的承载能力等因素, 因此最大传输单元是由各种综合因素决定的。为了避免增加额外的配置,
通常以太网的有效数据字段小于1500个字节。
帧检验序列(FCS, Frame Check Sequence) : 为了确保数据的正确传输, 在数据的尾部加入了4个字节的循环冗余校验码(CRC校验) 来检测数据是否传输错误。 CRC数据校验从以太网帧头开始即不包含前导码和帧起始界定符。 通用的CRC标准有CRC-8、 CRC-16、 CRC-32、 CRCCCIT,其中在网络通信系统中应用最广泛的是CRC-32标准。
在这里还有一个要注意的地方就是以太网相邻两帧之间的时间间隔, 即帧间隙(IFG,Interpacket Gap) 。 帧间隙的时间就是网络设备和组件在接收一帧之后,需要短暂的时间来恢复并为接收下一帧做准备的时间, IFG的最小值是96 bit time,即在媒介中发送96位原始数
据所需要的时间,在不同媒介中IFG的最小值是不一样的。 不管10M/100M/1000M的以太网,两帧之间最少要有96bit time, IFG的最少间隔时间计算方法如下:
10Mbit/s最小时间为: 96*100ns = 9600ns;
100Mbit/s最小时间为: 96*10ns = 960ns;
1000Mbit/s最小时间为: 96*1ns = 96ns。
接下来我们介绍IP协议以及它和以太网MAC层的关系。在介绍IP协议之前,我们先了解下TCP(传输控制协议) /IP(网际协议)协议簇。 TCP/IP是网络使用中最基本的通信协议,虽然从名字看上去TCP/IP包括两个协议, TCP和IP,但TCP/IP实际上是一组协议,它包括上百个各种功能的协议,如: TCP、 IP、 UDP、 ICMP(网际控制报文协议)等。 而TCP协议和IP协议是保证数据完整传输的两个重要的协议, 因此TCP/IP协议用来表示Internet协议簇。
TCP/IP协议不仅可以运行在以太网上,也可以运行在FDDI(光纤分布式数据接口)和WLAN(无线局域网)上。反过来, 以太网的高层协议不仅可以是TCP/IP协议,也可以是IPX协议(互联网分组交换协议)等,只不过以太网+TCP/IP成为IT行业中应用最普遍的技术。下面我们来熟悉下IP协议。
IP协议
IP协议是TCP/IP协议簇中的核心协议,也是TCP/IP协议的载体, IP协议规定了数据传输时的基本单元和格式。从前面介绍的图 43.1.3中可以看出, IP协议位于以太网MAC帧格式的数据段, IP协议内容由IP首部和数据字段组成。所有的TCP、UDP及ICMP数据都以IP数据报格式传输,
IP数据包格式如图 43.1.5所示。
前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协议内容已经介绍完了,我们从前面介绍的图 43.1.3可以知道, 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协议的区别:
- TCP协议面向连接,是流传输协议,通过连接发送数据,而UDP协议传输不需要连接,是数据报协议;
- TCP为可靠传输协议,而UDP为不可靠传输协议。即TCP协议可以保证数据的完整和有序,而UDP不能保证;
- UDP由于不需要连接,故传输速度比TCP快,且占用资源比TCP少;
- 应用场合: TCP协议常用在对数据文件完整性较高的一些场景中,如文件传输等。 UDP常用于对通讯速度有较高要求或者传输数据较少时, 比如对速度要求较高的视频直播和传输数据较少的QQ等。
首先可以肯定的告诉大家,使用FPGA实现TCP协议是完全没有问题的,但是, FPGA发展到现在, 却鲜有成功商用的RTL级的TCP协议设计,大部分以太网传输都是基于比较简单的UDP协议。 TCP协议设计之初是根据软件灵活性设计的, 如果使用硬件逻辑实现,工程量会十分巨大,而且功能和性能无法得到保证, 因此, TCP协议设计并不适合使用硬件逻辑实现。 UDP协议是一种不可靠传输,发送方只负责数据发送出去,而不管接收方是否正确的接收。 在很多场合,是可以接受这种潜在的不可靠性的,例如视频实时传输显示等。
UDP数据格式如图 43.1.7所示:
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报文中的数据段为用户希望传输的数据内容。现在再回过头看图 43.1.8的内容是不是很好理解了呢?
前面介绍的内容全部都是和协议相关的, 以太网通信如果只有协议,没有硬件芯片的支持是不行的, 这个硬件芯片就是以太网PHY芯片。PHY是物理接口收发器, 它实现物理层IEEE 802.3标准定义的以太网PHY, 包括MII(媒体独立接口) /GMII(千兆媒体独立接口)等。
以太网PHY芯片
PHY在发送数据的时候,接收MAC发过来的数据(对PHY来说,没有帧的概念,都是数据而不管什么地址,数据还是CRC),把并行数据转化为串行流数据,按照物理层的编码规则把数据编码转换为模拟信号发送出去,收数据时的流程反之。 PHY还提供了和对端设备连接的重要功能并通过LED灯显示出自己目前的连接状态和工作状态。当我们给网卡接入网线的时候, PHY芯片不断发出脉冲信号来检测对端是否有设备,它们通过标准的“语言”交流,互相协商并确定连接速度、双工模式、是否采用流控等。通常情况下,协商的结果是两个设备中能同时支持的最大速度和最好的双工模式。这个技术被称为Auto Negotiation,即自动协商。以太网MAC和PHY之间有一个接口,常用的接口有MII、 RMII、 GMII、 RGMII等。
MII(Medium Independent Interface): MII支持10Mbps和100Mbps的操作,数据位宽为4位,在100Mbps传输速率下,时钟频率为25Mhz。
RMII(Reduced MII): RMII是MII的简化版, 数据位宽为2位,在100Mbps传输速率下,时钟频率为50Mhz。
GMII(Gigabit MII): GMII接口向下兼容MII接口, 支持10Mbps、 100Mbps和1000Mbps的操作,数据位宽为8位,在1000Mbps传输速率下,时钟频率为125Mhz。
RGMII(Reduced GMII): RGMII是GMII的简化版,数据位宽为4位,在1000Mbps传输速率下,时钟频率为125Mhz, 但是在时钟的上下沿同时采样数据。
在百兆以太网中,常用的接口为MII接口, 下图是MAC侧与PHY侧接口的连接。
ETH_RXC:接收数据参考时钟, 100Mbps速率下,时钟频率为25MHz; 10Mbps速率下,时钟频率为2.5MHz, RX_CLK由PHY侧提供。
ETH_RXDV:接收数据有效信号,只有在ETH_RXDV为高电平时接收到的数据才有效。
ETH_RXD:四位并行的接收数据线。
ETH_TXC:发送参考时钟, 100Mbps速率下,时钟频率为25MHz; 10Mbps速率下,时钟频率为2.5MHz, TX_CLK由PHY侧提供。
ETH_TXEN:发送数据有效信号,只有在ETH_TXEN为高电平时发送的数据才有效。
ETH_RESET:芯片复位信号,低电平有效
ETH_MDC:数据管理时钟(Management Data Clock),该引脚对ETH_MDIO信号提供了一个同步的时钟。
ETH_MDIO:数据输入/输出管理(Management Data Input/Output),该引脚提供了一个双向信号用于传递管理信息。
MII发送和接收时序图如下图所示, 数据传输时先发送字节的低4位, 再发送字节的高4位。
FPGA RGMII GMII MII接口以太网通讯实验任务
本节实验任务是上位机通过网口调试助手发送数据给FPGA, FPGA通过以太网接口接收数据并将接收到的数据发送给上位机,完成以太网数据的环回。
FPGA开发板上有一个RJ45以太网接口,用于连接网线, 其原理图如图 43.3.1
所示:
以太网的数据传输离不开以太网PHY(物理层)芯片的支持, 物理层定义了数据发送与接收所需要的电信号、线路状态、时钟基准、数据编码和电路等,并向数据链路层设备提供标准接口。FPGA开发板上使用的PHY芯片为Realtek公司的RTL8201CP,其原理图如图43.3.2所示:
Realtek RTL8201CP是一个快速以太网物理层收发器,它为MAC层提供了可选择的MII(媒体独立接口)或SNI(串行网络接口)接口, 实现了全部的10/100M以太网物理层功能。 SNI接口仅支持10Mbps的通信速率,而MII最大支持100Mbps的通信速率, 所以本次收发实验采用的是MII接口。 RTL8201CP芯片的参数可以通过MDC/MDIO接口来配置, 因为其默认的参数就可以实现MII接口的自适应10M/100M收发数据, 因此可不必对芯片做配置。
本实验中,以太网管脚分配如下表所示, 从管脚分配图中可知, 表中没有FPGA输入的系统时钟信号(板载的50Mhz) , 从上图中的原理图可知,第46,47脚已经接了一个25Mhz的晶振,ETH_RXC(接收时钟)和ETH_TXC(发送时钟) 都是由以太网PHY侧提供的,我们程序设计直接使用这两个时钟就可以了。
表 43.3.1 以太网通信实验管脚分配
信号名 | 方向 | 管脚 | 端口说明 | ||
---|---|---|---|---|---|
sys_rst_n | input | M1 | 系统复位, 低有效 | ||
eth_rx_clk | input | L8 | MII接收数据时钟 | ||
eth_rxdv | input | F6 | MII输入数据有效信号 | ||
eth_rx_data[0] | input | F5 | MII输入数据RXD[0] | ||
eth_rx_data[1] | input | G5 | MII输入数据RXD[1] | ||
eth_rx_data[2] | input | F7 | MII输入数据RXD[2] | ||
eth_rx_data[3] | input | K8 | MII输入数据RXD[3] | ||
eth_tx_clk | input | J6 | MII发送数据时钟 | ||
eth_tx_en | output | L4 | MII输出数据有效信号 | ||
eth_tx_data[0] | output | K6 | MII输出数据TXD[0] | ||
eth_tx_data[1] | output | K5 | MII输出数据TXD[1] | ||
eth_tx_data[2] | output | L7 | MII输出数据TXD[2] | ||
eth_tx_data[3] | output | L3 | MII输出数据TXD[3] | ||
eth_rst_n | output | L6 | 以太网芯片复位信号,低电平有效 |
FPGA RGMII GMII MII接口以太网通讯程序设计
通过前面介绍的以太网相关协议和MII接口可知,我们只需要把数据封装成以太网包的格式通过MII接口传输数据即可。 根据实验任务, 以太网环回实验应该有一个以太网接收模块和以太网发送模块,因为发送模块需要CRC校验, 因此还需要一个以太网发送CRC校验模块; 为了在其它工程中比较方便的调用以太网的程序以提高项目的开发效率,我们把上面三个模块封装成一个UDP模块。 以太网单次会接收到大量数据, 因此还需要一个FIFO模块用来缓存数据, 尽管ETH_RXC和ETH_TXC时钟频率相同,但是相位的偏差是不确定的,所以ETH_RXC和ETH_TXC为异步时钟,因此我们需要使用异步fifo来缓存数据。 对于异步时钟域下需要传递的数据量较多的情况, 一般使用异步fifo来同步数据; 而对于数据量较少或者数据长时间才会改变一次的情况,一般使用脉冲信号同步处理的方法来采集数据;以太网发送模块和接收模块会有除有效数据外的其它少量数据的交互,因此我们还需要一个脉冲信号同步处理模块。由此画出系统总体框架如图 43.4.1所示。以太网收到的数据缓存到FIFO中,单包数据接收完成后发送FIFO中的数据,
实现以太网通信的环回实验。
以太网通信实验的系统框图如下图所示:
顶层模块的原理图如下图所示:
FPGA顶层模块(eth_pc_loop)例化了以下三个模块: UDP模块(udp)、脉冲信号同步处理模块(pulse_sync_pro)和FIFO缓存数据模块(async_fifo_2048x32b) 。
顶层模块(eth_pc_loop) :顶层模块完成了对其它三个模块的例化,将UDP的收发数据用户接口连接到FIFO的读写端口, UDP两个时钟域需要交互数据的接口连接到了脉冲同步处理模块, 从而实现了数据的接收、缓存、发送以及数据的脉冲同步处理。
UDP模块(udp): UDP模块是本实验以太网传输数据的核心代码,其输入输出端口被封装成用户方便调用的接口, 其它工程如果用到了UDP通信,可直接例化此模块。
脉冲信号同步处理模块(pulse_sync_pro):脉冲信号同步处理模块负责将一个时钟域下的脉冲信号同步到另一个时钟域下的脉冲信号。
FIFO缓存数据模块(async_fifo_2048x32b): 缓存数据模块是由Quartus软件自带的FIFO软核生成的, 缓存的大小为2048个32bit, 为了能够满足单包数据量较大的情况(尽管通常情况下,以太网帧有效数据不超过1500个字节), 所以FIFO的深度最好设置的大一点,这里把深度设置为2048, 宽度为32位。
其中UDP模块(udp) 例化了以太网接收模块(ip_receive) 、 以太网发送模块(ip_send)和发送CRC校验模块(crc32_d4) , UDP模块(udp)端口及信号连接如图 43.4.3所示:
由上图可知, 以太网发送模块(ip_send)和以太网接收模块(ip_receive)在UDP模块内数据端口没有信号连接, 数据的交互放在了eth_pc_loop模块里面。 主要是以太网发送模块和CRC32校验模块(crc32_d4) 数据信号的连接, 发送模块端口的eth_tx_data(MII输出数据)
连接到CRC32的输入数据端口, CRC32校验模块将校验结果连接到发送模块。
以太网接收模块(ip_receive):以太网的接收模块较为简单,因为我们不需要对数据做IP首部校验也不需要做CRC循环冗余校验,只需要判断目的MAC地址与开发板MAC地址、目的IP地址与开发板IP地址是否一致即可。接收模块的解析顺序是:前导码+帧起始界定符→以太网帧头→IP首部→UDP首部→UDP数据(有效数据) →接收结束。 MII接口数据为4位数据, 先把4位数据转成8位数据会方便解析数据, IP数据报一般以32bit为单位, 为了和IP数据报格式保持一致, 所以要把8位数据转成32位数据, 因此接收模块实际上是完成了4位数据转32位数据的功
能。
以太网发送模块(ip_send):以太网发送模块和接收模块比较类似,但是多了IP首部校验和和CRC循环冗余校验的计算。 CRC的校验并不是在发送模块完成, 而是在CRC校验模块(crc32_d4) 里完成的。 发送模块的发送顺序是前导码+帧起始界定符→以太网帧头→IP首部→UDP首部→UDP数据(有效数据) →CRC校验。输入的有效数据为32位数据, MII接口为4位数据接口,因此发送模块实际上完成的是32位数据转4位数据的功能。
CRC校验模块(crc32_d4) : CRC校验模块是对以太网发送模块的数据(不包括前导码和帧起始界定符) 做校验,把校验结果值拼在以太网帧格式的FCS字段, 如果CRC校验值计算错误或者没有的话, 那么电脑网卡会直接丢弃该帧导致收不到数据(有些网卡是可以设置不做校验的)。
CRC32校验在FPGA实现的原理是LFSR(Linear Feedback Shift Register,线性反馈移位寄存器),其思想是各个寄存器储存着上一次CRC32运算的结果,寄存器的输出即为CRC32的值。
以上是对各个模块的划分、信号的连接以及模块的设计思路,下面是具体的源代码。
顶层模块的代码如下:
module eth_pc_loop(
input sys_rst_n , //系统复位信号,低电平有效
//以太网接口
input eth_rx_clk , //MII接收数据时钟
input eth_rxdv , //MII输入数据有效信号
input eth_tx_clk , //MII发送数据时钟
input [3:0] eth_rx_data , //MII输入数据
output eth_tx_en , //MII输出数据有效信号
output [3:0] eth_tx_data , //MII输出数据
output eth_rst_n //以太网芯片复位信号,低电平有效
);
//parameter define
//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
//开发板IP地址 192.168.1.123
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd123};
//目的MAC地址 ff_ff_ff_ff_ff_ff
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff;
//目的IP地址 192.168.1.102
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102};
//wire define
wire rec_pkt_done; //以太网单包数据接收完成信号
wire rec_en ; //以太网接收的数据使能信号
wire [31:0] rec_data ; //以太网接收的数据
wire [15:0] rec_byte_num; //以太网接收的有效字节数 单位:byte
wire tx_done ; //以太网发送完成信号
wire tx_req ; //读数据请求信号
wire tx_start_en ; //以太网开始发送信号
wire [31:0] tx_data ; //以太网待发送数据
//*****************************************************
//** main code
//*****************************************************
//UDP模块
udp //参数例化
#(
.BOARD_MAC (BOARD_MAC),
.BOARD_IP (BOARD_IP ),
.DES_MAC (DES_MAC ),
.DES_IP (DES_IP )
)
u_udp(
.eth_rx_clk (eth_rx_clk ),
.rst_n (sys_rst_n ),
.eth_rxdv (eth_rxdv ),
.eth_rx_data (eth_rx_data ),
.eth_tx_clk (eth_tx_clk ),
.tx_start_en (tx_start_en ),
.tx_data (tx_data ),
.tx_byte_num (rec_byte_num),
.tx_done (tx_done ),
.tx_req (tx_req ),
.rec_pkt_done (rec_pkt_done),
.rec_en (rec_en ),
.rec_data (rec_data ),
.rec_byte_num (rec_byte_num),
.eth_tx_en (eth_tx_en ),
.eth_tx_data (eth_tx_data ),
.eth_rst_n (eth_rst_n )
);
//脉冲信号同步处理模块
pulse_sync_pro u_pulse_sync_pro(
.clk_a (eth_rx_clk),
.rst_n (sys_rst_n),
.pulse_a (rec_pkt_done),
.clk_b (eth_tx_clk),
.pulse_b (tx_start_en)
);
//fifo模块,用于缓存单包数据
async_fifo_2048x32b u_fifo_2048x32b(
.aclr (~sys_rst_n),
.data (rec_data ), //fifo写数据
.rdclk (eth_tx_clk),
.rdreq (tx_req ), //fifo读使能
.wrclk (eth_rx_clk),
.wrreq (rec_en ), //fifo写使能
.q (tx_data ), //fifo读数据
.rdempty (),
.wrfull ()
);
endmodule
在代码的第58至第59行中, UDP模块输出的rec_en(数据有效信号)和rec_data(有效数据)连接到FIFO缓存数据模块的写入端口, 从而将以太网接收到的数据缓存至FIFO。在代码的第56行和第53行中, UDP模块输出的tx_req(读数据请求信号)和输入的tx_data(发送的数据)连接到FIFO缓存模块的读出端口,从而将FIFO中的数据发送出去。
以太网通信的环回功能是在单包数据接收完成之后才开始发送数据的,所以可以将以太网接收模块输出的接收完成信号(rec_pkt_done)作为以太网发送模块的开始发送信号(tx_start_en) 。 我们知道, eth_rx_clk和eth_tx_clk为两个异步时钟, 如果接收完成信号直接作为以太网发送模块的开始发送信号的话,会出现亚稳态, 即信号有可能没有被正确采集到。 所以我们添加一个脉冲信号同步处理模块,将接收完成信号连接至脉冲信号同步处理模块的pulse_a(输入脉冲同步信号), 脉冲信号同步处理模块输出的pulse_b(输出脉冲同步信号)连接至开始发送信号(如代码的第52行、第57行、 第70行和第72行所示) ,有关脉冲信号同步处理模块是如何完成两个时钟域下的数据同步的,我们在后面会有详细的介绍。
在代码 的第60行rec_byte_num(以 太网 接 收的有 效字 节数 )直 接连接 到了 第54行tx_byte_num(以太网发送的有效字节数),开发板将接收到的有效字节数直接返回给PC,因此收发数据的有效字节数是一致的 。需要注意 的是rec_byte_num和tx_byte_num分别是eth_rx_clk和eth_tx_clk两个异步时钟下的数据, 处理这种异步时钟下的数据要格外小心, 必须在数据保持稳定之后采集,否则很容易出现亚稳态, 即数据采集错误的情况。 以太网发送模块是在rec_pkt_done信号的上升沿锁存发送字节个数的,接收字节个数在此时一段时间内是保持不变的, 不会出现数据采集错误的情况, 因此rec_byte_num和tx_byte_num这两个信号可以直接连在一起。
需要注意的是,顶层模块中第15至第21行定义了四个参数:开发板MAC地址BOARD_MAC, 开发板IP地址BOARD_IP,目的MAC地址DES_MAC(和PC做环回实验,这里指PC MAC地址), 目的IP地址DES_IP(PC IP地址) 。开发板的MAC地址和IP地址是我们随意定义的,只要不和目的MAC地址和目的IP地址一样就可以,否则会产生地址冲突。 目的MAC地址这里写的是公共MAC地址(48’hff_ff_ff_ff_ff_ff) ,也可以修改成电脑网口的MAC地址, DES_IP是对应电脑以太网的IP地址,这里定义的四个参数是向下传递的, 需要修改MAC地址或者IP地址时直接在这里修改即可,而不用在udp模块里面修改。
脉冲信号同步处理模块的代码如下所示:
module pulse_sync_pro(
input clk_a , //输入时钟A
input rst_n , //复位信号
input pulse_a, //输入脉冲A
input clk_b , //输入时钟B
output pulse_b //输出脉冲B
);
//reg define
reg pulse_inv ; //脉冲信号转换成电平信号
reg pulse_inv_d0 ; //时钟B下打拍
reg pulse_inv_d1 ;
reg pulse_inv_d2 ;
//*****************************************************
//** main code
//*****************************************************
assign pulse_b = pulse_inv_d1 ^ pulse_inv_d2 ;
//输入脉冲转成电平信号,确保时钟B可以采到
always @(posedge clk_a or negedge rst_n) begin
if(rst_n==1'b0)
pulse_inv <= 1'b0 ;
else if(pulse_a)
pulse_inv <= ~pulse_inv;
end
//A时钟下电平信号转成时钟B下的脉冲信号
always @(posedge clk_b or negedge rst_n) begin
if(rst_n==1'b0) begin
pulse_inv_d0 <= 1'b0;
pulse_inv_d1 <= 1'b0;
pulse_inv_d2 <= 1'b0;
end
else begin
pulse_inv_d0 <= pulse_inv ;
pulse_inv_d1 <= pulse_inv_d0;
pulse_inv_d2 <= pulse_inv_d1;
end
end
endmodule
代码中的pulse_a信号是clk_a时钟域下的脉冲信号(之所以称为脉冲信号, 是因为pulse_a高电平持续一个时钟周期) , pulse_b信号是clk_b时钟域下的脉冲信号,脉冲信号同步处理模块完成一个时钟域下的脉冲信号同步到另一时钟域下的脉冲信号。 在代码的第22行开始的always块中, 将输入的脉冲信号(pulse_a) 转换成电平信号(pulse_inv) ,转换后的电平信号在clk_b时钟域下打拍, 打拍后的信号进行异或处理后,输出一个clk_b时钟域下的脉冲信号。
图 43.4.4为脉冲同步过程中SignalTap抓取的波形图, pulse_a信号拉高后, pulse_inv信号取反由低电平变成高电平。 clk_b时钟对取反后的信号进行打拍,打拍后的信号进行异或运算得到持续一个时钟周期的pulse_b信号。
UDP模块的代码如下所示:
module udp
#(
//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
//开发板IP地址 192.168.1.123
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd123},
//目的MAC地址 ff_ff_ff_ff_ff_ff
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff,
//目的IP地址 192.168.1.102
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102}
)
(
input eth_rx_clk , //MII接收数据时钟
input rst_n , //复位信号,低电平有效
input eth_rxdv , //MII输入数据有效信号
input [3:0] eth_rx_data , //MII输入数据
input eth_tx_clk , //MII发送数据时钟
input tx_start_en , //以太网开始发送信号
input [31:0] tx_data , //以太网待发送数据
input [15:0] tx_byte_num , //以太网发送的有效字节数 单位:byte
output tx_done , //以太网发送完成信号
output tx_req , //读数据请求信号
output rec_pkt_done, //以太网单包数据接收完成信号
output rec_en , //以太网接收的数据使能信号
output [31:0] rec_data , //以太网接收的数据
output [15:0] rec_byte_num, //以太网接收的有效字节数 单位:byte
output eth_tx_en , //MII输出数据有效信号
output [3:0] eth_tx_data , //MII输出数据
output eth_rst_n //以太网芯片复位信号,低电平有效
);
//wire define
wire crc_en ; //CRC开始校验使能
wire crc_clr ; //CRC数据复位信号
wire [3:0] crc_d4 ; //输入待校验4位数据
wire [31:0] crc_data ; //CRC校验数据
wire [31:0] crc_next ; //CRC下次校验完成数据
//*****************************************************
//** main code
//*****************************************************
assign crc_d4 = eth_tx_data;
assign eth_rst_n = 1'b1; //复位信号一直拉高
//以太网接收模块
ip_receive
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP)
)
u_ip_receive(
.clk (eth_rx_clk),
.rst_n (rst_n),
.eth_rxdv (eth_rxdv),
.eth_rx_data (eth_rx_data),
.rec_pkt_done (rec_pkt_done),
.rec_en (rec_en),
.rec_data (rec_data),
.rec_byte_num (rec_byte_num)
);
//以太网发送模块
ip_send
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP),
.DES_MAC (DES_MAC),
.DES_IP (DES_IP)
)
u_ip_send(
.clk (eth_tx_clk),
.rst_n (rst_n),
.tx_start_en (tx_start_en),
.tx_data (tx_data),
.tx_byte_num (tx_byte_num),
.crc_data (crc_data),
.crc_next (crc_next[31:28]),
.tx_done (tx_done),
.tx_req (tx_req),
.eth_tx_en (eth_tx_en),
.eth_tx_data (eth_tx_data),
.crc_en (crc_en),
.crc_clr (crc_clr)
);
//以太网发送CRC校验模块
crc32_d4 u_crc32_d4(
.clk (eth_tx_clk),
.rst_n (rst_n ),
.data (crc_d4),
.crc_en (crc_en),
.crc_clr (crc_clr),
.crc_data (crc_data),
.crc_next (crc_next)
);
endmodule
UDP(udp)模块是对以太网接收模块(ip_receive)、以太网发送模块(ip_send)、 CRC校验模块(crc32_d4)的例化, 其中crc32_d4模块是对发送模块做CRC校验。下面着重介绍各个子模块代码的实现。
我们在前面介绍过, 以太网接收模块实现的是4位转32位的功能以及解析数据的顺序,可以发现, 解析数据的顺序很适合使用状态机来实现,下图为以太网接收模块的状态跳转图。
接收模块使用三段式状态机来解析以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。这里需要注意的一点是, 在中间状态如前导码错误、 MAC地址错误以及IP地址错误时跳转到st_rx_end状态而不是跳转到st_idle转态。因为中间状态在解析到数据错误时, 单包数据的接收还没有结束,如果此时跳转到st_idle状态会误把有效数据当成前导码来解析,所以状态跳转到st_rx_end。而eth_rxdv信号为0时,单包数据才算接收结束, 所以st_rx_end跳转到st_idle的条件是eth_rxdv=0, 准备接收下一包数据。 ip_receive代码如下:
图 43.4.6为接收过程中 SignalTap抓取的波形图,上位机通过网口 调试助手发送(十六进制为: 68 74 74 70 3A 2F 2F 77 77 77 2E 61 6C 69 65
6E 74 65 6B 2E 63 6F 6D 2F),图中eth_rxdv和eth_rx_data为MII接口的接收信号, skip_en为状态机的跳转信号。每次单包数据接收完成都会产生rec_pkt_done信号, rec_en和rec_data为收到的数据有效信号和数据。
以太网发送模块实际上完成的是32位数据转4位数据的功能,也就是接收模块的逆过程,同样也非常适合使用状态机来完成发送数据的功能,状态跳转图如下图所示:
发送模块和接收模块有很多相似之处, 同样使用三段式状态机来发送以太网包,从上图可以比较直观的看到每个状态实现的功能以及跳转到下一个状态的条件。
发送模块的代码中定义了数组来存储以太网的帧头、 IP首部以及UDP的首部, 在复位时初始化数组的值, 源代码如下。
module ip_send
#(
//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
//开发板IP地址 192.168.1.123
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd123},
//目的MAC地址 ff_ff_ff_ff_ff_ff
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff,
//目的IP地址 192.168.1.102
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102}
)
(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input tx_start_en, //以太网开始发送信号
input [31:0] tx_data , //以太网待发送数据
input [15:0] tx_byte_num, //以太网发送的有效字节数
input [31:0] crc_data , //CRC校验数据
input [3:0] crc_next , //CRC下次校验完成数据
output reg tx_done , //以太网发送完成信号
output reg tx_req , //读数据请求信号
output reg eth_tx_en , //MII输出数据有效信号
output reg [3:0] eth_tx_data, //MII输出数据
output reg crc_en , //CRC开始校验使能
output reg crc_clr //CRC数据复位信号
);
//parameter define
localparam st_idle = 7'b000_0001; //初始状态,等待开始发送信号
localparam st_check_sum = 7'b000_0010; //IP首部校验和
localparam st_preamble = 7'b000_0100; //发送前导码+帧起始界定符
localparam st_eth_head = 7'b000_1000; //发送以太网帧头
localparam st_ip_head = 7'b001_0000; //发送IP首部+UDP首部
localparam st_tx_data = 7'b010_0000; //发送数据
localparam st_crc = 7'b100_0000; //发送CRC校验值
localparam ETH_TYPE = 16'h0800 ; //以太网协议类型 IP协议
//以太网数据最小46个字节,IP首部20个字节+UDP首部8个字节
//所以数据至少46-20-8=18个字节
localparam MIN_DATA_NUM = 16'd18 ;
//reg define
reg [6:0] cur_state ;
reg [6:0] next_state ;
reg [7:0] preamble[7:0] ; //前导码
reg [7:0] eth_head[13:0] ; //以太网首部
reg [31:0] ip_head[6:0] ; //IP首部 + UDP首部
reg start_en_d0 ;
reg start_en_d1 ;
reg [15:0] tx_data_num ; //发送的有效数据字节个数
reg [15:0] total_num ; //总字节数
reg [15:0] udp_num ; //UDP字节数
reg skip_en ; //控制状态跳转使能信号
reg [4:0] cnt ;
reg [31:0] check_buffer ; //首部校验和
reg [2:0] tx_bit_sel ;
reg [15:0] data_cnt ; //发送数据个数计数器
reg tx_done_t ;
reg [4:0] real_add_cnt ; //以太网数据实际多发的字节数
//wire define
wire pos_start_en ; //开始发送数据上升沿
wire [15:0] real_tx_data_num ; //实际发送的字节数(以太网最少字节要求)
//*****************************************************
//** main code
//*****************************************************
assign pos_start_en = (~start_en_d1) & start_en_d0;
assign real_tx_data_num = (tx_data_num >= MIN_DATA_NUM)
? tx_data_num : MIN_DATA_NUM;
//采tx_start_en的上升沿
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
start_en_d0 <= 1'b0;
start_en_d1 <= 1'b0;
end
else begin
start_en_d0 <= tx_start_en;
start_en_d1 <= start_en_d0;
end
end
//寄存数据有效字节
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_data_num <= 16'd0;
total_num <= 16'd0;
udp_num <= 16'd0;
end
else begin
if(pos_start_en && cur_state==st_idle) begin
//数据长度
tx_data_num <= tx_byte_num;
//IP长度:有效数据+IP首部长度
total_num <= tx_byte_num + 16'd28;
//UDP长度:有效数据+UDP首部长度
udp_num <= tx_byte_num + 16'd8;
end
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end
always @(*) begin
next_state = st_idle;
case(cur_state)
st_idle : begin //等待发送数据
if(skip_en)
next_state = st_check_sum;
else
next_state = st_idle;
end
st_check_sum: begin //IP首部校验
if(skip_en)
next_state = st_preamble;
else
next_state = st_check_sum;
end
st_preamble : begin //发送前导码+帧起始界定符
if(skip_en)
next_state = st_eth_head;
else
next_state = st_preamble;
end
st_eth_head : begin //发送以太网首部
if(skip_en)
next_state = st_ip_head;
else
next_state = st_eth_head;
end
st_ip_head : begin //发送IP首部+UDP首部
if(skip_en)
next_state = st_tx_data;
else
next_state = st_ip_head;
end
st_tx_data : begin //发送数据
if(skip_en)
next_state = st_crc;
else
next_state = st_tx_data;
end
st_crc: begin //发送CRC校验值
if(skip_en)
next_state = st_idle;
else
next_state = st_crc;
end
default : next_state = st_idle;
endcase
end
//发送数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
skip_en <= 1'b0;
cnt <= 5'd0;
check_buffer <= 32'd0;
ip_head[1][31:16] <= 16'd0;
tx_bit_sel <= 3'b0;
crc_en <= 1'b0;
eth_tx_en <= 1'b0;
eth_tx_data <= 4'd0;
tx_req <= 1'b0;
tx_done_t <= 1'b0;
data_cnt <= 16'd0;
real_add_cnt <= 5'd0;
//初始化数组
//前导码 7个8'h55 + 1个8'hd5
preamble[0] <= 8'h55;
preamble[1] <= 8'h55;
preamble[2] <= 8'h55;
preamble[3] <= 8'h55;
preamble[4] <= 8'h55;
preamble[5] <= 8'h55;
preamble[6] <= 8'h55;
preamble[7] <= 8'hd5;
//目的MAC地址
eth_head[0] <= DES_MAC[47:40];
eth_head[1] <= DES_MAC[39:32];
eth_head[2] <= DES_MAC[31:24];
eth_head[3] <= DES_MAC[23:16];
eth_head[4] <= DES_MAC[15:8];
eth_head[5] <= DES_MAC[7:0];
//源MAC地址
eth_head[6] <= BOARD_MAC[47:40];
eth_head[7] <= BOARD_MAC[39:32];
eth_head[8] <= BOARD_MAC[31:24];
eth_head[9] <= BOARD_MAC[23:16];
eth_head[10] <= BOARD_MAC[15:8];
eth_head[11] <= BOARD_MAC[7:0];
//以太网类型
eth_head[12] <= ETH_TYPE[15:8];
eth_head[13] <= ETH_TYPE[7:0];
end
else begin
skip_en <= 1'b0;
tx_req <= 1'b0;
crc_en <= 1'b0;
eth_tx_en <= 1'b0;
tx_done_t <= 1'b0;
case(cur_state)
st_idle : begin
if(pos_start_en)
skip_en <= 1'b1;
if(skip_en) begin
//版本号:4 首部长度:5(单位:32bit,20byte/4=5)
ip_head[0] <= {8'h45,8'h00,total_num};
//16位标识,每次发送累加1
ip_head[1][31:16] <= ip_head[1][31:16] + 1'b1;
//bit[15:13]: 010表示不分片
ip_head[1][15:0] <= 16'h4000;
//协议:17(udp)
ip_head[2] <= {8'h40,8'd17,16'h0};
//源IP地址
ip_head[3] <= BOARD_IP;
//目的IP地址
ip_head[4] <= DES_IP;
//16位源端口号:1234 16位目的端口号:1234
ip_head[5] <= {16'd1234,16'd1234};
//16位udp长度,16位udp校验和
ip_head[6] <= {udp_num,16'h0000};
end
end
st_check_sum: begin //IP首部校验
cnt <= cnt + 5'd1;
if(cnt == 5'd0) begin
check_buffer <= ip_head[0][31:16] + ip_head[0][15:0]
+ ip_head[1][31:16] + ip_head[1][15:0]
+ ip_head[2][31:16] + ip_head[2][15:0]
+ ip_head[3][31:16] + ip_head[3][15:0]
+ ip_head[4][31:16] + ip_head[4][15:0];
end
else if(cnt == 5'd1) //可能出现进位,累加一次
check_buffer <= check_buffer[31:16] + check_buffer[15:0];
else if(cnt == 5'd2) begin //可能再次出现进位,累加一次
check_buffer <= check_buffer[31:16] + check_buffer[15:0];
skip_en <= 1'b1;
end
else if(cnt == 5'd3) begin //按位取反
cnt <= 5'd0;
ip_head[2][15:0] <= ~check_buffer[15:0];
end
end
st_preamble : begin //发送前导码+帧起始界定符
eth_tx_en <= 1'b1;
if(tx_bit_sel == 3'd0) begin
tx_bit_sel <= tx_bit_sel + 3'd1;
eth_tx_data <= preamble[cnt][3:0]; //先发送字节低四位
if(cnt == 5'd7) begin
skip_en <= 1'b1;
end
end
else begin
tx_bit_sel <= 3'd0;
eth_tx_data <= preamble[cnt][7:4]; //再发送字节高四位
if(skip_en)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
end
end
st_eth_head : begin //发送以太网首部
eth_tx_en <= 1'b1;
crc_en <= 1'b1;
if(tx_bit_sel == 3'd0) begin
tx_bit_sel <= tx_bit_sel + 3'd1;
eth_tx_data <= eth_head[cnt][3:0];
if(cnt == 5'd13) begin
skip_en <= 1'b1;
end
end
else begin
tx_bit_sel <= 3'd0;
eth_tx_data <= eth_head[cnt][7:4];
if(skip_en)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
end
end
st_ip_head : begin //发送IP首部 + UDP首部
crc_en <= 1'b1;
eth_tx_en <= 1'b1;
tx_bit_sel <= tx_bit_sel + 3'd1;
if(tx_bit_sel == 3'd0)
eth_tx_data <= ip_head[cnt][27:24];
else if(tx_bit_sel == 3'd1)
eth_tx_data <= ip_head[cnt][31:28];
else if(tx_bit_sel == 3'd2)
eth_tx_data <= ip_head[cnt][19:16];
else if(tx_bit_sel == 3'd3)
eth_tx_data <= ip_head[cnt][23:20];
else if(tx_bit_sel == 3'd4)
eth_tx_data <= ip_head[cnt][11:8];
else if(tx_bit_sel == 3'd5)
eth_tx_data <= ip_head[cnt][15:12];
else if(tx_bit_sel == 3'd6) begin
eth_tx_data <= ip_head[cnt][3:0];
if(cnt == 5'd6) begin
skip_en <= 1'b1;
//提前读请求数据,等待数据有效时发送
tx_req <= 1'b1;
end
end
else if(tx_bit_sel == 3'd7) begin
eth_tx_data <= ip_head[cnt][7:4];
if(skip_en)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
end
end
st_tx_data : begin //发送数据
crc_en <= 1'b1;
eth_tx_en <= 1'b1;
tx_bit_sel <= tx_bit_sel + 3'd1;
if(tx_bit_sel[0] == 1'b0) begin
if(data_cnt < tx_data_num - 16'd1)
data_cnt <= data_cnt + 16'd1;
else if(data_cnt == tx_data_num - 16'd1)begin
//如果发送的有效数据少于18个字节,在后面填补充位
//补充的值为最后一次发送的有效数据
if(data_cnt + real_add_cnt < real_tx_data_num - 16'd1)
real_add_cnt <= real_add_cnt + 5'd1;
else
skip_en <= 1'b1;
end
end
if(tx_bit_sel == 3'd0) begin
eth_tx_data <= tx_data[27:24];
end
else if(tx_bit_sel == 3'd1)
eth_tx_data <= tx_data[31:28];
else if(tx_bit_sel == 3'd2)
eth_tx_data <= tx_data[19:16];
else if(tx_bit_sel == 3'd3)
eth_tx_data <= tx_data[23:20];
else if(tx_bit_sel == 3'd4)
eth_tx_data <= tx_data[11:8];
else if(tx_bit_sel == 3'd5)
eth_tx_data <= tx_data[15:12];
else if(tx_bit_sel == 3'd6) begin
eth_tx_data <= tx_data[3:0];
if(data_cnt != tx_data_num - 16'd1)
tx_req <= 1'b1;
end
else if(tx_bit_sel == 3'd7)
eth_tx_data <= tx_data[7:4];
if(skip_en) begin
data_cnt <= 16'd0;
real_add_cnt <= 5'd0;
tx_bit_sel <= 3'd0;
end
end
st_crc : begin //发送CRC校验值
eth_tx_en <= 1'b1;
tx_bit_sel <= tx_bit_sel + 3'd1;
if(tx_bit_sel == 3'd0)
//注意是crc_next
eth_tx_data <= {~crc_next[0], ~crc_next[1], ~crc_next[2],
~crc_next[3]};
else if(tx_bit_sel == 3'd1)
eth_tx_data <= {~crc_data[24],~crc_data[25],~crc_data[26],
~crc_data[27]};
else if(tx_bit_sel == 3'd2)
eth_tx_data <= {~crc_data[20],~crc_data[21],~crc_data[22],
~crc_data[23]};
else if(tx_bit_sel == 3'd3)
eth_tx_data <= {~crc_data[16],~crc_data[17],~crc_data[18],
~crc_data[19]};
else if(tx_bit_sel == 3'd4)
eth_tx_data <= {~crc_data[12],~crc_data[13],~crc_data[14],
~crc_data[15]};
else if(tx_bit_sel == 3'd5)
eth_tx_data <= {~crc_data[8],~crc_data[9],~crc_data[10],
~crc_data[11]};
else if(tx_bit_sel == 3'd6) begin
eth_tx_data <= {~crc_data[4],~crc_data[5],~crc_data[6],
~crc_data[7]};
skip_en <= 1'b1;
end
else if(tx_bit_sel == 3'd7) begin
eth_tx_data <= {~crc_data[0],~crc_data[1],~crc_data[2],
~crc_data[3]};
tx_done_t <= 1'b1;
end
end
default :;
endcase
end
end
//发送完成信号及crc值复位信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_done <= 1'b0;
crc_clr <= 1'b0;
end
else begin
tx_done <= tx_done_t;
crc_clr <= tx_done_t;
end
end
endmodule
我们前面讲过以太网帧格式的数据部分最少是46个字节, 去掉IP首部字节和UDP首部字节后,有效数据至少为18个字节, 程序设计中已经考虑到这种情况, 当发送的有效数据少于18个字节时, 会在有效数据后面填补充位, 第三段状态机发送状态源代码如下所示
st_tx_data : begin //发送数据
crc_en <= 1'b1;
eth_tx_en <= 1'b1;
tx_bit_sel <= tx_bit_sel + 3'd1;
if(tx_bit_sel[0] == 1'b0) begin
if(data_cnt < tx_data_num - 16'd1)
data_cnt <= data_cnt + 16'd1;
else if(data_cnt == tx_data_num - 16'd1)begin
//如果发送的有效数据少于18个字节,在后面填补充位
//补充的值为最后一次发送的有效数据
if(data_cnt + real_add_cnt < real_tx_data_num - 16'd1)
real_add_cnt <= real_add_cnt + 5'd1;
else
skip_en <= 1'b1;
end
end
if(tx_bit_sel == 3'd0) begin
eth_tx_data <= tx_data[27:24];
end
else if(tx_bit_sel == 3'd1)
eth_tx_data <= tx_data[31:28];
else if(tx_bit_sel == 3'd2)
eth_tx_data <= tx_data[19:16];
else if(tx_bit_sel == 3'd3)
eth_tx_data <= tx_data[23:20];
else if(tx_bit_sel == 3'd4)
eth_tx_data <= tx_data[11:8];
else if(tx_bit_sel == 3'd5)
eth_tx_data <= tx_data[15:12];
else if(tx_bit_sel == 3'd6) begin
eth_tx_data <= tx_data[3:0];
if(data_cnt != tx_data_num - 16'd1)
tx_req <= 1'b1;
end
else if(tx_bit_sel == 3'd7)
eth_tx_data <= tx_data[7:4];
if(skip_en) begin
data_cnt <= 16'd0;
real_add_cnt <= 5'd0;
tx_bit_sel <= 3'd0;
end
end
发送模块的CRC校验是由crc32_d4模块完成的, 发送模块将输入的crc的计算结果每4位高低位互换,按位取反发送出去, crc计算部分在后面阐述,第三段状态机发送CRC校验源代码如下所示:
st_crc : begin //发送CRC校验值
eth_tx_en <= 1'b1;
tx_bit_sel <= tx_bit_sel + 3'd1;
if(tx_bit_sel == 3'd0)
//注意是crc_next
eth_tx_data <= {~crc_next[0], ~crc_next[1], ~crc_next[2],
~crc_next[3]};
else if(tx_bit_sel == 3'd1)
eth_tx_data <= {~crc_data[24],~crc_data[25],~crc_data[26],
~crc_data[27]};
else if(tx_bit_sel == 3'd2)
eth_tx_data <= {~crc_data[20],~crc_data[21],~crc_data[22],
~crc_data[23]};
else if(tx_bit_sel == 3'd3)
eth_tx_data <= {~crc_data[16],~crc_data[17],~crc_data[18],
~crc_data[19]};
else if(tx_bit_sel == 3'd4)
eth_tx_data <= {~crc_data[12],~crc_data[13],~crc_data[14],
~crc_data[15]};
else if(tx_bit_sel == 3'd5)
eth_tx_data <= {~crc_data[8],~crc_data[9],~crc_data[10],
~crc_data[11]};
else if(tx_bit_sel == 3'd6) begin
eth_tx_data <= {~crc_data[4],~crc_data[5],~crc_data[6],
~crc_data[7]};
skip_en <= 1'b1;
end
else if(tx_bit_sel == 3'd7) begin
eth_tx_data <= {~crc_data[0],~crc_data[1],~crc_data[2],
~crc_data[3]};
tx_done_t <= 1'b1;
end
end
图 43.4.8为发送过程中SignalTap抓取的波形图,图中tx_start_en作为开始发送的启动信号, eth_tx_en和eth_tx_data即为MII接口的发送接口。 在开始发送以太网帧头时crc_en拉高开始CRC校验的计算, 在将要发送有效数据时拉高tx_req(发送数据请求)信号, tx_data即为待发送的有效数据, 在所有数据发送完成后输出tx_done(发送完成)信号和crc_clr(CRC校验值复位)信号。
以太网发送模块CRC校验代码如下所示
module crc32_d4(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input [3:0] data , //输入待校验4位数据
input crc_en , //crc使能,开始校验标志
input crc_clr , //crc数据复位信号
output reg [31:0] crc_data, //CRC校验数据
output [31:0] crc_next //CRC下次校验完成数据
);
//*****************************************************
//** main code
//*****************************************************
//输入待校验4位数据,需要先将高低位互换
wire [3:0] data_t;
assign data_t = {data[0],data[1],data[2],data[3]};
//CRC32的生成多项式为:G(x)= x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11
//+ x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x^1 + 1
assign crc_next[0] = crc_en & (data_t[0] ^ crc_data[28]);
assign crc_next[1] = crc_en & (data_t[1] ^ data_t[0] ^ crc_data[28]
^ crc_data[29]);
assign crc_next[2] = crc_en & (data_t[2] ^ data_t[1] ^ data_t[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[30]);
assign crc_next[3] = crc_en & (data_t[3] ^ data_t[2] ^ data_t[1] ^ crc_data[29]
^ crc_data[30] ^ crc_data[31]);
assign crc_next[4] = (crc_en & (data_t[3] ^ data_t[2] ^ data_t[0] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31])) ^ crc_data[0];
assign crc_next[5] = (crc_en & (data_t[3] ^ data_t[1] ^ data_t[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[31])) ^ crc_data[1];
assign crc_next[6] = (crc_en & (data_t[2] ^ data_t[1] ^ crc_data[29]
^ crc_data[30])) ^ crc_data[ 2];
assign crc_next[7] = (crc_en & (data_t[3] ^ data_t[2] ^ data_t[0] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31])) ^ crc_data[3];
assign crc_next[8] = (crc_en & (data_t[3] ^ data_t[1] ^ data_t[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[31])) ^ crc_data[4];
assign crc_next[9] = (crc_en & (data_t[2] ^ data_t[1] ^ crc_data[29]
^ crc_data[30])) ^ crc_data[5];
assign crc_next[10] = (crc_en & (data_t[3] ^ data_t[2] ^ data_t[0] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31])) ^ crc_data[6];
assign crc_next[11] = (crc_en & (data_t[3] ^ data_t[1] ^ data_t[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[31])) ^ crc_data[7];
assign crc_next[12] = (crc_en & (data_t[2] ^ data_t[1] ^ data_t[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[30])) ^ crc_data[8];
assign crc_next[13] = (crc_en & (data_t[3] ^ data_t[2] ^ data_t[1] ^ crc_data[29]
^ crc_data[30] ^ crc_data[31])) ^ crc_data[9];
assign crc_next[14] = (crc_en & (data_t[3] ^ data_t[2] ^ crc_data[30]
^ crc_data[31])) ^ crc_data[10];
assign crc_next[15] = (crc_en & (data_t[3] ^ crc_data[31])) ^ crc_data[11];
assign crc_next[16] = (crc_en & (data_t[0] ^ crc_data[28])) ^ crc_data[12];
assign crc_next[17] = (crc_en & (data_t[1] ^ crc_data[29])) ^ crc_data[13];
assign crc_next[18] = (crc_en & (data_t[2] ^ crc_data[30])) ^ crc_data[14];
assign crc_next[19] = (crc_en & (data_t[3] ^ crc_data[31])) ^ crc_data[15];
assign crc_next[20] = crc_data[16];
assign crc_next[21] = crc_data[17];
assign crc_next[22] = (crc_en & (data_t[0] ^ crc_data[28])) ^ crc_data[18];
assign crc_next[23] = (crc_en & (data_t[1] ^ data_t[0] ^ crc_data[29]
^ crc_data[28])) ^ crc_data[19];
assign crc_next[24] = (crc_en & (data_t[2] ^ data_t[1] ^ crc_data[30]
^ crc_data[29])) ^ crc_data[20];
assign crc_next[25] = (crc_en & (data_t[3] ^ data_t[2] ^ crc_data[31]
^ crc_data[30])) ^ crc_data[21];
assign crc_next[26] = (crc_en & (data_t[3] ^ data_t[0] ^ crc_data[31]
^ crc_data[28])) ^ crc_data[22];
assign crc_next[27] = (crc_en & (data_t[1] ^ crc_data[29])) ^ crc_data[23];
assign crc_next[28] = (crc_en & (data_t[2] ^ crc_data[30])) ^ crc_data[24];
assign crc_next[29] = (crc_en & (data_t[3] ^ crc_data[31])) ^ crc_data[25];
assign crc_next[30] = crc_data[26];
assign crc_next[31] = crc_data[27];
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
crc_data <= 32'hff_ff_ff_ff;
else if(crc_clr) //CRC校验值复位
crc_data <= 32'hff_ff_ff_ff;
else if(crc_en)
crc_data <= crc_next;
end
endmodule
这里要注意的是代码的第18行, 输入待校验的4位数据,高低位数据需要互换再进行CRC校验。 CRC32校验在FPGA实现的原理是线性反馈移位寄存器,其思想是各个寄存器储存着上一次CRC32运算的结果,寄存器的输出即为CRC32的值。 CRC32的原理与公式推导较复杂, 在此可不必深究,CRC校验的源代码可直接通过网页生 成工具直接下载, 网 址 :
CRC32的生成多项式为: G(x)= x^32 + x^26 + x^23 + x^22 + x^16 + x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x^1 + 1
,
设置界面如下图所示:
下载之后只需稍作修改就可以直接使用。