本文将介绍如何实现上位机与FPGA之间的UDP数据环回。上位机通过网口调试助手发送数据,FPGA通过以太网接口接收数据,并将其回传给上位机。
- 此章节内容将适用于Smart Artix 的主板
- 此章节内容同样适用于 Smart ZYNQ 系列主板(包括SP SP2 SL 和标准版)
- 因为Lemon Zynq 的网络是连接在PS端的,所以本文并不适用于Lemon ZYNQ主板
- 本文在 vivado2018.3版本上演示
备注:本文中的部分内容及代码使用和参考了正点原子FPGA的教程,也在此说明
一、关于主板网络部分的硬件介绍:
我们的 Smart Artix 和 我们的Smart ZYNQ 开发板上焊了一颗RTL8211e 千兆网络PHY芯片,并通过RGMII接口与FPGA的IO口相连(Smart Artix 主板接FPGA IO, Smart ZYNQ 接在PL的IO )可为FPGA提供网络访问的功能(对于ZYNQ主板也可以通过EMIO方式供PS 电用)。RTL8211E支持10/100/1000 Mbps网络传输速率。
RTL8211E是Realtek瑞昱推出的一款高集成的网络接收PHY芯片,它符合10Base-T,100Base-TX和1000Base-T IEEE802.3标准,该芯片在网络通信中属于物理层,用于MAC与PHY之间的数据通信。 当网口LINK到千兆网时通过RGMII 与FPGA进行通讯,另外FPGA可以通过MDI/MDIO管理接口来配置或读取PHY芯片内部的寄存器。
芯片工作框图如下:


关于硬件部分的一些重要信息:(关于TXDLY 和 RXDLY)
硬件电路上已经使能了PHY内部的 RXDLY 和 TXDLY 功能 (可以让时钟与数据的中心对齐,以保证采样到的数据是正确的,延时放在PHY端也可以减少软件开发的难度),所以在FPGA 程序上不能再开启软件DELAY功能。(16及32引脚均通过上拉电阻拉高,即PHY芯片已开启2ns 的delay)

如果你不是自己编写verilog代码去驱动 phy,而是使用vivado 官方的MAC IP 如tri_mode_ethernet_mac则需要选Include Shard Logic in example design并把gtx_clk90信号 直接和refclk 直连,不要再做90度相位偏移了
如果移植别家的代码(如正点原子之类的)里的输入输出有加DELAY模块,可以把DELAY的参数改成0(正点原子的代码默认也0)
二、实验内容:
本实验的目标是实现上位机与FPGA之间的UDP数据环回。上位机通过网口调试助手发送数据,FPGA通过以太网接口接收数据,并将其回传给上位机。
虽然本实验介绍的是UDP通信,但保留了ARP功能,因为上位机只知道目标IP地址和端口号,无法直接获取接收端的MAC地址。通过ARP协议可以自动获取MAC地址,避免了手动绑定MAC地址的繁琐过程。
实验中,实现了ARP和UDP协议的功能。FPGA通过GMII接口接收数据,并根据协议类型选择不同的解析模块。接收的GMII数据引脚同时连接至ARP和UDP模块,两个模块分别解析对应的协议数据。由于GMII发送侧引脚只能连接一个模块,以太网控制模块会根据接收到的数据协议类型,动态切换GMII发送侧的连接,选择与ARP或UDP模块连接。同时,控制模块还负责根据ARP接收到的请求类型,控制ARP模块返回相应的ARP应答信号。为了应对大流量数据的接收,实验中使用了同步FIFO模块进行数据缓存,且由于接收和发送使用的是相同的时钟,采用同步FIFO确保数据传输的高效性和稳定性。
三、RGMII 接口介绍
RGMII(Reduced Gigabit Media Independent Interface)是一种用于连接以太网物理层设备(PHY)和FPGA或其他处理器的接口标准。它是Gigabit Ethernet(千兆以太网)的一种实现方式,旨在简化传统GMII(Gigabit Media Independent Interface)的设计,同时降低信号引脚的数量。RGMII使用了信号时序和数据线的优化设计,减少了接口所需的引脚数量,从而降低了硬件成本和复杂度。
RGMII 数据位宽为4 位,在1000Mbps 传输速率下,时钟频率为125Mhz,在时钟的上下沿同时采样数据。在100Mbps 和10Mbps 通信速率下,为单个时钟沿采样。
- RGMII 接口定义:
- TXC : MAC -> PHY 发送参考时钟,
- 1000Mbps 速率下,时钟频率为125MHz,双边沿采样
- 100Mbps 速率下,时钟频率为25MHz, 单边沿采样
- 10Mbps 速率下,时钟频率为2.5MHz, 单边沿采样
- TXD : MAC -> PHY 4位发送数据
- TXCTL:MAC -> PHY 发送使能
- RXC : MAC <- PHY 接收参考时钟,
- 1000Mbps 速率下,时钟频率为125MHz,双边沿采样
- 100Mbps 速率下,时钟频率为25MHz, 单边沿采样
- 10Mbps 速率下,时钟频率为2.5MHz, 单边沿采样
- RXD : MAC <- PHY 4位接收数据
- RXCTL:MAC <- PHY 接收使能
- TXC : MAC -> PHY 发送参考时钟,
信号的详细解释:
RGMII接口的4位数据信号在1000Mbps速率下,ETH_TXC和ETH_RXC的时钟频率为125MHz,采用双边沿采样方式,在上升沿传输数据的低4位,在下降沿传输数据的高4位。 在100Mbps速率下,ETH_TXC和ETH_RXC时钟频率降低为25Mhz,并且数据采用上升沿采样(SDR),即每个周期传输4位数据, 在10Mbps速率下,ETH_TXC和ETH_RXC的时钟速率降低为2.5Mhz,也为上升沿采样。
ETH_TXCTL和RX_CTL 在1000Mbps和100Mbps下是双边沿采样的,上升沿用于传输有效信号TX_EN/RX_DV),下降沿传输(TX_ERR xor TX_EN、RX_ERR xor RX_DV)。当RX_DV为高电平(表示数据有效)且RX_ERR为低电平(表示没有错误)时,异或的结果为高电平,意味着只有当ETH_TXCTL和ETH_RXCTL的上下沿同时为高电平时,数据才是有效且正确的。 在10Mbs下ETH_TXCTL和ETH_RXCTL控制信号改为单边沿采样。
下面是RGMII的时序图:(时序图中的TXC和RXC已增加 delay的情况),备注,开启内部PHY DELAY的情形下, 我们FPGA输出的TXC不需要增加延时,PHY芯片内部会增加这部分功能,RXC的时候 FPGA内部的代码也不需要增加IDELAY部分(PHY芯片输出的RXC就已经滞后了2ns)


四、RGMII模块设计
因为我们输入的网络数据是DDR双边沿采样的,这里我们增加一个rgmii_to_gmii模块来实现DDR双边沿数据 和 SDR 单边沿数据的转换。
`timescale 1ns / 1ps
module rgmii_to_gmii(
//rgmii
input rgmii_rxc,
input rgmii_rx_ctl,
input [3:0] rgmii_rxd,
output rgmii_txc,
output rgmii_tx_ctl,
output [3:0] rgmii_txd,
//gmii
output gmii_rxc,
output gmii_rx_dv,
output [7:0] gmii_rxd,
output gmii_txc,
input gmii_tx_en,
input [7:0] gmii_txd
);
wire rgmii_rxc_bufg;
wire rgmii_rxc_bufio;
wire [1:0] gmii_rxdv_t;
assign gmii_rxc = rgmii_rxc_bufg;
assign gmii_txc = gmii_rxc;
assign rgmii_txc = gmii_txc;
//////////////////////////////RX Part////////////////////
BUFG BUFG_inst (
.I (rgmii_rxc),
.O (rgmii_rxc_bufg)
);
BUFIO BUFIO_inst (
.I (rgmii_rxc),
.O (rgmii_rxc_bufio)
);
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
.INIT_Q1 (1'b0),
.INIT_Q2 (1'b0),
.SRTYPE ("SYNC")
)
u_iddr_rx_ctl (
.Q1 (gmii_rxdv_t[0]),
.Q2 (gmii_rxdv_t[1]),
.C (rgmii_rxc_bufio),
.CE (1'b1),
.D (rgmii_rx_ctl),
.R (1'b0),
.S (1'b0)
);
assign gmii_rx_dv = gmii_rxdv_t[0] & gmii_rxdv_t[1];
genvar i;
generate for (i=0; i<4; i=i+1)begin
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
.INIT_Q1 (1'b0),
.INIT_Q2 (1'b0),
.SRTYPE ("SYNC")
)
u_iddr_rxd (
.Q1 (gmii_rxd[i]),
.Q2 (gmii_rxd[4+i]),
.C (rgmii_rxc_bufio),
.CE (1'b1),
.D (rgmii_rxd[i]),
.R (1'b0),
.S (1'b0)
);
end
endgenerate
//////////////////////////////TX Part////////////////////
ODDR #(
.DDR_CLK_EDGE ("SAME_EDGE"),
.INIT (1'b0),
.SRTYPE ("SYNC")
)
ODDR_inst (
.Q (rgmii_tx_ctl),
.C (gmii_txc),
.CE (1'b1),
.D1 (gmii_tx_en),
.D2 (gmii_tx_en),
.R (1'b0),
.S (1'b0)
);
genvar j;
generate for (j=0; j<4; j=j+1)begin
ODDR #(
.DDR_CLK_EDGE ("SAME_EDGE"),
.INIT (1'b0),
.SRTYPE ("SYNC")
)
ODDR_inst (
.Q (rgmii_txd[j]),
.C (gmii_txc),
.CE (1'b1),
.D1 (gmii_txd[j]),
.D2 (gmii_txd[4+j]),
.R (1'b0),
.S (1'b0)
);
end
endgenerate
endmodule
模块主要实现的功能
- rx部分,通过IDDR原语,将输入的双边沿数据(每个边沿4bit)整合成一个8bit数据
- tx部分,将输入的8bit数据以及使能信号通过ODDR原语转换成双边沿输出。
特别要注意的是,这里GMII 的TX 和RX 时钟,以及RGMII的TX时钟都使用了RGMII的RX输入时钟。
这段代码中 并没有增加idelay部分,输出的TXC也没有增加时钟偏移, 原因在文章开头也有提到(PHY 已经开启了内部DLY功能)
五、以太网帧格式介绍,以及ARP模块设计
(部分图片引用自正点原子的资料)
以太网的帧格式:
以太网帧遵循下列图中的格式

- 前导码 (Preamble): 7字节,用于同步。 (前导码由56个连续的
1和0组成(55-55-55-55-55-55-55))。它的作用是为接收方提供一个同步信号,帮助设备在传输数据前同步时钟) - 起始定界符 (SFD): 1字节,标志帧的开始。 (由
10101011(0xd5)组成,标志着以太网帧的开始) - 目的MAC地址: 6字节,接收端的物理地址。
- 源MAC地址: 6字节,发送端的物理地址。
- 长度/类型: 2字节,表示数据长度或协议类型。(当值小于或等于 1500(0x05DC)时,表示数据部分的长度(46到1500字节);当值大于或等于 1536(0x0600)时,表示数据部分使用的协议类型,如
0x0800代表IPv4,0x0806代表ARP。这一字段帮助接收方区分数据的长度和协议类型) - 数据段: 46-1500字节,有效数据部分。
- 填充: 如果数据段不满足46个字节,则这里进行数据填充确保数据部分长度最小为46字节。
- CRC: 4字节,循环冗余校验,确保数据正确。
以太网地址有三种类型:单播(Unicast)是指数据发送给一个特定设备,目的MAC地址唯一;组播(Multicast)是数据发送给一组设备,目的MAC地址以01开头,映射到IP组播地址;广播(Broadcast)是数据发送给网络中所有设备,目的MAC地址为FF:FF:FF:FF:FF:FF。单播用于点对点通信,组播用于特定群体,广播用于全网通信。
在通讯的过程中,还有一个概念要注意,那就是两帧之间的时间间隔帧间隙,帧间隙(Inter-frame Gap, IFG)是指两帧数据之间的空闲时间,用于确保设备有足够的时间处理当前帧并准备接收下一帧,避免发生数据碰撞或干扰。帧间隙的长度通常为 12 字节(96 位),但随着网络速率的提高,空闲时间会缩短:在 10Mbps 网络中,帧间隙时间为 9600ns;在 100Mbps 网络中,空闲时间为 960ns;而在 1000Mbps(1Gbps)网络中,空闲时间进一步缩短至 96ns。帧间隙有助于在不同速率下避免帧之间的重叠,保证数据传输的顺畅。
ARP 协议:
ARP(地址解析协议)的作用是将网络层的IP地址映射到数据链路层的MAC地址。当设备需要与网络中的其他设备通信时,它通过ARP协议查询目标IP地址对应的物理地址(MAC地址)。如果没有找到对应的MAC地址,设备会发送ARP请求进行广播,目标设备响应后,源设备就能获取并缓存该MAC地址,确保后续通信能够直接使用物理地址进行数据交换。
ARP的工作原理:
ARP协议允许设备根据已知的IP地址来查找目标设备的物理地址(MAC地址)。其工作流程如下:
- ARP请求:当一个设备需要通过IP地址与网络中的另一设备通信时,首先它会检查本地的ARP缓存,看看是否已经存有目标IP地址对应的MAC地址。如果没有找到,它就会发出一个ARP请求,这个请求是一个广播消息(MAC地址是48‘hff_ff_ff_ff_ff_ff),发送到整个局域网。ARP请求包含了目标IP地址和发送设备的IP及MAC地址。
- ARP响应:局域网中所有的设备都会接收到这个请求,但只有目标IP地址匹配的设备会作出响应。响应内容包括发送设备的MAC地址,这样源设备就能根据收到的MAC地址与目标设备通信。
- 缓存:收到ARP响应后,源设备会将目标IP地址与对应的MAC地址存储在本地ARP缓存中,以便下次通信时无需再次进行ARP请求。
- 超时机制:ARP缓存中的条目会有一个过期时间,通常是几分钟。当缓存过期后,源设备必须重新发起ARP请求来更新缓存。
下图为以太网通过ARP 传输单包的数据帧格式,以太网的数据包就是对协议的封装来实现数据的传输,即ARP 数据位于以太网帧格式的数据段(图中画红线框的部分)(ARP数据包需要遵循以太网帧格式,并且ARP数据是28个字节的,数据段需填充满至46个字节)

下图是ARP数据包格式,数据包存在于以太网帧的数据段部分:

ARP数据包的具体说明
- 硬件类型(Hardware Type):
- 用来标识物理网络类型。以太网的硬件类型为
0x0001,而对于其他网络类型如FDDI、无线网络等,硬件类型会不同。
- 用来标识物理网络类型。以太网的硬件类型为
- 协议类型(Protocol Type):
- 表示协议类型,通常是IPv4 (
0x0800)。如果是IPv6,则是0x86DD。
- 表示协议类型,通常是IPv4 (
- 硬件地址长度(Hardware Address Length):
- 定义硬件地址(MAC地址)的长度。在以太网中,MAC地址的长度是6字节。
- 协议地址长度(Protocol Address Length):
- 定义协议地址(IP地址)的长度。IPv4的长度是4字节。
- 操作码(Operation):
- ARP请求(0x0001):表示请求目标设备的MAC地址。
- ARP响应(0x0002):表示回复目标设备的MAC地址。
- 发送方硬件地址(Sender Hardware Address):
- 发送方设备的MAC地址,用于识别源设备。
- 发送方协议地址(Sender Protocol Address):
- 发送方设备的IP地址。
- 目标硬件地址(Target Hardware Address):
- 目标设备的MAC地址。在ARP请求时,目标硬件地址通常为全ff(48’hff_ff_ff_ff_ff_ff 广播地址)
- 目标协议地址(Target Protocol Address):
- 目标设备的IP地址,发送方用来指定目标。
ARP请求和响应的区别
- ARP请求:用于请求某个IP地址对应的MAC地址。请求消息的目标硬件地址字段通常设置为全0(因为未知),而目标协议地址是请求的IP地址。
- ARP响应:用于返回IP地址对应的MAC地址。响应消息中,目标硬件地址填充为发送方的MAC地址,而发送方硬件地址则是提供MAC地址的设备的地址。
ARP的代码构成包括3个部分 arp arp_rx arp_tx,具体内容如下:
arp模块:负责例化 arp_tx arp_rx 和crc校验三个模块
//arp.v
module arp
#(
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10},
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff,
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102}
)
(
input rst_n , //复位信号,低电平有效
//GMII接口
input gmii_rx_clk, //GMII接收数据时钟
input gmii_rx_dv , //GMII输入数据有效信号
input [7:0] gmii_rxd , //GMII输入数据
input gmii_tx_clk, //GMII发送数据时钟
output gmii_tx_en , //GMII输出数据有效信号
output [7:0] gmii_txd , //GMII输出数据
//用户接口
output arp_rx_done, //ARP接收完成信号
output arp_rx_type, //ARP接收类型 0:请求 1:应答
output [47:0] src_mac , //接收到目的MAC地址
output [31:0] src_ip , //接收到目的IP地址
input arp_tx_en , //ARP发送使能信号
input arp_tx_type, //ARP发送类型 0:请求 1:应答
input [47:0] des_mac , //发送的目标MAC地址
input [31:0] des_ip , //发送的目标IP地址
output tx_done //以太网发送完成信号
);
//wire define
wire crc_en ; //CRC开始校验使能
wire crc_clr ; //CRC数据复位信号
wire [7:0] crc_d8 ; //输入待校验8位数据
wire [31:0] crc_data; //CRC校验数据
wire [31:0] crc_next; //CRC下次校验完成数据
//*****************************************************
//** main code
//*****************************************************
assign crc_d8 = gmii_txd;
//ARP接收模块
arp_rx
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP )
)
u_arp_rx(
.clk (gmii_rx_clk),
.rst_n (rst_n),
.gmii_rx_dv (gmii_rx_dv),
.gmii_rxd (gmii_rxd ),
.arp_rx_done (arp_rx_done),
.arp_rx_type (arp_rx_type),
.src_mac (src_mac ),
.src_ip (src_ip )
);
//ARP发送模块
arp_tx
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP ),
.DES_MAC (DES_MAC ),
.DES_IP (DES_IP )
)
u_arp_tx(
.clk (gmii_tx_clk),
.rst_n (rst_n),
.arp_tx_en (arp_tx_en ),
.arp_tx_type (arp_tx_type),
.des_mac (des_mac ),
.des_ip (des_ip ),
.crc_data (crc_data ),
.crc_next (crc_next[31:24]),
.tx_done (tx_done ),
.gmii_tx_en (gmii_tx_en),
.gmii_txd (gmii_txd ),
.crc_en (crc_en ),
.crc_clr (crc_clr )
);
//以太网发送CRC校验模块
crc32_d8 u_crc32_d8(
.clk (gmii_tx_clk),
.rst_n (rst_n ),
.data (crc_d8 ),
.crc_en (crc_en ),
.crc_clr (crc_clr ),
.crc_data (crc_data ),
.crc_next (crc_next )
);
endmodule
arp_rx.v:
arp_rx.v实现了一个ARP接收模块(arp_rx),它通过GMII接口接收以太网数据帧,并解析其中的ARP请求和ARP应答数据。模块的主要功能是根据以太网帧的格式,逐步接收数据并解析识别网络帧是否是ARP协议,并进一步解析ARP数据中的操作码、源和目标MAC地址、源和目标IP地址。如果接收到的ARP请求或应答中的目标IP地址匹配预设的板卡IP地址,则模块会输出相应的结果。
//arp_rx.v
module arp_rx
#(
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10}
)
(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input gmii_rx_dv , //GMII输入数据有效信号
input [7:0] gmii_rxd , //GMII输入数据
output reg arp_rx_done, //ARP接收完成信号
output reg arp_rx_type, //ARP接收类型 0:请求 1:应答
output reg [47:0] src_mac , //接收到的源MAC地址
output reg [31:0] src_ip //接收到的源IP地址
);
//parameter define
localparam st_idle = 5'b0_0001; //初始状态,等待接收前导码
localparam st_preamble = 5'b0_0010; //接收前导码状态
localparam st_eth_head = 5'b0_0100; //接收以太网帧头
localparam st_arp_data = 5'b0_1000; //接收ARP数据
localparam st_rx_end = 5'b1_0000; //接收结束
localparam ETH_TPYE = 16'h0806; //以太网帧类型 ARP
//reg define
reg [4:0] cur_state ;
reg [4:0] next_state;
reg skip_en ; //控制状态跳转使能信号
reg error_en ; //解析错误使能信号
reg [4:0] cnt ; //解析数据计数器
reg [47:0] des_mac_t ; //接收到的目的MAC地址
reg [31:0] des_ip_t ; //接收到的目的IP地址
reg [47:0] src_mac_t ; //接收到的源MAC地址
reg [31:0] src_ip_t ; //接收到的源IP地址
reg [15:0] eth_type ; //以太网类型
reg [15:0] op_data ; //操作码
reg rx_done_t ; //ARP接收完成信号
//*****************************************************
//** main code
//*****************************************************
//(三段式状态机)同步时序描述状态转移
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_preamble;
else
next_state = st_idle;
end
st_preamble : begin //接收前导码
if(skip_en)
next_state = st_eth_head;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_preamble;
end
st_eth_head : begin //接收以太网帧头
if(skip_en)
next_state = st_arp_data;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_eth_head;
end
st_arp_data : begin //接收ARP数据
if(skip_en)
next_state = st_rx_end;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_arp_data;
end
st_rx_end : begin //接收结束
if(skip_en)
next_state = st_idle;
else
next_state = st_rx_end;
end
default : next_state = st_idle;
endcase
end
//时序电路描述状态输出,解析以太网数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
skip_en <= 1'b0;
error_en <= 1'b0;
cnt <= 5'd0;
des_mac_t <= 48'd0;
des_ip_t <= 32'd0;
src_mac_t <= 48'd0;
src_ip_t <= 32'd0;
eth_type <= 16'd0;
op_data <= 16'd0;
rx_done_t <= 1'b0;
arp_rx_type <= 1'b0;
src_mac <= 48'd0;
src_ip <= 32'd0;
end
else begin
skip_en <= 1'b0;
error_en <= 1'b0;
rx_done_t <= 1'b0;
case(next_state)
st_idle : begin //检测到第一个8'h55
if((gmii_rx_dv == 1'b1) && (gmii_rxd == 8'h55))
skip_en <= 1'b1;
end
st_preamble : begin
if(gmii_rx_dv) begin //解析前导码
cnt <= cnt + 5'd1;
if((cnt < 5'd6) && (gmii_rxd != 8'h55)) //7个8'h55
error_en <= 1'b1;
else if(cnt==5'd6) begin
cnt <= 5'd0;
if(gmii_rxd==8'hd5) //1个8'hd5
skip_en <= 1'b1;
else
error_en <= 1'b1;
end
end
end
st_eth_head : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'b1;
if(cnt < 5'd6)
des_mac_t <= {des_mac_t[39:0],gmii_rxd};
else if(cnt == 5'd6) begin
//判断MAC地址是否为开发板MAC地址或者公共地址
if((des_mac_t != BOARD_MAC)
&& (des_mac_t != 48'hff_ff_ff_ff_ff_ff))
error_en <= 1'b1;
end
else if(cnt == 5'd12)
eth_type[15:8] <= gmii_rxd; //以太网协议类型
else if(cnt == 5'd13) begin
eth_type[7:0] <= gmii_rxd;
cnt <= 5'd0;
if(eth_type[15:8] == ETH_TPYE[15:8] //判断是否为ARP协议
&& gmii_rxd == ETH_TPYE[7:0])
skip_en <= 1'b1;
else
error_en <= 1'b1;
end
end
end
st_arp_data : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'd1;
if(cnt == 5'd6)
op_data[15:8] <= gmii_rxd; //操作码
else if(cnt == 5'd7)
op_data[7:0] <= gmii_rxd;
else if(cnt >= 5'd8 && cnt < 5'd14) //源MAC地址
src_mac_t <= {src_mac_t[39:0],gmii_rxd};
else if(cnt >= 5'd14 && cnt < 5'd18) //源IP地址
src_ip_t<= {src_ip_t[23:0],gmii_rxd};
else if(cnt >= 5'd24 && cnt < 5'd28) //目标IP地址
des_ip_t <= {des_ip_t[23:0],gmii_rxd};
else if(cnt == 5'd28) begin
cnt <= 5'd0;
if(des_ip_t == BOARD_IP) begin //判断目的IP地址和操作码
if((op_data == 16'd1) || (op_data == 16'd2)) begin
skip_en <= 1'b1;
rx_done_t <= 1'b1;
src_mac <= src_mac_t;
src_ip <= src_ip_t;
src_mac_t <= 48'd0;
src_ip_t <= 32'd0;
des_mac_t <= 48'd0;
des_ip_t <= 32'd0;
if(op_data == 16'd1)
arp_rx_type <= 1'b0; //ARP请求
else
arp_rx_type <= 1'b1; //ARP应答
end
else
error_en <= 1'b1;
end
else
error_en <= 1'b1;
end
end
end
st_rx_end : begin
cnt <= 5'd0;
//单包数据接收完成
if(gmii_rx_dv == 1'b0 && skip_en == 1'b0)
skip_en <= 1'b1;
end
default : ;
endcase
end
end
//输出arp_rx_done信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
arp_rx_done <= 1'b0;
else
arp_rx_done <= rx_done_t;
end
endmodule
arp_tx.v :
这段代码实现了一个ARP帧发送功能,负责构建和发送ARP请求或应答。模块通过状态机来控制发送的过程,依次发送前导码、以太网帧头、ARP数据和CRC校验内容。
模块根据输入的使能信号 arp_tx_en 启动发送,并通过状态机管理不同阶段的数据传输。
arp_tx_type为高电平,表示发送ARP应答数据包。ARP应答数据包中的目的MAC地址和目的IP地址从ARP接收数据包中获取(可动态更新)
发送过程通过 gmii_tx_en 和 gmii_txd 控制,最后通过 tx_done 输出发送完成信号。
//arp_tx.v
module arp_tx
#(
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10},
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff,
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102}
)
(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input arp_tx_en , //ARP发送使能信号
input arp_tx_type, //ARP发送类型 0:请求 1:应答
input [47:0] des_mac , //发送的目标MAC地址
input [31:0] des_ip , //发送的目标IP地址
input [31:0] crc_data , //CRC校验数据
input [7:0] crc_next , //CRC下次校验完成数据
output reg tx_done , //以太网发送完成信号
output reg gmii_tx_en , //GMII输出数据有效信号
output reg [7:0] gmii_txd , //GMII输出数据
output reg crc_en , //CRC开始校验使能
output reg crc_clr //CRC数据复位信号
);
localparam st_idle = 5'b0_0001; //初始状态,等待开始发送信号
localparam st_preamble = 5'b0_0010; //发送前导码+帧起始界定符
localparam st_eth_head = 5'b0_0100; //发送以太网帧头
localparam st_arp_data = 5'b0_1000; //
localparam st_crc = 5'b1_0000; //发送CRC校验值
localparam ETH_TYPE = 16'h0806 ; //以太网帧类型 ARP协议
localparam HD_TYPE = 16'h0001 ; //硬件类型 以太网
localparam PROTOCOL_TYPE= 16'h0800 ; //上层协议为IP协议
//以太网数据最小为46个字节,不足部分填充数据
localparam MIN_DATA_NUM = 16'd46 ;
//reg define
reg [4:0] cur_state ;
reg [4:0] next_state ;
reg [7:0] preamble[7:0] ; //前导码+SFD
reg [7:0] eth_head[13:0]; //以太网首部
reg [7:0] arp_data[27:0]; //ARP数据
reg tx_en_d0 ; //arp_tx_en信号延时
reg tx_en_d1 ;
reg skip_en ; //控制状态跳转使能信号
reg [5:0] cnt ;
reg [4:0] data_cnt ; //发送数据个数计数器
reg tx_done_t ;
//wire define
wire pos_tx_en ; //arp_tx_en信号上升沿
//*****************************************************
//** main code
//*****************************************************
assign pos_tx_en = (~tx_en_d1) & tx_en_d0;
//对arp_tx_en信号延时打拍两次,用于采arp_tx_en的上升沿
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_en_d0 <= 1'b0;
tx_en_d1 <= 1'b0;
end
else begin
tx_en_d0 <= arp_tx_en;
tx_en_d1 <= tx_en_d0;
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_preamble;
else
next_state = st_idle;
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_arp_data;
else
next_state = st_eth_head;
end
st_arp_data : begin //发送ARP数据
if(skip_en)
next_state = st_crc;
else
next_state = st_arp_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 <= 6'd0;
data_cnt <= 5'd0;
crc_en <= 1'b0;
gmii_tx_en <= 1'b0;
gmii_txd <= 8'd0;
tx_done_t <= 1'b0;
//初始化数组
//前导码 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;
//以太网帧头
eth_head[0] <= DES_MAC[47:40]; //目的MAC地址
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];
eth_head[6] <= BOARD_MAC[47:40]; //源MAC地址
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];
//ARP数据
arp_data[0] <= HD_TYPE[15:8]; //硬件类型
arp_data[1] <= HD_TYPE[7:0];
arp_data[2] <= PROTOCOL_TYPE[15:8]; //上层协议类型
arp_data[3] <= PROTOCOL_TYPE[7:0];
arp_data[4] <= 8'h06; //硬件地址长度,6
arp_data[5] <= 8'h04; //协议地址长度,4
arp_data[6] <= 8'h00; //OP,操作码 8'h01:ARP请求 8'h02:ARP应答
arp_data[7] <= 8'h01;
arp_data[8] <= BOARD_MAC[47:40]; //发送端(源)MAC地址
arp_data[9] <= BOARD_MAC[39:32];
arp_data[10] <= BOARD_MAC[31:24];
arp_data[11] <= BOARD_MAC[23:16];
arp_data[12] <= BOARD_MAC[15:8];
arp_data[13] <= BOARD_MAC[7:0];
arp_data[14] <= BOARD_IP[31:24]; //发送端(源)IP地址
arp_data[15] <= BOARD_IP[23:16];
arp_data[16] <= BOARD_IP[15:8];
arp_data[17] <= BOARD_IP[7:0];
arp_data[18] <= DES_MAC[47:40]; //接收端(目的)MAC地址
arp_data[19] <= DES_MAC[39:32];
arp_data[20] <= DES_MAC[31:24];
arp_data[21] <= DES_MAC[23:16];
arp_data[22] <= DES_MAC[15:8];
arp_data[23] <= DES_MAC[7:0];
arp_data[24] <= DES_IP[31:24]; //接收端(目的)IP地址
arp_data[25] <= DES_IP[23:16];
arp_data[26] <= DES_IP[15:8];
arp_data[27] <= DES_IP[7:0];
end
else begin
skip_en <= 1'b0;
crc_en <= 1'b0;
gmii_tx_en <= 1'b0;
tx_done_t <= 1'b0;
case(next_state)
st_idle : begin
if(pos_tx_en) begin
skip_en <= 1'b1;
//如果目标MAC地址和IP地址已经更新,则发送正确的地址
if((des_mac != 48'b0) || (des_ip != 32'd0)) begin
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];
arp_data[18] <= des_mac[47:40];
arp_data[19] <= des_mac[39:32];
arp_data[20] <= des_mac[31:24];
arp_data[21] <= des_mac[23:16];
arp_data[22] <= des_mac[15:8];
arp_data[23] <= des_mac[7:0];
arp_data[24] <= des_ip[31:24];
arp_data[25] <= des_ip[23:16];
arp_data[26] <= des_ip[15:8];
arp_data[27] <= des_ip[7:0];
end
if(arp_tx_type == 1'b0)
arp_data[7] <= 8'h01; //ARP请求
else
arp_data[7] <= 8'h02; //ARP应答
end
end
st_preamble : begin //发送前导码+帧起始界定符
gmii_tx_en <= 1'b1;
gmii_txd <= preamble[cnt];
if(cnt == 6'd7) begin
skip_en <= 1'b1;
cnt <= 1'b0;
end
else
cnt <= cnt + 1'b1;
end
st_eth_head : begin //发送以太网首部
gmii_tx_en <= 1'b1;
crc_en <= 1'b1;
gmii_txd <= eth_head[cnt];
if (cnt == 6'd13) begin
skip_en <= 1'b1;
cnt <= 1'b0;
end
else
cnt <= cnt + 1'b1;
end
st_arp_data : begin //发送ARP数据
crc_en <= 1'b1;
gmii_tx_en <= 1'b1;
//至少发送46个字节
if (cnt == MIN_DATA_NUM - 1'b1) begin
skip_en <= 1'b1;
cnt <= 1'b0;
data_cnt <= 1'b0;
end
else
cnt <= cnt + 1'b1;
if(data_cnt <= 6'd27) begin
data_cnt <= data_cnt + 1'b1;
gmii_txd <= arp_data[data_cnt];
end
else
gmii_txd <= 8'd0; //Padding,填充0
end
st_crc : begin //发送CRC校验值
gmii_tx_en <= 1'b1;
cnt <= cnt + 1'b1;
if(cnt == 6'd0)
gmii_txd <= {~crc_next[0], ~crc_next[1], ~crc_next[2],~crc_next[3],
~crc_next[4], ~crc_next[5], ~crc_next[6],~crc_next[7]};
else if(cnt == 6'd1)
gmii_txd <= {~crc_data[16], ~crc_data[17], ~crc_data[18],
~crc_data[19], ~crc_data[20], ~crc_data[21],
~crc_data[22],~crc_data[23]};
else if(cnt == 6'd2) begin
gmii_txd <= {~crc_data[8], ~crc_data[9], ~crc_data[10],
~crc_data[11],~crc_data[12], ~crc_data[13],
~crc_data[14],~crc_data[15]};
end
else if(cnt == 6'd3) begin
gmii_txd <= {~crc_data[0], ~crc_data[1], ~crc_data[2],~crc_data[3],
~crc_data[4], ~crc_data[5], ~crc_data[6],~crc_data[7]};
tx_done_t <= 1'b1;
skip_en <= 1'b1;
cnt <= 1'b0;
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
六、UDP模块设计
UDP(用户数据报协议)是一种无连接的传输层协议,主要用于需要高速传输但对可靠性要求较低的场景。与TCP不同,UDP不建立连接、不进行数据重传和顺序控制,因此可以减少延迟和提高传输效率。它将数据分为独立的“数据报”进行发送,每个数据报包含完整的目标信息,包括目标IP地址和端口号。由于其简单的协议结构,UDP适用于实时性要求较高的应用,比如视频流、语音通信、在线游戏等。
UDP的主要特点是没有握手过程,数据传输不保证可靠性和顺序,且没有流量控制或拥塞控制。虽然这种设计使得UDP在某些场景下传输效率更高,但也意味着如果数据在传输过程中丢失或出现错误,接收方不会收到任何通知。因此,应用程序需要自行处理丢包和错误校验,通常适用于对丢失少量数据不敏感的应用。
下图是UDP 的数据传输包格式(图片来自正点原子FPGA部分资料),从图中可以看出 UDP数据包和我们之前的ARP数据包有很多相似的地方(都包含前导码,FSD,以太网帧头,数据段以及FCS校验部分)区别在于数据段的内容有所不同。从图中我们还能看出从图中可以看出,以太网的数据包就是对各层协议的逐层封装来实现数据的传输。用户数据打包在UDP协议中,UDP协议又是基于IP协议之上的,IP协议又是走MAC层发送的,即从包含关系来说:MAC帧中的数据段为IP数据报,IP报文中的数据段为UDP报文,UDP报文中的数据段为用户希望传输的数据内容。

因为UDP协议包属于IP协议包的一种,所以这里我们简单介绍下IP协议包的数据报部分。

IP数据包的结构
IP数据包主要由两个部分组成:
- IP首部(IP Header)
- 数据部分(Payload)
IP头部结构
IP头部包含了路由、目标地址等信息,通常包括以下几个字段:
| 字段 | 长度 | 描述 |
|---|---|---|
| 版本(Version) | 4 bits | IP协议的版本,IPv4为4,IPv6为6 |
| 首部长度(IHL) | 4 bits | IP首部的长度,单位是32位(4字节),即有多少个32位,因为首部长度是4bits的即0-15,所以首部最大16×4=64字节 |
| 服务类型(Type of Service) | 8 bits | 控制服务质量的字段,在旧标准中叫做服务类型(现在基本不适用) |
| 总长度(Total Length) | 16 bits | 包括首部和数据的总长度(单位是字节)占16bits 因此数据报的最大长度为 65535 字节.总长度必须不超过最大传送单元 MTU |
| 标识符(Identification) | 16 bits | 数据包的标识符,它是一个计数器,用来产生数据报的标识 |
| 标志(Flags) | 3 bits | 分片控制标志,3位标志(Flags)字段,第1位为保留位;第2位表示禁止分片(1表示不分片 0:允许分片);第3位标识更多分片(除了数据报的最后一个分片外,其它分片都为1) |
| 片偏移(Fragment Offset) | 13 bits | 数据包分片的位置偏移,在接收方进行数据报重组时用来标识分片的顺序 |
| 生存时间(TTL) | 8 bits | 数据报在网络中可通过的路由器数的最大值 |
| 协议(Protocol) | 8 bits | 上层协议(1表示为 ICMP 协议, 2表示为 IGMP 协议, 6表示为 TCP 协议, 17表示为 UDP 协议) |
| 头部校验和(Header Checksum) | 16 bits | 校验和,用于检查头部错误(不包含数据部分) |
| 源IP地址(Source Address) | 32 bits | 发送端的IP地址 |
| 目标IP地址(Destination Address) | 32 bits | 接收端的IP地址 |
| 选项(Options) | 可变长度 | 可选字段,通常不常用 |
IP数据包的数据部分(即IP的负载)
在不同的协议情况下会有所不同。对于UDP的情况,这个数据部分确实是UDP数据包本身。UDP数据包包含了它自己的头部和数据部分。
数据部分可以是任何需要通过网络传输的数据(这包括但不限于UDP包、TCP包、ICMP包等)
UDP数据包的结构

UDP数据包相对于IP数据包较为简单,包含以下几个部分:
| 字段 | 长度 | 描述 |
|---|---|---|
| 源端口(Source Port) | 16 bits | 源端口号 |
| 目标端口(Destination Port) | 16 bits | 目标端口号 |
| 长度(Length) | 16 bits | UDP数据部分的长度(包含UDP首部长度+数据长度,单位是字节(byte)) |
| 校验和(Checksum) | 16 bits | 校验和,用于检测数据是否正确 |
| 数据(Data) | 可变 | 实际传输的应用数据 |
UDP的代码构成包括3个部分 udp顶层模块,udp_rx udp_tx,crc校验模块,具体内容如下:
udp顶层模块:负责例化 udp_tx, udp_rx 和crc校验三个模块
module udp
#(
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10},
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff,
parameter DES_IP = {8'd192,8'd168,8'd1,8'd102}
)
(
input rst_n , //复位信号,低电平有效
//GMII接口
input gmii_rx_clk , //GMII接收数据时钟
input gmii_rx_dv , //GMII输入数据有效信号
input [7:0] gmii_rxd , //GMII输入数据
input gmii_tx_clk , //GMII发送数据时钟
output gmii_tx_en , //GMII输出数据有效信号
output [7:0] gmii_txd , //GMII输出数据
//用户接口
output rec_pkt_done, //以太网单包数据接收完成信号
output rec_en , //以太网接收的数据使能信号
output [31:0] rec_data , //以太网接收的数据
output [15:0] rec_byte_num, //以太网接收的有效字节数 单位:byte
input tx_start_en , //以太网开始发送信号
input [31:0] tx_data , //以太网待发送数据
input [15:0] tx_byte_num , //以太网发送的有效字节数 单位:byte
input [47:0] des_mac , //发送的目标MAC地址
input [31:0] des_ip , //发送的目标IP地址
output tx_done , //以太网发送完成信号
output tx_req //读数据请求信号
);
//wire define
wire crc_en ; //CRC开始校验使能
wire crc_clr ; //CRC数据复位信号
wire [7:0] crc_d8 ; //输入待校验8位数据
wire [31:0] crc_data; //CRC校验数据
wire [31:0] crc_next; //CRC下次校验完成数据
//*****************************************************
//** main code
//*****************************************************
assign crc_d8 = gmii_txd;
//以太网接收模块
udp_rx
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP )
)
u_udp_rx(
.clk (gmii_rx_clk ),
.rst_n (rst_n ),
.gmii_rx_dv (gmii_rx_dv ),
.gmii_rxd (gmii_rxd ),
.rec_pkt_done (rec_pkt_done),
.rec_en (rec_en ),
.rec_data (rec_data ),
.rec_byte_num (rec_byte_num)
);
//以太网发送模块
udp_tx
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP ),
.DES_MAC (DES_MAC ),
.DES_IP (DES_IP )
)
u_udp_tx(
.clk (gmii_tx_clk),
.rst_n (rst_n ),
.tx_start_en (tx_start_en),
.tx_data (tx_data ),
.tx_byte_num (tx_byte_num),
.des_mac (des_mac ),
.des_ip (des_ip ),
.crc_data (crc_data ),
.crc_next (crc_next[31:24]),
.tx_done (tx_done ),
.tx_req (tx_req ),
.gmii_tx_en (gmii_tx_en ),
.gmii_txd (gmii_txd ),
.crc_en (crc_en ),
.crc_clr (crc_clr )
);
//以太网发送CRC校验模块
crc32_d8 u_crc32_d8(
.clk (gmii_tx_clk),
.rst_n (rst_n ),
.data (crc_d8 ),
.crc_en (crc_en ),
.crc_clr (crc_clr ),
.crc_data (crc_data ),
.crc_next (crc_next )
);
endmodule
udp_rx 模块:
实现了一个基于 GMII 接口的 UDP 数据接收与解析电路。它采用三段式有限状态机,从以太网前导码开始,依次解析 以太网帧头、IP 首部、UDP 首部,并校验目的 MAC 地址、IP 地址以及协议类型 是否与本机参数匹配;若匹配则继续接收 UDP 负载数据,否则丢弃该帧。接收过程中将 8 bit GMII 数据拼接成 32 bit 数据输出,通过 rec_en 指示数据有效,通过 rec_pkt_done 表示一帧 UDP 数据接收完成,并在 rec_byte_num 中给出有效字节数。即在硬件中从以太网数据流中筛选并提取发往本机的 UDP 有效数据。
module udp_rx
#(
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10}
)
(
input clk , //时钟信号
input rst_n , //复位信号,低电平有效
input gmii_rx_dv , //GMII输入数据有效信号
input [7:0] gmii_rxd , //GMII输入数据
output reg rec_pkt_done, //以太网单包数据接收完成信号
output reg rec_en , //以太网接收的数据使能信号
output reg [31:0] rec_data , //以太网接收的数据
output reg [15:0] rec_byte_num //以太网接收的有效字节数 单位:byte
);
localparam st_idle = 7'b000_0001; //初始状态,等待接收前导码
localparam st_preamble = 7'b000_0010; //接收前导码状态
localparam st_eth_head = 7'b000_0100; //接收以太网帧头
localparam st_ip_head = 7'b000_1000; //接收IP首部
localparam st_udp_head = 7'b001_0000; //接收UDP首部
localparam st_rx_data = 7'b010_0000; //接收有效数据
localparam st_rx_end = 7'b100_0000; //接收结束
localparam ETH_TYPE = 16'h0800 ; //以太网协议类型 IP协议
localparam UDP_TYPE = 8'd17 ; //UDP协议类型
//reg define
reg [6:0] cur_state ;
reg [6:0] next_state ;
reg skip_en ; //控制状态跳转使能信号
reg error_en ; //解析错误使能信号
reg [4:0] cnt ; //解析数据计数器
reg [47:0] des_mac ; //目的MAC地址
reg [15:0] eth_type ; //以太网类型
reg [31:0] des_ip ; //目的IP地址
reg [5:0] ip_head_byte_num; //IP首部长度
reg [15:0] udp_byte_num ; //UDP长度
reg [15:0] data_byte_num ; //数据长度
reg [15:0] data_cnt ; //有效数据计数
reg [1:0] rec_en_cnt ; //8bit转32bit计数器
//*****************************************************
//** main code
//*****************************************************
//(三段式状态机)同步时序描述状态转移
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_preamble;
else
next_state = st_idle;
end
st_preamble : begin //接收前导码
if(skip_en)
next_state = st_eth_head;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_preamble;
end
st_eth_head : begin //接收以太网帧头
if(skip_en)
next_state = st_ip_head;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_eth_head;
end
st_ip_head : begin //接收IP首部
if(skip_en)
next_state = st_udp_head;
else if(error_en)
next_state = st_rx_end;
else
next_state = st_ip_head;
end
st_udp_head : begin //接收UDP首部
if(skip_en)
next_state = st_rx_data;
else
next_state = st_udp_head;
end
st_rx_data : begin //接收有效数据
if(skip_en)
next_state = st_rx_end;
else
next_state = st_rx_data;
end
st_rx_end : begin //接收结束
if(skip_en)
next_state = st_idle;
else
next_state = st_rx_end;
end
default : next_state = st_idle;
endcase
end
//时序电路描述状态输出,解析以太网数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
skip_en <= 1'b0;
error_en <= 1'b0;
cnt <= 5'd0;
des_mac <= 48'd0;
eth_type <= 16'd0;
des_ip <= 32'd0;
ip_head_byte_num <= 6'd0;
udp_byte_num <= 16'd0;
data_byte_num <= 16'd0;
data_cnt <= 16'd0;
rec_en_cnt <= 2'd0;
rec_en <= 1'b0;
rec_data <= 32'd0;
rec_pkt_done <= 1'b0;
rec_byte_num <= 16'd0;
end
else begin
skip_en <= 1'b0;
error_en <= 1'b0;
rec_en <= 1'b0;
rec_pkt_done <= 1'b0;
case(next_state)
st_idle : begin
if((gmii_rx_dv == 1'b1) && (gmii_rxd == 8'h55))
skip_en <= 1'b1;
end
st_preamble : begin
if(gmii_rx_dv) begin //解析前导码
cnt <= cnt + 5'd1;
if((cnt < 5'd6) && (gmii_rxd != 8'h55)) //7个8'h55
error_en <= 1'b1;
else if(cnt==5'd6) begin
cnt <= 5'd0;
if(gmii_rxd==8'hd5) //1个8'hd5
skip_en <= 1'b1;
else
error_en <= 1'b1;
end
end
end
st_eth_head : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'b1;
if(cnt < 5'd6)
des_mac <= {des_mac[39:0],gmii_rxd}; //目的MAC地址
else if(cnt == 5'd12)
eth_type[15:8] <= gmii_rxd; //以太网协议类型
else if(cnt == 5'd13) begin
eth_type[7:0] <= gmii_rxd;
cnt <= 5'd0;
//判断MAC地址是否为开发板MAC地址或者公共地址
if(((des_mac == BOARD_MAC) ||(des_mac == 48'hff_ff_ff_ff_ff_ff))
&& eth_type[15:8] == ETH_TYPE[15:8] && gmii_rxd == ETH_TYPE[7:0])
skip_en <= 1'b1;
else
error_en <= 1'b1;
end
end
end
st_ip_head : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'd1;
if(cnt == 5'd0)
ip_head_byte_num <= {gmii_rxd[3:0],2'd0}; //寄存IP首部长度
else if(cnt == 5'd9) begin
if(gmii_rxd != UDP_TYPE) begin
//如果当前接收的数据不是UDP协议,停止解析数据
error_en <= 1'b1;
cnt <= 5'd0;
end
end
else if((cnt >= 5'd16) && (cnt <= 5'd18))
des_ip <= {des_ip[23:0],gmii_rxd}; //寄存目的IP地址
else if(cnt == 5'd19) begin
des_ip <= {des_ip[23:0],gmii_rxd};
//判断IP地址是否为开发板IP地址
if((des_ip[23:0] == BOARD_IP[31:8])
&& (gmii_rxd == BOARD_IP[7:0])) begin
if(cnt == ip_head_byte_num - 1'b1) begin
skip_en <=1'b1;
cnt <= 5'd0;
end
end
else begin
//IP错误,停止解析数据
error_en <= 1'b1;
cnt <= 5'd0;
end
end
else if(cnt == ip_head_byte_num - 1'b1) begin
skip_en <=1'b1; //IP首部解析完成
cnt <= 5'd0;
end
end
end
st_udp_head : begin
if(gmii_rx_dv) begin
cnt <= cnt + 5'd1;
if(cnt == 5'd4)
udp_byte_num[15:8] <= gmii_rxd; //解析UDP字节长度
else if(cnt == 5'd5)
udp_byte_num[7:0] <= gmii_rxd;
else if(cnt == 5'd7) begin
//有效数据字节长度,(UDP首部8个字节,所以减去8)
data_byte_num <= udp_byte_num - 16'd8;
skip_en <= 1'b1;
cnt <= 5'd0;
end
end
end
st_rx_data : begin
//接收数据,转换成32bit
if(gmii_rx_dv) begin
data_cnt <= data_cnt + 16'd1;
rec_en_cnt <= rec_en_cnt + 2'd1;
if(data_cnt == data_byte_num - 16'd1) begin
skip_en <= 1'b1; //有效数据接收完成
data_cnt <= 16'd0;
rec_en_cnt <= 2'd0;
rec_pkt_done <= 1'b1;
rec_en <= 1'b1;
rec_byte_num <= data_byte_num;
end
//先收到的数据放在了rec_data的高位,所以当数据不是4的倍数时,
//低位数据为无效数据,可根据有效字节数来判断(rec_byte_num)
if(rec_en_cnt == 2'd0)
rec_data[31:24] <= gmii_rxd;
else if(rec_en_cnt == 2'd1)
rec_data[23:16] <= gmii_rxd;
else if(rec_en_cnt == 2'd2)
rec_data[15:8] <= gmii_rxd;
else if(rec_en_cnt==2'd3) begin
rec_en <= 1'b1;
rec_data[7:0] <= gmii_rxd;
end
end
end
st_rx_end : begin //单包数据接收完成
if(gmii_rx_dv == 1'b0 && skip_en == 1'b0)
skip_en <= 1'b1;
end
default : ;
endcase
end
end
endmodule
udp_tx 模块:
实现了一个基于 GMII 接口的 UDP 数据发送电路:在检测到 tx_start_en 上升沿后,模块自动封装 以太网帧、IP 首部和 UDP 首部,根据输入的数据长度计算 IP/UDP 长度并生成 IP 首部校验和,随后依次发送 前导码、以太网首部、IP+UDP 首部、有效数据以及 CRC 校验值。发送过程中将 32bit 数据拆分为 8bit 通过 GMII 输出,并在数据不足以太网最小帧长度时自动补齐;通过 tx_req 请求上层提供数据,通过 tx_done 指示整帧 UDP 数据发送完成。整体功能就是:在硬件中将用户数据打包成完整的 UDP/IP 以太网帧并发送出去
module udp_tx
#(
parameter BOARD_MAC = 48'h00_11_22_33_44_55,
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10},
parameter DES_MAC = 48'hff_ff_ff_ff_ff_ff,
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 [47:0] des_mac , //发送的目标MAC地址
input [31:0] des_ip , //发送的目标IP地址
input [31:0] crc_data , //CRC校验数据
input [7:0] crc_next , //CRC下次校验完成数据
output reg tx_done , //以太网发送完成信号
output reg tx_req , //读数据请求信号
output reg gmii_tx_en , //GMII输出数据有效信号
output reg [7:0] gmii_txd , //GMII输出数据
output reg crc_en , //CRC开始校验使能
output reg crc_clr //CRC数据复位信号
);
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 ;
localparam UDP_TYPE = 8'd17 ; //UDP协议类型
//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 trig_tx_en ;
reg [15:0] udp_num ; //UDP字节数
reg skip_en ; //控制状态跳转使能信号
reg [4:0] cnt ;
reg [31:0] check_buffer ; //首部校验和
reg [1:0] tx_byte_sel ; //32位数据转8位数据计数器
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;
//UDP长度:UDP首部长度 + 有效数据
udp_num <= tx_byte_num + 16'd8;
//IP长度:IP首部长度 + UDP首部 + 有效数据
total_num <= tx_byte_num + 16'd20 + 16'd8;
end
end
end
//触发发送信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
trig_tx_en <= 1'b0;
else
trig_tx_en <= pos_start_en;
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_byte_sel <= 2'b0;
crc_en <= 1'b0;
gmii_tx_en <= 1'b0;
gmii_txd <= 8'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;
gmii_tx_en <= 1'b0;
tx_done_t <= 1'b0;
case(next_state)
st_idle : begin
if(trig_tx_en) begin
skip_en <= 1'b1;
//版本号: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,UDP_TYPE,16'h0};
//源IP地址
ip_head[3] <= BOARD_IP;
//目的IP地址
if(des_ip != 32'd0)
ip_head[4] <= des_ip;
else
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};
//更新MAC地址
if(des_mac != 48'b0) begin
//目的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];
end
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];
end
else if(cnt == 5'd3) begin //按位取反
skip_en <= 1'b1;
cnt <= 5'd0;
ip_head[2][15:0] <= ~check_buffer[15:0];
end
end
st_preamble : begin //发送前导码+帧起始界定符
gmii_tx_en <= 1'b1;
gmii_txd <= preamble[cnt];
if(cnt == 5'd7) begin
skip_en <= 1'b1;
cnt <= 5'd0;
end
else
cnt <= cnt + 5'd1;
end
st_eth_head : begin //发送以太网首部
gmii_tx_en <= 1'b1;
crc_en <= 1'b1;
gmii_txd <= eth_head[cnt];
if (cnt == 5'd13) begin
skip_en <= 1'b1;
cnt <= 5'd0;
end
else
cnt <= cnt + 5'd1;
end
st_ip_head : begin //发送IP首部 + UDP首部
crc_en <= 1'b1;
gmii_tx_en <= 1'b1;
tx_byte_sel <= tx_byte_sel + 2'd1;
if(tx_byte_sel == 2'd0)
gmii_txd <= ip_head[cnt][31:24];
else if(tx_byte_sel == 2'd1)
gmii_txd <= ip_head[cnt][23:16];
else if(tx_byte_sel == 2'd2) begin
gmii_txd <= ip_head[cnt][15:8];
if(cnt == 5'd6) begin
//提前读请求数据,等待数据有效时发送
tx_req <= 1'b1;
end
end
else if(tx_byte_sel == 2'd3) begin
gmii_txd <= ip_head[cnt][7:0];
if(cnt == 5'd6) begin
skip_en <= 1'b1;
cnt <= 5'd0;
end
else
cnt <= cnt + 5'd1;
end
end
st_tx_data : begin //发送数据
crc_en <= 1'b1;
gmii_tx_en <= 1'b1;
tx_byte_sel <= tx_byte_sel + 2'd1;
if(tx_byte_sel == 1'b0)
gmii_txd <= tx_data[31:24];
else if(tx_byte_sel == 2'd1)
gmii_txd <= tx_data[23:16];
else if(tx_byte_sel == 2'd2) begin
gmii_txd <= tx_data[15:8];
if(data_cnt != tx_data_num - 16'd2)
tx_req <= 1'b1;
end
else if(tx_byte_sel == 2'd3)
gmii_txd <= tx_data[7:0];
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个字节,在后面填补充位
//补充的值为最后一次发送的有效数据
tx_req <= 1'b0;
if(data_cnt + real_add_cnt < real_tx_data_num - 16'd1)
real_add_cnt <= real_add_cnt + 5'd1;
else begin
skip_en <= 1'b1;
data_cnt <= 16'd0;
real_add_cnt <= 5'd0;
tx_byte_sel <= 2'd0;
end
if(real_add_cnt > 0) begin
gmii_txd <= 8'd0;
end
end
end
st_crc : begin //发送CRC校验值
gmii_tx_en <= 1'b1;
tx_byte_sel <= tx_byte_sel + 2'd1;
if(tx_byte_sel == 2'd0)
gmii_txd <= {~crc_next[0], ~crc_next[1], ~crc_next[2],~crc_next[3],
~crc_next[4], ~crc_next[5], ~crc_next[6],~crc_next[7]};
else if(tx_byte_sel == 2'd1)
gmii_txd <= {~crc_data[16], ~crc_data[17], ~crc_data[18],~crc_data[19],
~crc_data[20], ~crc_data[21], ~crc_data[22],~crc_data[23]};
else if(tx_byte_sel == 2'd2) begin
gmii_txd <= {~crc_data[8], ~crc_data[9], ~crc_data[10],~crc_data[11],
~crc_data[12], ~crc_data[13], ~crc_data[14],~crc_data[15]};
end
else if(tx_byte_sel == 2'd3) begin
gmii_txd <= {~crc_data[0], ~crc_data[1], ~crc_data[2],~crc_data[3],
~crc_data[4], ~crc_data[5], ~crc_data[6],~crc_data[7]};
tx_done_t <= 1'b1;
skip_en <= 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
六、网络调度控制模块 eth_ctrl
最后是最重要的以太网控制模块 eth_ctrl 模块
模块用于 以太网 ARP 与 UDP 发送通道的统一调度与切换控制。它通过 protocol_sw信号在 ARP 与 UDP 两种协议发送数据之间进行选择,将对应的 GMII 发送信号输出到物理接口;当检测到 UDP 发送开始时优先切换到 UDP 通道,并在 UDP 发送完成前保持忙状态;当接收到 ARP 请求且当前没有 UDP 正在发送时,自动使能 ARP 应答发送。整体作用是:协调 ARP 与 UDP 的发送时序,避免冲突,并保证 UDP 优先、ARP 自动响应。
module eth_ctrl(
input clk , //系统时钟
input rst_n , //系统复位信号,低电平有效
//ARP相关端口信号
input arp_rx_done, //ARP接收完成信号
input arp_rx_type, //ARP接收类型 0:请求 1:应答
output reg arp_tx_en, //ARP发送使能信号
output arp_tx_type, //ARP发送类型 0:请求 1:应答
input arp_tx_done, //ARP发送完成信号
input arp_gmii_tx_en, //ARP GMII输出数据有效信号
input [7:0] arp_gmii_txd, //ARP GMII输出数据
//UDP相关端口信号
input udp_tx_start_en,//UDP开始发送信号
input udp_tx_done, //UDP发送完成信号
input udp_gmii_tx_en, //UDP GMII输出数据有效信号
input [7:0] udp_gmii_txd, //UDP GMII输出数据
//GMII发送引脚
output gmii_tx_en, //GMII输出数据有效信号
output [7:0] gmii_txd //UDP GMII输出数据
);
//reg define
reg protocol_sw; //协议切换信号
reg udp_tx_busy; //UDP正在发送数据标志信号
reg arp_rx_flag; //接收到ARP请求信号的标志
//*****************************************************
//** main code
//*****************************************************
assign arp_tx_type = 1'b1; //ARP发送类型固定为ARP应答
assign gmii_tx_en = protocol_sw ? udp_gmii_tx_en : arp_gmii_tx_en;
assign gmii_txd = protocol_sw ? udp_gmii_txd : arp_gmii_txd;
//控制UDP发送忙信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
udp_tx_busy <= 1'b0;
else if(udp_tx_start_en)
udp_tx_busy <= 1'b1;
else if(udp_tx_done)
udp_tx_busy <= 1'b0;
end
//控制接收到ARP请求信号的标志
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
arp_rx_flag <= 1'b0;
else if(arp_rx_done && (arp_rx_type == 1'b0))
arp_rx_flag <= 1'b1;
else if(protocol_sw == 1'b0)
arp_rx_flag <= 1'b0;
end
//控制protocol_sw和arp_tx_en信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
protocol_sw <= 1'b0;
arp_tx_en <= 1'b0;
end
else begin
arp_tx_en <= 1'b0;
if(udp_tx_start_en)
protocol_sw <= 1'b1;
else if(arp_rx_flag && (udp_tx_busy == 1'b0)) begin
protocol_sw <= 1'b0;
arp_tx_en <= 1'b1;
end
end
end
endmodule
七、顶层模块
负责例化各个模块
module NET_TEST(
input sys_rst_n ,
//以太网RGMII接口
input eth_rxc ,
input eth_rx_ctl,
input [3:0] eth_rxd ,
output eth_txc ,
output eth_tx_ctl,
output [3:0] eth_txd,
output eth_rst_n
);
//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
//开发板IP地址 192.168.1.10
parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10};
//目的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 gmii_rx_clk; //GMII接收时钟
wire gmii_rx_dv ; //GMII接收数据有效信号
wire [7:0] gmii_rxd ; //GMII接收数据
wire gmii_tx_clk; //GMII发送时钟
wire gmii_tx_en ; //GMII发送数据使能信号
wire [7:0] gmii_txd ; //GMII发送数据
wire arp_gmii_tx_en; //ARP GMII输出数据有效信号
wire [7:0] arp_gmii_txd ; //ARP GMII输出数据
wire arp_rx_done ; //ARP接收完成信号
wire arp_rx_type ; //ARP接收类型 0:请求 1:应答
wire [47:0] src_mac ; //接收到目的MAC地址
wire [31:0] src_ip ; //接收到目的IP地址
wire arp_tx_en ; //ARP发送使能信号
wire arp_tx_type ; //ARP发送类型 0:请求 1:应答
wire [47:0] des_mac ; //发送的目标MAC地址
wire [31:0] des_ip ; //发送的目标IP地址
wire arp_tx_done ; //ARP发送完成信号
wire udp_gmii_tx_en; //UDP GMII输出数据有效信号
wire [7:0] udp_gmii_txd ; //UDP GMII输出数据
wire rec_pkt_done ; //UDP单包数据接收完成信号
wire rec_en ; //UDP接收的数据使能信号
wire [31:0] rec_data ; //UDP接收的数据
wire [15:0] rec_byte_num ; //UDP接收的有效字节数 单位:byte
wire [15:0] tx_byte_num ; //UDP发送的有效字节数 单位:byte
wire udp_tx_done ; //UDP发送完成信号
wire tx_req ; //UDP读数据请求信号
wire [31:0] tx_data ; //UDP待发送数据
//*****************************************************
//** main code
//*****************************************************
wire tx_start_en = rec_pkt_done;
assign tx_byte_num = rec_byte_num;
assign des_mac = src_mac;
assign des_ip = src_ip;
assign eth_rst_n = sys_rst_n;
//GMII接口转RGMII接口
rgmii_to_gmii u_gmii_to_rgmii(
.gmii_rx_clk (gmii_rx_clk ),
.gmii_rx_dv (gmii_rx_dv ),
.gmii_rxd (gmii_rxd ),
.gmii_tx_clk (gmii_tx_clk ),
.gmii_tx_en (gmii_tx_en ),
.gmii_txd (gmii_txd ),
.rgmii_rxc (eth_rxc ),
.rgmii_rx_ctl (eth_rx_ctl ),
.rgmii_rxd (eth_rxd ),
.rgmii_txc (eth_txc ),
.rgmii_tx_ctl (eth_tx_ctl ),
.rgmii_txd (eth_txd )
);
//ARP通信
arp
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP ),
.DES_MAC (DES_MAC ),
.DES_IP (DES_IP )
)
u_arp(
.rst_n (sys_rst_n ),
.gmii_rx_clk (gmii_rx_clk),
.gmii_rx_dv (gmii_rx_dv ),
.gmii_rxd (gmii_rxd ),
.gmii_tx_clk (gmii_tx_clk),
.gmii_tx_en (arp_gmii_tx_en ),
.gmii_txd (arp_gmii_txd),
.arp_rx_done (arp_rx_done),
.arp_rx_type (arp_rx_type),
.src_mac (src_mac ),
.src_ip (src_ip ),
.arp_tx_en (arp_tx_en ),
.arp_tx_type (arp_tx_type),
.des_mac (des_mac ),
.des_ip (des_ip ),
.tx_done (arp_tx_done)
);
//UDP通信
udp
#(
.BOARD_MAC (BOARD_MAC), //参数例化
.BOARD_IP (BOARD_IP ),
.DES_MAC (DES_MAC ),
.DES_IP (DES_IP )
)
u_udp(
.rst_n (sys_rst_n ),
.gmii_rx_clk (gmii_rx_clk ),
.gmii_rx_dv (gmii_rx_dv ),
.gmii_rxd (gmii_rxd ),
.gmii_tx_clk (gmii_tx_clk ),
.gmii_tx_en (udp_gmii_tx_en),
.gmii_txd (udp_gmii_txd),
.rec_pkt_done (rec_pkt_done),
.rec_en (rec_en ),
.rec_data (rec_data ),
.rec_byte_num (rec_byte_num),
.tx_start_en (tx_start_en ),
.tx_data (tx_data ),
.tx_byte_num (tx_byte_num ),
.des_mac (des_mac ),
.des_ip (des_ip ),
.tx_done (udp_tx_done ),
.tx_req (tx_req )
);
//同步FIFO
sync_fifo_2048x32b u_sync_fifo_2048x32b (
.clk (gmii_rx_clk), // input wire clk
.rst (~sys_rst_n), // input wire rst
.din (rec_data ), // input wire [31 : 0] din
.wr_en (rec_en ), // input wire wr_en
.rd_en (tx_req ), // input wire rd_en
.dout (tx_data ), // output wire [31 : 0] dout
.full (), // output wire full
.empty () // output wire empty
);
//以太网控制模块
eth_ctrl u_eth_ctrl(
.clk (gmii_rx_clk),
.rst_n (sys_rst_n),
.arp_rx_done (arp_rx_done ),
.arp_rx_type (arp_rx_type ),
.arp_tx_en (arp_tx_en ),
.arp_tx_type (arp_tx_type ),
.arp_tx_done (arp_tx_done ),
.arp_gmii_tx_en (arp_gmii_tx_en),
.arp_gmii_txd (arp_gmii_txd ),
.udp_tx_start_en(tx_start_en ),
.udp_tx_done (udp_tx_done ),
.udp_gmii_tx_en (udp_gmii_tx_en),
.udp_gmii_txd (udp_gmii_txd ),
.gmii_tx_en (gmii_tx_en ),
.gmii_txd (gmii_txd )
);
endmodule
- 在顶层模块中,我们可以修改工程的如下信息:
- BOARD_MAC: 开发板的MAC 地址
- BOARD_IP:开发板IP地址
- DES_MAC:目的MAC地址 (一般通过ARP 获得 这里不需要修改)
- DES_IP: 目的IP地址
这里需要说明一点 ,整个工程只需要修改顶层模块中的IP MAC 等信息, 对应ARP 或者UDP 内的预设 IP 和MAC 也会一同被修改,所以只需要修改顶层模块对应的内容即可。
八、增加管脚约束文件
1 )Smart Artix 主板管脚约束文件
# Smart Artix
create_clock -period 8.000 -name eth_rxc [get_ports eth_rxc]
set_property -dict {PACKAGE_PIN T20 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN V19 IOSTANDARD LVCMOS33} [get_ports eth_rst_n]
set_property -dict {PACKAGE_PIN W19 IOSTANDARD LVCMOS33} [get_ports eth_rxc]
set_property -dict {PACKAGE_PIN AA21 IOSTANDARD LVCMOS33} [get_ports eth_rx_ctl]
set_property -dict {PACKAGE_PIN AA20 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[0]}]
set_property -dict {PACKAGE_PIN W22 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[1]}]
set_property -dict {PACKAGE_PIN W21 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[2]}]
set_property -dict {PACKAGE_PIN W20 IOSTANDARD LVCMOS33} [get_ports {eth_rxd[3]}]
set_property -dict {PACKAGE_PIN AB21 IOSTANDARD LVCMOS33} [get_ports eth_txc]
set_property -dict {PACKAGE_PIN AA18 IOSTANDARD LVCMOS33} [get_ports eth_tx_ctl]
set_property -dict {PACKAGE_PIN AB20 IOSTANDARD LVCMOS33} [get_ports {eth_txd[0]}]
set_property -dict {PACKAGE_PIN AA19 IOSTANDARD LVCMOS33} [get_ports {eth_txd[1]}]
set_property -dict {PACKAGE_PIN Y19 IOSTANDARD LVCMOS33} [get_ports {eth_txd[2]}]
set_property -dict {PACKAGE_PIN AB18 IOSTANDARD LVCMOS33} [get_ports {eth_txd[3]}]
set_property SLEW FAST [get_ports {eth_txd[0]}]
set_property SLEW FAST [get_ports eth_rst_n]
set_property SLEW FAST [get_ports eth_tx_ctl]
set_property SLEW FAST [get_ports {eth_txd[3]}]
set_property SLEW FAST [get_ports {eth_txd[2]}]
set_property SLEW FAST [get_ports {eth_txd[1]}]
set_property SLEW FAST [get_ports eth_txc]
九、下载和测试
下载代码以及IP设置
1.编译和综合我们的工程
2.将网线一端接到我们的开发板(Smart ZYNQ 或者Smart Artix 主板)的RJ45端,另一端接电脑的网口。(这里采用直连方式)
3.下载我们刚刚编译综合得到的bit文件到我们的FPGA主板
4.如果前面操作无误的话,电脑端会显示以太网有网络连接(未识别的网络,连接速度1G)

5. 因为我们的主板在工程上设置的IP是默认固定的(静态IP)192.168.1.10,目的地的IP地址是192.168.1.102 , 所以在设置上,我们需要手动为电脑设置IP段(默认网关 192.168.1.1)和IP地址(192.168.1.102)。

通过网络调试助手来验证开发板的UDP收发功能
打开网络调试助手(百度可以下载,也可以通过本站连接下载:网络调试助手 v4.3.29下载),协议类型选UDP,本地主机地址 192.168.1.102,端口号写1234,之后点选”打开”。然后在右下脚的远程主机填上开发板的IP地址 和端口号(192.168.1.10:1234),之后在数据发送区发送任何数据,都可以在软件中接收到相同的数据,证明数据环回成功。

用Wireshark 软件来抓通讯过程中的数据包
1. 用Wireshark 抓UDP数据包
接下来我们通过Wireshark软件来抓通讯数据,打开wireshark软件,双击下图所示的以太网,即可开始抓取本地连接的数据包

在Wireshark中可以看到,我们已经抓取到通过以太网端口进出的数据包(此时这些数据包很多都是由系统,或者其他软件发送的,和我们本次实验无关),这个时候我们按照上文的操作重新在网口调试助手中点击“发送”按钮,可以看到Wireshark软件中抓到了我们发送的数据(可以同时看到我们发送给开发板的数据,和开发板收到后立刻回传的帧。

详细查看每一个数据包的内容,我们还可以得知源IP地址(192.168.1.10),目的IP地址(192.168.1.102),源端口号和目的地端口号。以及传输的内容(包含我们发送的WWW.HELLOFPGA.COM的内容))

2 用Wireshark 抓ARP请求和应答数据包
接下来我们尝试在Wireshark中抓取ARP数据包。因为在之前的实验中,我们已经记录了192.168.1.10对应的IP和MAC地址,所以首先可以通过arp -d命令清除ARP表。
操作步骤如下:
- 首先,通过
arp -a命令查询ARP表。在这里,你会看到192.168.1.10对应的MAC地址(例如:00-11-22-33-44-55)。这因为在之前的实验中,系统已经完成了ARP请求流程,所以下面的ARP表中已经有了这个IP和MAC的映射。 - 接着,使用
arp -d命令清空ARP表。清空后,再次输入arp -a命令查看,你会发现192.168.1.10的条目已经消失了。其他IP地址的信息会是刚刚通过ARP请求获得的新条目。

之后再通过网络调试助手或者用ping之类的命令去访问192.168.1.10地址,因为之前系统的arp表已经被我们清除没有对应的信息了,所以电脑会主动发送arp请求的命令,在Wireshark 中也会抓到ARP对应的数据包 (who has 192.168.1.10)以及开发板回复的应答包 (192.168.1.10 is at …..)(ARP数据包一般在win 的arp 表中还没有记录对应IP信息的时候才会发出,也可通过在cmd中输入 arp -d 指令清空arp表,再进行抓包)

关于本次实验的几个说明:
- 因为PHY部分电路已经在硬件上开启了PHY芯片内部的Rx DLY和Tx DLY功能,所以本次实验中对于rgmii rx部分不需要额外在FPGA内部加入IDELAY部分功能, 也不需要对TXC的输出增加90度相位差(这部分和正点原子的代码有区别,正点是保持了IDELAY功能,但是延时为0)
- Smart Artix 的 sys_rst_n 引到 对应的rst引脚上了, Smart ZYNQ 因为只有por 复位脚,所以Smart ZYNQ 暂时用 KEY1来替代 rst引脚的功能
- 工程本身没有增加icmp部分功能,所以虽然可以在cmd中通过ping命令来激活arp请求 和应答的功能,但是ping命令无法得到开发板的应答,造成ping请求超时的情况(大家可以自行尝试移植icmp功能)
- 本实验仅针对1G连接速度,其他速度大家根据需求自行修改
本次实验的说明:
- PHY部分电路设置:
- 本次实验中,PHY芯片内部的Rx DLY和Tx DLY功能已经在硬件上开启。因此,对于RGMII接收部分(rx),不需要在FPGA内部额外添加IDELAY功能。
- 另外,TXC输出也不需要增加90度相位差。
- 复位信号设置:
- Smart Artix的
sys_rst_n引脚连接到了开发板对应的复位按键引脚。 - Smart ZYNQ由于只有POR复位脚,因此
sys_rst_n可暂时连接到板子的普通按键脚上进行调试适用。
- Smart Artix的
- ICMP部分功能:
- 工程中没有实现ICMP协议功能,尽管可以通过
ping命令触发ARP请求和应答,但开发板无法响应ping请求,导致ping命令超时。如果需要ICMP功能,大家可以尝试自行移植。
- 工程中没有实现ICMP协议功能,尽管可以通过
- 连接速度设置:
- 本实验仅针对1G连接速度进行测试,其他连接速度可以根据需求进行相应修改。
- 本文的完整工程下载: 17_NET_TEST(Smart Artix 50T)
- VIVADO的版本:2018.3
- 工程创建目录:E:\Smart_Artix\4_Code\17_NET_TEST
- 测试工具网络调试助手:网络调试助手 v4.3.29下载
- 测试工具Wireshark官方下载地址:https://www.wireshark.org/download.html
