目录
前言
一、RGMII接口转换模块
1.1 顶层文件:rgmii_to_gmii.v
1.2 发送模块:rgmii_tx.v
1.3 接收模块:rgmii_rx.v
二、ARP 协议模块实现
2.1 顶层模块:arp.v
2.2 发送模块:arp_tx.v
2.3 接收模块:arp_rx.v
2.4 CRC32校验模块:crc32_data.v
三、UDP 协议模块实现
3.1 UDP协议发送模块:udp_tx.v
3.2 UDP协议接收模块:udp_rx.v
四、辅助模块实现
4.1 ARP协议控制模块:arp_ctrl.v
4.2 控制使能选择模块:en_ctrl.v
4.3 按键消抖模块:key_fliter.v
五、约束文件
5.1 引脚分配设置
5.2 时钟约束设置
5.3 应用说明
六、综合设计
6.1 波形图
6.2 电路图
七、网络测试
7.1 测试工具
7.2 具体使用
八、注意事项
九、本文总结
十、更多操作
前言
在《FPGA 39:FPGA网络通信协议栈进阶》中,我们探讨了RGMII接口、ARP与UDP协议的核心原理及模块划分。本文将基于实际代码实现,深入解析如何通过Verilog构建完整的协议栈模块,涵盖RGMII接口转换、ARP地址解析、UDP数据封装与校验,并总结设计中的关键注意事项。
一、RGMII接口转换模块
RGMII接口转换模块实现
1.1 顶层文件:rgmii_to_gmii.v
功能 :实现RGMII与GMII协议的双向转换。通过模块的实例化与信号连接,完成 RGMII 双沿数据和 GMII 单沿数据之间的相互转换,便于后续协议处理。
代码片段 :
`timescale 1ns / 1ps
// 定义模块rgmii_to_gmii,该模块用于实现RGMII接口与GMII接口之间的转换
module rgmii_to_gmii(
// RGMII接口部分
input wire rgmii_rxc , // 由PHY芯片提供的接收时钟信号
input wire rgmii_rx_ctrl , // 由使能信号en和错误信号error的异或值组成
input wire [3:0] rgmii_rxd , // 由PHY芯片提供的双沿数据信号
output wire rgmii_txc , // 发送给PHY芯片的发送时钟信号
output wire rgmii_tx_ctrl , // 由使能信号en和错误信号error的异或值组成
output wire [3:0] rgmii_data , // 双沿数据信号,用于发送给PHY芯片
// GMII接口部分
output wire gmii_txc , // FPGA产生的时钟信号,频率为125MHz,也可以使用PHY提供的时钟,但需要进行约束处理
input wire gmii_tx_en , // 发送使能信号,高电平有效
input wire [7:0] gmii_tx_data , // 单沿数据信号,用于FPGA发送数据
output wire gmii_rxc , // 经过缓冲处理后的时钟信号
output wire gmii_rx_en , // 接收使能信号
output wire [7:0] gmii_rxd // 经过单双沿转换原语处理后的单沿数据信号
);
// 将GMII的发送时钟信号赋值为接收时钟信号
assign gmii_txc = gmii_rxc;
// 实例化rgmii_rx模块,用于处理RGMII接收数据
rgmii_rx rgmii_rx_u(
. rgmii_rxc (rgmii_rxc ) , // 连接由PHY提供的接收时钟信号
. rgmii_rx_ctrl(rgmii_rx_ctrl) , // 连接由en和error异或得到的控制信号
. rgmii_rxd (rgmii_rxd ) , // 连接由PHY芯片提供的双沿数据信号
. gmii_rxc (gmii_rxc ) , // 连接经过buf处理后的时钟信号
. gmii_rx_en (gmii_rx_en ) , // 连接接收使能信号
. gmii_rxd (gmii_rxd ) // 连接经过单双沿源语处理后的单沿数据信号
);
// 实例化rgmii_tx模块,用于处理RGMII发送数据
rgmii_tx rgmii_tx_u(
. gmii_txc (gmii_txc ), // 连接FPGA的时钟信号,频率为125MHz
. gmii_tx_en (gmii_tx_en ), // 连接发送使能信号,高电平有效
. gmii_tx_data (gmii_tx_data ), // 连接单沿数据信号,用于FPGA发送数据
. rgmii_txc (rgmii_txc ), // 连接发送给PHY芯片的时钟信号
. rgmii_tx_ctrl(rgmii_tx_ctrl), // 连接由en和error异或得到的控制信号
. rgmii_data (rgmii_data ) // 连接双沿数据信号,用于发送给PHY芯片
);
endmodule
主要逻辑:
- 接收方向:从RGMII接口接收由PHY芯片提供的双沿数据信号,并将其转换为FPGA可以处理的单沿GMII信号。
- 发送方向:将FPGA产生的单沿GMII信号转换为PHY芯片能够接受的双沿RGMII信号。
此模块设计旨在解决不同以太网接口标准(RGMII与GMII)间的数据传输适配问题,确保兼容不同速度和类型的网络设备。通过这种信号连接和转换,可以高效地在两种协议间进行通信协议转换,从而提升硬件设备间的互操作性。
1.2 发送模块:rgmii_tx.v
功能:此模块用于将 GMII 接口的单沿数据转换为 RGMII 接口所需的双沿数据格式,便于发送给以太网 PHY 芯片。通过将 8 位单沿数据拆分为 4 位双沿数据,并将控制信号同步转换,实现 GMII 到 RGMII 的协议适配。
代码片段 :
`timescale 1ns / 1ps
// GMII to RGMII---单沿转双沿数据,此模块用于将GMII接口的单沿数据转换为RGMII接口的双沿数据
module rgmii_tx(
input wire gmii_txc ,// FPGA的时钟,频率为125MHz,也可使用PHY提供的时钟,但需进行约束处理
input wire gmii_tx_en ,// 发送使能信号,高电平有效,控制数据发送
input wire [7:0] gmii_tx_data ,// 单沿数据,宽度为8位,是待转换的原始数据
output wire rgmii_txc ,// 发送给PHY芯片的时钟,为数据传输提供同步
output wire rgmii_tx_ctrl,// 由使能信号en和错误信号error的异或值组成,用于控制数据传输状态
output wire [3:0] rgmii_data // 双沿数据,宽度为4位,是转换后的输出数据
);
assign rgmii_txc = gmii_txc; // 将GMII的时钟信号直接赋值给RGMII的时钟信号,保证时钟同步
// --------ctrl的单双沿转换
ODDR #(
.DDR_CLK_EDGE("SAME_EDGE"), // 选择时钟边沿类型为相同边沿,确定数据采样时机
.INIT(1'b0), // 输出Q的初始值设置为0,初始化输出状态
.SRTYPE("SYNC") // 设置复位类型为同步复位,保证复位操作与时钟同步
) ODDR_tx_ctrl (
.Q(rgmii_tx_ctrl), // 1位双沿输出信号,转换后的RGMII控制信号
.C(gmii_txc), // 1位时钟输入信号,连接FPGA时钟作为同步信号
.CE(1'b1), // 1位时钟使能信号,始终使能,确保数据转换持续进行
.D1(gmii_tx_en), // 时钟正边沿的数据输入,连接单沿使能信号
.D2(gmii_tx_en), // 时钟负边沿的数据输入,连接单沿使能信号
.R(1'b0), // 1位复位信号,不复位,保持正常工作状态
.S(1'b0) // 1位置位信号,不置位,保持正常工作状态
);
// --------数据的单双沿转换
genvar i; // 定义循环变量,只能在generate循环语句中使用,用于循环实例化ODDR模块
generate
for (i = 0; i < 4; i = i + 1) begin:gmii_txd
ODDR #(
.DDR_CLK_EDGE("SAME_EDGE"), // 选择时钟边沿类型为相同边沿,确定数据采样时机
.INIT(1'b0), // 输出Q的初始值设置为0,初始化输出状态
.SRTYPE("SYNC") // 设置复位类型为同步复位,保证复位操作与时钟同步
) ODDR_tx_data (
.Q(rgmii_data[i]), // 1位双沿输出信号,转换后的RGMII数据位
.C(gmii_txc), // 1位时钟输入信号,连接FPGA时钟作为同步信号
.CE(1'b1), // 1位时钟使能信号,始终使能,确保数据转换持续进行
.D1(gmii_tx_data[i]), // 时钟正边沿的数据输入,连接GMII单沿数据的低4位
.D2(gmii_tx_data[4 + i]), // 时钟负边沿的数据输入,连接GMII单沿数据的高4位
.R(1'b0), // 1位复位信号,不复位,保持正常工作状态
.S(1'b0) // 1位置位信号,不置位,保持正常工作状态
);
end
endgenerate
endmodule
主要逻辑:
- 时钟连接:将 FPGA 提供的 125MHz GMII 发送时钟
gmii_txc
直接赋值给 RGMII 接口的时钟信号rgmii_txc
。- 控制信号转换:使用 ODDR 原语 将单沿的使能信号
gmii_tx_en
转换为双沿输出信号rgmii_tx_ctrl
。- 数据转换:通过 4 个 ODDR 原语并行处理,将 8 位单沿数据
gmii_tx_data[7:0]
拆分为 4 位双沿数据(每个时钟边沿传输 4 位),组合后形成 RGMII 所需的 4 位双沿数据rgmii_data[3:0]
。
该模块实现了从 GMII 单沿数据到 RGMII 双沿数据的转换,解决了两种接口在数据采样方式上的差异问题,从而确保 FPGA 可以正确地与外部 PHY 芯片进行高速数据通信。这种结构提高了硬件平台在不同网络设备间的兼容性与灵活性。
1.3 接收模块:rgmii_rx.v
功能:此模块用于将RGMII接口的双沿数据转换为GMII接口的单沿数据,以便于FPGA进行后续的数据处理。通过对接收到的双沿信号进行转换,生成适用于GMII接口的数据和控制信号。
代码片段 :
`timescale 1ns / 1ps
// gmii to rgmii----双沿转单沿数据,此模块用于将 RGMII 接口的双沿数据转换为 GMII 接口的单沿数据
module rgmii_rx(
input wire rgmii_rxc ,// 由 PHY 芯片提供的时钟信号,作为数据接收的时序基准
input wire rgmii_rx_ctrl ,// 由使能信号 en 和错误信号 error 的异或值组成,用于控制数据接收
input wire [3:0] rgmii_rxd ,// 由 PHY 芯片提供的 4 位双沿数据,在时钟的上升沿和下降沿都有数据传输
output wire gmii_rxc ,// 经过缓冲处理后的时钟信号,为后续 GMII 接口的数据处理提供稳定时钟
output wire gmii_rx_en ,// 使能信号,用于指示 GMII 接口数据的有效性
output wire [7:0] gmii_rxd // 经过单双沿原语处理后的 8 位单沿数据,供后续模块使用
);
wire [1:0] gmii_rx_ctrl; // 用于寄存 rgmii_rx_ctrl 的双沿数据,方便后续处理
// ---------- 时钟处理
wire rgmii_rxc_bufio; // 经过 I/O 时钟资源的输入时钟,对输入时钟进行初步缓冲和驱动
wire rgmii_rxc_bufg; // 经过全局时钟网络资源的输入时钟,将时钟信号分配到整个 FPGA 的全局时钟网络
assign gmii_rxc = rgmii_rxc_bufg; // 将经过全局时钟网络处理后的时钟信号赋值给 gmii_rxc
// BUFIO 原语实例化,用于对输入时钟进行 I/O 缓冲
BUFIO BUFIO_inst (
.O(rgmii_rxc_bufio), // 1 位输出:时钟输出,连接到 I/O 时钟负载
.I(rgmii_rxc) // 1 位输入:时钟输入,连接到 IBUF 或 BUFMR
);
// BUFG 原语实例化,用于将时钟信号分配到全局时钟网络
BUFG BUFG_inst (
.O(rgmii_rxc_bufg), // 1 位输出:时钟输出
.I(rgmii_rxc) // 1 位输入:时钟输入
);
// -------- ctrl 的双单沿转换
// 通过对寄存的 rgmii_rx_ctrl 双沿数据进行与运算,得到 GMII 接口的使能信号
assign gmii_rx_en = gmii_rx_ctrl [0] & gmii_rx_ctrl[1];
// IDDR 原语实例化,用于将 rgmii_rx_ctrl 的双沿数据转换为单沿数据
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), // 时钟边沿选择,使用流水线模式的同一边沿
// 可选值为 "OPPOSITE_EDGE", "SAME_EDGE" 或 "SAME_EDGE_PIPELINED"
.INIT_Q1(1'b0), // Q1 的初始值,设置为 0
.INIT_Q2(1'b0), // Q2 的初始值,设置为 0
.SRTYPE("SYNC") // 置位/复位类型,选择同步复位
) IDDR_inst_rx_ctrl (
.Q1(gmii_rx_ctrl[0]), // 1 位输出,对应时钟正边沿的数据
.Q2(gmii_rx_ctrl[1]), // 1 位输出,对应时钟负边沿的数据
.C(rgmii_rxc_bufio), // 1 位时钟输入
.CE(1'b1), // 1 位时钟使能输入,始终使能
.D(rgmii_rx_ctrl), // 1 位 DDR 数据输入
.R(1'b0), // 1 位复位信号,不复位
.S(1'b0) // 1 位置位信号,不置位
);
// ---------- 数据的双单沿转换
genvar i;
// 使用 generate 语句和 for 循环,对 rgmii_rxd 的 4 位数据进行双单沿转换
generate
for (i = 0; i < 4; i = i + 1) begin:iddr_rxd
// 每个数据位都实例化一个 IDDR 原语进行转换
IDDR #(
.DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), // 时钟边沿选择,使用流水线模式的同一边沿
// 可选值为 "OPPOSITE_EDGE", "SAME_EDGE" 或 "SAME_EDGE_PIPELINED"
.INIT_Q1(1'b0), // Q1 的初始值,设置为 0
.INIT_Q2(1'b0), // Q2 的初始值,设置为 0
.SRTYPE("SYNC") // 置位/复位类型,选择同步复位
) IDDR_inst_rxd (
.Q1(gmii_rxd[i]), // 1 位输出,对应时钟正边沿的数据
.Q2(gmii_rxd[4 + i]), // 1 位输出,对应时钟负边沿的数据
.C(rgmii_rxc_bufio), // 1 位时钟输入
.CE(1'b1), // 1 位时钟使能输入,始终使能
.D(rgmii_rxd[i]), // 1 位 DDR 数据输入
.R(1'b0), // 1 位复位信号,不复位
.S(1'b0) // 1 位置位信号,不置位
);
end
endgenerate
endmodule
主要逻辑:
时钟处理:对输入的RGMII时钟信号进行缓冲处理,确保生成的GMII时钟信号能够稳定地支持后续的数据处理流程。
控制信号转换:接收由PHY芯片提供的控制信号(
rgmii_rx_ctrl
),该信号是使能信号和错误信号的异或值,并将其从双沿格式转换为单沿格式,以产生GMII接口使用的使能信号(gmii_rx_en
)。数据转换:将来自PHY芯片的4位双沿数据信号(
rgmii_rxd
)转换为8位单沿数据信号(gmii_rxd
),使得这些数据能够在FPGA内部被正确识别和处理。
此模块设计主要是为了实现从RGMII到GMII的数据适配,特别是针对双沿采样与单沿采样的差异进行调整,从而提高不同速度和类型的网络设备之间的互操作性。通过这样的转换,可以有效地在两个不同的以太网接口标准之间传输数据,增强硬件设备间的兼容性和通信效率。
二、ARP 协议模块实现
2.1 顶层模块:arp.v
功能:此模块实现了ARP(Address Resolution Protocol)协议的发送、接收以及CRC32校验功能。它能够处理ARP请求与响应,确保数据包正确无误地在网络中传输,并能对接收到的数据进行解析和响应。
代码片段 :
`timescale 1ns / 1ps
// ARP 模块,用于实现 ARP 协议的发送、接收以及 CRC32 校验功能
module arp(
input wire gmii_txc , // 125MHz 时钟信号,经过 BUFG 处理后提供给模块使用
input wire rst_n , // 复位信号,低电平有效,用于对模块进行复位操作
output wire [7:0] arp_gmii_txd , // 发送到 GMII 接口的 8 位数据,用于传输 ARP 相关数据
output wire arp_gmii_tx_en , // 发送使能信号,高电平有效,控制 ARP 数据向 GMII 接口的发送
output wire [47:0] pc_mac , // 本地 PC 的 48 位 MAC 地址,用于 ARP 协议交互
output wire [31:0] pc_ip , // 本地 PC 的 32 位 IP 地址,用于 ARP 协议交互
input wire gmii_rx_en , // GMII 接口的接收使能信号,高电平有效,指示开始接收数据
input wire [7 :0] gmii_rxd // GMII 接口接收到的 8 位数据,用于接收外部的 ARP 相关数据
);
wire gmii_rxc; // 定义 GMII 接收时钟信号
wire arp_tx_done ; // ARP 发送完成标志信号,高电平表示 ARP 数据发送完成
wire crc_en ; // CRC32 校验使能信号,高电平有效,启动 CRC32 校验过程
wire crc_clr ; // CRC32 校验清除信号,高电平有效,对 CRC32 校验器进行复位操作
wire [31:0] crc_data ; // 用于 CRC32 校验的数据,长度为 32 位
wire [31:0] crc_next ; // CRC32 校验的下一个计算结果,长度为 32 位
// 通常接收时钟和发送时钟可以使用同一个时钟源,这里简单将 gmii_txc 赋值给 gmii_rxc
assign gmii_rxc = gmii_txc;
// 实例化 ARP 发送模块
arp_tx arp_tx_u(
. gmii_txc (gmii_txc ) , // 连接 125MHz 时钟信号,为 ARP 发送模块提供时钟
. rst_n (rst_n ) , // 连接复位信号,用于对 ARP 发送模块进行复位
. arp_tx_start (arp_tx_start) , // ARP 发送开始信号,由外部控制 ARP 发送操作的起始
. arp_tx_type (arp_tx_type ) , // ARP 发送类型,0 表示请求,1 表示响应
. pc_mac (pc_mac ) , // 连接本地 PC 的 MAC 地址,用于 ARP 发送数据
. pc_ip (pc_ip ) , // 连接本地 PC 的 IP 地址,用于 ARP 发送数据
. arp_gmii_txd (arp_gmii_txd ) , // 输出 8 位 ARP 数据到 GMII 接口
. arp_gmii_tx_en(arp_gmii_tx_en) , // 输出发送使能信号到 GMII 接口,控制数据发送
. arp_tx_done (arp_tx_done ) , // 接收 ARP 发送完成标志信号
. crc_en (crc_en ) , // 输出 CRC32 校验使能信号到 CRC 模块
. crc_clr (crc_clr ) , // 输出 CRC32 校验清除信号到 CRC 模块
. crc_data (crc_data ) , // 连接用于 CRC32 校验的数据
. crc_next (crc_next[31:24] ) // 连接 CRC32 校验的下一个计算结果(取高 8 位)
);
// 实例化 ARP 接收模块
arp_rx arp_rx_u(
. gmii_rxc (gmii_rxc ) , // 连接 GMII 接收时钟信号,为 ARP 接收模块提供时钟
. rst_n (rst_n ) , // 连接复位信号,用于对 ARP 接收模块进行复位
. gmii_rx_en (gmii_rx_en ) , // 连接 GMII 接口的接收使能信号,控制 ARP 数据的接收
. gmii_rxd (gmii_rxd ) , // 连接 GMII 接口接收到的 8 位数据,用于接收外部 ARP 数据
. pc_mac (pc_mac ) , // 连接本地 PC 的 MAC 地址,用于 ARP 接收数据的比对
. pc_ip (pc_ip ) , // 连接本地 PC 的 IP 地址,用于 ARP 接收数据的比对
. arp_rx_type(arp_rx_type) , // 输出接收到的 ARP 数据类型,供后续处理使用
. arp_rx_done(arp_rx_done) // 输出 ARP 接收完成标志信号
);
// 实例化 CRC32 数据处理模块
crc32_data crc32_data_u(
. clk (gmii_txc ), // 连接 125MHz 时钟信号,为 CRC32 模块提供时钟
. rst_n (rst_n ), // 连接复位信号,用于对 CRC32 模块进行复位
. data (arp_gmii_txd ), // 连接要进行 CRC32 校验的 8 位 ARP 发送数据
. crc_en (crc_en ), // 连接 CRC32 校验使能信号,控制 CRC32 校验的启动
. crc_clr (crc_clr ), // 连接 CRC32 校验清除信号,控制 CRC32 校验器的复位
. crc_data(crc_data ), // 输出用于 CRC32 校验的数据
. crc_next(crc_next ) // 输出 CRC32 校验的下一个计算结果
);
endmodule
主要逻辑:
- 时钟信号:使用
gmii_txc
作为125MHz的时钟信号,同时该信号也被用作接收时钟gmii_rxc
。- 复位信号:低电平有效的复位信号
rst_n
用于初始化模块状态。- 发送功能:
- 实例化
arp_tx
模块,负责构造并发送ARP请求或响应。通过本地PC的MAC地址pc_mac
和IP地址pc_ip
构建ARP数据包,并通过GMII接口发送出去。- 在发送过程中,利用CRC32校验来确保数据的完整性。
- 接收功能:实例化
arp_rx
模块,用于接收来自网络的ARP数据包。根据本地PC的MAC和IP地址验证接收到的数据包,并判断其类型(请求或响应)。- CRC32校验:实例化
crc32_data
模块,为发送的数据提供CRC32校验,保证数据传输的准确性。通过控制信号crc_en
启动校验过程,crc_clr
用于复位校验器。
该模块的设计旨在解决在以太网通信中,如何准确获取目标设备的MAC地址的问题。通过实现ARP协议的发送和接收机制,使得网络中的设备可以动态发现彼此的硬件地址,从而支持更高层次的网络通信。此外,CRC32校验的加入进一步增强了数据传输的可靠性,减少了因数据错误导致的通信故障。此模块是构建高效、可靠的网络通信系统的关键组件之一,特别适用于需要自动管理网络层地址映射的应用场景。
2.2 发送模块:arp_tx.v
功能:实现ARP协议数据包的封装与发送。根据输入控制信号(请求或应答类型),构造符合以太网标准的ARP帧,并通过GMII接口发送出去。同时支持CRC32校验数据的生成与输出。
代码片段 :
`timescale 1ns / 1ps
//ARP协议----发送端
module arp_tx (
input wire gmii_txc , //125Mhz的时钟,经过bufg的处理,为模块提供时钟基准
input wire rst_n , //复位信号,低电平有效,用于对模块进行复位
input wire arp_tx_start , //arp的开始信号,高电平有效时启动ARP发送操作
input wire arp_tx_type , //0:发送请求包 1:发送应答包,指示ARP包的类型
input wire [47:0] pc_mac , //PC的MAC地址,用于发送应答包时设置目的MAC地址
input wire [31:0] pc_ip , //PC的IP地址,目前未使用到该信号,可能用于后续扩展
output reg [7:0] arp_gmii_txd , //单沿数据,给rgmii_to_gmii模块的数据,发送的ARP数据
output reg arp_gmii_tx_en , //给rgmii_to_gmii模块的使能,高电平有效时允许发送数据
output reg arp_tx_done , //arp发送结束信号,高电平表示ARP数据发送完成
//CRC32校验端口
output reg crc_en , //crc使能,开始校验标志,高电平有效时启动CRC32校验
output reg crc_clr , //crc数据复位信号,高电平有效时对CRC32校验器进行复位
input wire [31:0] crc_data , //CRC校验数据,用于CRC32校验计算
input wire [31:0] crc_next //CRC下次校验完成数据,用于获取CRC32校验结果
);
// FPGA的MAC地址,固定值
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
// FPGA的IP地址,固定值
parameter BOARD_IP = {8'd192,8'd168,8'd0,8'd5};
// PC的IP地址,固定值
parameter DES_IP = {8'd192,8'd168,8'd0,8'd3};
// 定义状态机的状态
localparam IDLE = 5'b0_0001; //空闲状态,等待发送开始信号
localparam PRE_DATA = 5'b0_0010; //7个字节的前导码+1个 字节的帧起始界定符状态
localparam ETH_HEAD = 5'b0_0100; //14个字节的以太网帧头 包括目的mac(6B),源mac(6B),长度类型(2B)状态
localparam ARP_DATA = 5'b0_1000; //arp数据 28个有效字节 + 18个无效填充数据状态
localparam CRC = 5'b1_0000; //4个字节的crc32校验状态
reg [10:0] cnt_byte; //状态跳转条件,在每个状态下计数,用于控制数据发送的进度
reg [4:0] c_state,n_state; // 当前状态和下一个状态
// 以太网的帧头数据,14个字节,每个字节8位
reg [7:0] head_data [13:0] ;
// ARP的数据包,28个有效字节,每个字节8位
reg [7:0] arp_data_reg [27:0] ;
// 状态机第一段,用于更新当前状态
always @(posedge gmii_txc ) begin
if(!rst_n)
c_state <= IDLE;
else
c_state <= n_state;
end
// 状态机第二段,根据当前状态和条件确定下一个状态
always @(*) begin
if(!rst_n)
n_state = IDLE;
else
case (c_state)
IDLE :begin
if(arp_tx_start)
n_state = PRE_DATA;
else
n_state = IDLE;
end
PRE_DATA:begin
if(cnt_byte == 7)
n_state = ETH_HEAD;
else
n_state = PRE_DATA;
end
ETH_HEAD:begin
if(cnt_byte == 13)
n_state = ARP_DATA;
else
n_state = ETH_HEAD;
end
ARP_DATA:begin
if(cnt_byte == 45)
n_state = CRC;
else
n_state = ARP_DATA;
end
CRC :begin
if(cnt_byte == 3)
n_state = IDLE;
else
n_state = CRC;
end
default: n_state = IDLE;
endcase
end
// 状态跳转计数,根据当前状态更新计数值
always @(posedge gmii_txc ) begin
if(!rst_n)
cnt_byte <= 0;
else
case (c_state)
IDLE :cnt_byte <= 0;
PRE_DATA:begin
if(cnt_byte == 7)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
ETH_HEAD:begin
if(cnt_byte == 13)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
ARP_DATA:begin
if(cnt_byte == 45)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
CRC :begin
if(cnt_byte == 3)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
default: cnt_byte <= 0;
endcase
end
// 初始化和更新以太网帧头和ARP数据包数据
always @(posedge gmii_txc ) begin
if (!rst_n) begin //将其初始化为请求包
arp_gmii_txd <= 0;
//----以太网帧头
head_data [0] <= 8'hff;//目的MAC地址采用广播地址
head_data [1] <= 8'hff;//目的MAC地址采用广播地址
head_data [2] <= 8'hff;//目的MAC地址采用广播地址
head_data [3] <= 8'hff;//目的MAC地址采用广播地址
head_data [4] <= 8'hff;//目的MAC地址采用广播地址
head_data [5] <= 8'hff;//目的MAC地址采用广播地址
head_data [6] <= BOARD_MAC[47:40];//源MAC地址,fpga的MAC地址
head_data [7] <= BOARD_MAC[39:32];//源MAC地址,fpga的MAC地址
head_data [8] <= BOARD_MAC[31:24];//源MAC地址,fpga的MAC地址
head_data [9] <= BOARD_MAC[23:16];//源MAC地址,fpga的MAC地址
head_data [10] <= BOARD_MAC[15: 8];//源MAC地址,fpga的MAC地址
head_data [11] <= BOARD_MAC[7 : 0];//源MAC地址,fpga的MAC地址
head_data [12] <= 8'h08;//长度类型---0806---ARP协议
head_data [13] <= 8'h06;//长度类型---0806---ARP协议
//----arp数据段
arp_data_reg [0] <= 8'h00;//硬件类型
arp_data_reg [1] <= 8'h01;//硬件类型
arp_data_reg [2] <= 8'h08;//协议类型
arp_data_reg [3] <= 8'h00;//协议类型
arp_data_reg [4] <= 8'h06;//硬件地址长度
arp_data_reg [5] <= 8'h04;//协议地址长度
arp_data_reg [6] <= 8'h00;//OP操作码 1---表示请求包
arp_data_reg [7] <= 8'h01;//OP操作码 2---表示应答包
arp_data_reg[8] <= BOARD_MAC[47:40]; //源MAC地址
arp_data_reg[9] <= BOARD_MAC[39:32]; //源MAC地址
arp_data_reg[10] <= BOARD_MAC[31:24]; //源MAC地址
arp_data_reg[11] <= BOARD_MAC[23:16]; //源MAC地址
arp_data_reg[12] <= BOARD_MAC[15: 8]; //源MAC地址
arp_data_reg[13] <= BOARD_MAC[7 : 0]; //源MAC地址
arp_data_reg[14] <= BOARD_IP[31:24]; //源IP地址
arp_data_reg[15] <= BOARD_IP[23:16]; //源IP地址
arp_data_reg[16] <= BOARD_IP[15: 8]; //源IP地址
arp_data_reg[17] <= BOARD_IP[7 : 0]; //源IP地址
arp_data_reg[18] <= 8'hff; //目的MAC地址---广播地址
arp_data_reg[19] <= 8'hff; //目的MAC地址---广播地址
arp_data_reg[20] <= 8'hff; //目的MAC地址---广播地址
arp_data_reg[21] <= 8'hff; //目的MAC地址---广播地址
arp_data_reg[22] <= 8'hff; //目的MAC地址---广播地址
arp_data_reg[23] <= 8'hff; //目的MAC地址---广播地址
arp_data_reg[24] <= DES_IP[31:24]; //目的IP地址
arp_data_reg[25] <= DES_IP[23:16]; //目的IP地址
arp_data_reg[26] <= DES_IP[15: 8]; //目的IP地址
arp_data_reg[27] <= DES_IP[7 : 0]; //目的IP地址
end
else
case (c_state)
IDLE :begin
arp_gmii_txd <= 0;
if(arp_tx_type)begin //发送应答包
head_data [0] <= pc_mac[47:40]; //目的MAC地址
head_data [1] <= pc_mac[39:32]; //目的MAC地址
head_data [2] <= pc_mac[31:24]; //目的MAC地址
head_data [3] <= pc_mac[23:16]; //目的MAC地址
head_data [4] <= pc_mac[15: 8]; //目的MAC地址
head_data [5] <= pc_mac[7 : 0]; //目的MAC地址
arp_data_reg [7] <= 8'h02;
arp_data_reg[18] <= pc_mac[47:40]; //目的MAC地址
arp_data_reg[19] <= pc_mac[39:32]; //目的MAC地址
arp_data_reg[20] <= pc_mac[31:24]; //目的MAC地址
arp_data_reg[21] <= pc_mac[23:16]; //目的MAC地址
arp_data_reg[22] <= pc_mac[15: 8]; //目的MAC地址
arp_data_reg[23] <= pc_mac[7 : 0]; //目的MAC地址
end
else
arp_data_reg [7] <= 8'h01;//请求包
end
PRE_DATA:begin
if(cnt_byte <= 6)
arp_gmii_txd <= 8'h55;//七个字节的前导码--8'h55
else
arp_gmii_txd <= 8'hd5;//一个字节的帧起始界定符--8'hd5
end
ETH_HEAD:begin
arp_gmii_txd <= head_data[cnt_byte];
end
ARP_DATA:begin
if(cnt_byte < 28) //arp有效数据
arp_gmii_txd <= arp_data_reg[cnt_byte];
else
arp_gmii_txd <= 'd0; //填充数据
end
CRC :begin
if(cnt_byte == 0)
arp_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_byte == 1)
arp_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_byte == 2)
arp_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]};
else if(cnt_byte == 3)
arp_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]};
end
default:begin
arp_gmii_txd <= 0;
end
endcase
end
// 发送使能信号,根据状态机状态更新
always @(posedge gmii_txc ) begin
if(!rst_n)
arp_gmii_tx_en <= 0;
else if(c_state == IDLE)
arp_gmii_tx_en <= 0;
else
arp_gmii_tx_en <= 1;
end
// ARP发送完成信号和CRC复位信号,根据状态机状态更新
always @(posedge gmii_txc ) begin
if(!rst_n)begin
arp_tx_done <= 0;
crc_clr <= 0;
end
else if(c_state == CRC && cnt_byte == 3)begin
arp_tx_done <= 1;
crc_clr <= 1;
end
else begin
arp_tx_done <= 0;
crc_clr <= 0;
end
end
// CRC使能信号,根据状态机状态更新
always @(posedge gmii_txc ) begin
if(!rst_n)begin
crc_en <= 0;
end
else if(c_state == ETH_HEAD || c_state == ARP_DATA)begin
crc_en <= 1;
end
else begin
crc_en <= 0;
end
end
endmodule
在上述 Verilog 硬件描述语言编写的 arp_tx 模块中,8'h08、8'h06 等这样的数值表示,通常是用来表示 8 位的十六进制数,是构建 ARP 数据包的关键数据,它们在 ARP 协议各字段中承担着重要作用:
- 8'h00:十六进制的 0,转换为二进制是00000000,在数值上表示最小的 8 位无符号整数。
- 8'h01:十六进制的 1,二进制为00000001,表示比 0 大 1 的数。
- 8'h02:十六进制的 2,二进制是00000010。
- 8'h04:十六进制的 4,二进制为00000100,是 2 的平方。
- 8'h06:十六进制的 6,二进制是00000110,数值上比 4 大 2。
- 8'h08:十六进制的 8,二进制为00001000,是 2 的三次方,也是 10 进制的 8。
这些 8 位十六进制数值,从定义数据包类型、标识地址长度,到区分请求与应答操作,每一个数值都精准地承载着 ARP 协议规范赋予的特定功能。正是这些数值的有序组合与准确赋值,确保了 ARP 数据包能够在以太网环境中被正确解析与处理,实现 IP 地址与 MAC 地址之间的高效映射与通信。
主要逻辑:
状态机控制发送流程:
- 使用五段式状态机完成数据发送控制,各状态如下:
IDLE
:空闲状态,等待发送启动信号;PRE_DATA
:发送前导码和帧起始界定符;ETH_HEAD
:发送以太网帧头(包含目的MAC、源MAC、协议类型);ARP_DATA
:发送ARP协议数据字段(28字节有效数据 + 填充);CRC
:发送CRC32校验结果(4字节)。- 字节计数器
cnt_byte
控制每个状态发送的字节数。数据封装:
- 定义本地MAC地址和IP地址作为默认值;
- 根据
arp_tx_type
判断是发送请求包还是响应包,动态修改目的MAC和ARP操作码;- 构造完整的ARP数据包并依次发送。
CRC32校验处理:
- 在CRC状态发送计算后的CRC校验码;
- 提供
crc_en
和crc_clr
信号用于控制CRC模块的使能与复位;- CRC校验数据来自
crc_data
和crc_next
输入,按字节顺序反向输出。输出控制信号:
arp_gmii_tx_en
:在非空闲状态下拉高,表示数据有效;arp_tx_done
:在CRC发送完成后置高一个时钟周期,标志发送完成。
该模块实现了ARP协议中主机发起ARP请求或响应的完整过程,能够构建标准的以太网帧结构并通过GMII接口向外发送。结合CRC32校验机制,确保所发送的数据包在网络中具有良好的完整性与可靠性,是实现网络通信中地址解析的重要组成部分。
2.3 接收模块:arp_rx.v
功能:实现ARP协议数据包的接收与解析。对接收到的以太网帧进行逐层解析,提取出源MAC地址、源IP地址、目的IP地址以及操作码(请求/应答),并判断是否为目标为本地的数据包。
代码片段 :
`timescale 1ns / 1ps
//ARP协议----接收端
module arp_rx (
input wire gmii_rxc ,//125Mhz的时钟,为模块提供时序基准
input wire rst_n ,//复位信号,低电平有效,用于将模块状态复位到初始状态
input wire gmii_rx_en ,//开始信号,高电平表示开始接收数据
input wire [7 :0] gmii_rxd ,//单沿数据,接收到的8位数据
output reg [47:0] pc_mac ,//pc端的mac地址,接收完成后输出PC端的MAC地址
output reg [31:0] pc_ip ,//pc端的ip地址,接收完成后输出PC端的IP地址
output reg arp_rx_type ,//确定是请求包还是应答包,0表示请求包,1表示应答包
output reg arp_rx_done //结束信号,高电平表示一次ARP数据接收完成
);
// 定义FPGA的MAC地址
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
// 定义FPGA的IP地址
parameter BOARD_IP = {8'd192,8'd168,8'd0,8'd5};
// 定义状态机的各个状态
localparam IDLE = 5'b0_0001; //空闲状态,等待接收开始信号
localparam PRE_DATA = 5'b0_0010; //7个字节的前导码+1个 字节的帧起始界定符状态
localparam ETH_HEAD = 5'b0_0100; //14个字节的以太网帧头 包括目的mac(6B),源mac(6B),长度类型(2B)状态
localparam ARP_DATA = 5'b0_1000; //arp数据 28个有效字节 + 18个无效填充数据状态
localparam ARP_END = 5'b1_0000; //通信结束状态
reg error ; //状态跳转错误标志,为1时表示接收过程出现错误
reg [10:0] cnt_byte; //状态跳转条件,在每个状态下计数,用于控制状态跳转
reg [4 :0] c_state,n_state;//现态、次态,分别表示当前状态和下一个状态
reg [15:0] eth_type; //长度/类型寄存器---做状态跳转判断,存储以太网帧的长度/类型字段
reg [47:0] des_mac ; //目的mac地址寄存器,存储接收到的目的MAC地址
reg [31:0] des_ip ; //目的ip地址,存储接收到的目的IP地址
reg [15:0] OP_data ; //OP操作码寄存器,存储ARP数据包的操作码
// 状态机第一段:同步更新当前状态
always @(posedge gmii_rxc ) begin
if(!rst_n)
c_state <= IDLE;
else
c_state <= n_state;
end
// 状态机第二段:根据当前状态和输入信号确定下一个状态
always @(*) begin
if(!rst_n)
n_state = IDLE;
else begin
case (c_state)
IDLE :begin
// 当开始信号有效且接收到的第一个字节是前导码时,进入前导码和帧起始界定符状态
if(gmii_rx_en && gmii_rxd == 8'h55)
n_state = PRE_DATA;
else
n_state = IDLE;
end
PRE_DATA:begin
// 如果出现错误标志,直接进入通信结束状态
if(error)
n_state = ARP_END;
// 前导码接收完7个字节后,判断下一个字节是否为帧起始界定符
else if(cnt_byte == 6 )begin
if(gmii_rxd == 8'hd5)
n_state = ETH_HEAD;
else
n_state = ARP_END;
end
else
n_state = PRE_DATA;
end
ETH_HEAD:begin
// 如果出现错误标志,直接进入通信结束状态
if(error)
n_state = ARP_END;
// 以太网帧头接收完14个字节后,判断长度/类型字段是否为ARP协议
else if(cnt_byte == 13)begin
if(eth_type[7:0] == 8'h08 && gmii_rxd == 8'h06)
n_state = ARP_DATA;
else
n_state = ARP_END;
end
else
n_state = ETH_HEAD;
end
ARP_DATA:begin
// ARP数据接收完28个有效字节后,进入通信结束状态
if(cnt_byte == 28)
n_state = ARP_END;
else
n_state = ARP_DATA;
end
ARP_END :begin
// 当开始信号无效时,回到空闲状态
if(gmii_rx_en == 0)
n_state = IDLE;
else
n_state = ARP_END;
end
default: n_state = IDLE;
endcase
end
end
// 状态跳转计数器:根据当前状态更新计数值
always @(posedge gmii_rxc ) begin
if(!rst_n)
cnt_byte <= 0;
else
case (c_state)
IDLE :cnt_byte <= 0;
PRE_DATA:begin
if(cnt_byte == 6)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
ETH_HEAD:begin
if(cnt_byte == 13)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
ARP_DATA:begin
if(cnt_byte == 28)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
ARP_END :begin
cnt_byte <= 0;
end
default: cnt_byte <= 0;
endcase
end
// 错误标志:根据当前状态和接收到的数据判断是否出现错误
always @(posedge gmii_rxc ) begin
if(!rst_n)
error <= 0;
else
case (c_state)
IDLE :begin
error <= 0;
end
PRE_DATA:begin
// 前导码阶段,如果接收到的字节不是前导码,设置错误标志
if(cnt_byte < 6 && gmii_rxd != 8'h55)
error <= 1;
else
error <= 0;
end
ETH_HEAD:begin
// 以太网帧头阶段,判断目的MAC地址是否正确
if(cnt_byte == 6 && des_mac != BOARD_MAC && des_mac != 48'hff_ff_ff_ff_ff_ff)
error <= 1;
else
error <= 0;
end
default: error <= 0;
endcase
end
// 以太网长度/类型字段:在以太网帧头阶段接收长度/类型字段
always @(posedge gmii_rxc ) begin
if(!rst_n)
eth_type <= 0;
else if(c_state == ETH_HEAD && cnt_byte > 11)
eth_type <={eth_type[7:0],gmii_rxd};//先接高位
else
eth_type <= eth_type;
end
// 目的MAC地址:在以太网帧头阶段接收目的MAC地址
always @(posedge gmii_rxc ) begin
if(!rst_n)
des_mac <= 0;
else if(c_state == ETH_HEAD && cnt_byte < 6)
des_mac <={des_mac[39:0],gmii_rxd};//先接收高位
else
des_mac <= des_mac;
end
// 目的IP地址:在ARP数据阶段接收目的IP地址
always @(posedge gmii_rxc ) begin
if(!rst_n)
des_ip <= 0;
else if(c_state == ARP_DATA && cnt_byte > 23)
des_ip <={des_ip[23:0],gmii_rxd};//先接收高位
else
des_ip <= des_ip;
end
// OP操作码:在ARP数据阶段接收OP操作码
always @(posedge gmii_rxc ) begin
if(!rst_n)
OP_data <= 0;
else if(c_state == ARP_DATA && cnt_byte > 5 && cnt_byte < 8)
OP_data <= {OP_data[7:0],gmii_rxd};
else
OP_data <= OP_data;
end
// ARP包类型:根据OP操作码判断是请求包还是应答包
always @(posedge gmii_rxc ) begin
if(!rst_n)
arp_rx_type <= 0;
else if(OP_data == 1)//请求包
arp_rx_type <= 0;
else if(OP_data == 2)//应答包
arp_rx_type <= 1;
else
arp_rx_type <= 0;
end
// PC端MAC地址:在ARP数据阶段接收PC端的MAC地址
always @(posedge gmii_rxc ) begin
if(!rst_n)begin
pc_mac <= 0;
end
else if(c_state == ARP_DATA && cnt_byte > 7 && cnt_byte < 14)begin
pc_mac <= {pc_mac[39:0],gmii_rxd};
end
else begin
pc_mac <= pc_mac;
end
end
// PC端IP地址:在ARP数据阶段接收PC端的IP地址
always @(posedge gmii_rxc ) begin
if(!rst_n)
pc_ip <= 0;
else if(c_state == ARP_DATA && cnt_byte > 13 && cnt_byte < 18)
pc_ip <={pc_ip[23:0],gmii_rxd};//先接收高位
else
pc_ip <= pc_ip;
end
// ARP接收完成标志:当ARP数据接收完且目的IP地址是FPGA的IP地址时,设置接收完成标志
always @(posedge gmii_rxc ) begin
if(!rst_n)begin
arp_rx_done <= 0;
end
else if(c_state == ARP_DATA && cnt_byte == 28 && des_ip == BOARD_IP)begin
arp_rx_done <= 1;
end
else begin
arp_rx_done <= 0;
end
end
endmodule
主要逻辑:
状态机控制接收流程:
- 使用五段式状态机依次处理不同阶段的数据接收:
IDLE
:空闲状态,等待有效数据到来;PRE_DATA
:接收前导码和帧起始界定符;ETH_HEAD
:接收以太网头部(目的MAC、源MAC、协议类型);ARP_DATA
:接收ARP协议数据字段(28字节有效数据);ARP_END
:接收结束,等待下一帧。- 字节计数器
cnt_byte
控制每个状态接收的字节数。错误检测机制:
- 在接收过程中通过
error
标志判断是否存在格式错误或非目标地址;- 若发现目的MAC既不是本地MAC也不是广播地址,则视为无效帧,跳转至结束状态。
关键字段提取:
- 提取目的MAC地址
des_mac
和目的IP地址des_ip
;- 提取OP操作码
OP_data
,用于判断是请求包还是应答包;- 提取源MAC地址
pc_mac
和源IP地址pc_ip
,供后续处理使用。输出控制信号:
arp_rx_type
:标志接收到的是ARP请求(0)还是应答(1);arp_rx_done
:当完整接收一个目标为本机的有效ARP数据包后拉高,表示接收完成。
该模块实现了对ARP协议数据包的接收与解析功能,能够从GMII接口获取到以太网帧,并正确识别其中的ARP信息。通过对MAC地址和IP地址的匹配过滤,确保只处理发给本机的数据包,提高了系统在网络通信中的准确性和安全性。作为ARP模块的重要组成部分,它为上层协议处理提供了可靠的数据支持。
2.4 CRC32校验模块:crc32_data.v
功能:实现CRC32(IEEE 802.3标准)循环冗余校验算法,用于以太网数据帧的完整性校验。该模块接收8位数据输入,并在有效信号控制下计算并更新当前的CRC32校验值。
代码片段 :
`timescale 1ns / 1ps
//CRC32---循环冗余校验
module crc32_data(
input clk , //时钟信号,用于同步模块的操作
input rst_n , //复位信号,低电平有效,用于将模块状态复位到初始状态
input [7:0] data , //输入待校验8位数据,即要进行CRC32校验的数据字节
input crc_en , //crc使能,开始校验标志,高电平有效时启动CRC32校验过程
input crc_clr , //crc数据复位信号,高电平有效时将CRC校验数据复位到初始值
output reg [31:0] crc_data, //CRC校验数据,存储当前的CRC32校验结果
output [31:0] crc_next //CRC下次校验完成数据,存储下一次CRC32校验的结果
);
//*****************************************************
//** main code
//*****************************************************
//输入待校验8位数据,需要先将高低位互换
wire [7:0] data_t;
// 将输入的8位数据进行高低位互换,以便后续按照CRC32算法进行处理
assign data_t = {data[0],data[1],data[2],data[3],data[4],data[5],data[6],data[7]};
//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
// 根据CRC32的生成多项式和当前的CRC校验数据以及输入数据,计算下一次CRC校验结果的第0位
assign crc_next[0] = crc_data[24] ^ crc_data[30] ^ data_t[0] ^ data_t[6];
// 计算下一次CRC校验结果的第1位
assign crc_next[1] = crc_data[24] ^ crc_data[25] ^ crc_data[30] ^ crc_data[31]
^ data_t[0] ^ data_t[1] ^ data_t[6] ^ data_t[7];
// 计算下一次CRC校验结果的第2位
assign crc_next[2] = crc_data[24] ^ crc_data[25] ^ crc_data[26] ^ crc_data[30]
^ crc_data[31] ^ data_t[0] ^ data_t[1] ^ data_t[2] ^ data_t[6]
^ data_t[7];
// 计算下一次CRC校验结果的第3位
assign crc_next[3] = crc_data[25] ^ crc_data[26] ^ crc_data[27] ^ crc_data[31]
^ data_t[1] ^ data_t[2] ^ data_t[3] ^ data_t[7];
// 计算下一次CRC校验结果的第4位
assign crc_next[4] = crc_data[24] ^ crc_data[26] ^ crc_data[27] ^ crc_data[28]
^ crc_data[30] ^ data_t[0] ^ data_t[2] ^ data_t[3] ^ data_t[4]
^ data_t[6];
// 计算下一次CRC校验结果的第5位
assign crc_next[5] = crc_data[24] ^ crc_data[25] ^ crc_data[27] ^ crc_data[28]
^ crc_data[29] ^ crc_data[30] ^ crc_data[31] ^ data_t[0]
^ data_t[1] ^ data_t[3] ^ data_t[4] ^ data_t[5] ^ data_t[6]
^ data_t[7];
// 计算下一次CRC校验结果的第6位
assign crc_next[6] = crc_data[25] ^ crc_data[26] ^ crc_data[28] ^ crc_data[29]
^ crc_data[30] ^ crc_data[31] ^ data_t[1] ^ data_t[2] ^ data_t[4]
^ data_t[5] ^ data_t[6] ^ data_t[7];
// 计算下一次CRC校验结果的第7位
assign crc_next[7] = crc_data[24] ^ crc_data[26] ^ crc_data[27] ^ crc_data[29]
^ crc_data[31] ^ data_t[0] ^ data_t[2] ^ data_t[3] ^ data_t[5]
^ data_t[7];
// 计算下一次CRC校验结果的第8位
assign crc_next[8] = crc_data[0] ^ crc_data[24] ^ crc_data[25] ^ crc_data[27]
^ crc_data[28] ^ data_t[0] ^ data_t[1] ^ data_t[3] ^ data_t[4];
// 计算下一次CRC校验结果的第9位
assign crc_next[9] = crc_data[1] ^ crc_data[25] ^ crc_data[26] ^ crc_data[28]
^ crc_data[29] ^ data_t[1] ^ data_t[2] ^ data_t[4] ^ data_t[5];
// 计算下一次CRC校验结果的第10位
assign crc_next[10] = crc_data[2] ^ crc_data[24] ^ crc_data[26] ^ crc_data[27]
^ crc_data[29] ^ data_t[0] ^ data_t[2] ^ data_t[3] ^ data_t[5];
// 计算下一次CRC校验结果的第11位
assign crc_next[11] = crc_data[3] ^ crc_data[24] ^ crc_data[25] ^ crc_data[27]
^ crc_data[28] ^ data_t[0] ^ data_t[1] ^ data_t[3] ^ data_t[4];
// 计算下一次CRC校验结果的第12位
assign crc_next[12] = crc_data[4] ^ crc_data[24] ^ crc_data[25] ^ crc_data[26]
^ crc_data[28] ^ crc_data[29] ^ crc_data[30] ^ data_t[0]
^ data_t[1] ^ data_t[2] ^ data_t[4] ^ data_t[5] ^ data_t[6];
// 计算下一次CRC校验结果的第13位
assign crc_next[13] = crc_data[5] ^ crc_data[25] ^ crc_data[26] ^ crc_data[27]
^ crc_data[29] ^ crc_data[30] ^ crc_data[31] ^ data_t[1]
^ data_t[2] ^ data_t[3] ^ data_t[5] ^ data_t[6] ^ data_t[7];
// 计算下一次CRC校验结果的第14位
assign crc_next[14] = crc_data[6] ^ crc_data[26] ^ crc_data[27] ^ crc_data[28]
^ crc_data[30] ^ crc_data[31] ^ data_t[2] ^ data_t[3] ^ data_t[4]
^ data_t[6] ^ data_t[7];
// 计算下一次CRC校验结果的第15位
assign crc_next[15] = crc_data[7] ^ crc_data[27] ^ crc_data[28] ^ crc_data[29]
^ crc_data[31] ^ data_t[3] ^ data_t[4] ^ data_t[5] ^ data_t[7];
// 计算下一次CRC校验结果的第16位
assign crc_next[16] = crc_data[8] ^ crc_data[24] ^ crc_data[28] ^ crc_data[29]
^ data_t[0] ^ data_t[4] ^ data_t[5];
// 计算下一次CRC校验结果的第17位
assign crc_next[17] = crc_data[9] ^ crc_data[25] ^ crc_data[29] ^ crc_data[30]
^ data_t[1] ^ data_t[5] ^ data_t[6];
// 计算下一次CRC校验结果的第18位
assign crc_next[18] = crc_data[10] ^ crc_data[26] ^ crc_data[30] ^ crc_data[31]
^ data_t[2] ^ data_t[6] ^ data_t[7];
// 计算下一次CRC校验结果的第19位
assign crc_next[19] = crc_data[11] ^ crc_data[27] ^ crc_data[31] ^ data_t[3] ^ data_t[7];
// 计算下一次CRC校验结果的第20位
assign crc_next[20] = crc_data[12] ^ crc_data[28] ^ data_t[4];
// 计算下一次CRC校验结果的第21位
assign crc_next[21] = crc_data[13] ^ crc_data[29] ^ data_t[5];
// 计算下一次CRC校验结果的第22位
assign crc_next[22] = crc_data[14] ^ crc_data[24] ^ data_t[0];
// 计算下一次CRC校验结果的第23位
assign crc_next[23] = crc_data[15] ^ crc_data[24] ^ crc_data[25] ^ crc_data[30]
^ data_t[0] ^ data_t[1] ^ data_t[6];
// 计算下一次CRC校验结果的第24位
assign crc_next[24] = crc_data[16] ^ crc_data[25] ^ crc_data[26] ^ crc_data[31]
^ data_t[1] ^ data_t[2] ^ data_t[7];
// 计算下一次CRC校验结果的第25位
assign crc_next[25] = crc_data[17] ^ crc_data[26] ^ crc_data[27] ^ data_t[2] ^ data_t[3];
// 计算下一次CRC校验结果的第26位
assign crc_next[26] = crc_data[18] ^ crc_data[24] ^ crc_data[27] ^ crc_data[28]
^ crc_data[30] ^ data_t[0] ^ data_t[3] ^ data_t[4] ^ data_t[6];
// 计算下一次CRC校验结果的第27位
assign crc_next[27] = crc_data[19] ^ crc_data[25] ^ crc_data[28] ^ crc_data[29]
^ crc_data[31] ^ data_t[1] ^ data_t[4] ^ data_t[5] ^ data_t[7];
// 计算下一次CRC校验结果的第28位
assign crc_next[28] = crc_data[20] ^ crc_data[26] ^ crc_data[29] ^ crc_data[30]
^ data_t[2] ^ data_t[5] ^ data_t[6];
// 计算下一次CRC校验结果的第29位
assign crc_next[29] = crc_data[21] ^ crc_data[27] ^ crc_data[30] ^ crc_data[31]
^ data_t[3] ^ data_t[6] ^ data_t[7];
// 计算下一次CRC校验结果的第30位
assign crc_next[30] = crc_data[22] ^ crc_data[28] ^ crc_data[31] ^ data_t[4] ^ data_t[7];
// 计算下一次CRC校验结果的第31位
assign crc_next[31] = crc_data[23] ^ crc_data[29] ^ data_t[5];
// 根据时钟信号和控制信号更新CRC校验数据
always @(posedge clk or negedge rst_n) begin
// 复位信号有效时,将CRC校验数据初始化为全1(32'hff_ff_ff_ff)
if(!rst_n)
crc_data <= 32'hff_ff_ff_ff;
// CRC数据复位信号有效时,将CRC校验数据复位到全1(32'hff_ff_ff_ff)
else if(crc_clr)
crc_data <= 32'hff_ff_ff_ff;
// CRC使能信号有效时,将CRC校验数据更新为下一次的校验结果
else if(crc_en)
crc_data <= crc_next;
end
endmodule
主要逻辑:
CRC32多项式:
- 使用标准多项式:
G(x)=x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1G(x)=x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1状态控制信号:
crc_clr
:复位信号,将crc_data
恢复为初始值32'hffffffff
;crc_en
:使能信号,控制是否进行CRC计算;数据处理方式:
- 输入数据
data[7:0]
在进入计算前进行高低位翻转,确保符合CRC32逐字节处理时的顺序要求;- 输出两个结果:
crc_data
:当前CRC值;crc_next
:下一周期CRC值,供外部模块使用(如arp_tx
中在发送CRC字段时输出反向CRC值)。组合逻辑实现CRC计算:
- 所有32位CRC输出通过组合逻辑表达式逐位计算,基于当前
crc_data
和输入数据data_t
得出;- 实现方式高效,适用于高速通信场景下的实时校验。
初始化与复位机制:
- 初始值为全1(
32'hffffffff
),这是CRC32标准推荐的初始化值;- 当
crc_clr
有效时,重新初始化CRC寄存器。
接口说明:
端口名 | 方向 | 位宽 | 描述 |
---|---|---|---|
clk | 输入 | 1 bit | 主时钟信号 |
rst_n | 输入 | 1 bit | 异步复位信号,低电平有效 |
data | 输入 | 8 bits | 需要校验的数据 |
crc_en | 输入 | 1 bit | CRC计算使能信号 |
crc_clr | 输入 | 1 bit | CRC值清零信号 |
crc_data | 输出 | 32 bits | 当前CRC校验值 |
crc_next | 输出 | 32 bits | 下一周期CRC值 |
该模块为以太网通信中数据完整性的保障提供关键支持,常用于数据包发送端(如ARP、IP、UDP等协议)生成校验码。其可独立作为通用CRC32计算单元,也可与其他模块(如 arp_tx
)配合使用,提高系统在网络环境中的可靠性与兼容性。
结合 arp_tx
模块使用时,crc_next
可用于实时输出CRC字段内容,在发送最后4个CRC字节时将其反向输出,以满足以太网帧格式要求。
三、UDP 协议模块实现
3.1 UDP协议发送模块:udp_tx.v
功能:实现以太网UDP数据包的封装与发送。该模块根据输入的数据和目标地址信息,构建完整的以太网帧(包括以太网头部、IP头部、UDP头部),并控制CRC校验的启动与插入。
代码片段 :
`timescale 1ns / 1ps
//UDP协议---发送端
module udp_tx (
input wire gmii_txc ,//125Mhz的时钟,为模块提供时钟基准
input wire rst_n ,//复位信号,低电平有效,用于将模块复位到初始状态
input wire udp_tx_start ,//udp发送的开始信号,高电平有效时启动UDP发送操作
input wire [7 :0] udp_data ,//需要发送的数据,8位数据
input wire [47:0] pc_mac ,//arp_rx解析后的PC端的MAC地址
input wire [31:0] pc_ip ,//arp_rx解析后的PC端的IP地址
input wire [31:0] crc_data ,//校验数据,用于CRC校验
input wire [31:0] crc_next ,//下次校验的数据,用于CRC校验
output reg [7 :0] udp_gmii_txd ,//给rgmii模块的数据--进行单双沿数据转换,发送的UDP数据
output wire udp_tx_data_en ,//udp数据的请求信号---有效时表示有数据可发送
output reg udp_gmii_tx_en ,//给rgmii模块的使能信号,高电平有效时允许发送数据
output reg udp_tx_done ,//发送结束信号,高电平表示UDP数据发送完成
output reg crc_en ,//校验开始信号,高电平有效时启动CRC校验
output reg crc_clr //校验复位信号,高电平有效时对CRC校验器进行复位
);
// IP数据长度(首部+有效数据)
parameter IP_DATA_LEN = 46;
// UDP数据长度(不包含IP首部)
parameter LEN = IP_DATA_LEN - 20;
// FPGA的MAC地址
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
// FPGA的IP地址
parameter BOARD_IP = {8'd192,8'd168,8'd0,8'd5};
// 定义状态机的各个状态
localparam IDLE = 7'b000_0001;//空闲状态,等待发送开始信号
localparam CHECK_SUM = 7'b000_0010;//IP首部校验和计算状态
localparam PRE_DATA = 7'b000_0100;//7个字节的前导码+1个 字节的帧起始界定符状态
localparam ETH_HEAD = 7'b000_1000;//14个字节的以太网帧头 包括目的mac(6B),源mac(6B),长度类型(2B)状态
localparam IP_HEAD = 7'b001_0000;//ip首部+udp首部状态
localparam UDP_DATA = 7'b010_0000;//udp数据包发送状态
localparam CRC = 7'b100_0000;//CRC校验状态
reg [6 :0] c_state,n_state ;//现态、次态,分别表示当前状态和下一个状态
reg [10:0] cnt_byte ;//字节计数器寄存器,用于记录每个状态下已处理的字节数
reg [31:0] check_sum_data ;//ip首部校验和,用于存储IP首部的校验和结果
reg [7 :0] eth_head_data[13:0];//以太网帧头,存储14个字节的以太网帧头数据
reg [7 :0] ip_head_data [27:0];//ip首部+udp首部 ,存储IP首部和UDP首部的数据
reg [15:0] sign_sum ;//标识号--16bit,用于标识同一网段中的数据包
// 数据有效信号,当处于UDP数据发送状态时有效
assign udp_tx_data_en = (c_state == UDP_DATA) ? 1:0;
// 状态机第一段:同步更新当前状态
always @(posedge gmii_txc ) begin
if(!rst_n)
c_state <= IDLE;
else
c_state <= n_state;
end
// 状态机第二段:根据当前状态和输入信号确定下一个状态
always @(*) begin
if(!rst_n)
n_state = IDLE;
else begin
case (c_state)
IDLE :begin
// 当发送开始信号有效时,进入IP首部校验和计算状态
if(udp_tx_start)
n_state = CHECK_SUM;
else
n_state = IDLE;
end
CHECK_SUM:begin
// 计算完IP首部校验和后,进入前导码和帧起始界定符状态
if(cnt_byte == 3)
n_state = PRE_DATA;
else
n_state = CHECK_SUM;
end
PRE_DATA :begin
// 发送完前导码和帧起始界定符后,进入以太网帧头发送状态
if(cnt_byte == 7)
n_state = ETH_HEAD;
else
n_state = PRE_DATA;
end
ETH_HEAD :begin
// 发送完以太网帧头后,进入IP首部和UDP首部发送状态
if(cnt_byte == 13)
n_state = IP_HEAD;
else
n_state = ETH_HEAD;
end
IP_HEAD :begin
// 发送完IP首部和UDP首部后,进入UDP数据发送状态
if(cnt_byte == 27)
n_state = UDP_DATA;
else
n_state = IP_HEAD;
end
UDP_DATA :begin
// 发送完UDP数据后,进入CRC校验状态
if(cnt_byte == IP_DATA_LEN - 29)
n_state = CRC;
else
n_state = UDP_DATA;
end
CRC :begin
// 完成CRC校验后,回到空闲状态
if(cnt_byte == 3)
n_state = IDLE;
else
n_state = CRC;
end
default: n_state = IDLE;
endcase
end
end
// 字节计数器:根据当前状态更新计数值
always @(posedge gmii_txc ) begin
if(!rst_n)
cnt_byte <= 0;
else begin
case (c_state)
CHECK_SUM:begin
if(cnt_byte == 3)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
PRE_DATA :begin
if(cnt_byte == 7)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
ETH_HEAD :begin
if(cnt_byte == 13)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
IP_HEAD :begin
if(cnt_byte == 27)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
UDP_DATA :begin
if(cnt_byte == IP_DATA_LEN - 29)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
CRC :begin
if(cnt_byte == 3)
cnt_byte <= 0;
else
cnt_byte <= cnt_byte + 1;
end
default: cnt_byte <= 0;
endcase
end
end
// 发送数据更新:根据当前状态更新要发送的数据
always @(posedge gmii_txc ) begin
if(!rst_n)begin
udp_gmii_txd <= 0;
end
else begin
case (c_state)
IDLE :begin
udp_gmii_txd <= 0;
// 初始化以太网帧头数据
eth_head_data [0] <= pc_mac[47:40];//目的MAC地址采用解析后的mac地址
eth_head_data [1] <= pc_mac[39:32];//目的MAC地址采用解析后的mac地址
eth_head_data [2] <= pc_mac[31:24];//目的MAC地址采用解析后的mac地址
eth_head_data [3] <= pc_mac[23:16];//目的MAC地址采用解析后的mac地址
eth_head_data [4] <= pc_mac[15: 8];//目的MAC地址采用解析后的mac地址
eth_head_data [5] <= pc_mac[7 : 0];//目的MAC地址采用解析后的mac地址
eth_head_data [6] <= BOARD_MAC[47:40];//源MAC地址,fpga的MAC地址
eth_head_data [7] <= BOARD_MAC[39:32];//源MAC地址,fpga的MAC地址
eth_head_data [8] <= BOARD_MAC[31:24];//源MAC地址,fpga的MAC地址
eth_head_data [9] <= BOARD_MAC[23:16];//源MAC地址,fpga的MAC地址
eth_head_data [10] <= BOARD_MAC[15: 8];//源MAC地址,fpga的MAC地址
eth_head_data [11] <= BOARD_MAC[7 : 0];//源MAC地址,fpga的MAC地址
eth_head_data [12] <= 8'h08;//长度类型---0800---IP协议
eth_head_data [13] <= 8'h00;//长度类型---0800---IP协议
// 初始化IP首部数据
ip_head_data [0] <= 8'h45;//版本号(4)+首部长度(5)
ip_head_data [1] <= 8'h00;//区分服务--一般不用--置0
ip_head_data [2] <= IP_DATA_LEN[15:8];//总长度--IP首部+数据之和
ip_head_data [3] <= IP_DATA_LEN[7 :0];//总长度--IP首部+数据之和
ip_head_data [4] <= sign_sum[15:8];//标识--同一网段中的数据包计数器
ip_head_data [5] <= sign_sum[7 :0];//标识--同一网段中的数据包计数器
ip_head_data [6] <= 8'h00;//标志(3bit)+片偏移
ip_head_data [7] <= 8'h00;//片偏移(8bit)
ip_head_data [8] <= 8'h80;//生存时间---一般为'd64 'd128
ip_head_data [9] <= 8'h11;//协议--TCP(8'd6)---UDP(8'd17)--ICMP(8'd2)
ip_head_data [10] <= 8'h00;//首部校验和
ip_head_data [11] <= 8'h00;//首部校验和
ip_head_data [12] <= BOARD_IP[31:24]; //源IP地址
ip_head_data [13] <= BOARD_IP[23:16]; //源IP地址
ip_head_data [14] <= BOARD_IP[15: 8]; //源IP地址
ip_head_data [15] <= BOARD_IP[7 : 0]; //源IP地址
ip_head_data [16] <= pc_ip[31:24]; //目的IP地址
ip_head_data [17] <= pc_ip[23:16]; //目的IP地址
ip_head_data [18] <= pc_ip[15: 8]; //目的IP地址
ip_head_data [19] <= pc_ip[7 : 0]; //目的IP地址
// 初始化UDP首部数据
ip_head_data [20] <= 8'h80;//源端口号---自定义的端口号一般要大于5000
ip_head_data [21] <= 8'h00;//源端口号
ip_head_data [22] <= 8'h80;//目的端口号
ip_head_data [23] <= 8'h00;//目的端口号
ip_head_data [24] <= LEN[15:8];//UDP数据长度(UDP首部+UDP数据)
ip_head_data [25] <= LEN[7:0];//UDP数据长度(UDP首部+UDP数据)
ip_head_data [26] <= 8'h00;//UDP校验和(UDP 伪首部 + UDP 首部 + UDP 数据)
ip_head_data [27] <= 8'h00;//UDP校验和
end
CHECK_SUM:begin
udp_gmii_txd <= 0;
// 计算完IP首部校验和后,更新IP首部的校验和字段
if(cnt_byte == 3)begin
ip_head_data [10] <= ~check_sum_data[15:8];//首部校验和
ip_head_data [11] <= ~check_sum_data[7 :0];//首部校验和
end
end
PRE_DATA :begin
// 发送前导码和帧起始界定符
if(cnt_byte < 7)
udp_gmii_txd <= 8'h55;//七个字节的前导码--8'h55
else
udp_gmii_txd <= 8'hd5;//一个字节的帧起始界定符--8'hd5
end
ETH_HEAD :begin
// 发送以太网帧头数据
udp_gmii_txd <= eth_head_data[cnt_byte];
end
IP_HEAD :begin
// 发送IP首部和UDP首部数据
udp_gmii_txd <= ip_head_data[cnt_byte];
end
UDP_DATA :begin
// 发送UDP数据
udp_gmii_txd <= udp_data;
end
CRC :begin
// 发送CRC校验结果
if(cnt_byte == 0)
udp_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_byte == 1)
udp_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_byte == 2)
udp_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]};
else if(cnt_byte == 3)
udp_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]};
end
default: udp_gmii_txd <= 0;
endcase
end
end
// IP首部校验和计算:根据IP首部数据计算校验和
always @(posedge gmii_txc ) begin
if(!rst_n)
check_sum_data <= 0;
else if (c_state == CHECK_SUM) begin
if(cnt_byte == 0)begin
// 计算IP首部数据的和
check_sum_data <= {ip_head_data[0],ip_head_data[1]} + {ip_head_data[2],ip_head_data[3]} + {ip_head_data[4],ip_head_data[5]} +
{ip_head_data[6],ip_head_data[7]} + {ip_head_data[8],ip_head_data[9]} + {ip_head_data[10],ip_head_data[11]} +
{ip_head_data[12],ip_head_data[13]} + {ip_head_data[14],ip_head_data[15]} + {ip_head_data[16],ip_head_data[17]} +
{ip_head_data[18],ip_head_data[19]};
end
else if(cnt_byte == 1)//直接做溢出操作
check_sum_data <= check_sum_data[31:16]+check_sum_data[15:0];
else if(cnt_byte == 2)//溢出判断
check_sum_data <= check_sum_data[31:16]+check_sum_data[15:0];
else
check_sum_data <= check_sum_data;
end
end
//udp_gmii_tx_en
always @(posedge gmii_txc ) begin
if(!rst_n)
udp_gmii_tx_en <= 0;
else if(c_state == IDLE || c_state == CHECK_SUM)
udp_gmii_tx_en <= 0;
else
udp_gmii_tx_en <= 1;
end
//sign_sum
always @(posedge gmii_txc ) begin
if(!rst_n)
sign_sum <= 0;
else if(udp_tx_done)
sign_sum <= sign_sum + 1;
else
sign_sum <= sign_sum;
end
//udp_tx_done、crc_clr
always @(posedge gmii_txc ) begin
if(!rst_n)begin
udp_tx_done <= 0;
crc_clr <= 0;
end
else if(c_state == CRC && cnt_byte == 3)begin
udp_tx_done <= 1;
crc_clr <= 1;
end
else begin
udp_tx_done <= 0;
crc_clr <= 0;
end
end
//crc_en
always @(posedge gmii_txc ) begin
if(!rst_n)begin
crc_en <= 0;
end
else if(c_state == ETH_HEAD || c_state == UDP_DATA || c_state == IP_HEAD)begin
crc_en <= 1;
end
else begin
crc_en <= 0;
end
end
endmodule
主要逻辑:
状态机设计:
- 模块采用一个7段式状态机来管理从空闲到数据发送完成的整体流程。
- 状态包括:空闲(IDLE)、计算校验和(CHECK_SUM)、前导码发送(PRE_DATA)、以太网头发送(ETH_HEAD)、IP头发送(IP_HEAD)、UDP数据发送(UDP_DATA)以及CRC校验值发送(CRC)。
数据准备阶段:
- 在进入数据发送流程之前,先初始化必要的参数如MAC地址、IP地址等,并填充对应的头部字段。
- 根据不同的网络层需求配置源/目的MAC地址、源/目的IP地址、UDP端口号等。
发送过程:
- 前导码及帧起始界定符:在PRE_DATA状态下,发送7字节的前导码(0x55)和1字节的帧起始界定符(0xD5)。
- 以太网头部:在ETH_HEAD状态下,发送包含目标和源MAC地址以及类型字段的以太网头部(共14字节)。
- IP头部:在IP_HEAD状态下,发送包括版本号、首部长度、总长度、TTL、协议类型、源/目的IP地址在内的IP头部(通常为20字节)。
- UDP头部与数据:在UDP_DATA状态下,发送UDP头部(8字节)及用户数据。
- CRC校验值:最后,在CRC状态下,发送4字节的CRC校验值。此值由外部CRC计算模块提供,并在此过程中反向输出。
控制信号:
udp_tx_start
: 启动UDP数据包发送过程。udp_gmii_tx_en
: 控制GMII接口上的数据传输使能。crc_en
和crc_clr
: 分别用于启动和复位外部CRC计算模块。
该 udp_tx
发送模块,作为网络通信系统的一部分,可以有效地处理UDP协议的数据封装与发送任务,适用于需要高速以太网通信的应用场景。配合其他相关模块(如ARP解析模块、CRC计算模块),能够构建出完整且高效的网络协议栈。
3.2 UDP协议接收模块:udp_rx.v
功能:实现以太网UDP数据包的接收与解析。该模块从GMII接口接收网络中的UDP数据帧,依次校验并解析以太网头部、IP头部、UDP头部,最终提取出有效载荷数据并输出。
代码片段 :
`timescale 1ns / 1ps
// UDP 接收模块
module udp_rx(
// 125MHz 时钟信号,为模块提供时序基准
input wire gmii_rxc ,
// 复位信号,低电平有效,用于将模块状态复位到初始状态
input wire rst_n ,
// RGMII 模块给出的使能信号,指示是否有数据可以接收
input wire gmii_rx_en ,
// RGMII 模块提供的单沿数据,即接收到的网络数据
input wire [7 :0] gmii_rxd ,
// ARP 解析模块解析得到的 PC 端的 MAC 地址
input wire [47:0] pc_mac ,
// ARP 解析模块解析得到的 PC 端的 IP 地址
input wire [31:0] pc_ip ,
// 接收到的 UDP 数据
output reg [7 :0] udp_rx_data ,
// 数据有效信号,高电平表示当前的 udp_rx_data 是有效的 UDP 数据
output reg udp_rx_data_en ,
// 接收结束信号,高电平表示一次 UDP 数据接收完成
output reg udp_rx_done
);
// 定义 FPGA 的 MAC 地址
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
// 定义 FPGA 的 IP 地址
parameter BOARD_IP = {8'd192,8'd168,8'd0,8'd5};
// 状态机的各个状态定义
localparam IDLE = 7'b000_0001; // 空闲状态,等待接收数据
localparam PRE_DATA = 7'b000_0010; // 处理 7 个字节的前导码和 1 个字节的帧起始界定符的状态
localparam ETH_HEAD = 7'b000_0100; // 处理 14 个字节的以太网帧头的状态,包含目的 MAC(6B)、源 MAC(6B)、长度类型(2B)
localparam IP_HEAD = 7'b000_1000; // 处理 IP 首部的状态
localparam UDP_HEAD = 7'b001_0000; // 处理 UDP 首部的状态
localparam UDP_DATA = 7'b010_0000; // 处理 UDP 数据包数据的状态
localparam END = 7'b100_0000; // 接收结束状态
// 状态机的当前状态和下一个状态
reg [6 :0] c_state,n_state;
// 字节计数器,用于记录每个状态下处理的字节数
reg [10:0] cnt_byte;
// 错误标志,高电平表示接收过程中出现错误
reg error;
// 以太网帧头的长度类型字段
reg [15:0] eth_type;
// 目的 IP 地址,即板子的 IP 地址
reg [31:0] des_ip;
// 目的 MAC 地址
reg [47:0] des_mac;
// IP 协议类型,UDP 协议类型为 8'h11
reg [7 :0] ip_type;
// 源 MAC 地址,即 PC 的 MAC 地址
reg [47:0] src_mac;
// 源 IP 地址,即 PC 的 IP 地址
reg [31:0] src_ip;
// IP 数据包的总长度
reg [15:0] ip_data_len;
// 状态机第一段:同步时序逻辑,在时钟上升沿更新当前状态
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将当前状态设置为空闲状态
c_state <= IDLE;
else
// 正常情况下,将下一个状态赋值给当前状态
c_state <= n_state;
end
// 状态机第二段:组合逻辑,根据当前状态和输入信号确定下一个状态
always @(*) begin
if(!rst_n)
// 复位时,将下一个状态设置为空闲状态
n_state = IDLE;
else begin
case (c_state)
IDLE :begin
// 如果使能信号有效且接收到的数据为 8'h55,进入前导码处理状态
if(gmii_rx_en && gmii_rxd == 8'h55)
n_state = PRE_DATA;
else
// 否则保持空闲状态
n_state = IDLE;
end
PRE_DATA:begin
// 如果检测到错误,进入结束状态
if(error)
n_state = END;
else if(cnt_byte == 6 && gmii_rxd == 8'hd5)
// 如果前导码处理完成且接收到帧起始界定符,进入以太网帧头处理状态
n_state = ETH_HEAD;
else
// 否则继续处理前导码
n_state = PRE_DATA;
end
ETH_HEAD:begin
// 如果检测到错误,进入结束状态
if(error)
n_state = END;
else if(cnt_byte == 13)begin
// 如果以太网帧头处理完成,且长度类型为 0x0800(IP 协议),进入 IP 首部处理状态
if(eth_type[7:0] == 8'h08 && gmii_rxd == 8'h00)
n_state = IP_HEAD;
else
// 否则进入结束状态
n_state = END;
end
else
// 否则继续处理以太网帧头
n_state = ETH_HEAD;
end
IP_HEAD :begin
// 如果检测到错误,进入结束状态
if (error) begin
n_state = END;
end
else if(cnt_byte == 19)begin
// 如果 IP 首部处理完成,且目的 IP 地址是板子的 IP 地址,进入 UDP 首部处理状态
if(des_ip[23:0] == BOARD_IP[31:8] && gmii_rxd == BOARD_IP[7:0])
n_state = UDP_HEAD;
else
// 否则进入结束状态
n_state = END;
end
else
// 否则继续处理 IP 首部
n_state = IP_HEAD;
end
UDP_HEAD:begin
// 如果 UDP 首部处理完成,进入 UDP 数据处理状态
if (cnt_byte == 7) begin
n_state = UDP_DATA;
end
else
// 否则继续处理 UDP 首部
n_state = UDP_HEAD;
end
UDP_DATA:begin
// 如果 UDP 数据处理完成,进入结束状态
if (cnt_byte == ip_data_len - 29) begin
n_state = END;
end
else
// 否则继续处理 UDP 数据
n_state = UDP_DATA;
end
END :begin
// 如果使能信号无效,回到空闲状态
if(gmii_rx_en == 0)
n_state = IDLE;
else
// 否则保持结束状态
n_state = END;
end
default:
// 默认情况下,进入空闲状态
n_state = IDLE;
endcase
end
end
// 字节计数器逻辑,在时钟上升沿更新计数器值
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将计数器清零
cnt_byte <= 0;
else begin
case (c_state)
PRE_DATA:begin
// 如果前导码处理完成,将计数器清零
if(cnt_byte == 6)
cnt_byte <= 0;
else
// 否则计数器加 1
cnt_byte <= cnt_byte + 1;
end
ETH_HEAD:begin
// 如果以太网帧头处理完成,将计数器清零
if(cnt_byte == 13)
cnt_byte <= 0;
else
// 否则计数器加 1
cnt_byte <= cnt_byte + 1;
end
IP_HEAD :begin
// 如果 IP 首部处理完成,将计数器清零
if(cnt_byte == 19)
cnt_byte <= 0;
else
// 否则计数器加 1
cnt_byte <= cnt_byte + 1;
end
UDP_HEAD:begin
// 如果 UDP 首部处理完成,将计数器清零
if(cnt_byte == 7)
cnt_byte <= 0;
else
// 否则计数器加 1
cnt_byte <= cnt_byte + 1;
end
UDP_DATA:begin
// 如果 UDP 数据处理完成,将计数器清零
if(cnt_byte == ip_data_len - 29)
cnt_byte <= 0;
else
// 否则计数器加 1
cnt_byte <= cnt_byte + 1;
end
default:
// 默认情况下,将计数器清零
cnt_byte <= 0;
endcase
end
end
// 错误检测逻辑,在时钟上升沿检测是否出现错误
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将错误标志清零
error <= 0;
else begin
case (c_state)
PRE_DATA:begin
// 如果前导码中出现非 8'h55 的数据,设置错误标志
if(cnt_byte < 6 && gmii_rxd != 8'h55)
error <= 1;
else
// 否则清除错误标志
error <= 0;
end
ETH_HEAD:begin
// 如果目的 MAC 地址不是板子的 MAC 地址,设置错误标志
if(cnt_byte == 6 && des_mac != BOARD_MAC)
error <= 1;
else if(cnt_byte == 12 && src_mac != pc_mac)
// 如果源 MAC 地址不是 PC 的 MAC 地址,设置错误标志
error <= 1;
else
// 否则清除错误标志
error <= 0;
end
IP_HEAD :begin
// 如果 IP 协议类型不是 UDP(8'h11),设置错误标志
if (cnt_byte == 10 && ip_type != 8'h11) begin
error <=1;
end
else if(cnt_byte == 16 && src_ip != pc_ip)
// 如果源 IP 地址不是 PC 的 IP 地址,设置错误标志
error <= 1;
else
// 否则清除错误标志
error <= 0;
end
default:
// 默认情况下,清除错误标志
error <= 0;
endcase
end
end
// 以太网帧头长度类型字段解析逻辑,在时钟上升沿更新 eth_type
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 eth_type 清零
eth_type <= 0;
else if(c_state == ETH_HEAD && cnt_byte > 11)
// 在以太网帧头处理状态下,当处理到长度类型字段时,更新 eth_type
eth_type <={eth_type[7:0],gmii_rxd};// 先接高位
else
// 否则保持 eth_type 不变
eth_type <= eth_type;
end
// 目的 MAC 地址解析逻辑,在时钟上升沿更新 des_mac
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 des_mac 清零
des_mac <= 0;
else if(c_state == ETH_HEAD && cnt_byte < 6)
// 在以太网帧头处理状态下,当处理到目的 MAC 地址字段时,更新 des_mac
des_mac <={des_mac[39:0],gmii_rxd};// 先接收高位
else
// 否则保持 des_mac 不变
des_mac <= des_mac;
end
// 目的 IP 地址解析逻辑,在时钟上升沿更新 des_ip
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 des_ip 清零
des_ip <= 0;
else if(c_state == IP_HEAD && cnt_byte > 15)
// 在 IP 首部处理状态下,当处理到目的 IP 地址字段时,更新 des_ip
des_ip <={des_ip[23:0],gmii_rxd};// 先接收高位
else
// 否则保持 des_ip 不变
des_ip <= des_ip;
end
// 源 MAC 地址解析逻辑,在时钟上升沿更新 src_mac
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 src_mac 清零
src_mac <= 0;
else if(c_state == ETH_HEAD && cnt_byte > 5 && cnt_byte < 12)
// 在以太网帧头处理状态下,当处理到源 MAC 地址字段时,更新 src_mac
src_mac <={src_mac[39:0],gmii_rxd};// 先接收高位
else
// 否则保持 src_mac 不变
src_mac <= src_mac;
end
// 源 IP 地址解析逻辑,在时钟上升沿更新 src_ip
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 src_ip 清零
src_ip <= 0;
else if(c_state == IP_HEAD && cnt_byte > 11 && cnt_byte <16)
// 在 IP 首部处理状态下,当处理到源 IP 地址字段时,更新 src_ip
src_ip <={src_ip[23:0],gmii_rxd};// 先接收高位
else
// 否则保持 src_ip 不变
src_ip <= src_ip;
end
// IP 协议类型解析逻辑,在时钟上升沿更新 ip_type
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 ip_type 清零
ip_type <= 0;
else if(c_state == IP_HEAD && cnt_byte > 8 && cnt_byte < 10)
// 在 IP 首部处理状态下,当处理到 IP 协议类型字段时,更新 ip_type
ip_type <= gmii_rxd;// 先接收高位
else
// 否则保持 ip_type 不变
ip_type <= ip_type;
end
// IP 数据包总长度解析逻辑,在时钟上升沿更新 ip_data_len
always @(posedge gmii_rxc ) begin
if(!rst_n)
// 复位时,将 ip_data_len 清零
ip_data_len <= 0;
else if(c_state == IP_HEAD && cnt_byte > 1 && cnt_byte <4)
// 在 IP 首部处理状态下,当处理到 IP 数据包总长度字段时,更新 ip_data_len
ip_data_len <= {ip_data_len[7:0],gmii_rxd};// 先接收高位
else
// 否则保持 ip_data_len 不变
ip_data_len <= ip_data_len;
end
// UDP 接收数据和数据有效信号处理逻辑,在时钟上升沿更新 udp_rx_data 和 udp_rx_data_en
always @(posedge gmii_rxc ) begin
if(!rst_n)begin
// 复位时,将 udp_rx_data 清零,数据有效信号置低
udp_rx_data <= 0;
udp_rx_data_en <= 0;
end
else if(c_state == UDP_DATA)begin
// 在 UDP 数据处理状态下,将接收到的数据赋值给 udp_rx_data,数据有效信号置高
udp_rx_data <= gmii_rxd;
udp_rx_data_en <= 1;
end
else begin
// 否则保持 udp_rx_data 不变,数据有效信号置低
udp_rx_data <= udp_rx_data;
udp_rx_data_en <= 0;
end
end
// UDP 接收结束信号处理逻辑,在时钟上升沿更新 udp_rx_done
always @(posedge gmii_rxc ) begin
if(!rst_n)begin
// 复位时,将接收结束信号置低
udp_rx_done <= 0;
end
else if(c_state == UDP_DATA && cnt_byte == ip_data_len - 29 )begin
// 在 UDP 数据处理状态下,当数据处理完成时,将接收结束信号置高
udp_rx_done <= 1;
end
else begin
// 否则将接收结束信号置低
udp_rx_done <= 0;
end
end
endmodule
主要逻辑:
状态机控制:
模块采用多状态机结构依次处理接收流程:
- 检测前导码与帧起始界定符;
- 解析以太网头部,判断是否为IP协议;
- 解析IP头部,判断是否为本机IP且上层协议为UDP;
- 解析UDP头部;
- 提取UDP有效数据;
- 接收完成后进入空闲状态等待下一帧。
数据帧过滤机制:自动过滤非目标MAC地址、非目标IP地址或非UDP协议的数据包,仅接收合法且发往本机的UDP数据。
数据输出控制:在接收到UDP数据段时,输出数据(
udp_rx_data
)并拉高数据有效信号(udp_rx_data_en
),一个完整数据包接收结束后产生完成信号(udp_rx_done
)。错误检测机制:在各解析阶段进行基本校验,若发现异常则置位错误标志并跳过当前帧,保障系统稳定性。
该 udp_rx
接收模块可作为以太网通信系统中UDP协议栈的接收端,实现对标准UDP数据包的完整解析。其功能涵盖从物理层接收到的数据帧中提取有效载荷,并通过逐层校验机制确保数据的合法性。模块可与UDP发送模块(如 udp_tx
)配合,完成双向UDP通信;同时也能与其他网络层模块(如ARP解析模块、IP处理模块等)协同工作,构建完整的UDP通信系统,适用于高速以太网数据传输和嵌入式网络应用开发。
四、辅助模块实现
4.1 ARP协议控制模块:arp_ctrl.v
功能:实现对ARP协议发送与接收的协调控制。该模块根据外部按键信号或接收到的ARP请求,决定是否启动ARP发送模块,并设置相应的发送类型(请求或应答),从而实现完整的ARP交互流程。
代码片段 :
`timescale 1ns / 1ps
//arp协议----控制模块
module arp_ctrl(
input wire gmii_txc ,// 125MHz时钟信号,为模块提供时钟基准,用于同步模块内的时序逻辑
input wire rst_n ,// 复位信号,低电平有效,用于将模块内的寄存器等复位到初始状态
input wire key_flag ,// 按键标志信号,高电平有效,可能由外部按键触发,用于手动启动ARP发送操作
input wire arp_rx_done ,// ARP接收模块的完成信号,高电平表示ARP数据接收完成
input wire arp_rx_type ,// ARP接收模块的类型信号,用于指示接收到的ARP数据的类型(例如请求或应答)
output reg arp_tx_start,// ARP发送启动信号,高电平有效,用于启动ARP发送模块进行数据发送
output reg arp_tx_type // ARP发送类型信号,用于指示ARP发送模块发送数据的类型(例如请求或应答)
);
always @(posedge gmii_txc ) begin
if (!rst_n) begin
arp_tx_start <= 0; // 将ARP发送启动信号置为低电平,即不启动发送
arp_tx_type <= 0; // 将ARP发送类型信号置为低电平,设置初始发送类型
end
else if(key_flag)begin
arp_tx_start <= 1; // 启动ARP发送操作,将发送启动信号置为高电平
arp_tx_type <= 0; // 设置ARP发送类型为0(具体含义需根据协议定义确定)
end
else if(arp_rx_done && arp_rx_type == 0)begin
arp_tx_start <= 1; // 启动ARP发送操作,将发送启动信号置为高电平
arp_tx_type <= 1; // 设置ARP发送类型为1(具体含义需根据协议定义确定)
end
else begin
arp_tx_start <= 0; // 不启动ARP发送操作,将发送启动信号置为低电平
arp_tx_type <= arp_tx_type; // 保持当前的ARP发送类型不变
end
end
endmodule
主要逻辑:
时钟与复位控制:
- 使用
gmii_txc
(125MHz)作为主时钟,确保所有操作与时序同步;- 复位信号
rst_n
低电平有效,用于将模块恢复到初始状态。启动条件判断:
- 当检测到外部按键信号
key_flag
高电平时,主动发起ARP请求(type = 0);- 若接收到ARP请求并完成解析(
arp_rx_done
为高且arp_rx_type
为0),则自动回复ARP应答(type = 1);- 其他情况下保持不发送状态。
输出控制信号:
arp_tx_start
:当满足发送条件时拉高,通知发送模块开始发送;arp_tx_type
:指示发送类型,0表示请求,1表示应答。
该 arp_ctrl
控制模块作为ARP协议栈的核心控制单元,负责触发和调度ARP数据包的发送行为。通过与ARP发送模块(如 arp_tx
)和ARP接收模块(如 arp_rx
)配合,能够构建完整的地址解析机制,是实现以太网通信中MAC地址获取的关键部分。
4.2 控制使能选择模块:en_ctrl.v
功能:实现对ARP与UDP发送数据的仲裁控制,根据当前发送需求选择输出ARP模块或UDP模块的数据与使能信号,确保二者在共享GMII接口时不会冲突。
代码片段 :
`timescale 1ns / 1ps
//控制使能信号----何时输出arp数据,何时输出udp数据
module en_ctrl(
input wire [7:0] arp_gmii_txd , // ARP模块输出的单沿数据,提供给rgmii_to_gmii模块
input wire arp_gmii_tx_en , // ARP模块输出的使能信号,用于控制rgmii_to_gmii模块
input wire [7 :0] udp_gmii_txd ,// UDP模块输出的数据,用于提供给RGMII模块进行单双沿数据转换
input wire udp_gmii_tx_en ,// UDP模块输出的使能信号,用于控制RGMII模块
output wire gmii_tx_en ,// 输出的使能信号,高电平有效,用于控制后续模块
output wire [7:0] gmii_tx_data // 输出的单沿数据,提供给后续模块
);
assign gmii_tx_en = (arp_gmii_tx_en) ? arp_gmii_tx_en : udp_gmii_tx_en; // 根据ARP的使能信号,选择ARP或UDP的使能信号输出
assign gmii_tx_data = (arp_gmii_tx_en) ? arp_gmii_txd : udp_gmii_txd ; // 根据ARP的使能信号,选择ARP或UDP的数据输出
endmodule
主要逻辑:
多路选择机制:
- 当
arp_gmii_tx_en
(ARP发送使能)为高电平时,优先选择ARP模块输出的数据和使能信号;- 否则选择UDP模块输出的数据和使能信号;
- 实现ARP与UDP发送通道之间的互斥切换,防止总线冲突。
信号传递:输出使能信号
gmii_tx_en
和数据信号gmii_tx_data
直接连接至rgmii_to_gmii
模块,用于后续的协议转换和物理层传输。
该 en_ctrl
控制使能信号模块作为以太网通信系统中的发送通道仲裁器,负责协调ARP协议与UDP协议在共用GMII接口时的数据输出顺序。通过简单的优先级判断逻辑(ARP优先于UDP),保障地址解析过程的顺利进行,同时不影响正常的UDP数据传输。适用于FPGA实现的嵌入式以太网控制器、软核网络协议栈等需要多协议复用同一物理接口的场景。
4.3 按键消抖模块:key_fliter.v
功能:实现对外部按键输入的硬件消抖处理,防止因机械抖动导致误触发。输出一个稳定的按键标志信号 key_flag
,可用于启动ARP请求或其他需要按键触发的操作。
代码片段 :
`timescale 1ns / 1ps
// 按键消抖模块
// 使用参数化设计,M表示按键有效电平(0或1),delay表示消抖延时的计数器上限值
module key_fliter #(
parameter M = 0 ,
parameter delay = 100_000_0
)
(
input clk , // 系统时钟信号,用于驱动模块的时序逻辑
input rst_n , // 复位信号,低电平有效,用于将模块复位到初始状态
input key , // 外部输入的按键信号
output key_flag // 经过消抖处理后的按键标志信号,高电平表示按键有效
);
reg [31:0] cnt; // 20ms延时计数器,用于记录按键状态的持续时间,位宽32位
// 时序逻辑,在时钟上升沿或复位信号下降沿触发
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt <= 0; // 复位时,将计数器清零
else if(key == M) // 如果按键信号等于设定的有效电平
if(cnt == delay - 1)
cnt <= cnt; // 当计数器达到延时上限时,保持计数器值不变
else
cnt <= cnt + 1; // 否则计数器递增
else
cnt <= 0; // 如果按键信号不是有效电平,将计数器清零
end
// 组合逻辑,根据计数器的值生成按键标志信号
assign key_flag = (cnt == delay - 2) ? 1'b1 : 1'b0; // 当计数器值等于延时上限减2时,认为按键稳定按下,输出按键有效标志
endmodule
主要逻辑:
参数化设计:
M
:定义按键有效电平(高电平或低电平有效);delay
:设定消抖计数阈值,决定消抖时间(例如对应20ms);消抖机制:
- 当检测到按键输入
key
处于有效电平(由M
决定)时,启动内部计数器;- 若按键持续稳定在有效电平达到预设时间(
delay
),则判定为有效按键;- 否则计数器清零,避免误触发;
- 在计数接近完成时(如
delay - 2
),输出标志信号key_flag
置高,表示按键有效按下。输出控制:
key_flag
:用于指示按键是否已稳定按下,常用于触发后续逻辑操作(如ARP发送请求)。
该 key_fliter
按键消抖模块是嵌入式系统中常见的按键处理单元,特别适用于FPGA项目中对物理按键进行软件防抖处理的场景。通过参数配置,可灵活适配不同按键特性及系统时钟频率。在以太网通信系统中,该模块通常用于检测用户发起ARP请求的按键动作,确保只在按键稳定时才触发网络操作,提高系统的稳定性和可靠性。
五、约束文件
约束文件说明:pin.xdc
约束文件是为FPGA设计中的引脚与时序约束配置,用于指导综合工具将设计中的信号正确映射到物理引脚,并对关键路径进行时序分析和优化。
# pin.xdc文件,用于对FPGA引脚进行约束设置
# 设置rgmii_rxc_0端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports rgmii_rxc_0]
# 设置rgmii_txc_0端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports rgmii_txc_0]
# 将rgmii_rxc_0端口绑定到FPGA封装的L16引脚
set_property PACKAGE_PIN L16 [get_ports rgmii_rxc_0]
# 将rgmii_txc_0端口绑定到FPGA封装的J14引脚
set_property PACKAGE_PIN J14 [get_ports rgmii_txc_0]
# 设置rst_n_0端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports rst_n_0]
# 设置rgmii_rxd_0[3]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_rxd_0[3]}]
# 设置rgmii_rxd_0[2]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_rxd_0[2]}]
# 设置rgmii_rxd_0[1]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_rxd_0[1]}]
# 设置rgmii_rxd_0[0]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_rxd_0[0]}]
# 设置rgmii_data_0[3]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_data_0[3]}]
# 设置rgmii_data_0[2]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_data_0[2]}]
# 设置rgmii_data_0[1]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_data_0[1]}]
# 设置rgmii_data_0[0]端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports {rgmii_data_0[0]}]
# 将rst_n_0端口绑定到FPGA封装的U19引脚
set_property PACKAGE_PIN U19 [get_ports rst_n_0]
# 将rgmii_rxd_0[0]端口绑定到FPGA封装的L20引脚
set_property PACKAGE_PIN L20 [get_ports {rgmii_rxd_0[0]}]
# 将rgmii_rxd_0[1]端口绑定到FPGA封装的K19引脚
set_property PACKAGE_PIN K19 [get_ports {rgmii_rxd_0[1]}]
# 将rgmii_rxd_0[2]端口绑定到FPGA封装的J18引脚
set_property PACKAGE_PIN J18 [get_ports {rgmii_rxd_0[2]}]
# 将rgmii_rxd_0[3]端口绑定到FPGA封装的J20引脚
set_property PACKAGE_PIN J20 [get_ports {rgmii_rxd_0[3]}]
# 将rgmii_data_0[0]端口绑定到FPGA封装的N16引脚
set_property PACKAGE_PIN N16 [get_ports {rgmii_data_0[0]}]
# 将rgmii_data_0[1]端口绑定到FPGA封装的J19引脚
set_property PACKAGE_PIN J19 [get_ports {rgmii_data_0[1]}]
# 将rgmii_data_0[2]端口绑定到FPGA封装的H20引脚
set_property PACKAGE_PIN H20 [get_ports {rgmii_data_0[2]}]
# 将rgmii_data_0[3]端口绑定到FPGA封装的N15引脚
set_property PACKAGE_PIN N15 [get_ports {rgmii_data_0[3]}]
# 设置key_0端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports key_0]
# 设置rgmii_rx_ctrl_0端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports rgmii_rx_ctrl_0]
# 设置rgmii_tx_ctrl_0端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports rgmii_tx_ctrl_0]
# 将key_0端口绑定到FPGA封装的M20引脚
set_property PACKAGE_PIN M20 [get_ports key_0]
# 将rgmii_rx_ctrl_0端口绑定到FPGA封装的L17引脚
set_property PACKAGE_PIN L17 [get_ports rgmii_rx_ctrl_0]
# 将rgmii_tx_ctrl_0端口绑定到FPGA封装的K14引脚
set_property PACKAGE_PIN K14 [get_ports rgmii_tx_ctrl_0]
# 设置key_1端口的输入输出标准为LVCMOS33
set_property IOSTANDARD LVCMOS33 [get_ports key_1]
# 将key_1端口绑定到FPGA封装的M19引脚
set_property PACKAGE_PIN M19 [get_ports key_1]
# 创建一个名为rgmii_rxc_0的时钟约束
# 时钟周期为8.000ns,即频率为125MHz
# 时钟波形的高电平起始时间为0.000ns,高电平持续时间为4.000ns
# 约束对象为rgmii_rxc_0端口
create_clock -period 8.000 -name rgmii_rxc_0 -waveform {0.000 4.000} [get_ports rgmii_rxc_0]
5.1 引脚分配设置
引脚分配与IO标准设置
功能描述:
- 将顶层模块中定义的各个端口信号绑定到FPGA芯片的具体物理引脚;
- 设置每个引脚的输入输出电平标准为
LVCMOS33
(即3.3V LVTTL兼容电平);- 确保外部接口如RGMII、复位按键、控制信号等能正确连接至外围电路或PHY芯片。
主要配置内容:
- RGMII时钟信号(
rgmii_rxc_0
、rgmii_txc_0
)分别绑定到引脚L16
和J14
;- 复位信号
rst_n_0
绑定到U19
;- 接收数据信号
rgmii_rxd_0[3:0]
分别绑定到L20
,K19
,J18
,J20
;- 发送数据信号
rgmii_data_0[3:0]
分别绑定到N16
,J19
,H20
,N15
;- 控制信号
rgmii_rx_ctrl_0
和rgmii_tx_ctrl_0
分别绑定到L17
和K14
;- 按键输入
key_0
和key_1
分别绑定到M20
和M19
。
5.2 时钟约束设置
功能描述:
- 定义系统主时钟的频率与时序特性,便于时序分析工具进行建立/保持时间检查;
- 保证高速通信接口(如RGMII)在125MHz时钟下的稳定运行。
主要配置示例:
create_clock -period 8.000 -name rgmii_rxc_0 -waveform {0.000 4.000} [get_ports rgmii_rxc_0]
- 表示
rgmii_rxc_0
为周期8ns(即频率125MHz)的时钟信号;- 高电平持续时间为4ns,占空比为50%;
- 是整个以太网通信系统的关键时序参考源。
5.3 应用说明
该约束文件通过合理的引脚分配和时钟约束,适用于基于Xilinx FPGA平台的以太网通信系统开发,具体说明
- 可确保RGMII接口与外部PHY芯片之间的时序匹配;
- 提高系统稳定性,避免因信号延迟不一致导致的数据采样错误;
- 支持后续时序分析与优化,提升设计的可移植性与工程维护效率。
在实际的FPGA项目开发中,根据所用FPGA型号和开发板布局,调整引脚编号与封装信息非常关键。可以先查阅官方文档了解引脚定义,然后在进行引脚分配时,考虑布局以避免干扰。同时需确保电源和地引脚规划正确,并通过约束文件锁定设置,从而保障系统稳定运行。
六、综合设计
6.1 波形图
SIMULATION > Run Simulation
从图中可以看到以下信息:
1. 信号名称与状态
- gmii_rxc: 这个信号在大部分时间里保持高电平(1),可能是一个时钟信号。
- rst_n: 复位信号,在整个仿真过程中一直为高电平(1),表示系统没有被复位。
- gmii_rx_en: 接收使能信号,同样在整个仿真过程中保持高电平(1),表明接收功能始终是启用的。
- gmii_rxd[7:0]: 这是一个8位的数据信号,显示了数据传输的过程。可以看到它在某些时刻有数据传输活动,例如传输了“55”、“00”等数据值。
- pc_mac[47:0] 和 pc_ip[31:0]: 分别代表MAC地址和IP地址,它们在整个仿真过程中保持不变,分别为
40c2ba578b3f
和c0a80003
。- udp_rx_data[7:0]: UDP接收数据信号,与
gmii_rxd[7:0]
类似,也展示了数据传输过程。- udp_rx_data_en 和 udp_rx_done: 这两个信号分别表示UDP数据接收使能和完成状态。
udp_rx_data_en
在某些时刻变为高电平,表示数据接收使能;而udp_rx_done
在数据传输完成后变为高电平,表示数据接收完成。- IP_DATA_LEN[31:0]: 表示IP数据长度,在某个时刻显示为
0000002e
,即十进制的46。2. 数据传输过程
- 在大约200,000 ns到300,000 ns之间,
gmii_rxd[7:0]
和udp_rx_data[7:0]
都显示了数据传输活动,传输了诸如“55”、“00”等数据值。udp_rx_data_en
在数据传输期间变为高电平,表示数据接收使能。- 数据传输完成后,
udp_rx_done
变为高电平,表示数据接收完成。3. 时序关系
- 各个信号之间的时序关系清晰可见,例如
gmii_rxc
作为时钟信号,控制着数据传输的节奏。gmii_rx_en
和rst_n
在整个仿真过程中保持稳定,确保了数据传输的正常进行。4. 网络通信相关参数
- MAC地址和IP地址的固定值表明这是一个特定设备在网络中的唯一标识。
- UDP数据的传输过程和相关控制信号的变化,反映了网络通信协议的具体实现细节。
通过这张波形仿真图,可以详细地了解数字电路在模拟环境下的行为,对于调试和验证设计具有重要意义。
6.2 电路图
RTL ANALYSIS > Open Elaborated Design
从图中可以看到以下信息:
输入缓冲器(IBUF):图中有多个标记为
IBUF
的三角形符号,它们代表输入缓冲器。这些缓冲器用于接收外部输入信号,并将其传递到内部逻辑电路中。例如,key_0_IBUF_inst
、key_1_IBUF_inst
等都是输入缓冲器实例。输出缓冲器(OBUF):图中也有几个标记为
OBUF
的三角形符号,它们代表输出缓冲器。这些缓冲器用于将内部逻辑电路的输出信号传输到外部。例如,rgmii_data_0[3:0]_OBUF_inst
、rgmii_tx_ctrl_0_OBUF_inst
等都是输出缓冲器实例。模块实例:图中有一个名为
arp_top_i
的矩形框,这代表一个顶层模块实例。这个模块包含了多个输入和输出端口,如key_0
、key_1
、rgmii_rx_ctrl_0
等。这些端口与输入缓冲器和输出缓冲器相连,形成了完整的信号路径。信号线:绿色的线条表示信号的连接关系。通过观察这些线条,可以追踪信号从输入到输出的完整路径。
统计信息:图的顶部显示了当前设计的一些统计信息,包括“16 Cells”、“15 I/O Ports”和“30 Nets”。这些信息分别表示设计中包含的单元数量、输入/输出端口数量以及网络(即信号线)的数量。
总的来说,这张原理图展示了FPGA设计中的信号流向和逻辑结构,帮助工程师理解和调试电路的行为。
七、网络测试
7.1 测试工具
这里我们使用一款网络协议分析工具:Wireshark;Wireshark 是一款功能强大、广受欢迎的开源网络协议分析工具,帮助网络管理员、安全专家和开发人员深入了解网络流量,诊断网络问题,确保网络安全。它提供实时数据捕获、支持数百种协议的强大过滤器、多种协议解析、支持多种捕获文件格式(如 pcap 和 pcapng),并具备数据去重与合并等功能。
另外,Wireshark 拥有直观的图形界面,便于用户捕获和分析网络中的数据包,广泛应用于网络通信分析与故障排除。无论是网络排障、安全分析、协议开发还是教学用途,Wireshark 都是一个不可或缺的工具。值得注意的是,在使用 Wireshark 捕获网络流量时,应确保在合法授权范围内操作,以避免隐私和法律问题。
你可以从官网获取最新版本:
https://www.wireshark.orghttps://www.wireshark.org/其源代码托管在 GitHub 上,地址为:
GitHub - Wiresharkhttps://github.com/wireshark/wireshark
7.2 具体使用
首先,在使用软件时,需将IP 分配设置为手动,并配置好地址和掩码
这里将代码上板后,当按键按下时,这里的 ff ff ff ff ff ff 就是我们的广播,就是我们发送的请求包,具体含义
- ff ff ff ff ff ff是目的MAC地址,以广播的形式进行发送;
- 后面的 00 11 22 33 44 55 是我们板子的MAC地址;
- 08 06 是我们的协议类型,这里表示ARP协议;
- 00 01 是以太网类型;08 00 是协议类型;
- 06 是MAC地址长度;04 是IP地址长度;
- 00 01 是数据包类型,OP;
- 22 33 44 55 是电脑源MAC地址;
- c0 a8 00 03 是电脑源IP地址;
- ff ff ff ff ff 是目的MAC地址;
- c0 a8 00 02 是目的IP地址;
- 最后是00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00占位18个零。
这就是ARP协议,以太网帧,这一帧发送出去所包含的数据,如图所示:
我们给电脑一个请求包之后,电脑会给我们一个应答包,就是将我们电脑的相关数据发送给FPGA:
- 00 11 22 33 44 55目的MAC地址,也就是我们的FPGA;
- 90 2E 16 DB C5 19是源MAC地址,也就是我们电脑的MAC地址;可以看到电脑的MAC地址与发送的应答包源MAC地址,保持一致。
- 其它同样......
这里是ARP协议,以太网帧,在接收数据包后,响应的应答包所包含的数据,如图所示:
这里我们可以通过Win+R输入cmd打开命令提示符窗口,然后再输入以下命令,查看交互状态和交互信息:
arp -a
第一个接口是WALN相关信息;第二个接口是FPGA板子的相关信息,有这个地址就说明电脑和板子建立好了连接,可以交互,如图所示:
八、注意事项
在基于上述模块构建和实现网络通信系统的过程中,有一些关键的注意事项需要特别关注,以确保系统的稳定性、兼容性和可扩展性。
时序约束与同步设计
网络通信模块通常涉及高速数据传输,尤其在RGMII接口等模块中对时钟同步要求极高。在FPGA或ASIC实现中,必须合理设置时序约束,避免跨时钟域导致的数据不稳定或亚稳态问题。协议标准一致性
ARP、UDP等模块应严格遵循IEEE或IETF定义的标准协议格式与行为规范,确保与其他设备之间的互操作性,特别是在字段顺序、校验和计算等方面不能有偏差。资源占用与优化平衡
在硬件实现中,各模块会消耗一定的逻辑资源(如LUT、FF、BRAM等),需根据目标平台性能进行合理评估与优化,尤其是在多通道或多协议并行处理时,更应注意资源分配和带宽管理。调试与验证机制
建议为每个模块提供完善的测试接口或状态寄存器,便于在线调试和故障排查。同时,在仿真阶段应覆盖各种边界条件和异常场景(如丢包、错序、超时等),提升系统的鲁棒性。可扩展性与模块化设计
模块之间应保持良好的接口隔离和功能解耦,以便未来扩展其他协议(如TCP、ICMP、IP分片重组等)时能够灵活接入,降低集成成本。安全与过滤机制
在实际应用中,应考虑添加必要的数据过滤与安全机制,例如ARP欺骗检测、非法IP或端口过滤等,防止恶意攻击或无效流量对系统造成影响。
综上所述,在开发和部署该网络通信系统时,不仅要关注各个模块的功能实现,还需从整体架构、性能优化、安全性及可维护性等多个维度综合考量,确保系统在复杂网络环境中的高效稳定运行。
九、本文总结
本文围绕网络通信系统中的核心模块进行了分类、介绍与功能实现,涵盖了地址解析、物理接口转换、传输层协议处理以及各类辅助控制模块。这些模块共同构建了一个完整的网络数据处理架构,实现了从底层硬件交互到高层协议解析的全流程通信能力。整个系统主要分为几个关键功能区:地址解析、接口转换、传输协议处理以及辅助控制。
- 首先,地址解析部分实现了ARP协议的功能,负责IP地址与MAC地址之间的映射,确保数据能够准确无误地发送至目标设备。这一过程对于网络通信至关重要,因为它解决了不同网络层之间的地址识别问题。
- 其次,接口转换模块专注于物理层接口间的适配和转换工作,特别是RGMII到GMII的转换,使得不同的硬件设备能够在物理层面上实现互联互通。这对于保证网络数据流在不同设备间顺利传输具有重要意义。
- 再者,传输协议处理模块主要关注于UDP协议的实现,它提供了轻量级的、不可靠的用户数据报文传输服务。该模块确保了应用层数据可以高效地在网络中进行传输,并且根据需要对数据报文进行构建和解析。
- 最后,辅助模块提供了多种支持功能,如控制逻辑管理、信号处理等,它们虽然不直接参与数据的发送和接收,但对于系统的稳定运行不可或缺。这些模块帮助协调各个核心模块的工作,提供必要的管理和优化功能。
这个网络协议处理系统通过各个模块间的紧密协作,实现了从底层硬件接口到高层协议处理的全方位覆盖,从而支持了复杂而高效的网络通信。此外,除了上述列出的模块外,实际系统中还可能存在其他辅助模块,比如时钟管理模块、复位管理模块等,它们共同协作以保证整个系统的稳定运行。每个模块都专注于特定的任务,通过彼此之间的协调合作来实现复杂网络通信协议栈的功能。
十、更多操作
完整FPGA系列,请看