19. 以太网数据回环实验

互联网技术对人类社会的影响不言而喻。当今大部分电子设备都能以不同的方式接入互联网(Internet),在家庭中PC常见的互联网接入方式是使用路由器(Router)组建小型局域网(LAN),利用互联网专线或者调制调解器(modem)经过电话线网络,连接到互联网服务提供商(ISP),由互联网服务提供商把 用户的局域网接入互联网。而企业或学校的局域网规模较大,常使用交换机组成局域网,经过路由以不同的方式接入到互联网中。

而以太网就是一种计算机局域网 技术,具有传输速率高、结构简单、工作可靠等优点,是当今现有局域网采用的最通用的通信协议标准。在本章节,我们将开始以太网相关内容的学习。

19.1. 理论学习

19.1.1. osi七层模型

通信至少是两个设备的事,设备间要完成正确的通信,相互兼容的硬件和软件是必不可少的,这就是我们要说的网络通信协议。

为了实现网络通信的标准化,普及网络应用,国际标准化组织(ISO)将整个以太网通信结构制定了 OSI (Open System Interconnection)模型,译为开放式系统互联。

OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即OSI开放互连系统参考模型。每个层功能不同,网络通信中各司其职,整个模型包括硬件和软件定义。 OSI 模型只是是理想分层,一般的网络系统只是涉及其中几层。OSI参考模型,具体见图 69‑1。

Enet002

图 69‑1 OSI参考模型

OSI参考模型各层功能定义:

  • 应用层:OSI参考模型中最靠近用户的一层,为计算机用户提供应用接口和各种网络服务。主要协议:HTTP,HTTPS,FTP,POP3、SMTP、SNMP、DHCP、DNS。

  • 表示层:表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。同时,数据压缩和加密也是表示层可提供的转换功能之一。主要格式:JPEG、ASCll、D

ECOIC、加密格式。

  • 会话层:会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

  • 传输层:传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。主要协议TCP、UDP。

  • 网络层:网络层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的传输层,就是我们常说的IP层。主要协议:ICMP IGMP IP(IPV4 IPV6)。

  • 数据链路层:实现比特到字节再到帧的组合,使用链路层地址

(以太网使用MAC地址)来访问介质,并进行差错检测。数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。 MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的。主要协议ARP、RARP、IEEE802.3、PPP、CSMA/CD。

  • 物理层:最终信号的传输是通过物理层实现的,通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备:集线器、中继器、调制解调器、网线、双绞线、同轴电缆等。

19.1.2. tcpip五层模型

在一定程度上参考了OSI模型后,产生了TCP/IP协议。TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP和IP两个协议,而是指一个由FTP、SMTP 、TCP、UDP 、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。

在TCP/IP协议中,OSI七层参考模型被简化为五层。在TCP/IP五层模型中,将提供服务类似的应用层、表示层、会话层合并为应用层一个层次,其他层次不变。TCP/IP五层模型,具体见图 69‑2。

Enet003

图 69‑2 TCP/IP五层模型

在 TCP/IP 五层参考模型中,数据链路层又被分为 LLC 层(逻辑链路层)和 MAC 层(媒体介质访问层)。目前,对于普通的接入网络终端的设备, LLC 层和 MAC 层是软、硬件的分界线。如 PC 的网卡主要负责实现参考模型中的 MAC 子层和物理层,在 PC 的软件系统中则有一套庞大程序实现了 LLC 层及以上的所有网络层次的协议。

19.1.3. 以太网

在前文我们介绍了OSI和TCP/IP两中参考模型,接下来我们开始以太网相关内容的介绍。以太网(Ethernet)是互联网技术的一种,由于它是在组网技术中占的比例最高,很多人直接把以太网理解为互联网。

以太网是指遵守 IEEE 802.3 标准组成的局域网,由 IEEE 802.3 标准规定的主要是位于参考模型的物理层(PHY)和数据链路层中的介质访问控制子层(MAC)。在家庭、企业和学校所组建的 PC 局域网形式一般也是以太网,其标志是使用水晶头网线来连接(当然还有其它形式)。 IEEE 还有其它局域网标准,如 IEEE 802.11 是无线局域网,俗称 Wi-Fi。 IEEE802.15 是个人域网,即蓝牙技术,其中的 802.15.4 标准则是 ZigBee 技术。

现阶段,工业控制、环境监测、智能家居的嵌入式设备产生了接入互联网的需求,利用以太网技术,嵌入式设备可以非常容易地接入到现有的计算机网络中。

19.1.3.1. 物理层

在物理层,由 IEEE 802.3 标准规定了以太网使用的传输介质、传输速度、数据编码方式和冲突检测机制,物理层一般是通过一个 PHY 芯片实现其功能的。

传输介质

传输介质包括同轴电缆、双绞线(水晶头网线是一种双绞线)、光纤。根据不同的传输速度和距离要求,基于这三类介质的信号线又衍生出很多不同的种类。最常用的是“五类线”适用于 100BASE-T 和 10BASE-T 的网络,它们的网络速率分别为 100Mbps 和 10Mbps。

编码

为了让接收方在没有外部时钟参考的情况也能确定每一位的起始、结束和中间位置,在传输信号时不直接采用二进制编码。在 10BASE-T 的传输方式中采用曼彻斯特编码,在100BASE-T 中则采用 4B/5B 编码。曼彻斯特编码把每一个二进制位的周期分为两个间隔,在表示“ 1”时,以前半个周期为高电平,后半个周期为低电平。表示“ 0”时则相反,见图 69‑3。

Enet004

图 69‑3 曼彻斯特编码

采用曼彻斯特码在每个位周期都有电压变化,便于同步。但这样的编码方式效率太低,只有 50%。在 100BASE-T 采用的 4B/5B 编码是把待发送数据位流的每 4 位分为一组,以特定的 5位编码来表示,这些特定的 5 位编码能使数据流有足够多的跳变,达到同步的目的,而且效率也从曼彻斯特编码的 50%提高到了 80%。

CSMA/CD 冲突检测

早期的以太网大多是多个节点连接到同一条网络总线上(总线型网络),存在信道竞争问题,因而每个连接到以太网上的节点都必须具备冲突检测功能。以太网具备 CSMA/CD冲突检测机制,如果多个节点同时利用同一条总线发送数据,则会产生冲突,总线上的节点可通过接收到的信号与原始发送的信号的比较检测是否存在冲突,若 存在冲突则停止发送数据,随机等待一段时间再重传。现在大多数局域网组建的时候很少采用总线型网络,大多是一个设备接入到一个独立的路由或交换机接口,组成星型网络,不会产生冲突。但为了兼容,新出的产品还是带有冲突检测机制。

MII/RMII接口

MII (Media Independent Interface(介质无关接口)或称为媒体独立接口,它是IEEE-802.3定义的以太网行业标准。用以连接以太网MAC层和PHY芯片,常用接口有:MII、RMII、SMII、GMII、RGMII。

RMII 接口是 MII 接口的简化版本, MII 需要 16 根通信线, RMII 只需 7 根通信,在功能上是相同的。图 69‑4为 MII 接口连接示意图,图 69‑5为 RMII 接口连接示意图。

Enet005

图 69‑4 MII接口连接

Enet006

图 69‑5 RMII接口连接

  • TX_CLK:数据发送时钟线。标称速率为 10Mbit/s 时为 2.5MHz;速率为 100Mbit/s 时为 25MHz。 RMII 接口没有该线。

  • RX_CLK:数据接收时钟线。标称速率为 10Mbit/s 时为 2.5MHz;速率为 100Mbit/s 时为 25MHz。 RMII 接口没有该线。

  • TX_EN:数据发送使能。在整个数据发送过程保存有效电平。

  • TXD[3:0]或 TXD[1:0]:数据发送数据线。对于 MII 有 4 位, RMII 只有 2 位。只有在TX_EN 处于有效电平数据线才有效。

  • CRS:载波侦听信号,由 PHY 芯片负责驱动,当发送或接收介质处于非空闲状态时使能该信号。在全双工模式该信号线无效。

  • COL:冲突检测信号,由 PHY 芯片负责驱动,检测到介质上存在冲突后该线被使能,并且保持至冲突解除。在全双工模式该信号线无效。

  • RXD[3:0]或 RXD[1:0]:数据接收数据线,由 PHY 芯片负责驱动。对于 MII 有 4 位,RMII 只有 2 位。在 MII 模式,当 RX_DV 禁止、 RX_ER 使能时,特定的 RXD[3:0]值用于传输来自 PHY 的特定信息。

  • RX_DV:接收数据有效信号,功能类似 TX_EN,只不过用于数据接收,由 PHY 芯片负责驱动。对于 RMII 接口,是把 CRS 和 RX_DV 整合成 CRS_DV 信号线,当介质处于不同状态时会自切换该信号状态。

  • RX_ER:接收错误信号线,由 PHY 驱动,向 MAC 控制器报告在帧某处检测到错误。

  • REF_CLK:仅用于 RMII 接口,由外部时钟源提供 50MHz 参考时钟。 | 因为要达到 100Mbit/s 传输速度, MII 和 RMII 数据线数量不同,使用 MII 和 RMII 在时钟线的设计是完全不同的。对于 MII 接口,一般是外部为 PHY 提供 25MHz

时钟源,再由 PHY 提供 TX_CLK 和 RX_CLK 时钟。对于 RMII 接口,一般需要外部直接提供 50MHz时钟源,同时接入 MAC 和 PHY。

19.1.4. mac-子层

MAC 的功能

MAC 子层是属于数据链路层的下半部分,它主要负责与物理层进行数据交接,如是否可以发送数据,发送的数据是否正确,对数据流进行控制等。它自动对来自上层的数据包加上一些控制信号,交给物理层。接收方得到正常数据时,自动去除 MAC 控制信号,把该数据包交给上层。

MAC 数据包

IEEE 对以太网上传输的数据包格式也进行了统一规定,见图 69‑6。该数据包被称为MAC 数据包。

Enet007

图 69‑6 MAC数据包格式

MAC 数据包由前导字段、帧起始定界符、目标地址、源地址、数据包类型、数据域、填充域、校验和域组成。

  • 前导字段(7Byte),也称报头,这是一段方波,用于使收发节点的时钟同步。内容为连续 7 个字节的 0x55。字段和帧起始定界符在 MAC 收到数据包后会自动过滤掉。

  • 帧起始定界符(SFD,1Byte): 用于区分前导段与数据段的,内容为 0xD5。

  • MAC 地址(6Byte):MAC 地址由 48 位数字组成,它是网卡的物理地址,在以太网传输的最底层,就是根据 MAC 地址来收发数据的。部分 MAC 地址用于广播和多播,在同一个网络里不能有两个相同的 MAC 地址。 PC 的网卡在出厂时已经设置好了 MAC

地址,但也可以通过一些软件来进行修改,在嵌入式的以太网控制器中可由程序进行配置。数据包中的 DA 是目标地址, SA 是源地址。

  • 数据包类型(2Byte):本区域可以用来描述本 MAC 数据包是属于 TCP/IP 协议层的 IP 包、 ARP包还是 SNMP 包,也可以用来描述本 MAC 数据包数据段的长度。 如果该值被设置大于 0x0600,不用于长度描述,而是用于类型描述功能,表示与以太网帧相关的

MAC客户端协议的种类。

  • 数据段(46 - 1500Byte):数据段是 MAC 包的核心内容,它包含的数据来自 MAC 的上层。其长度可以从 0~1500 字节间变化。

  • 填充域:由于协议要求整个 MAC 数据包的长度至少为 64 字节(接收到的数据包如果少于 64 字节会被认为发生冲突,数据包被自动丢弃),当数据段的字节少于 46 字节时,在填充域会自动填上无效数据,以使数据包符合长度要求。

  • 校验和域(4Byte):MAC 数据包的尾部是校验和域,它保存了 CRC 校验序列,用于检错。以上是标准的 MAC 数据包, IEEE 802.3 同时还规定了扩展的 MAC 数据包,它是在标准的 MAC 数据包的 SA 和数据包类型之间添加 4 个字节的 QTag 前缀字段,用于获取标志的

MAC 帧。前 2 个字节固定为 0x8100,用于识别 QTag 前缀的存在;后两个字节内容分别为 3 个位的用户优先级、 1 个位的标准格式指示符(CFI)和一个 12 位的 VLAN 标识符。

19.2. ip协议

19.2.1. ip数据包格式

MAC数据包位于TCP/IP协议的数据链路层,当MAC数据包经过数据链路层到达网络层时,前导码、帧起始界定符、目的MAC地址、源MAC地址、类型/长度以及校验字节均被去除,只有有效数据传入了网络层。

网络层互联主要负责主机间或与路由器 、交换机间对分组数据的路由选择和传递。要实现这一功能,就需要相关协议。常用的网络层协议就是IP协议。

传入网络层的数据包并不完全是需要传输的有效数据,他的前面还包含着20字节的IP协议首部。IP数据包格式,具体见图 69‑7。

Enet008

图 69‑7 IP数据包格式

目前使用的IP协议有Ipv4和Ipv6两种,本章节介绍的是Ipv4版本。IP协议网际协议第4版(Internet Protocol version 4,IPv4)是TCP/IP协议使用的数据报传输机制。数据报是一个可变长分组,有两部分组成:IP首部和数据。首部长度可由20~60个字节组成,该部分包含有与路由选择和传输有关的重要信息。首部各字段意义按顺序如下:

  • 版本(4bit):该字段定义IP协议版本,负责向处理机所运行的IP软件指明此IP数据报是哪个版本,所有字段都要按照此版本的协议来解释。如果计算机使用其他版本,则丢弃数据包。

  • 首部长度(4 bit):该字段定义数据报协议头长度,表示协议头部具有32位字长的数量。协议头最小值为5,最大值为15。

  • 服务类型(8 bit):该字段定义上层协议对处理当前数据报所期望的服务质量,并对数据报按照重要性级别进行分配。前3位成为优先位,后面4位成为服务类型,最后1位没有定义。这些8位字段用于分配优先级、延迟、吞吐量以及可靠性。

  • 总长度(16 bit):该字段定义整个IP数据报的字节长度,包括协议头部和数据。其最大值为65535字节。以太网协议对能够封装在一个帧中的数据有最小值和最大值的限制(46~1500个字节)。

  • 标识(16 bit):该字段包含一个整数,用于识别当前数据报。当数据报分段时,标识字段的值被复制到所有的分段之中。该字段由发送端分配帮助接收端集中数据报分段。

  • 标记(3 bit):该字段由3位字段构成,其中最低位(MF)控制分段,存在下一个分段置为1,否则置0代表该分段是最后一个分段。中间位(DF)指出数据报是否可进行分段,如果为1则机器不能将该数据报进行分段。第三位即最高位保留不使用,值为0。

  • 分段偏移(13 bit):该字段指出分段数据在源数据报中的相对位置,支持目标IP适当重建源数据。

  • 生存时间(8 bit):该字段是一种计数器,在丢弃数据报的每个点值依次减1直至减少为0。这样确保数据报拥有有限的环路过程(即TTL),限制了数据报的寿命。

  • 协议(8 bit):该字段指出在IP处理过程完成之后,有哪种上层协议接收导入数据报。这个字段的值对接收方的网络层了解数据属于哪个协议很有帮助。

  • 首部校验和(16 bit):该字段帮助确保IP协议头的完整性。由于某些协议头字段的改变,这就需要对每个点重新计算和检验。计算过程是先将校验和字段置为0,然后将整个头部每16位划分为一部分,将个部分相加,再将计算结果取反码,插入到校验和字段中。

  • 源地址(32 bit):源主机IP地址 ,该字段在IPv4数据报从源主机到目的主机传输期间必须保持不变。

  • 目的地址(32 bit):目标主机IP地址,该字段在IPv4数据报从源主机到目的主机传输期间同样必须保持不变。

19.2.2. ip首部校验和checksum

计算方法

校验字节强制置0,将IP首部20字节 按2字节, 即16比特,分开分别相加,若如果大于FFFF那么把高16位与低16位相加,直到最终结果为16比特数据。将计算结果取反作为IP首部校验和字节。

例:抓取IP数据包,取IP数据报报头部分(20B),数据如下,45 00 00 30 80 4c 40 00 80 06 b5 2e d3 43 11 7b cb 51 15 3d,计算IP首部校验和。

  1. 将校验和字段b5 2e置为00 00,数据变为:

45 00 00 30 80 4c 40 00 80 06 00 00 d3 43 11 7b cb 51 15 3d

  1. 以2字节为单位,数据反码求和:

4500+0030+804c+4000+8006+0000+d343+117b+cb51+153d=34ace

  1. 将进位(3)加到低16位(4ace)上:

0003+4ace=4ad1

  1. 将4ad1取反得:checksum = b52e

IP首部校验和检验

对IP首部中每个16 bit进行二进制反码求和,将计算结果再取反码,若结果为0,通过检验,否则,不通过检验。

例:验证IP首部45 00 00 30 80 4c 40 00 80 06 b5 2e d3 43 11 7b cb 51 15 3d

  1. 对IP首部进行反码求和:

4500+0030+804c+4000+8006+b52e+d343+117b+cb51+153d=3fffc

0003+fffc=ffff

  1. 求和结果取反码:~ffff=0 ,校验正确。

19.3. udp协议

19.3.1. udp数据包格式

网络层在接收到数据包后,取下数据包的IP首部,将剩余有效数据包发送到传输层。传输层提供了主机应用程序进程之间的端到端的服务,基本功能是:分割与重组数据、按端口号寻址、连接管理、差错控制和流量控制、纠错功能。

传输层有两种传输协议:基于字节流的TCP协议、基于报文流的 UDP协议。两种协议各有优缺点,应用于不同场景。TCP协议是面向连接的流传输协议,可以保证数据传输的完整、有序,是可靠协议,常用在对数据完整性和正确性要求较高的场合,如文件传输。占用资源较UDP多,速度较UDP慢;UDP协议则是一种无连接的 传输层协议,提供面向事务的简单不可靠信息传送服务,因为无需连接,传输速度较TCP快,占用资源量较TCP少,适合实时数据的传输,如视频通话。

使用FPGA 实现TCP协议传输并非不可能,但占用逻辑量较大,设计较为复杂,我们使用较为简单的UDP协议。

若传输层使用UDP协议,那么传入传输层的数据包为UDP数据包,前8各字节为UDP首部。UDP数据包格式,具体见图 69‑8。

Enet009

图 69‑8 UDP数据包格式

UDP数据包分为UDP首部和有效数据两个部分。UDP首部由源端口,目的端口,报文长度以及校验和组成。相比TCP,UDP的传输效率更高,开销更小,但是无法保证数据传输可靠性。首部各字段意义按顺序如下:

  • 源端口号(16Byte):源主机的应用程序使用的端口号。

  • 目的端口号(16Byte):目的主机的应用程序使用的端口号。

  • UDP长度(16Byte):是指UDP头部和UDP数据的字节长度。因为UDP头部长度是8字节,所以字段的最小值为8。

  • UDP校验和(16Byte):该字段提供了与TCP校验字段同样的功能;该字段是可选的。

19.3.2. udp校验和checksum

UDP校验和的计算需要三部分数据:UDP伪首部、UDP首部和有效数据。伪首部包含IP首部一些字段,其目的是让UDP两次检查数据是否已经正确到达目的地,只是单纯做校验使用。UDP校验计算相关参数示意图,具体见图 69‑9。

Enet010

图 69‑9 UDP校验计算相关参数示意图

知道了UDP校验所需要的相关参数,我们开始UDP校验和计算方法的讲解。UDP校验和计算步骤:校验字节强制置0,将三部分数据按2字节, 即16比特,分开分别相加,若如果大于FFFF那么把高16位与低16位相加,直到最终结果为16比特数据。将计算结果取反作为UDP校验和字节。

例:相关数据如下图所示,计算UDP校验和。

Enet011

图 69‑10 UDP校验和计算

  1. 将校验和字段00 92 置为00 00:

a9 fe bf 1f a9 fe 01 17 00 11 00 28 04 d2 04 d2 00 28 00 00 68 74 74 70 3a 2f 2f 77 77 77 2e 63 6d 73 6f 66 74 2e 63 6e 20 51 51 3a 31 30 38 36 35 36 30 30

  1. 以2字节为单位,数据反码求和:

a9fe + bf1f + a9fe + 0117 + 0011 + 0028 + 04d2 + 04d2 + 0028 + 0000 + 6874 + 7470 + 3a2f + 2f77 + 7777 + 2e63 + 6d73 + 6f66 + 742e + 636e + 2051 + 513a + 3130 + 3836 + 3536 + 3030 = 6 ff67

  1. 将进位(6)加到低16位(ff67)上:

6 + ff67 = ff6d

  1. 将ff6d取反得:checksum = 0092

19.4. 实战演练

19.4.1. 实验目标

利用所学知识,设计并实现基于UDP协议的以太网数据收发控制器。使用控制器控制器实现PC机与板卡的数据回环。在PC机上使用网口调试调试助手向板卡发送数据,板卡将接收到的数据回传给PC端。

19.4.2. 硬件资源

征途Pro开发板使用的以太网PHY芯片型号为LAN8720A,是低功耗的10-BASE-T/100-BASE- TX全双工以太网PHY层芯片,支持10Mbps和100Mbps,I/O引脚电压可变,符合IEEE802.3-2005标准,支持通过RMII接口与以太网MAC层通信,可通过自协商的的方式与目的主机实现最佳的连接方式(调整最佳速度和双工模式),具有HP Auto- MDIX自动翻转功能,实现直连或交叉连接。LAN8720A功能框图,具体见图 69‑11。

Enet012

图 69‑11 LAN8720A功能框图

这里只对LAN8720A做简单介绍,详细信息参见数据手册。征途Pro开发板以太网部分实物图、原理图,具体见图 69‑12、图 69‑13。

Enet013

图 69‑12 以太网部分实物图

Enet014

图 69‑13 以太网部分原理图

19.5. 程序设计

19.5.1. 整体说明

实验目标已经明确,硬件资源也已经介绍完毕,我们开始程序设计部分的讲解。我们先来对实验工程进行一个整体说明,让读者了解整个实验工程的框架结构。

为了便于读者理解,实验工程的整体框架我们分两部分来说明,第一部分为以太网数据收发器(基于MII接口);第二部分在第一部分的基础上添加MII与RMII转换模块,实现基于RMII接口的以太网数据收发器。之所有这样讲解,一是为了方便读者理解,二是为了读者能够将MII和RMII的相关知识同时掌握。

我们先来第一部分以太网数据收发器的介绍,模块框图,具体见图 69‑14;子功能模块简介,具体见表格 69‑1。

Enet015

图 69‑14 基于MII接口的UDP顶层模块框图

表格 69‑1 子功能模块功能描述

模块名称

功能描述

ip_receive

以太网数据接收模块

ip_send

以太网数据发送模块

crc_32_d4

CRC校验模块

eth_udp_mii

顶层模块

由上述图表可知,实验工程的第一部分包含4个子功能模块:以太网数据接收模块(ip_receive)、以太网数据发送模块(ip_send)、CRC校验模块(crc_32_d4)和UDP顶层模块(eth_udp_mii)。

以太网数据接收模块(ip_receive),模块功能是接收并处理PC机传入板卡的以太网数据信息;以太网数据发送模块(ip_send),模块功能是发送以太网数据到PC机;CRC校验模块(crc_32_d4),模块功能是对回传至PC机的以太网数据做CRC冗余校验;UDP顶层模块(eth_udp_mii) 为第一部分的顶层模块,内部例化3个子功能模块,连接各自对应信号。

第一部分介绍完毕,开始第二部分基于RMII接口的以太网数据收发器部分的简要说明。模块框图,具体见图 69‑15;子功能模块简介,具体见表格 69‑2。

Enet016

图 69‑15 实验工程整体框图

表格 69‑2 子功能模块功能描述

模块名称

功能描述

eth_udp_mii

基于MII接口的UDP顶层模块

fifo_2048x32

数据缓存FIFO(IP核)

rmii_to_mii

RMII接口转MII接口模块

mii_to_rmii

MII接口转RMII接口模块

ethernet_udp_rmii

基于RMII接口的UDP顶层模块

由上述图表可知,实验工程的第二部分包含4个子功能模块:UDP顶层模块(eth_udp_mii)、数据缓存FIFO(IP核)、RMII接口转MII接口模块(rmii_to_mii) 、MII接口转RMII接口模块(mii_to_rmii)和顶层模块(ethernet_udp_rmii)。

UDP顶层模块(eth_udp)是第一部分的基于MII接口的以太网数据收发控制器,实现MII接口的以太网数据收发操作,在第二部分作为子功能模块;数据缓存FIFO(IP核),此部分是调用IP核生成的FIFO,缓存以太网有效数据;RMII接口转MII接口模块(rmii_to_mii) 、MII接口转RMII接口模块(mii_to_rmii)实现MII接口与RMII接口的转换;顶层模块(ethernet_udp_rmii)为第二部分的顶层模块,也是整个实验工程的顶层模块,内部例化4个子功能模块,连接各自对应信号。

这一部分我们对实验工程整体做了简要介绍,下面我们将会对实验工程涉及的子功能模块的设计与实现做详细说明。

19.5.1.1. 以太网数据接收模块

模块框图

以太网数据接收模块ip_receive,是第一部分以太网数据收发器的子功能模块之一,作用是接收PC端传入板卡的以太网数据信息,检验目的MAC地址、目的IP地址是否与板卡一致,无check_sum和CRC冗余校验,提取有效数据信息,完成4位数据到8位再到32位的转换。模块框图,具体见图 69‑16;输入输出端口简介,具体见表格 69‑3。

Enet017

图 69‑16 以太网数据接收模块框图

表格 69‑3 输入输出端口简介

信号

位宽

类型

功能描述

sys_clk

1bit

input

模块工作时钟

sys_rst_n

1bit

input

复位信号,低有效

eth_rxdv

1bit

input

以太网数据有效信号

eth_rx_data

4bit

input

输入以太网数据

rec_data_en

1bit

output

数据接收使能信号

rec_data

32bit

output

输出有效以太网数据

rec_end

1bit

output

单包数据接收完成标志信号

rec_data_num

16bit

output

接收有效数据字节数

由上述图表可知,本模块输入输出信号共8路,输入输出各4路。

输入信号中,时钟信号sys_clk,由外部PHY芯片传入(eth_rx_clk),频率25MHz,作为模块工作时钟;复位信号sys_rst_n,低电平有效,由板卡复位按键传入;以太网数据有效信号eth_rxdv,由外部PHY芯片传入,高电平有效; 输入以太网数据eth_rx_data,由外部PHY芯片传入,位宽为4bit,只有当以太网数据有效信号eth_rxdv为有效的高电平时,传入的以太网数据有效。

输出信号中,输出有效以太网数据rec_data,位宽32bit,通过提取并拼接输入的以太网有效数据得到;与其成对出现的是数据接收使能信号rec_data_en,两信号分别作为写数据和写使能,将以太网有效数据暂存到工程顶层的FIFO中;单包数据接收完成标志信号rec_end,单包数据接收完成后保持一个 时钟周期高电平;接收有效数据字节数rec_data_num,表示以太网有效数据的字节个数。

波形图绘制

在模块框图小节,我们已经对以太网数据接收模块的模块功能和输入输出端口做了详细介绍。接下来我们通过波形图的绘制,为读者说明各信号的设计与实现方法,以及模块的功能实现。以太网数据接收模块整体波形图,具体见图 69‑17、图 69‑18、图 69‑19、图 69‑20。

Enet018

图 69‑17 以太网数据接收模块整体波形图(一)

Enet019

图 69‑18 以太网数据接收模块整体波形图(二)

Enet020

图 69‑19 以太网数据接收模块整体波形图(三)

Enet021

图 69‑20 以太网数据接收模块整体波形图(四)

模块整体波形图已经列出来了,波形图中的各信号波形是如何设计与实现的呢,下面我们会对各波形的设计与实现做详细说明。

第一部分:输入信号

在前文中我们提到,PHY芯片MAC层之间的连接涉及到一个接口协议,常用的接口协议有MII、RMII、SMII、GMII、RGMII。

对于MII、RMII这两种接口协议我们也做了介绍,我们实验中使用的是MII接口协议,MII支持10Mbps和100Mbps的操作,数据位宽为4位,在100Mbps传输速率下,时钟频率为25MHz。

在MII接口协议中,每个时钟周期只传输4位数据, 数据传输时先发送字节的低4位, 再发送字节的高4位,数据和使能信号在时钟的下降沿变化,时钟上升沿可以采到数据稳定状态。MII接口数据接收、发送时序图,具体见图 69‑21、图 69‑22。

Enet022

图 69‑21 MII接口数据接收时序图

Enet023

图 69‑22 MII接口数据发送时序图

根据MII接口数据接收、发送时序图,绘制输入信号波形,输入各信号波形如下。

Enet024

图 69‑23 输入各信号波形图(一)

Enet025

图 69‑24 输入各信号波形图(二)

第二部分:数据拼接相关信号波形的设计与实现

在MII接口协议中,每个时钟周期只传输4位数据,且字节低4位在前,字节高4位在后,这种传输方式不利于数据的读取与处理,我们需要对传入的数据进行处理,处理为便于读取与检测的单字节数据。

那么如何实现这一转化呢,我们可以使用数据拼接的方式。

首先要对输入的数据使能信号eth_rxdv和数据信号eth_rx_data进行打拍处理,使两信号延后各自源信号一个时钟周期,同步时钟使用系统时钟sys_clk,数据变化沿使用时钟的下降沿,这样能够保证打拍处理是一个完整的时钟周期,以及后续数据的正确拼接。

信号eth_rxdv_reg、eth_rx_data_reg分别由数据使能信号eth_rxdv、数据信号eth_rx_data产生,延后一个时钟周期,相关信号波形如下。

Enet026

图 69‑25 数据拼接相关信号波形图(一)

Enet027

图 69‑26 数据拼接相关信号波形图(二)

输入数据在每个完整的时钟周期内出入有效数据4bit,只有两个完整的时钟周期才能传入完整的单字节数据,虽然数据和使能信号完成打拍,但还不能开始进行数据拼接,因为约束条件不足。所以我们需要一个使能信号作为约束条件,每两个时钟周期完成一次数据拼接。

声明数据拼接使能信号data_sw_en,初值为0,当数据使能信号eth_rxdv_reg有效时,每个时钟周期的下降沿对自身取反,这样就会产生一个类似于对系统时钟二分频的使能信号。

在系统时钟的上升沿、数据拼接使能信号为低电平时,进行输入数据的拼接,拼接后数据data = {eth_rx_data, eth_rx_data_reg}。此时时钟信号上升沿采集到数据稳定状态,保证了拼接数据的正确性。

将数据拼接使能信号延后一个时钟周期产生数据使能信号data_en,产生这一信号的作用是作为拼接数据的使能信号,方便拼接数据的读取。

上述各信号信号波形图如下。

Enet028

图 69‑27 数据拼接相关信号波形图(三)

Enet029

图 69‑28 数据拼接相关信号波形图(四)

第三部分:状态机及相关信号波形的设计与实现

在第二部分我们已经完成了单字节数据的拼接,拼接后的数据是一个完整的以太网数据包,数据包包括:前导码+帧起始界定符、以太网帧头、IP首部、UDP首部、UDP数据(有效数据)、CRC校验字节。

在以太网数据接收模块中,我们要对输入的目的MAC地址、目的IP地址进行检验,提取有效数据,不进行check_sum和CRC冗余校验。

那么如何进行地址校验与数据提取呢,我们可以使用状态机来实现。

根据以太网数据包中数据类型的不同,我们在模块内定义7个状态:初始状态(IDLE)、数据包头接收状态(PACKET_HEAD)、以太网帧头接收状态(ETH_HEAD)、IP首部接收状态(IP_HEAD)、UDP首部接收状态(UDP_HEAD)、有效数据接收状态(REC_DATA)、结束状态(REC_E ND)。

结合模块定义和模块实现功能,绘制状态机状态转移图。状态状态机状态转移图,具体见图 69‑29。

Enet030

图 69‑29 状态机状态转移图

由状态机状态转移图可知,我们需要声明两使能信号sw_en、err_en作为约束条件实现状态机的状态跳转。那么这两使能信号是如和设计与产生的呢,下面我们进行详细讲解。

在前文我们也提到了,拼接后的数据包以字节为单位,包含前导码+帧起始界定符、以太网帧头、IP首部、UDP首部、UDP数据(有效数据)、CRC校验字节着几种不同的数据类型,本模块的目的就是核对目的MAC地址、目的IP地址,以及提取有效数据。

为了实现不同类型数据的提取,我们使用状态机,不同状态对应不同不同数据类型,状态机的跳转就对应着不同不同数据间的分界点,那么如何区别不同类型数据呢,我么可以使用字节宽度。以太网数据包中的各类型数据的字节宽度是固定的或包含在数据中的,我们可以利用字节宽度信息作为状态机跳转的约束条件。但有一点还要注意,我 们可以利用字节宽度对不同类型数据进行数据采集,但数据的正确性是未知的,我们还需要结合包内数据对数据正确性进行检测。

系统上电后,模块内状态机一直处于初始状态(IDLE),在系统时钟上升沿,当检测到数据使能信号data_en为高电平、拼接数据为8’h55时,使能信号 sw_en拉高一个时钟周期,状态机跳转到数据包头接收状态(PACKET_HEAD);

在数据包头接收状态内,我们要完成前导码+帧起始界定符(7个8’h55 + 1个8’hd5)的接收与检验。声明字节计数器cnt_byte,计数器初值为0,状态机处于数据包头接收状态时,数据使能信号data_en拉高一次,计数器自加1。当输入拼接数据data为连续的6个8’h55,加上之前的1个8’h5 5,共7个8’h55,前导码通过检验;当计数器cnt_byte计数值为6,拼接数据data为8’hd5,帧起始界定符通过检验,计数器归零,使能信号 sw_en拉高一个时钟周期,状态机跳转到以太网帧头接收状态(ETH_HEAD)。若其中任何一个字节与规定的前导码+帧起始界定符(7个8’h55 + 1个8’hd5)不同,错误使能信号err_en拉高一个时钟周期,计数器归0,状态机跳回初始状态;

状态机跳转到以太网帧头接收状态(ETH_HEAD),在此状态内我们要完成目的MAC地址的检验,此状态内会接收14字节数据,包括目的MAC地址(6字节)、源MAC地址(6字节)、长度/类型字节(2字节)。

字节计数器cnt_byte会在本状态完成0-13,共14个字节的计数,0-5字节就是我们要校验的目的MAC地址。

声明目的MAC地址寄存器des_mac,位宽为6个字节,当字节计数器在0-5计数范围时,对输入拼接数据data进行寄存。声明标志信号mac_flag,当寄存器des_mac寄存的MAC地址与板卡MAC地址相同时,mac_flag信号保持高电平,否者为低电平。

字节计数器计数到13时,表示本状态14个字节接收完毕,此时若标志信号mac_flag信号为高电平,表示目的MAC地址正确,使能信号sw_en拉高一个时钟周期,状态机跳转到IP首部接收状态(IP_HEAD);标志信号mac_flag为低电平,目的MAC地址错误,错误使能信号err_en拉高一个时钟周期 ,状态机跳回到初始状态(IDLE)。

上述三状态各信号波形如下:

Enet031

图 69‑30 状态机相关信号波形图(一)

目的MAC地址校验通过后,状态机跳转到IP首部接收状态,在此状态我们需要接收并校验目的IP地址。

IP首部所包含的字节数是不固定的,但在IP首部第一字节的低4位数据会告诉我们IP首部所包含的字节数,要注意的是,它的单位是4字节。声明寄存器ip_len寄存IP首部地址字节数,当数据使能信号data_en为高电平、字节计数器计数值为0时,将拼接数据data低4位数据左移2位赋值给ip_len。

IP首部的目的IP地址在首部的第17至20字节,声明目的IP地址寄存器des_ip,当字节计数器16-19计数范围时,对输入拼接数据data进行寄存;声明标志信号ip_flag,当寄存器des_ip寄存的目的IP地址与板卡IP地址相同时,ip_flag信号保持高电平,否者为低电平。

字节计数器计数到(ip_len - 1)时,表示本状态ip_len个字节接收完毕,此时若标志信号ip_flag信号为高电平,表示目的IP地址正确,使能信号sw_en拉高一个时钟周期,状态机跳转到UDP首部接收状态(UDP_HEAD);标志信号ip_flag为低电平,目的IP地址错误,错误使能信号er r_en拉高一个时钟周期,状态机跳回到初始状态(IDLE)。

IP首部接收状态(IP_HEAD)下,各信号波形如下:

Enet032

图 69‑31 状态机相关信号波形图(二)

目的IP地址校验通过后,状态机跳转到UDP首部接收状态,UDP首部包含8字节数据,分别是源端口、目的端口、数据长度(UDP首部 + 数据长度)、checksum校验字节。在此状态我们不进行端口和checksum的字节校验,只提取数据长度信息,为后面数据接收做准备。

UDP首部的8字节数据中,第5、6字节包含数据数据长度信息,它的单位是字节。声明寄存器udp_len寄存udp首部数据长度信息,长度为2字节,当数据使能信号data_en为高电平、字节计数器cnt_byte计数值为4、5时,将拼接数据data分别赋值给寄存器udp_len的高字节、低字节,对数据长度 信息进行寄存。

完成UDP首部的8字节接收后,即字节计数器cnt_byte计数值为7,数据使能信号data_en为高电平时,使能信号sw_en拉高一个时钟周期,状态机跳转到有效数据接收状态(REC_DATA)。

在有效数据接收状态(REC_DATA),我们需要接收传入的有效数据,那么有效数据的字节长度怎么确定呢。

UDP首部接收状态中,我们声明了寄存器udp_len,它所寄存的数据字节长度信息是UDP首部和有效数据的总字节长度;有效数据的字节长度是要在此基础上减去UDP首部的字节长度。

所以,我们声明寄存器data_len,寄存有效数据字节长度信息,寄存值为udp_len寄存的数据字节长度信息减去UDP首部的字节长度。

同时,声明一个字节计数器cnt_data对传入的有效字节数据进行计数,计数器初值为0,当数据使能信号data_en为高电平时,计数器自加1,当计数到最大值(data_len - 1)时,有效数据接收完毕,计数器归0,使能信号sw_en拉高一个时钟周期,状态机跳转到结束状态(REC_END)。

在结束状态中,模块会接收4字节的CRC校验位,但本模块没有进行CRC冗余校验。当数据使能信号eth_rxdv_reg为无效低电平时,以太网数据接收完毕,状态机跳回到初始状态,等待下一个以太网数据包的传入。各信号波形如下。

Enet033

图 69‑32 状态机相关信号波形图(三)

Enet034

图 69‑33 状态机相关信号波形图(四)

Enet035

图 69‑34 状态机相关信号波形图(五)

第四部分:输出信号

在数据包中完成有效数据提取后,我们需要将提取的有效数据进行输出,但是以太网数据包一般以32bit为单位,为了保持格式一致,我们要把8位数据转成32位数据。

为了实现位宽的转换,声明数据字节计数器cnt_rec_data,对有效字节数据进行计数,初值为0,计数范围0-3;声明输出数据rec_data,位宽为32位,4字节,计数器cnt_rec_data一个循环计数完成一次数据拼接;当计数器计数到最大值3,输出拼接的32位数据rec_data,与输出数据同 步输出的还有输出数据使能信号rec_data_en;数据输出完成后,声明输出信号rec_data_num记录输出数据字节数。上述各信号波形如下图所示。

Enet036

图 69‑35 输出各信号波形图(一)

Enet037

图 69‑36 输出各信号波形图(二)

整合各部分信号波形,就能得到模块整体波形图,设计思路仅供参考,读者可按照自己理解进行波形图的设计。

代码编写

以上述绘制波形图为参照,编写模块参考代码。模块参考代码具体见代码清单 69‑1。

代码清单 69‑1 以太网数据接收模块参考代码(ip_receive.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
module ip_receive
#(
parameter BOARD_MAC = 48'hFF_FF_FF_FF_FF_FF , //板卡MAC地址
parameter BOARD_IP = 32'hFF_FF_FF_FF //板卡IP地址
)
(
input wire sys_clk , //时钟信号
input wire sys_rst_n , //复位信号,低电平有效
input wire eth_rxdv , //数据有效信号
input wire [3:0] eth_rx_data , //输入数据

output reg rec_data_en , //数据接收使能信号
output reg [31:0] rec_data , //接收数据
output reg rec_end , //数据包接收完成信号
output reg [15:0] rec_data_num //接收数据字节数
);

////
//\* Parameter and Internal Signal \//
////

//parameter define
localparam IDLE = 7'b000_0001, //初始状态
PACKET_HEAD = 7'b000_0010, //接收数据包头
ETH_HEAD = 7'b000_0100, //接收以太网首部
IP_HEAD = 7'b000_1000, //接收IP首部
UDP_HEAD = 7'b001_0000, //接收UDP首部
REC_DATA = 7'b010_0000, //接收数据
REC_END = 7'b100_0000; //单包数据传输结束

//wire define
wire [15:0] data_len ; //有效数据字节长度
wire ip_flag ; //IP地址正确标志
wire mac_flag ; //MAC地址正确标志

//reg define
reg eth_rxdv_reg ; //数据有效信号打拍
reg [3:0] eth_rx_data_reg ; //输入数据打拍
reg data_sw_en ; //数据拼接使能信号
reg data_en ; //拼接后的数据使能信号
reg [7:0] data ; //拼接后的数据
reg [6:0] state ; //状态机状态变量
reg sw_en ; //状态跳转标志信号
reg err_en ; //数据读取错误信号
reg [4:0] cnt_byte ; //字节计数器
reg [47:0] des_mac ; //目的MAC地址,本模块中表示开发板MAC地址
reg [31:0] des_ip ; //目的IP地址,本模块中表示开发板IP地址
reg [5:0] ip_len ; //IP首部字节长度
reg [15:0] udp_len ; //UDP部分字节长度
reg [15:0] cnt_data ; //接收数据字节计数器
reg [1:0] cnt_rec_data ; //数据计数器,单位4字节

////
//\* Main Code \//
////

//eth_rxdv_reg:数据有效信号打拍
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_rxdv_reg <= 1'b0;
else
eth_rxdv_reg <= eth_rxdv;

//eth_rx_data_reg:输入数据打拍
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_rx_data_reg <= 4'b0;
else
eth_rx_data_reg <= eth_rx_data;

//data_sw_en:数据拼接使能
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_sw_en <= 1'b0;
else if(eth_rxdv_reg == 1'b1)
data_sw_en <= ~data_sw_en;
else
data_sw_en <= 1'b0;

//data_en:拼接后的数据使能信号
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_en <= 1'b0;
else
data_en <= data_sw_en;

//data:拼接后的数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data <= 8'b0;
else if((eth_rxdv_reg == 1'b1) && (data_sw_en == 1'b0))
data <= {eth_rx_data,eth_rx_data_reg};
else
data <= data;

//state:状态机状态变量
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE:
if(sw_en == 1'b1)
state <= PACKET_HEAD;
else
state <= IDLE;
PACKET_HEAD:
if(sw_en == 1'b1)
state <= ETH_HEAD;
else if(err_en == 1'b1)
state <= IDLE;
else
state <= PACKET_HEAD;
ETH_HEAD:
if(sw_en == 1'b1)
state <= IP_HEAD;
else if(err_en == 1'b1)
state <= IDLE;
else
state <= ETH_HEAD;
IP_HEAD:
if(sw_en == 1'b1)
state <= UDP_HEAD;
else if(err_en == 1'b1)
state <= IDLE;
else
state <= IP_HEAD;
UDP_HEAD:
if(sw_en == 1'b1)
state <= REC_DATA;
else
state <= UDP_HEAD;
REC_DATA:
if(sw_en == 1'b1)
state <= REC_END;
else
state <= REC_DATA;
REC_END:
if(sw_en == 1'b1)
state <= IDLE;
else
state <= REC_END;
default:state <= IDLE;
endcase

//sw_en:状态跳转标志信号
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sw_en <= 1'b0;
else if((state == IDLE) && (data_en == 1'b1) && (data == 8'h55))
sw_en <= 1'b1;
else if((state == PACKET_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 5'd6) && (data == 8'hd5))
sw_en <= 1'b1;
else if((state == ETH_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 8'd13) && (mac_flag == 1'b1))
sw_en <= 1'b1;
else if((state == IP_HEAD) && (data_en == 1'b1)
&& (cnt_byte == ip_len - 1'b1)
&& (des_ip[23:0] == BOARD_IP[31:8]) && (data == BOARD_IP[7:0]))
sw_en <= 1'b1;
else if((state == UDP_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 8'd7))
sw_en <= 1'b1;
else if((state == REC_DATA) && (data_en == 1'b1)
&& (cnt_data == data_len - 1'b1))
sw_en <= 1'b1;
else if((state == REC_END) && (eth_rxdv_reg == 1'b0)
&& (sw_en == 1'b0))
sw_en <= 1'b1;
else
sw_en <= 1'b0;

//err_en:数据读取错误信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
err_en <= 1'b0;
else if((state == PACKET_HEAD) && (data_en == 1'b1)
&& (cnt_byte < 5'd6) && (data != 8'h55))
err_en <= 1'b1;
else if((state == PACKET_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 5'd6) && (data != 8'hd5))
err_en <= 1'b1;
else if((state == ETH_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 5'd13) && (mac_flag == 1'b0))
err_en <= 1'b1;
else if((state == IP_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 5'd19) && (ip_flag == 1'b0))
err_en <= 1'b1;
else
err_en <= 1'b0;

//cnt_byte:字节计数器
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_byte <= 5'd0;
else if((state == PACKET_HEAD) && (data_en == 1'b1))
if(cnt_byte == 5'd6)
cnt_byte <= 5'd0;
else
cnt_byte <= cnt_byte + 5'd1;
else if((state == ETH_HEAD) && (data_en == 1'b1))
if(cnt_byte == 5'd13)
cnt_byte <= 5'd0;
else
cnt_byte <= cnt_byte + 5'b1;
else if((state == IP_HEAD) && (data_en == 1'b1))
if(cnt_byte == 5'd19)
if(ip_flag == 1'b1)
begin
if(cnt_byte == ip_len - 1'b1)
cnt_byte <= 5'd0;
end
else
cnt_byte <= 5'd0;
else if(cnt_byte == ip_len - 1'b1)
cnt_byte <= 5'd0;
else
cnt_byte <= cnt_byte + 5'd1;
else if((state == UDP_HEAD) && (data_en == 1'b1))
if(cnt_byte == 5'd7)
cnt_byte <= 5'd0;
else
cnt_byte <= cnt_byte + 5'd1;
else
cnt_byte <= cnt_byte;

//des_mac:目的MAC地址,本模块中表示开发板MAC地址
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
des_mac <= 48'h0;
else if((state == ETH_HEAD) && (data_en == 1'b1)
&& (cnt_byte < 8'd6))
des_mac <= {des_mac[39:0],data};

//mac_flag:MAC地址正确标志
assign mac_flag = ((state == ETH_HEAD) && (des_mac == BOARD_MAC))
? 1'b1 : 1'b0;

//des_ip:目的IP地址,本模块中表示开发板IP地址
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
des_ip <= 32'd0;
else if((state == IP_HEAD) && (data_en == 1'b1)
&& (cnt_byte > 5'd15) && (cnt_byte <= 5'd19))
des_ip <= {des_ip[23:0],data};

//ip_flag:IP地址正确标志
assign ip_flag = ((des_ip[23:0] == BOARD_IP[31:8])
&& (data == BOARD_IP[7:0])) ? 1'b1 : 1'b0;
//ip_len:IP首部字节长度
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ip_len <= 6'b0;
else if((state == IP_HEAD) && (data_en == 1'b1)
&& (cnt_byte == 8'd0))
ip_len <= {data[3:0],2'b00};

//udp_len:UDP部分字节长度
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
udp_len <= 16'd0;
else if((state == UDP_HEAD) && (data_en == 1'b1)
&& (cnt_byte >= 8'd4) && (cnt_byte <= 8'd5))
udp_len <= {udp_len[7:0],data};

//data_len:有效数据字节长度
assign data_len = (state == REC_DATA) ? (udp_len - 16'd8) : 16'd0;

//cnt_data:接收数据字节计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data <= 16'd0;
else if((state == REC_DATA) && (data_en == 1'b1))
if(cnt_data == (data_len - 1'b1))
cnt_data <= 16'd0;
else
cnt_data <= cnt_data + 16'd1;

// rec_data_en:数据接收使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rec_data_en <= 1'b0;
else if((state == REC_DATA) && (data_en == 1'b1)
&& ((cnt_data == (data_len - 1'b1)) \|\| (cnt_rec_data == 2'd3)))
rec_data_en <= 1'b1;
else
rec_data_en <= 1'b0;

//rec_data:接收数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rec_data <= 32'b0;
else if((state == REC_DATA) && (data_en == 1'b1))
if(cnt_rec_data == 2'd0)
rec_data[31:24] <= data;
else if(cnt_rec_data == 2'd1)
rec_data[23:16] <= data;
else if(cnt_rec_data == 2'd2)
rec_data[15:8] <= data;
else if(cnt_rec_data==2'd3)
rec_data[7:0] <= data;
else
rec_data <= rec_data;
else
rec_data <= rec_data;

//rec_data_num:接收数据字节数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rec_data_num <= 16'b0;
else if((state == REC_DATA) && (data_en == 1'b1)
&& (cnt_data == (data_len - 1'b1)))
rec_data_num <= data_len;

//cnt_rec_data:数据计数器,单位4字节
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_rec_data <= 2'd0;
else if((state == REC_DATA) && (data_en == 1'b1))
if(cnt_data == (data_len - 1'b1))
cnt_rec_data <= 2'd0;
else
cnt_rec_data <= cnt_rec_data + 2'd1;

//rec_end:数据包接收完成信
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rec_end <= 1'b0;
else if((state == REC_DATA) && (data_en == 1'b1)
&& (cnt_data == (data_len - 1'b1)))
rec_end <= 1'b1;
else
rec_end <= 1'b0;

endmodule

参考代码是参照绘制波形图编写,在前文各信号已经做了详细介绍,此处不再赘述。

仿真文件编写

模块参考代码编写完成后,为验证参考代码是否能够实现有效数据的提取,编写仿真参考代码对初始化模块参考代码进行仿真验证。仿真参考代码,具体见代码清单 69‑2。

代码清单 69‑2 以太网数据接收模块仿真参考代码(tb_ip_receive.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
\`timescale 1ns/1ns
module tb_ip_receive();
////
//\* Parameter and Internal Signal \//
////
//parameter define
//板卡MAC地址
parameter BOARD_MAC = 48'h12_34_56_78_9A_BC;
//板卡IP地址
parameter BOARD_IP = {8'd169,8'd254,8'd1,8'd23};

//reg define
reg eth_rx_clk ; //PHY芯片接收数据时钟信号
reg eth_tx_clk ; //PHY芯片发送数据时钟信号
reg sys_rst_n ; //系统复位,低电平有效
reg eth_rxdv ; //PHY芯片输入数据有效信号
reg [3:0] data_mem [171:0] ; //data_mem是一个存储器,相当于一个ram
reg [7:0] cnt_data ; //数据包字节计数器
reg start_flag ; //数据输入开始标志信号

//wire define
wire rec_end ; //数据接收使能信号
wire [3:0] rec_en ; //接收数据
wire rec_data ; //数据包接收完成信号
wire rec_data_num ; //接收数据字节数
wire [3:0] eth_rx_data ; //PHY芯片输入数据

////
//\* Main Code \//
////
//读取sim文件夹下面的data.txt文件,并把读出的数据定义为data_mem
initial $readmemh
("E:/GitLib/Altera/EP4CE10/code/54_ethernet/sim/data.txt",data_mem);

//时钟、复位信号
initial
begin
eth_rx_clk = 1'b1 ;
eth_tx_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
start_flag <= 1'b0 ;
#200
sys_rst_n <= 1'b1 ;
#100
start_flag <= 1'b1 ;
#50
start_flag <= 1'b0 ;
end

always #20 eth_rx_clk = ~eth_rx_clk;
always #20 eth_tx_clk = ~eth_tx_clk;

//eth_rxdv:PHY芯片输入数据有效信号
always@(negedge eth_rx_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_rxdv <= 1'b0;
else if(cnt_data == 171)
eth_rxdv <= 1'b0;
else if(start_flag == 1'b1)
eth_rxdv <= 1'b1;
else
eth_rxdv <= eth_rxdv;

//cnt_data:数据包字节计数器
always@(negedge eth_rx_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data <= 8'd0;
else if(eth_rxdv == 1'b1)
cnt_data <= cnt_data + 1'b1;
else
cnt_data <= cnt_data;

//eth_rx_data:PHY芯片输入数据
assign eth_rx_data = (eth_rxdv == 1'b1)
? data_mem[cnt_data] : 4'b0;

////
//\* Instantiation \//
////
//------------- ethernet_inst -------------
ip_receive
#(
.BOARD_MAC (BOARD_MAC ), //板卡MAC地址
.BOARD_IP (BOARD_IP ) //板卡IP地址
)
ip_receive_inst
(
.sys_clk (eth_rx_clk ), //时钟信号
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.eth_rxdv (eth_rxdv ), //数据有效信号
.eth_rx_data (eth_rx_data ), //输入数据

.rec_end (rec_end ), //数据接收使能信号
.rec_data_en (rec_en ), //接收数据
.rec_data (rec_data ), //数据包接收完成信号
.rec_data_num (rec_data_num ) //接收数据字节数
);

endmodule

仿真波形分析

模块仿真波形图如图 69‑37至图 69‑41,仿真波形图与绘制波形图个信号波形吻合,模块通过仿真验证。

Enet038

图 69‑37 模块整体仿真波形图

Enet039

图 69‑38 模块局部仿真波形图(一)

Enet040

图 69‑39 模块局部仿真波形图(二)

Enet041

图 69‑40 模块局部仿真波形图(三)

Enet042

图 69‑41 模块局部仿真波形图(四)

19.5.1.2. 以太网数据发送模块

模块框图

以太网数据发送模块ip_send,是第一部分以太网数据收发器的子功能模块之一,作用是将有效数据按照以太网数据包格式进行打包,并将数据回传给PC端。模块框图,具体见图 69‑42;输入输出端口简介,具体见表格 69‑4。

Enet043

图 69‑42 以太网数据发送模块框图

表格 69‑4 输入输出端口简介

信号

位宽

类型

功能描述

sys_clk

1bit

input

模块工作时钟

sys_rst_n

1bit

input

复位信号,低有效

send_en

1bit

input

数据发送开始信号

send_data

32bit

input

待发送数据

send_data_num

16bit

input

发送数据有效字节数

crc_data

32bit

input

CRC校验数据

crc_next

4bit

input

CRC下次校验完成数据

send_end

1bit

output

单包数据发送完成标志信号

read_data_req

1bit

output

读数据请求信号

eth_tx_en

1bit

output

输出数据有效信号

eth_tx_data

4bit

output

输出数据

crc_en

1bit

output

CRC开始校验使能

crc_clr

1bit

output

CRC复位信号

由上述图表可知,本模块输入输出信号共13路,输入信号7路,输出信号6路。

输入信号中,时钟信号sys_clk,由外部PHY芯片传入时钟(eth_clk)二分频得到,频率25MHz,作为模块工作时钟;复位信号sys_rst_n,低电平有效,由板卡复位按键传入;数据发送开始信号send_en,由外部传入,控制数据发送起始;信号send_data是作为待发送数据传入模块;传入信 号send_data_num包含的信息是待发送数据有效字节数;输入信号crc_data、crc_next,由CRC校验模块传入,包含的是CRC校验字节数据。

输出信号中,信号send_end,高电平有效,表示单包数据发送完成;read_data_req表示待发送数据读取请求信号;eth_tx_en、eth_tx_data同步输出,表示输出数据使能信号、输出数据,可类比于信号eth_rxdv、eth_rx_data;信号crc_en、crc_clr输出至C RC校验模块,信号crc_en有效时,CRC校验模块执行CRC校验字节的计算,单包数据发送完毕后,输出有效的crc_clr信号,清零CRC校验数据。

波形图绘制

在模块框图小节,我们已经对以太网数据发送模块的模块功能和输入输出端口做了详细介绍。接下来我们通过波形图的绘制,为读者说明各信号的设计与实现方法,以及模块的功能实现。以太网数据发送模块整体波形图,具体见图 69‑43、图 69‑44、图 69‑45。

Enet044

图 69‑43 以太网数据发送模块整体波形图(一)

Enet045

图 69‑44 以太网数据发送模块整体波形图(二)

Enet046

图 69‑45 以太网数据发送模块整体波形图(三)

模块整体波形图已经列出来了,波形图中的各信号波形是如何设计与实现的呢,下面我们会对各波形的设计与实现做详细说明。

以太网数据发送模块的作用是将待发送的有效数据按照以太网格式进行数据打包并发送。一个完整的以太网数据包,数据包包括:前导码+帧起始界定符、以太网帧头、IP首部、UDP首部、UDP数据(有效数据)、CRC校验字节。

完成数据包的生成,我们可以使用状态机来实现,根据以太网数据包中数据类型的不同,我们在模块内定义7个状态:初始状态(IDLE)、IP首部校验状态(CHECK_SUM)、数据包头发送状态(PACKET_HEAD)、以太网首部发送状态(ETH_HEAD)、IP和UDP首部发送状态(IP_UDP_HEAD) 、有效数据发送状态(SEND_DATA)、CRC校验字节发送状态(CRC)。

结合模块功能、以太网数据包格式和状态机设计思路,绘制状态机状态转移图,具体见图 69‑46。

Enet047

图 69‑46 状态机状态转移图

系统上电后,模块内状态机一直处于初始状态(IDLE),在初始状态,完成数据包头(前导码(7个8’h55) + 帧起始界定符(1个8’hd5))和以太网帧头(目的MAC地址 + 源MAC地址 + 协议类型)的定义,因为这些数据都是确定的。

当有效的数据发送开始信号send_en传入后,声明信号send_en_dly对send_en打一拍,目的是生成发送开始信号上升沿rise_send_en;当检测到发送开始信号上升沿rise_send_en有效,将状态跳转使能信号sw_en拉高一个时钟周期,同时利用传入的发送数据有效字节数信号send _data_num确定有效数据字节数data_len、UDP数据长度(UDP首部字节数 + 有效数据字节数)、IP数据长度(IP首部字节数 + UDP首部字节数 + 有效数据字节数);

当状态跳转使能信号sw_en为有效的高电平时,我们开始IP首部和UDP首部部分已知数据的定义;同时以此为约束条件,实现状态机由初始状态(IDLE)到IP首部校验状态(CHECK_SUM)的跳转。初始状态涉及相关信号波形图如下:

Enet048

图 69‑47 初始状态涉及相关信号波形图

状态机跳转到IP首部校验状态(CHECK_SUM),在此状态完成IP首部check_sum的计算,IP首部check_sum的计算方法前面已经讲过,此处不再赘述。

因为check_sum计算过程涉及反码求和,所以需要若干个时钟周期才能完成数据计算,所以我们我们声明计数器cnt来对计算周期进行计数,计数器初值为0,在IP首部校验状态下,每个时钟周期自加1;当计数到最大值3时,计数器归0;当计数器计数到2时,拉高状态跳转使能信号sw_en,状态机跳转到数据包头发送 状态(PACKET_HEAD)。本状态涉及各信号波形如下。

Enet049

图 69‑48 IP首部校验状态涉及相关信号波形图

完成IP首部check_sum计算后,以太网数据包涉及的各部分数据已经准备完毕,下面可以开始数据打包与传输。按照以太网数据包格式,在数据包头发送状态(PACKET_HEAD),我们开始前导码 + 帧起始界定符的传输。

在初始状态我们声明了宽为8bit,深度为8的memory型数据变量对前导码 + 帧起始界定符进行寄存,而前文讲解的MII接口数据发送时序图中提到,在数据发送时,每个时钟周期发送数据4bit,且低位在前高位在后,所以我们需要声明计数器辅助数据的发送。

首先,可以利用前面的声明的计数器cnt,对声明的memory型数据变量进行深度计数;然后声明计数器cnt_send_bit,对单个数据存储单元包含的4bit数据个数进行计数,同时作为约束条件控制计数器cnt的计数。

将自memory寄存器中读出的数据包头,按照以太网数据发送时序,赋值给输出数据eth_tx_data;与有效输出数据同步输出的还有输出数据使能信号eth_tx_en。本状态涉及的各信号波形图如下:

Enet050

图 69‑49 数据包发送状态涉及相关信号波形图

在完成数据包头的发送后,状态机跳转到以太网帧头发送状态(ETH_HEAD),在初始状态我们声明了宽为8bit,深度为14的memory型数据变量对以太网帧头(目的MAC地址 + 源MAC地址 + 协议类型)进行寄存,采用与数据包头相同的方法对以太网帧头进行输出。

完成以太网帧头的输出后,状态机跳转到IP和UDP首部发送状态(IP_UDP_HEAD),在初始状态我们声明了宽为32bit,深度为7的memory型数据变量对IP和UDP首部进行寄存,采用与数据包头相同的方法对以太网帧头进行输出。本状态涉及的各信号波形图如下:

Enet051

图 69‑50 IP和UDP首部发送状态涉及相关信号波形图

完成IP和UDP首部的输出后,状态机跳转到有效数据发送状态(SEND_DATA),在此状态中,我们要完成有效数据的发送,待发送的有效数据是以32bit为单位暂存在FIFO中,要实现FIFO中数据的读取,时钟和读请求信号必不可少,时钟信号使用与本模块工作时钟相同的时钟,请求信号需要自己声明。

声明有效数据请求信号read_data_req作为FIFO读请求信号,读取有效数据;利用之前的计数器cnt、cnt_send_bit辅助有效数据的输出,同时作为有效数据请求信号read_data_req的约束条件。

读者还要注意的一点是,前面我们提到以太网传输字节数最小为46个字节,其中包括20字节的IP首部和8字节的UDP首部,所以有效数据最少为18字节,如传输有效数据小于18个字节该怎么办呢?

例如本实验中,发送的有效字节数据只有10字节,剩下的8个字节,使用随机数据补充。因为在前面IP和UDP首部中,我们已经确定可有效数据的字节数,当目的端接收到有效的10字节数据后,对后面补充的8字节数据不会处理。为了确定补充的字节数,声明计数器cnt_add,对补充字节数进行计数。

有效数据发送状态涉及各信号波形图如下:

Enet052

图 69‑51 有效数据发送状态涉及各信号波形图

完成有效数据的发送,状态机跳转到CRC校验字节发送状态(CRC),CRC冗余校验是针对IP首部、UDP首部以及有效数据的校验方法。

本实验中,将需要CRC校验的三部分数据 传入CRC校验模块,通过CRC校验算法处理后,生成4字节的CRC校验位回传到本模块;在CRC状态中将CRC校验位按以太网数据发送时序传出。本状态涉及各信号波形图如下:

Enet053

图 69‑52 CRC校验字节发送状态涉及各信号波形图(一)

Enet054

图 69‑53 CRC校验字节发送状态涉及各信号波形图(二)

Enet055

图 69‑54 CRC校验字节发送状态涉及各信号波形图(三)

代码编写

以上述绘制波形图为参照,编写模块参考代码。模块参考代码具体见代码清单 69‑3。

代码清单 69‑3 以太网数据发送模块参考代码(ip_send.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
module ip_send
#(
parameter BOARD_MAC = 48'hFF_FF_FF_FF_FF_FF , //板卡MAC地址
parameter BOARD_IP = 32'hFF_FF_FF_FF , //板卡IP地址
parameter BOARD_PORT = 16'd1234 , //板卡端口号
parameter PC_MAC = 48'hFF_FF_FF_FF_FF_FF , //PC机MAC地址
parameter PC_IP = 32'hFF_FF_FF_FF , //PC机IP地址
parameter PC_PORT = 16'd1234 //PC机端口号
)
(
input wire sys_clk , //时钟信号
input wire sys_rst_n , //复位信号,低电平有效
input wire send_en , //数据发送开始信号
input wire [31:0] send_data , //发送数据
input wire [15:0] send_data_num , //发送数据有效字节数
input wire [31:0] crc_data , //CRC校验数据
input wire [3:0] crc_next , //CRC下次校验完成数据

output reg send_end , //单包数据发送完成标志信号
output reg read_data_req , //读FIFO使能信号
output reg eth_tx_en , //输出数据有效信号
output reg [3:0] eth_tx_data , //输出数据
output reg crc_en , //CRC开始校验使能
output reg crc_clr //CRC复位信号
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
localparam IDLE = 7'b000_0001 , //初始状态
CHECK_SUM = 7'b000_0010 , //IP首部校验
PACKET_HEAD = 7'b000_0100 , //发送数据包头
ETH_HEAD = 7'b000_1000 , //发送以太网首部
IP_UDP_HEAD = 7'b001_0000 , //发送IP首部和UDP首部
SEND_DATA = 7'b010_0000 , //发送数据
CRC = 7'b100_0000 ; //发送CRC校验

localparam ETH_TYPE = 16'h0800 ; //协议类型 IP协议

//wire define
wire rise_send_en ; //数据发送开始信号上升沿
wire [15:0] send_data_len ; //实际发送的数据字节数

//reg define
reg send_en_dly ; //数据发送开始信号打拍
reg [7:0] packet_head[7:0]; //数据包头
reg [7:0] eth_head[13:0] ; //以太网首部
reg [31:0] ip_udp_head[6:0]; //IP首部 + UDP首部
reg [31:0] check_sum ; //IP首部check_sum校验
reg [15:0] data_len ; //有效数据字节个数
reg [15:0] ip_len ; //IP字节数
reg [15:0] udp_len ; //UDP字节数
reg [6:0] state ; //状态机状态变量
reg sw_en ; //状态跳转标志信号
reg [4:0] cnt ; //数据计数器
reg [2:0] cnt_send_bit ; //发送数据比特计数器
reg [15:0] data_cnt ; //发送有效数据个数计数器
reg [4:0] cnt_add ; //发送有效数据小于18字节,补充字节计数器

////
//\* Main Code \//
////

//send_en_dly:数据发送开始信号打拍
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
send_en_dly <= 1'b0;
else
send_en_dly <= send_en;

//rise_send_en:数据发送开始信号上升沿
assign rise_send_en = ((send_en == 1'b1) && (send_en_dly == 1'b0))
? 1'b1 : 1'b0;

//packet_head:数据包头,数据包头7个8'h55 + 1个8'hd5
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
packet_head[0] <= 8'h00;
packet_head[1] <= 8'h00;
packet_head[2] <= 8'h00;
packet_head[3] <= 8'h00;
packet_head[4] <= 8'h00;
packet_head[5] <= 8'h00;
packet_head[6] <= 8'h00;
packet_head[7] <= 8'h00;
end
else
begin
packet_head[0] <= 8'h55;
packet_head[1] <= 8'h55;
packet_head[2] <= 8'h55;
packet_head[3] <= 8'h55;
packet_head[4] <= 8'h55;
packet_head[5] <= 8'h55;
packet_head[6] <= 8'h55;
packet_head[7] <= 8'hd5;
end

//eth_head:以太网首部
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
eth_head[0] <= 8'h00;
eth_head[1] <= 8'h00;
eth_head[2] <= 8'h00;
eth_head[3] <= 8'h00;
eth_head[4] <= 8'h00;
eth_head[5] <= 8'h00;
eth_head[6] <= 8'h00;
eth_head[7] <= 8'h00;
eth_head[8] <= 8'h00;
eth_head[9] <= 8'h00;
eth_head[10] <= 8'h00;
eth_head[11] <= 8'h00;
eth_head[12] <= 8'h00;
eth_head[13] <= 8'h00;
end
else
begin
eth_head[0] <= PC_MAC[47:40] ;
eth_head[1] <= PC_MAC[39:32] ;
eth_head[2] <= PC_MAC[31:24] ;
eth_head[3] <= PC_MAC[23:16] ;
eth_head[4] <= PC_MAC[15:8] ;
eth_head[5] <= PC_MAC[7:0] ;
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

//ip_udp_head:IP首部 + UDP首部
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ip_udp_head[1][31:16] <= 16'd0;
else if((state == IDLE) && (sw_en == 1'b1))
begin
ip_udp_head[0] <= {8'h45,8'h00,ip_len};
ip_udp_head[1][31:16] <= ip_udp_head[1][31:16] + 1'b1;
ip_udp_head[1][15:0] <= 16'h4000;
ip_udp_head[2] <= {8'h40,8'd17,16'h0};
ip_udp_head[3] <= BOARD_IP;
ip_udp_head[4] <= PC_IP;
ip_udp_head[5] <= {BOARD_PORT,PC_PORT};
ip_udp_head[6] <= {udp_len,16'h0000};
end
else if((state == CHECK_SUM) && (cnt == 5'd3))
ip_udp_head[2][15:0] <= ~check_sum[15:0];

//check_sum:IP首部check_sum校验
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
check_sum <= 32'd0;
else if(state == CHECK_SUM)
if(cnt == 5'd0)
check_sum <= ip_udp_head[0][31:16] + ip_udp_head[0][15:0]
+ ip_udp_head[1][31:16] + ip_udp_head[1][15:0]
+ ip_udp_head[2][31:16] + ip_udp_head[2][15:0]
+ ip_udp_head[3][31:16] + ip_udp_head[3][15:0]
+ ip_udp_head[4][31:16] + ip_udp_head[4][15:0];
else if(cnt == 5'd1)
check_sum <= check_sum[31:16] + check_sum[15:0];
else if(cnt == 5'd2)
check_sum <= check_sum[31:16] + check_sum[15:0];
else
check_sum <= check_sum;
else
check_sum <= check_sum;

//data_len:有效数据字节个数
//ip_len:IP字节数
//udp_len:UDP字节数
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
data_len <= 16'd0;
ip_len <= 16'd0;
udp_len <= 16'd0;
end
else if((rise_send_en == 1'b1) && (state == IDLE))
begin
data_len <= send_data_num;
ip_len <= send_data_num + 16'd28;
udp_len <= send_data_num + 16'd8;
end

//send_data_len:实际发送的数据字节数
//以太网传输字节数最小为46个字节,其中包括20字节的IP首部和8字节的UDP首部
//有效数据最少为18字节
assign send_data_len = (data_len >= 16'd18) ? data_len : 16'd18;

//state:状态机状态变量
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE:
if(sw_en == 1'b1)
state <= CHECK_SUM;
else
state <= IDLE;
CHECK_SUM:
if(sw_en == 1'b1)
state <= PACKET_HEAD;
else
state <= CHECK_SUM;
PACKET_HEAD:
if(sw_en == 1'b1)
state <= ETH_HEAD;
else
state <= PACKET_HEAD;
ETH_HEAD:
if(sw_en == 1'b1)
state <= IP_UDP_HEAD;
else
state <= ETH_HEAD;
IP_UDP_HEAD:
if(sw_en == 1'b1)
state <= SEND_DATA;
else
state <= IP_UDP_HEAD;
SEND_DATA:
if(sw_en == 1'b1)
state <= CRC;
else
state <= SEND_DATA;
CRC:
if(sw_en == 1'b1)
state <= IDLE;
else
state <= CRC;
default:state <= IDLE;
endcase

//sw_en:状态跳转标志信号
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
sw_en <= 1'b0;
else if((state == IDLE) && (rise_send_en == 1'b1))
sw_en <= 1'b1;
else if((state == CHECK_SUM) && (cnt == 5'd2))
sw_en <= 1'b1;
else if((state == PACKET_HEAD) && (cnt_send_bit == 3'd0)
&& (cnt == 5'd7))
sw_en <= 1'b1;
else if((state == ETH_HEAD) && (cnt_send_bit == 3'd0)
&& (cnt == 5'd13))
sw_en <= 1'b1;
else if((state == IP_UDP_HEAD) && (cnt_send_bit == 3'd6)
&& (cnt == 5'd6))
sw_en <= 1'b1;
else if((state == SEND_DATA) && (cnt_send_bit[0] == 1'd0)
&& (data_cnt == data_len - 16'd1)
&& (data_cnt + cnt_add >= (send_data_len - 16'd1)))
sw_en <= 1'b1;
else if((state == CRC) && (cnt_send_bit == 3'd6))
sw_en <= 1'b1;
else
sw_en <= 1'b0;

//cnt:数据计数器,对以太网传输的除有效字节数据之外的其他数据计数,不同状态下单位不同
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 5'd0;
else if(state == CHECK_SUM)
if(cnt == 5'd3)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
else if(state == PACKET_HEAD)
if(cnt_send_bit != 3'd0)
if(sw_en == 1'b1)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
else
cnt <= cnt;
else if(state == ETH_HEAD)
if(cnt_send_bit != 3'd0)
if(sw_en == 1'b1)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
else
cnt <= cnt;
else if(state == IP_UDP_HEAD)
if(cnt_send_bit == 3'd7)
if(sw_en == 1'b1)
cnt <= 5'd0;
else
cnt <= cnt + 5'd1;
else
cnt <= cnt;
else
cnt <= cnt;

//cnt_send_bit:发送数据bit计数器
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_send_bit <= 3'd0;
else if(state == PACKET_HEAD)
if(cnt_send_bit == 3'd0)
cnt_send_bit <= cnt_send_bit + 3'd1;
else
cnt_send_bit <= 3'd0;
else if(state == ETH_HEAD)
if(cnt_send_bit == 3'd0)
cnt_send_bit <= cnt_send_bit + 3'd1;
else
cnt_send_bit <= 3'd0;
else if(state == IP_UDP_HEAD)
cnt_send_bit <= cnt_send_bit + 3'd1;
else if(state == SEND_DATA)
if(sw_en == 1'b1)
cnt_send_bit <= 3'd0;
else
cnt_send_bit <= cnt_send_bit + 3'd1;
else if(state == CRC)
cnt_send_bit <= cnt_send_bit + 3'd1;
else
cnt_send_bit <= cnt_send_bit;

//read_data_req:读FIFO使能信号
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
read_data_req <= 1'b0;
else if((state == IP_UDP_HEAD) && (cnt_send_bit == 3'd6)
&& (cnt == 5'd6))
read_data_req <= 1'b1;
else if((state == SEND_DATA) && (cnt_send_bit == 3'd6)
&& (data_cnt != data_len - 16'd1))
read_data_req <= 1'b1;
else
read_data_req <= 1'b0;

//eth_tx_en:输出数据有效信号
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_tx_en <= 1'b0;
else if((state != IDLE) && (state != CHECK_SUM))
eth_tx_en <= 1'b1;
else
eth_tx_en <= 1'b0;

//eth_tx_data:输出数据
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_tx_data <= 4'b0;
else if(state == PACKET_HEAD)
if(cnt_send_bit == 3'd0)
eth_tx_data <= packet_head[cnt][3:0];
else
eth_tx_data <= packet_head[cnt][7:4];
else if(state == ETH_HEAD)
if(cnt_send_bit == 3'd0)
eth_tx_data <= eth_head[cnt][3:0];
else
eth_tx_data <= eth_head[cnt][7:4];
else if(state == IP_UDP_HEAD)
if(cnt_send_bit == 3'd0)
eth_tx_data <= ip_udp_head[cnt][27:24];
else if(cnt_send_bit == 3'd1)
eth_tx_data <= ip_udp_head[cnt][31:28];
else if(cnt_send_bit == 3'd2)
eth_tx_data <= ip_udp_head[cnt][19:16];
else if(cnt_send_bit == 3'd3)
eth_tx_data <= ip_udp_head[cnt][23:20];
else if(cnt_send_bit == 3'd4)
eth_tx_data <= ip_udp_head[cnt][11:8];
else if(cnt_send_bit == 3'd5)
eth_tx_data <= ip_udp_head[cnt][15:12];
else if(cnt_send_bit == 3'd6)
eth_tx_data <= ip_udp_head[cnt][3:0];
else if(cnt_send_bit == 3'd7)
eth_tx_data <= ip_udp_head[cnt][7:4];
else
eth_tx_data <= eth_tx_data;
else if(state == SEND_DATA)
if(cnt_send_bit == 3'd0)
eth_tx_data <= send_data[27:24];
else if(cnt_send_bit == 3'd1)
eth_tx_data <= send_data[31:28];
else if(cnt_send_bit == 3'd2)
eth_tx_data <= send_data[19:16];
else if(cnt_send_bit == 3'd3)
eth_tx_data <= send_data[23:20];
else if(cnt_send_bit == 3'd4)
eth_tx_data <= send_data[11:8];
else if(cnt_send_bit == 3'd5)
eth_tx_data <= send_data[15:12];
else if(cnt_send_bit == 3'd6)
eth_tx_data <= send_data[3:0];
else if(cnt_send_bit == 3'd7)
eth_tx_data <= send_data[7:4];
else
eth_tx_data <= eth_tx_data;
else if(state == CRC)
if(cnt_send_bit == 3'd0)
eth_tx_data <= {~crc_next[0], ~crc_next[1], ~crc_next[2], ~crc_next[3]};
else if(cnt_send_bit == 3'd1)
eth_tx_data <= {~crc_data[24],~crc_data[25],~crc_data[26],~crc_data[27]};
else if(cnt_send_bit == 3'd2)
eth_tx_data <= {~crc_data[20],~crc_data[21],~crc_data[22],~crc_data[23]};
else if(cnt_send_bit == 3'd3)
eth_tx_data <= {~crc_data[16],~crc_data[17],~crc_data[18],~crc_data[19]};
else if(cnt_send_bit == 3'd4)
eth_tx_data <= {~crc_data[12],~crc_data[13],~crc_data[14],~crc_data[15]};
else if(cnt_send_bit == 3'd5)
eth_tx_data <= {~crc_data[8],~crc_data[9],~crc_data[10],~crc_data[11]};
else if(cnt_send_bit == 3'd6)
eth_tx_data <= {~crc_data[4],~crc_data[5],~crc_data[6],~crc_data[7]};
else if(cnt_send_bit == 3'd7)
eth_tx_data <= {~crc_data[0],~crc_data[1],~crc_data[2],~crc_data[3]};
else
eth_tx_data <= eth_tx_data;
else
eth_tx_data <= eth_tx_data;

//crc_en:CRC开始校验使能
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
crc_en <= 1'b0;
else if((state == ETH_HEAD)||(state == IP_UDP_HEAD)||(state == SEND_DATA))
crc_en <= 1'b1;
else
crc_en <= 1'b0;

//data_cnt:发送有效数据个数计数器
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_cnt <= 16'b0;
else if(state == SEND_DATA)
if(sw_en == 1'b1)
data_cnt <= 16'd0;
else if((cnt_send_bit[0] == 1'b0)&&(data_cnt < data_len - 16'd1))
data_cnt <= data_cnt + 16'd1;
else
data_cnt <= data_cnt;

//cnt_add:发送有效数据小于18字节,补充字节计数器
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_add <= 5'b0;
else if(state == SEND_DATA)
if(sw_en == 1'b1)
cnt_add <= 5'd0;
else if((cnt_send_bit[0] == 1'b0)&&(data_cnt == data_len - 16'd1)
&& (data_cnt + cnt_add < send_data_len - 16'd1))
cnt_add <= cnt_add + 5'd1;
else
cnt_add <= cnt_add;
else
cnt_add <= cnt_add;

//send_end:单包数据发送完成标志信号
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
send_end <= 1'b0;
else if((state == CRC) && (cnt_send_bit == 3'd7))
send_end <= 1'b1;
else
send_end <= 1'b0;

//crc_clr:crc值复位信号
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
crc_clr <= 1'b0;
else
crc_clr <= send_end;

endmodule

参考代码是参照绘制波形图编写,在前文各信号已经做了详细介绍,此处不再赘述;因为本模块与CRC校验模块联系较为紧密,待CRC校验模块讲解完毕,对两模块一起仿真验证。

19.5.1.3. CRC校验模块

代码编写

在理论学习小节,我们已经对CRC校验的原理以及计算方法做了详细介绍。并行计算crc用verilog语言描述是复杂繁琐的,所以我们选择使用在线工具生成CRC校验的verilog模板,网址:http://www.easics.com/webtools/crctool,CRC模版生成示意图如图 69‑55所示,生成后的模板稍加修改即可使用,修改后的CRC校验模块参考代码,具体见代码清单 69‑4。

Enet056

图 69‑55 CRC模版生成示意图

代码清单 69‑4 CRC模块参考代码(crc32_d4.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
module crc32_d4
(
input wire sys_clk , //时钟信号
input wire sys_rst_n , //复位信号,低电平有效
input wire [3:0] data , //待校验数据
input wire crc_en , //crc使能,校验开始标志
input wire crc_clr , //crc数据复位信号

output reg [31:0] crc_data , //CRC校验数据
output reg [31:0] crc_next //CRC下次校验完成数据
);

////
//\* Parameter and Internal Signal \//
////
// wire define
wire [3:0] data_sw; //待校验数据高低位互换

////
//\* Main Code \//
////

//data_sw:待校验数据高低位互换
assign data_sw = {data[0],data[1],data[2],data[3]};

//crc_next: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
always@(*)
begin
crc_next <= 32'b0;
if(crc_en == 1'b1)
begin
crc_next[0] <= (data_sw[0] ^ crc_data[28]);
crc_next[1] <= (data_sw[1] ^ data_sw[0] ^ crc_data[28]
^ crc_data[29]);
crc_next[2] <= (data_sw[2] ^ data_sw[1] ^ data_sw[0]
^ crc_data[28] ^ crc_data[29] ^ crc_data[30]);
crc_next[3] <= (data_sw[3] ^ data_sw[2] ^ data_sw[1]
^ crc_data[29] ^ crc_data[30] ^ crc_data[31]);
crc_next[4] <= (data_sw[3] ^ data_sw[2] ^ data_sw[0] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31]) ^ crc_data[0];
crc_next[5] <= (data_sw[3] ^ data_sw[1] ^ data_sw[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[31]) ^ crc_data[1];
crc_next[6] <= (data_sw[2] ^ data_sw[1] ^ crc_data[29]
^ crc_data[30]) ^ crc_data[ 2];
crc_next[7] <= (data_sw[3] ^ data_sw[2] ^ data_sw[0] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31]) ^ crc_data[3];
crc_next[8] <= (data_sw[3] ^ data_sw[1] ^ data_sw[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[31]) ^ crc_data[4];
crc_next[9] <= (data_sw[2] ^ data_sw[1] ^ crc_data[29]
^ crc_data[30]) ^ crc_data[5];
crc_next[10]<= (data_sw[3] ^ data_sw[2] ^ data_sw[0] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31]) ^ crc_data[6];
crc_next[11]<= (data_sw[3] ^ data_sw[1] ^ data_sw[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[31]) ^ crc_data[7];
crc_next[12]<= (data_sw[2] ^ data_sw[1] ^ data_sw[0] ^ crc_data[28]
^ crc_data[29] ^ crc_data[30]) ^ crc_data[8];
crc_next[13]<= (data_sw[3] ^ data_sw[2] ^ data_sw[1] ^ crc_data[29]
^ crc_data[30] ^ crc_data[31]) ^ crc_data[9];
crc_next[14]<= (data_sw[3] ^ data_sw[2] ^ crc_data[30]
^ crc_data[31]) ^ crc_data[10];
crc_next[15]<= (data_sw[3] ^ crc_data[31]) ^ crc_data[11];
crc_next[16]<= (data_sw[0] ^ crc_data[28]) ^ crc_data[12];
crc_next[17]<= (data_sw[1] ^ crc_data[29]) ^ crc_data[13];
crc_next[18]<= (data_sw[2] ^ crc_data[30]) ^ crc_data[14];
crc_next[19]<= (data_sw[3] ^ crc_data[31]) ^ crc_data[15];
crc_next[20]<= crc_data[16];
crc_next[21]<= crc_data[17];
crc_next[22]<= (data_sw[0] ^ crc_data[28]) ^ crc_data[18];
crc_next[23]<= (data_sw[1] ^ data_sw[0] ^ crc_data[29]
^ crc_data[28]) ^ crc_data[19];
crc_next[24]<= (data_sw[2] ^ data_sw[1] ^ crc_data[30]
^ crc_data[29]) ^ crc_data[20];
crc_next[25]<= (data_sw[3] ^ data_sw[2] ^ crc_data[31]
^ crc_data[30]) ^ crc_data[21];
crc_next[26]<= (data_sw[3] ^ data_sw[0] ^ crc_data[31]
^ crc_data[28]) ^ crc_data[22];
crc_next[27]<= (data_sw[1] ^ crc_data[29]) ^ crc_data[23];
crc_next[28]<= (data_sw[2] ^ crc_data[30]) ^ crc_data[24];
crc_next[29]<= (data_sw[3] ^ crc_data[31]) ^ crc_data[25];
crc_next[30]<= crc_data[26];
crc_next[31]<= crc_data[27];
end
end
//crc_data:CRC校验数据
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
crc_data <= 32'hff_ff_ff_ff;
else if(crc_clr == 1'b1)
crc_data <= 32'hff_ff_ff_ff;
else if(crc_en == 1'b1)
crc_data <= crc_next;


endmodule

仿真文件编写

在以太网数据发送模块部分我们说过,因为以太网数据发送模块与CRC校验模块联系较为紧密,待CRC校验模块讲解完毕,对两模块一起仿真验证。两模块仿真参考代码,具体见代码清单 69‑5。

代码清单 69‑5 以太网数据发送模块、CRC校验模块仿真参考代码(tb_ip_send.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
\`timescale 1ns/1ns
module tb_ip_send();
////
//\* Parameter and Internal Signal \//
////
//parameter define
//板卡MAC地址
parameter BOARD_MAC = 48'h12_34_56_78_9A_BC;
//板卡IP地址
parameter BOARD_IP = {8'd169,8'd254,8'd1,8'd23};
//PC机MAC地址
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff;
//PC机IP地址
parameter DES_IP = {8'd169,8'd254,8'd191,8'd31};

//reg define
reg eth_tx_clk ; //PHY芯片发送数据时钟信号
reg clk_50m ;
reg sys_rst_n ; //系统复位,低电平有效
reg send_en ; //数据输入开始标志信号
reg [31:0] send_data ; //待发送数据
reg [31:0] data_mem [2:0] ; //data_mem是一个存储器,相当于一个ram
reg [15:0] cnt_data ; //待发送数据

//wire define
wire send_end ; //单包数据发送完成标志信号
wire read_data_req ; //读FIFO使能信号
wire eth_tx_en ; //输出数据有效信号
wire [3:0] eth_tx_data ; //输出数据
wire crc_en ; //CRC开始校验使能
wire crc_clr ; //CRC复位信号
wire [31:0] crc_data ; //CRC校验数据
wire [31:0] crc_next ; //CRC下次校验完成数据

wire en ;
wire [1:0] data ;

////
//\* Main Code \//
////
//时钟、复位信号
initial
begin
eth_tx_clk = 1'b1 ;
clk_50m = 1'b1 ;
sys_rst_n <= 1'b0 ;
send_en <= 1'b0 ;
#200
sys_rst_n <= 1'b1 ;
#100
send_en <= 1'b1 ;
#50
send_en <= 1'b0 ;
end

always #20 eth_tx_clk = ~eth_tx_clk;
always #10 clk_50m = ~clk_50m;

always @(posedge eth_tx_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
data_mem[0] <= 32'h00_00_00_00;
data_mem[1] <= 32'h00_00_00_00;
data_mem[2] <= 32'h00_00_00_00;
end
else
begin
data_mem[0] <= 32'h68_74_74_70;
data_mem[1] <= 32'h3a_2f_2f_77;
data_mem[2] <= 32'h77_77_00_00;
end

//cnt_data:数据包字节计数器
always@(posedge eth_tx_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data <= 8'd0;
else if(read_data_req == 1'b1)
cnt_data <= cnt_data + 1'b1;
else
cnt_data <= cnt_data;

//send_data:PHY芯片输入数据
always @(posedge eth_tx_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
send_data <= 32'h0;
else if(read_data_req == 1'b1)
send_data <= data_mem[cnt_data];

////
//\* Instantiation \//
////
//------------ ip_send_inst -------------
ip_send
#(
.BOARD_MAC (BOARD_MAC ), //板卡MAC地址
.BOARD_IP (BOARD_IP ), //板卡IP地址
.DES_MAC (DES_MAC ), //PC机MAC地址
.DES_IP (DES_IP ) //PC机IP地址
)
ip_send_inst
(
.sys_clk (eth_tx_clk ), //时钟信号
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.send_en (send_en ), //数据发送开始信号
.send_data (send_data ), //发送数据
.send_data_num (16'd10 ), //发送数据有效字节数
.crc_data (crc_data ), //CRC校验数据
.crc_next (crc_next[31:28]), //CRC下次校验完成数据

.send_end (send_end ), //单包数据发送完成标志信号
.read_data_req (read_data_req ), //读FIFO使能信号
.eth_tx_en (eth_tx_en ), //输出数据有效信号
.eth_tx_data (eth_tx_data ), //输出数据
.crc_en (crc_en ), //CRC开始校验使能
.crc_clr (crc_clr ) //crc复位信号
);

//------------ crc32_d4_inst -------------
crc32_d4 crc32_d4_inst
(
.sys_clk (eth_tx_clk ), //时钟信号
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.data (eth_tx_data ), //待校验数据
.crc_en (crc_en ), //crc使能,校验开始标志
.crc_clr (crc_clr ), //crc数据复位信号

.crc_data (crc_data ), //CRC校验数据
.crc_next (crc_next ) //CRC下次校验完成数据
);

endmodule

仿真波形分析

以太网数据发送模块仿真波形图如图 69‑56至图 69‑59,仿真波形图与绘制波形图个信号波形吻合,模块通过仿真验证;CRC校验模块仿真波形图,具体见图 69‑60。

Enet057

图 69‑56 以太网数据发送模块仿真波形图(一)

Enet058

图 69‑57 以太网数据发送模块仿真波形图(二)

Enet059

图 69‑58 以太网数据发送模块仿真波形图(三)

Enet060

图 69‑59 以太网数据发送模块仿真波形图(四)

Enet061

图 69‑60 CRC校验模块仿真波形图

19.5.1.4. 基于MII接口的UDP顶层模块

CRC校验模块介绍完毕后,以太网数据收发器的子功能模块均已介绍完毕,下面来说明一下以太网数据收发器(MII)的最后一个模块,基于MII接口的UDP顶层模块eth_udp_mii。

作为以太网数据收发器(MII)的顶层模块,模块作用较为简单,内部实例化各子功能模块,连接子模块对应信号;外部输入输出使能信号和以太网数据。在此我们只对顶层模块的整体框图和参考代码做一下介绍,波形图不需要说明,内部实例化子模块均已仿真验证完毕,不再对顶层模块进行仿真验证。

模块框图

UDP顶层模框图设计和模块输入输出信号的功能描述,具体见图 69‑61、表格 69‑5。

Enet062

图 69‑61 UDP模块框图

表格 69‑5 输入输出信号功能描述

信号

位宽

类型

功能描述

eth_rx_clk

1Bit

Input

mii时钟,接收

sys_rst_n

1Bit

Input

复位信号,低电平有效

eth_rxdv

1Bit

Input

输入数据有效信号(mii)

eth_rx_data

4Bit

Input

输入数据(mii)

eth_tx_clk

1bit

Input

mii时钟,发送

send_en

1Bit

Input

开始发送信号

send_data

32Bit

Input

发送数据

send_data_num

16Bit

Input

发送有效数据字节数

send_end

1Bit

Output

单包数据发送完成信号

read_data_req

1Bit

Output

读数据请求信号

rec_end

1Bit

Output

单包数据接收完成信号

rec_en

1Bit

Output

接收数据使能信号

rec_data

32Bit

Output

接收数据

rec_data_num

16Bit

Output

接收有效数据字节数

eth_tx_en

1Bit

Output

输出数据有效信号(mii)

eth_tx_data

4Bit

Output

输出数据(mii)

eth_rst_n

1Bit

Output

复位信号,低电平有效

代码编写

UDP顶层模块参考代码,具体见代码清单 69‑6。

代码清单 69‑6 UDP顶层模块参考代码(eth_udp_mii.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
module eth_udp_mii
#(
parameter BOARD_MAC = 48'hFF_FF_FF_FF_FF_FF , //板卡MAC地址
parameter BOARD_IP = 32'hFF_FF_FF_FF , //板卡IP地址
parameter BOARD_PORT = 16'd1234 , //板卡端口号
parameter PC_MAC = 48'hFF_FF_FF_FF_FF_FF , //PC机MAC地址
parameter PC_IP = 32'hFF_FF_FF_FF , //PC机IP地址
parameter PC \_PORT = 16'd1234 //PC机端口号
)
(
input wire eth_rx_clk , //mii时钟,接收
input wire sys_rst_n , //复位信号,低电平有效
input wire eth_rxdv , //输入数据有效信号(mii)
input wire [3:0] eth_rx_data , //输入数据(mii)
input wire eth_tx_clk , //mii时钟,发送
input wire send_en , //开始发送信号
input wire [31:0] send_data , //发送数据
input wire [15:0] send_data_num , //发送有效数据字节数

output wire send_end , //单包数据发送完成信号
output wire read_data_req , //读数据请求信号
output wire rec_end , //单包数据接收完成信号
output wire rec_en , //接收数据使能信号
output wire [31:0] rec_data , //接收数据
output wire [15:0] rec_data_num , //接收有效数据字节数
output wire eth_tx_en , //输出数据有效信号(mii)
output wire [3:0] eth_tx_data , //输出数据(mii)
output wire eth_rst_n //复位信号,低电平有效
);

////
//\* Parameter and Internal Signal \//
////
//wire define
wire crc_en ; //CRC校验开始标志信号
wire crc_clr ; //CRC数据复位信号
wire [31:0] crc_data; //CRC校验数据
wire [31:0] crc_next; //CRC下次校验完成数据

////
//\* Main Code \//
////
//eth_rst_n:PHY芯片复位信号,低电平有效
assign eth_rst_n = sys_rst_n;

////
//\* Instantiation \//
////
//------------ ip_receive_inst -------------
ip_receive
#(
.BOARD_MAC (BOARD_MAC ), //板卡MAC地址
.BOARD_IP (BOARD_IP ) //板卡IP地址
)
ip_receive_inst
(
.sys_clk (eth_rx_clk ), //时钟信号
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.eth_rxdv (eth_rxdv ), //数据有效信号
.eth_rx_data (eth_rx_data ), //输入数据

.rec_end (rec_end ), //数据接收使能信号
.rec_data_en (rec_en ), //接收数据
.rec_data (rec_data ), //数据包接收完成信号
.rec_data_num (rec_data_num ) //接收数据字节数
);

//------------ ip_send_inst -------------
ip_send
#(
.BOARD_MAC (BOARD_MAC ), //板卡MAC地址
.BOARD_IP (BOARD_IP ), //板卡IP地址
.BOARD_PORT (BOARD_PORT ), //板卡端口号
.PC \_MAC (PC_MAC ), //PC机MAC地址
.PC \_IP (PC_IP ), //PC机IP地址
.PC \_PORT (PC_PORT ) //PC机端口号
)
ip_send_inst
(
.sys_clk (eth_tx_clk ), //时钟信号
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.send_en (send_en ), //数据发送开始信号
.send_data (send_data ), //发送数据
.send_data_num (send_data_num ), //发送数据有效字节数
.crc_data (crc_data ), //CRC校验数据
.crc_next (crc_next[31:28]), //CRC下次校验完成数据

.send_end (send_end ), //单包数据发送完成标志信号
.read_data_req (read_data_req ), //读FIFO使能信号
.eth_tx_en (eth_tx_en ), //输出数据有效信号
.eth_tx_data (eth_tx_data ), //输出数据
.crc_en (crc_en ), //CRC开始校验使能
.crc_clr (crc_clr ) //crc复位信号
);

//------------ crc32_d4_inst -------------
crc32_d4 crc32_d4_inst
(
.sys_clk (eth_tx_clk ), //时钟信号
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.data (eth_tx_data ), //待校验数据
.crc_en (crc_en ), //crc使能,校验开始标志
.crc_clr (crc_clr ), //crc数据复位信号

.crc_data (crc_data ), //CRC校验数据
.crc_next (crc_next ) //CRC下次校验完成数据
);

endmodule

19.5.1.5. RMII接口转MII接口模块

模块框图

RMII接口转MII接口模块的主要功能是,将FPGA传入的符合RMII接口时序的使能及数据信号转换为符合MII接口时序的使能、数据信号,将转换后的使能、数据信号传入基于MII接口的UDP控制模块。RMII接口转MII接口模块框图,具体见图 69‑62;输入说出端口简介,具体见表格 69‑6。

Enet063

图 69‑62 RMII接口转MII接口模块框图

表格 69‑6 输入输出端口简介

信号

位宽

类型

功能描述

eth_rmii_clk

1Bit

Input

板卡输入PHY时钟,50MHz

eth_mii_clk

1Bit

Input

基于PHY时钟生成的MII接口时钟,25MHz

sys_rst_n

1Bit

Input

复位信号,低有效

rx_dv

1Bit

Input

以太网数据有效信号(RMII),PHY芯片传入

rx_data

2Bit

Input

以太网数据(RMII),PHY芯片传入

eth_rx_dv

1Bit

Output

以太网数据有效信号(MII)

eth_rx_data

4Bit

Output

以太网数据(MII)

波形图绘制

在模块框图小节,我们已经对RMII接口转MII接口模块的模块功能和输入输出端口做了简单介绍。接下来我们通过波形图的绘制,为读者说明各信号的设计与实现方法,以及模块的功能实现。RMII接口转MII接口模块整体波形图,具体见图 69‑63。

Enet064

图 69‑63 RMII接口转MII接口模块整体波形图

结合模块框图和整体波形图可知,RMII接口转MII接口模块共有7路输入输出信号。

输入信号5路,信号sys_rst_n为全局复位信号,无需多说;信号eth_rmii_clk为自PHY芯片输入的时钟信号,频率50MHz;PHY芯片输入的以太网数据有效信号rx_dv和以太网有效数据rx_data的同步时钟均为eth_rmii_clk时钟信号;此外模块还有1路时钟信号eth_mii_c lk输入,为输出2路信号的同步时钟,基于eth_rmii_clk时钟信号生成,顶层模块传入,频率25MHz。

输出信号2路,信号eth_rx_dv为基于MII接口协议的以太网数据有效信号;信号eth_rx_data为基于MII接口协议的以太网有效数据;两信号的同步时钟均为输入时钟信号eth_mii_clk,传入基于MII接口的UDP以太网控制器。

输入输出信号均已介绍完毕,那么模块是如何通过输入信号产生我们需要的输出信号的呢?,也就是说模块功能是如何实现的呢?,接下来我们会详细为读者解答这一问题。

首先,为消除输入信号的亚稳态,我们需要对输入的以太网数据信号rx_data和以太网数据有效信号rx_dv做打拍处理,所以声明信号rx_dv_reg和信号rx_data_reg,分别对信号rx_dv、信号rx_data打一拍,两信号波形图如下图所示。

Enet065

图 69‑64 rx_dv_reg、rx_data_reg信号波形图

消除亚稳态后,我们需要先对输入的数据有效信号做处理,目的是保证数据有效信号和输入数据完全对应,去除前导码前存在无效数据对应有效数据有效信号的情况(波形图中并未画出),所以声明了rx_dv_reg1信号;同时,对信号rx_dv_reg信号再打一拍,得到信号rx_dv_reg2;对信号rx_dv_reg 1、信号rx_dv_reg2做与运算,得到真实有效的数据有效信号rx_dv_ture,各信号波形如下图所示。

Enet066

图 69‑65 rx_dv_reg1、rx_dv_reg2、rx_dv_ture信号波形图

为了保证以太网数据与数据有效信号保持同步,对输入的以太网数据同样要做打拍处理,最终得到与数据有效信号同步的以太网数据信号rx_data_ture,各信号波形如下图所示。

Enet067

图 69‑66 rx_data_ture信号波形图

经过上述操作,我们得到了真实有效的数据信号rx_data_ture和数据有效信号rx_dv_ture,接下来,就是将eth_rmii_clk时钟同步下的数据信号rx_data_ture和数据有效信号rx_dv_ture,同步到eth_mii_clk时钟下,及实现RMII接口到MII接口转换。

首先,要将2bit位宽的rx_data_ture拼接为4bit位宽的数据data;声明数据转换信号data_sw_en,设初值为0,在数据有效信号rx_dv_ture为高电平、eth_rmii_clk时钟下降沿时,自身不断取反,其他时刻为低电平;在eth_rmii_clk时钟的上升沿、rx_dv_t ure为高电平、data_sw_en为低电平时,将rx_data_reg1与rx_data_ture进行拼接,得到data = {rx_data_reg1,rx_data_ture};为了保证使能信号与数据信号同步,对rx_dv_ture信号打拍,得到rx_dv_ture_reg。各信号波形如下图所示。

Enet068

图 69‑67 data_sw_en、data信号波形图

数据拼接完成后,将rx_dv_ture_reg和data同步到eth_mii_clk时钟下,得到输出信号eth_rx_dv和eth_rx_data,信号波形如下图所示。

Enet069

图 69‑68 输出信号波形图

代码编写

以上述绘制波形图为参照,编写模块参考代码。模块参考代码具体见代码清单 69‑7。

代码清单 69‑7 RMII接口转MII接口模块参考代码(rmii_to_mii.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
module rmii_to_mii
(
input wire eth_rmii_clk, //rmii时钟
input wire eth_mii_clk , //mii时钟
input wire sys_rst_n , //复位信号
input wire rx_dv , //输入数据有效信号(rmii)
input wire [1:0] rx_data , //输入数据(rmii)

output reg eth_rx_dv , //输入数据有效信号(mii)
output reg [3:0] eth_rx_data //输入数据(mii)
);

////
//\* Parameter and Internal Signal \//
////
reg rx_dv_reg ; //输入数据有效信号寄存(rmii)
reg [1:0] rx_data_reg ; //输入数据寄存(rmii)
reg rx_dv_reg1 ; //rx_dv_reg寄存
reg rx_dv_reg2 ; //rx_dv_reg1寄存
reg rx_dv_ture ; //真实的输入数据有效信号
reg rx_dv_ture_reg ; //真实的输入数据有效信号打拍
reg [1:0] rx_data_reg1 ; //rx_data_reg寄存
reg [1:0] rx_data_ture ; //有效的输入数据
reg data_sw_en ; //数据拼接使能
reg [3:0] data ; //拼接后的数据

////
//\* Main Code \//
////
//rx_dv:输入数据有效信号寄存(rmii)
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_dv_reg <= 1'b0;
else
rx_dv_reg <= rx_dv;

//rx_data_reg:输入数据寄存(rmii)
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_data_reg <= 2'b0;
else
rx_data_reg <= rx_data;

//rx_dv_reg1:rx_dv_reg寄存
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_dv_reg1 <= 1'b0;
else if(rx_dv_reg == 1'b1)
if(rx_data_reg == 2'b1)
rx_dv_reg1 <= 1'b1;
else
rx_dv_reg1 <= rx_dv_reg1;
else
rx_dv_reg1 <= 1'b0;

//rx_dv_reg2:rx_dv_reg1寄存
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_dv_reg2 <= 1'b0;
else
rx_dv_reg2 <= rx_dv_reg;

//rx_dv_ture:真实的输入数据有效信号
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_dv_ture <= 1'b0;
else if((rx_dv_reg1) && (rx_dv_reg2))
rx_dv_ture <= 1'b1;
else
rx_dv_ture <= 1'b0;

//rx_data_reg1:rx_data_reg寄存
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_data_reg1 <= 2'b0;
else
rx_data_reg1 <= rx_data_reg;

//rx_data_ture:有效的输入数据
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_data_ture <= 2'b0;
else
rx_data_ture <= rx_data_reg1;

//data_sw_en:数据拼接使能
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_sw_en <= 1'b0;
else if(rx_dv_ture == 1'b1)
data_sw_en <= ~data_sw_en;
else
data_sw_en <= 1'b0;

//data:拼接后的数据
always@(posedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data <= 4'b0;
else if((rx_dv_ture == 1'b1) && (data_sw_en == 1'b0))
data <= {rx_data_reg1,rx_data_ture};
else
data <= data;

//rx_dv_ture_reg:真实的输入数据有效信号打一拍
always@(posedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rx_dv_ture_reg <= 1'b0;
else
rx_dv_ture_reg <= rx_dv_ture;


//eth_rx_dv:输入数据有效信号(mii)
always@(negedge eth_mii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_rx_dv <= 1'b0;
else
eth_rx_dv <= rx_dv_ture_reg;

//eth_rx_data:输入数据(mii)
always@(negedge eth_mii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_rx_data <= 4'b0;
else
eth_rx_data <= data;

endmodule

本模块的仿真验证待顶层模块讲解完毕后一同进行。

19.5.1.6. MII接口转RMII接口模块

模块框图

MII接口转RMII接口模块的主要功能与RMII接口转MII接口模块相反,将基于MII接口的UDP控制模块传入的符合MII接口时序的使能及数据信号转换为符合RMII接口时序的使能、数据信号,将转换后的使能、数据信号传给FPGA。MII接口转RMII接口模块框图,具体见图 69‑69;输入说出端口简介,具体见表格 69‑7。

Enet070

图 69‑69 MII接口转RMII接口模块框图

表格 69‑7 输入输出端口简介

信号

位宽

类型

功能描述

eth_rmii_clk

1Bit

Input

板卡输入PHY时钟,50MHz

eth_mii_clk

1Bit

Input

基于PHY时钟生成的MII接口时钟,25MHz

sys_rst_n

1Bit

Input

复位信号,低有效

tx_dv

1Bit

Input

以太网数据有效信号(RMII)

tx_data

4Bit

Input

以太网数据(RMII)

eth_tx_dv

1Bit

Output

以太网数据有效信号(MII)

eth_tx_data

2Bit

Output

以太网数据(MII)

波形图绘制

在模块框图小节,我们已经对MII接口转RMII接口模块的模块功能和输入输出端口做了简单介绍。接下来我们通过波形图的绘制,为读者说明各信号的设计与实现方法,以及模块的功能实现。MII接口转RMII接口模块整体波形图,具体见。

Enet071

图 69‑70 MII接口转RMII接口模块整体波形图

MII接口转RMII接口模块的功能实现是RMII接口转MII接口模块功能的逆操作,如上图所示,首先,声明tx_dv_reg、tx_data_reg对输入的tx_dv和tx_data进行打拍,目的是消除亚稳态;其次,声明rd_flag信号,设初值为0,在数据有效信号tx_dv_reg为高电平、eth_ rmii_clk时钟下降沿时,自身不断取反,其他时刻为低电平;再次,在eth_rmii_clk时钟的上升沿、tx_dv_reg为高电平时,对tx_data_reg进行拆分,rd_flag为低电平时,将tx_data_reg[1:0]赋值给eth_tx_data_reg;rd_flag为高电平时,将t x_data_reg[3:2]赋值给eth_tx_data_reg;最后,将数据信号、数据有效信号同步,得到输出信号eth_tx_dv、eth_tx_data。

代码编写

以上述绘制波形图为参照,编写模块参考代码。模块参考代码具体见代码清单 69‑8。

代码清单 69‑8 MII接口转RMII接口模块参考代码(mii_to_rmii.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
module mii_to_rmii
(
input wire eth_mii_clk , //mii时钟
input wire eth_rmii_clk, //rmii时钟
input wire sys_rst_n , //复位信号
input wire tx_dv , //输出数据有效信号(mii)
input wire [3:0] tx_data , //输出数据(mii)

output reg eth_tx_dv , //输出数据有效信号(rmii)
output reg [1:0] eth_tx_data //输出数据(rmii)
);

////
//\* Parameter and Internal Signal \//
////
reg tx_dv_reg ; //输出数据有效信号打一拍(mii)
reg [3:0] tx_data_reg ; //输出数据打一拍(mii)
reg rd_flag ; //eth_tx_data_reg读使能信号
reg [1:0] eth_tx_data_reg ; //输出数据打一拍(rmii)

////
//\* Main Code \//
////
//tx_dv_reg:输出数据有效信号打一拍(mii)
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx_dv_reg <= 1'b0;
else
tx_dv_reg <= tx_dv;

//tx_data_reg:输出数据打一拍(mii)
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
tx_data_reg <= 4'b0;
else
tx_data_reg <= tx_data;

//rd_flag:eth_tx_data_reg读使能信号
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_flag <= 1'b0;
else if(tx_dv_reg == 1'b1)
rd_flag <= ~rd_flag;
else
rd_flag <= 1'b0;

//eth_tx_data_reg:输出数据打一拍(rmii)
always@(posedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_tx_data_reg <= 2'b0;
else if(tx_dv_reg == 1'b1)
if(rd_flag == 1'b0)
eth_tx_data_reg <= {tx_data_reg[1:0]};
else if(rd_flag == 1'b1)
eth_tx_data_reg <= {tx_data_reg[3:2]};
else
eth_tx_data_reg <= 1'b0;

//eth_tx_dv:输出数据有效信号(rmii)
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_tx_dv <= 1'b0;
else
eth_tx_dv <= tx_dv_reg;

//eth_tx_data:输出数据(rmii)
always@(negedge eth_rmii_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_tx_data <= 1'b0;
else
eth_tx_data <= eth_tx_data_reg;

endmodule

本模块的仿真验证待顶层模块讲解完毕后一同进行。

19.5.1.7. 顶层模块

模块框图

顶层模块是实验工程必不可少的一部分,内部实例化个子功能模块,外部进行工程输入输出信号的连接,作用巨大。但顶层模块代码写起来较为简单,无需波形图绘制,只需连接对应端口即可。顶层模块模块框图,具体见图 69‑71;输入输出信号功能描述具体见表格 55‑2。

Enet072

图 69‑71 顶层模块框图

表格 69‑8 输入输出端口功能描述

信号

位宽

类型

功能描述

eth_clk

1Bit

Input

PHY芯片传入以太网时钟,频率50MHz

sys_rst_n

1Bit

Input

复位信号,低有效

eth_rxdv_r

1Bit

Input

输入数据有效信号

eth_rx_data_r

2Bit

Input

输入数据

eth_tx_en

1Bit

Output

输出数据使能信号

eth_tx_data_r

1Bit

Output

输出数据

eth_rst_n

1Bit

Output

PHY复位信号

代码编写

顶层模块参考代码,具体见代码清单 69‑9。

代码清单 69‑9 顶层模块参考代码(ethernet_udp_rmii.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
module ethernet_udp_rmii
(
input wire sys_rst_n , //系统复位,低电平有效
input wire eth_clk , //PHY芯片时钟信号
input wire eth_rxdv_r , //PHY芯片输入数据有效信号
input wire [1:0] eth_rx_data_r , //PHY芯片输入数据

output wire eth_tx_en_r , //PHY芯片输出数据有效信号
output wire [1:0] eth_tx_data_r , //PHY芯片输出数据
output wire eth_rst_n //PHY芯片复位信号,低电平有效
);

////
//\* Parameter and Internal Signal \//
////
//parameter define
parameter BOARD_MAC = 48'h12_34_56_78_9a_bc ; //板卡MAC地址
parameter BOARD_IP = 32'hA9_FE_01_17 ; //板卡IP地址
parameter BOARD_PORT = 16'd1234 ; //板卡端口号
parameter PC_MAC = 48'hFF_FF_FF_FF_FF_FF ; //PC机MAC地址
parameter PC_IP = 32'hA9_FE_F2_37 ; //PC机IP地址
parameter PC_PORT = 16'd1234 ; //PC机端口号

//wire define
wire rec_end ; //单包数据接收完成信号
wire rec_en ; //接收数据使能信号
wire [31:0] rec_data ; //接收数据
wire [15:0] rec_data_num ; //接收有效数据字节数
wire send_end ; //发送完成信号
wire read_data_req ; //读数据请求信号
wire send_en ; //数据开始发送信号
wire [31:0] send_data ; //发送数据
wire eth_rxdv ; //输入数据有效信号(mii)
wire [3:0] eth_rx_data ; //输入数据(mii)
wire eth_tx_en ; //输出数据有效信号(mii)
wire [3:0] eth_tx_data ; //输出数据(mii)

//reg define
reg clk_25m ; //mii时钟

////
//\* Main Code \//
////
//clk_25m:mii时钟
always@(negedge eth_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
clk_25m <= 1'b0;
else
clk_25m <= ~clk_25m;

////
//\* Instantiation \//
////
//------------- rmii_to_mii_inst -------------
rmii_to_mii rmii_to_mii_inst
(
.eth_rmii_clk (eth_clk ), //rmii时钟
.eth_mii_clk (clk_25m ), //mii时钟
.sys_rst_n (sys_rst_n ), //复位信号
.rx_dv (eth_rxdv_r ), //输入数据有效信号(rmii)
.rx_data (eth_rx_data_r ), //输入数据(rmii)

.eth_rx_dv (eth_rxdv ), //输入数据有效信号(mii)
.eth_rx_data (eth_rx_data ) //输入数据(mii)
);

//------------- eth_udp_inst -------------
eth_udp_mii
#(
.BOARD_MAC (BOARD_MAC ), //板卡MAC地址
.BOARD_IP (BOARD_IP ), //板卡IP地址
.BOARD_PORT (BOARD_PORT ), //板卡端口号
.PC_MAC (PC_MAC ), //PC机MAC地址
.PC_IP (PC_IP ), //PC机IP地址
.PC_PORT (PC_PORT ) //PC机端口号
)
eth_udp_mii_inst
(
.eth_rx_clk (clk_25m ), //mii时钟,接收
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.eth_rxdv (eth_rxdv ), //输入数据有效信号(mii)
.eth_rx_data (eth_rx_data ), //输入数据(mii)
.eth_tx_clk (clk_25m ), //mii时钟,发送
.send_en (rec_end ), //开始发送信号
.send_data (send_data ), //发送数据
.send_data_num (rec_data_num ), //发送有效数据字节数

.send_end (send_end ), //单包数据发送完成信号
.read_data_req (read_data_req ), //读数据请求信号
.rec_end (rec_end ), //单包数据接收完成信号
.rec_en (rec_en ), //接收数据使能信号
.rec_data (rec_data ), //接收数据
.rec_data_num (rec_data_num ), //接收有效数据字节数
.eth_tx_en (eth_tx_en ), //输出数据有效信号(mii)
.eth_tx_data (eth_tx_data ), //输出数据(mii)
.eth_rst_n (eth_rst_n ) //复位信号,低电平有效
);

//------------- mii_to_rmii_inst -------------
mii_to_rmii mii_to_rmii_inst
(
.eth_mii_clk (clk_25m ), //mii时钟
.eth_rmii_clk (eth_clk ), //rmii时钟
.sys_rst_n (sys_rst_n ), //复位信号
.tx_dv (eth_tx_en ), //输出数据有效信号(mii)
.tx_data (eth_tx_data ), //输出有效数据(mii)

.eth_tx_dv (eth_tx_en_r ), //输出数据有效信号(rmii)
.eth_tx_data (eth_tx_data_r ) //输出数据(rmii)
);

//------------- fifo_2048x32_inst -------------
//fifo模块,用于缓存单包数据
fifo_2048x32 fifo_2048x32_inst
(
.aclr (~sys_rst_n ), //fifo清零信号
.wrclk (clk_25m ), //fifo写时钟
.wrreq (rec_en ), //fifo写请求
.data (rec_data ), //fifo写数据

.rdclk (clk_25m ), //fifo读时钟
.rdreq (read_data_req ), //fifo读请求
.q (send_data ) //fifo读数据
);

endmodule

仿真文件编写

顶层模块仿真参考代码,具体见代码清单 69‑10。

代码清单 69‑10 顶层模块仿真参考代码(tb_ ethernet_udp_rmii.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
\`timescale 1ns/1ns
module tb_ethernet_udp_rmii();
////
//\* Parameter and Internal Signal \//
////
//reg define
reg sys_clk ; //PHY芯片接收数据时钟信号
reg sys_rst_n ; //系统复位,低电平有效
reg eth_rxdv ; //PHY芯片输入数据有效信号
reg [655:0] data_mem ; //data_mem是一个存储器,相当于一个ram
reg [11:0] cnt_data ; //数据包字节计数器
reg start_flag ; //数据输入开始标志信号

//wire define
wire eth_tx_en_r ; //PHY芯片输出数据有效信号
wire [1:0] eth_tx_data_r ; //PHY芯片输出数据
wire eth_rst_n ; //PHY芯片复位信号,低电平有效
wire [1:0] eth_rx_data ; //PHY芯片输入数据

////
//\* Main Code \//
////
//时钟、复位信号
initial
begin
sys_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
start_flag <= 1'b0 ;
#200
sys_rst_n <= 1'b1 ;
#100
start_flag <= 1'b1 ;
#50
start_flag <= 1'b0 ;
end
//sys_clk
always #10 sys_clk = ~sys_clk;

//data_mem
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_mem <= 'hFE_DC_BA_98_76_54_32_10_..._D5_55_55_55_55_55_55_55;
else if(eth_rxdv == 1'b1)
data_mem <= data_mem >>2;
else
data_mem <= data_mem;

//eth_rxdv:PHY芯片输入数据有效信号
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
eth_rxdv <= 1'b0;
else if(cnt_data == 327)
eth_rxdv <= 1'b0;
else if(start_flag == 1'b1)
eth_rxdv <= 1'b1;
else
eth_rxdv <= eth_rxdv;

//cnt_data:数据包字节计数器
always@(negedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_data <= 12'd0;
else if(eth_rxdv == 1'b1)
cnt_data <= cnt_data + 1'b1;
else
cnt_data <= cnt_data;

//eth_rx_data:PHY芯片输入数据
assign eth_rx_data = (eth_rxdv == 1'b1)
? data_mem[1:0] : 2'b0;

////
//\* Instantiation \//
////
//------------- ethernet_udp_rmii_inst -------------
ethernet_udp_rmii ethernet_udp_rmii_inst
(
.eth_clk (sys_clk ), //PHY芯片时钟
.sys_rst_n (sys_rst_n ), //系统复位,低电平有效
.eth_rxdv_r (eth_rxdv ), //PHY芯片输入数据有效信号
.eth_rx_data_r (eth_rx_data ), //PHY芯片输入数据

.eth_tx_en_r (eth_tx_en_r ), //PHY芯片输出数据有效信号
.eth_tx_data_r (eth_tx_data_r ), //PHY芯片输出数据

.eth_rst_n (eth_rst_n ) //PHY芯片复位信号,低电平有效
);

endmodule

仿真波形分析

实验Quartus、ModelSim联合仿真,这里只查看RMII接口转MII接口模块、MII接口转RMII接口模块的仿真波形,仿真波形图如下。

Enet073

图 69‑72 RMII接口转MII接口模块整体仿真波形图

Enet074

图 69‑73 RMII接口转MII接口模块局部仿真波形图(一)

Enet075

图 69‑74 RMII接口转MII接口模块局部仿真波形图(二)

Enet076

图 69‑75 RMII接口转MII接口模块局部仿真波形图(三)

Enet077

图 69‑76 MII接口转RMII接口模块整体仿真波形图

Enet078

图 69‑77 MII接口转RMII接口模块局部仿真波形图(一)

Enet079

图 69‑78 MII接口转RMII接口模块局部仿真波形图(二)

Enet080

图 69‑79 MII接口转RMII接口模块局部仿真波形图(三)

仿真波形图与绘制波形图,各信号波形变化相同,模块通过仿真验证。

19.6. 上板调试

19.6.1. 引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 55‑3所示。

表格 69‑9 引脚分配表

信号名

信号类型

对应引脚

备注

eth_clk

Input

C3

PHY芯片传入以太网时钟,频率50MHz

sys_rst_n

Input

M15

复位信号,低有效

eth_rxdv_r

Input

D3

输入数据有效信号

eth_rx_data_r[1]

Input

D8

输入数据

eth_rx_data_r[0]

Input

A4

输入数据

eth_tx_en_r

Output

D5

输出数据使能信号

eth_tx_data_r[1]

Output

D6

输出数据

eth_tx_data_r[0]

Output

C8

输出数据

eth_rst_n

Output

B3

PHY复位信号

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 69‑80所示。

Enet081

图 69‑80 管脚分配

19.6.1.1. 结果验证

如图 69‑81所示,开发板连接12V直流电源、USB-Blaster下载器JTAG端口、网线,网线另一端直接连接PC机。线路正确连接后,打开开关为板卡上电。

Enet082

图 69‑81 程序下载连线图

如图 69‑82所示,使用“Programmer”为开发板下载程序。

Enet083

图 69‑82 程序下载图

点击网络图标,打开“网络和Internet”设置,如图 69‑83、图 69‑84所示;打开“更改适配器选项”,可以看到以太网由“正在识别”变为“未识别网络”,如图 69‑85所示,表示自协商成功,硬件连接正常。

Enet084

图 69‑83 打开“网络和Internet”设置

Enet085

图 69‑84 打开“更改适配器选项”

Enet086

图 69‑85 自协商成功

以管理员身份运行“cmd.exe”输入命令“netsh i i show in”,查看要进行ARP绑定的网卡的idx编号,记住名称为“以太网”的idx编号,如图 69‑86所示。

Enet087

图 69‑86 查看idx编号

在“cmd.exe”输入命令“arp -a”,查看接口的ARP缓存表,找到与“以太网”的idx编号相同的接口,如图 69‑87所示。

Enet088

图 69‑87 以太网接口

将代码中“PC_IP”改为与此接口相同的IP地址;“PC_MAC”改为PC机的MAC地址,如图 69‑88所示。

Enet089

图 69‑88 PC端IP、MAC地址的设置

在“cmd.exe”输入命令“netsh -c i i add neighbors 6 169.254.1.23 12-34-56-78-9a-bc”,绑定开发板的IP地址和MAC地址,如图 69‑89所示。

Enet090

图 69‑89 绑定开发板的IP地址和MAC地址

在“cmd.exe”输入命令“arp -a”,查看接口的ARP缓存表,找到与“以太网”的idx编号相同接口,可以看见开发板的IP地址和MAC地址绑定成功,如图 69‑90所示。

Enet091

图 69‑90 开发板IP地址、MAC地址绑定成功

修改完成后,重新编译工程,下载SOF文件,打开网络调试助手,如图 69‑91所示。

Enet092

图 69‑91 打开网络调试助手

如图 69‑92所示,配置网络调试助手的IP和端口号,发送数据,可正确接收。

Enet093

图 69‑92 发送数据

如图 69‑93、图 69‑94、图 69‑95所示,使用wireshark抓取网络信号,IP地址、MAC地址、端口号以及发送和接收数据对应正确,验证通过。

Enet094

图 69‑93 wireshark抓包

Enet095

图 69‑94 wireshark抓包(PC—板卡)

Enet096

图 69‑95 wireshark抓包(板卡—PC)

19.7. 章末总结

在本章节中,我们详细讲解了UDP的相关知识,设计并实现了基于MII和RMII接口的UDP控制器,望读者切实掌握相关知识。

19.8. 拓展训练

本实验工程时通过转接模块和基于MII的UDP控制器实现基于RMII接口的UDP控制器,读者根据所学知识修改工程代码,尝试直接实现基于RMII接口的UDP控制器。