FPGA 40 ,基于 RGMII 的 ARP 与 UDP 协议栈实战:从物理层到传输层的网络通信及 Wireshark 网络分析工具( FPGA网络通信子系统 )

目录

前言
一、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

主要逻辑:

  1. 接收方向:从RGMII接口接收由PHY芯片提供的双沿数据信号,并将其转换为FPGA可以处理的单沿GMII信号。
  2. 发送方向:将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

主要逻辑

  1. 时钟连接:将 FPGA 提供的 125MHz GMII 发送时钟 gmii_txc 直接赋值给 RGMII 接口的时钟信号 rgmii_txc
  2. 控制信号转换:使用 ODDR 原语 将单沿的使能信号 gmii_tx_en 转换为双沿输出信号 rgmii_tx_ctrl
  3. 数据转换:通过 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

主要逻辑

  1. 时钟处理:对输入的RGMII时钟信号进行缓冲处理,确保生成的GMII时钟信号能够稳定地支持后续的数据处理流程。

  2. 控制信号转换:接收由PHY芯片提供的控制信号(rgmii_rx_ctrl),该信号是使能信号和错误信号的异或值,并将其从双沿格式转换为单沿格式,以产生GMII接口使用的使能信号(gmii_rx_en)。

  3. 数据转换:将来自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

主要逻辑:

  1. 时钟信号:使用gmii_txc作为125MHz的时钟信号,同时该信号也被用作接收时钟gmii_rxc
  2. 复位信号:低电平有效的复位信号rst_n用于初始化模块状态。
  3. 发送功能
    1. 实例化arp_tx模块,负责构造并发送ARP请求或响应。通过本地PC的MAC地址pc_mac和IP地址pc_ip构建ARP数据包,并通过GMII接口发送出去。
    2. 在发送过程中,利用CRC32校验来确保数据的完整性。
  4. 接收功能:实例化arp_rx模块,用于接收来自网络的ARP数据包。根据本地PC的MAC和IP地址验证接收到的数据包,并判断其类型(请求或响应)。
  5. 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 协议各字段中承担着重要作用:

  1. 8'h00:十六进制的 0,转换为二进制是00000000,在数值上表示最小的 8 位无符号整数。
  2. 8'h01:十六进制的 1,二进制为00000001,表示比 0 大 1 的数。
  3. 8'h02:十六进制的 2,二进制是00000010。
  4. 8'h04:十六进制的 4,二进制为00000100,是 2 的平方。
  5. 8'h06:十六进制的 6,二进制是00000110,数值上比 4 大 2。
  6. 8'h08:十六进制的 8,二进制为00001000,是 2 的三次方,也是 10 进制的 8。

这些 8 位十六进制数值,从定义数据包类型、标识地址长度,到区分请求与应答操作,每一个数值都精准地承载着 ARP 协议规范赋予的特定功能。正是这些数值的有序组合与准确赋值,确保了 ARP 数据包能够在以太网环境中被正确解析与处理,实现 IP 地址与 MAC 地址之间的高效映射与通信。

主要逻辑

  1. 状态机控制发送流程

    1. 使用五段式状态机完成数据发送控制,各状态如下:
      1. IDLE:空闲状态,等待发送启动信号;
      2. PRE_DATA:发送前导码和帧起始界定符;
      3. ETH_HEAD:发送以太网帧头(包含目的MAC、源MAC、协议类型);
      4. ARP_DATA:发送ARP协议数据字段(28字节有效数据 + 填充);
      5. CRC:发送CRC32校验结果(4字节)。
    2. 字节计数器 cnt_byte 控制每个状态发送的字节数。
  2. 数据封装

    1. 定义本地MAC地址和IP地址作为默认值;
    2. 根据 arp_tx_type 判断是发送请求包还是响应包,动态修改目的MAC和ARP操作码;
    3. 构造完整的ARP数据包并依次发送。
  3. CRC32校验处理

    1. 在CRC状态发送计算后的CRC校验码;
    2. 提供 crc_en 和 crc_clr 信号用于控制CRC模块的使能与复位;
    3. CRC校验数据来自 crc_data 和 crc_next 输入,按字节顺序反向输出。
  4. 输出控制信号

    1. arp_gmii_tx_en:在非空闲状态下拉高,表示数据有效;
    2. 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

主要逻辑

  1. 状态机控制接收流程

    1. 使用五段式状态机依次处理不同阶段的数据接收:
      1. IDLE:空闲状态,等待有效数据到来;
      2. PRE_DATA:接收前导码和帧起始界定符;
      3. ETH_HEAD:接收以太网头部(目的MAC、源MAC、协议类型);
      4. ARP_DATA:接收ARP协议数据字段(28字节有效数据);
      5. ARP_END:接收结束,等待下一帧。
    2. 字节计数器 cnt_byte 控制每个状态接收的字节数。
  2. 错误检测机制

    1. 在接收过程中通过 error 标志判断是否存在格式错误或非目标地址;
    2. 若发现目的MAC既不是本地MAC也不是广播地址,则视为无效帧,跳转至结束状态。
  3. 关键字段提取

    1. 提取目的MAC地址 des_mac 和目的IP地址 des_ip
    2. 提取OP操作码 OP_data,用于判断是请求包还是应答包;
    3. 提取源MAC地址 pc_mac 和源IP地址 pc_ip,供后续处理使用。
  4. 输出控制信号

    1. arp_rx_type:标志接收到的是ARP请求(0)还是应答(1);
    2. 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

主要逻辑:

  1. 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
  2. 状态控制信号

    1. crc_clr:复位信号,将 crc_data 恢复为初始值 32'hffffffff
    2. crc_en:使能信号,控制是否进行CRC计算;
  3. 数据处理方式

    1. 输入数据 data[7:0] 在进入计算前进行高低位翻转,确保符合CRC32逐字节处理时的顺序要求;
    2. 输出两个结果:
      1. crc_data:当前CRC值;
      2. crc_next:下一周期CRC值,供外部模块使用(如 arp_tx 中在发送CRC字段时输出反向CRC值)。
  4. 组合逻辑实现CRC计算

    1. 所有32位CRC输出通过组合逻辑表达式逐位计算,基于当前 crc_data 和输入数据 data_t 得出;
    2. 实现方式高效,适用于高速通信场景下的实时校验。
  5. 初始化与复位机制

    1. 初始值为全1(32'hffffffff),这是CRC32标准推荐的初始化值;
    2. 当 crc_clr 有效时,重新初始化CRC寄存器。

接口说明:

端口名方向位宽描述
clk输入1 bit主时钟信号
rst_n输入1 bit异步复位信号,低电平有效
data输入8 bits需要校验的数据
crc_en输入1 bitCRC计算使能信号
crc_clr输入1 bitCRC值清零信号
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

主要逻辑:

  1. 状态机设计

    1. 模块采用一个7段式状态机来管理从空闲到数据发送完成的整体流程。
    2. 状态包括:空闲(IDLE)、计算校验和(CHECK_SUM)、前导码发送(PRE_DATA)、以太网头发送(ETH_HEAD)、IP头发送(IP_HEAD)、UDP数据发送(UDP_DATA)以及CRC校验值发送(CRC)。
  2. 数据准备阶段

    1. 在进入数据发送流程之前,先初始化必要的参数如MAC地址、IP地址等,并填充对应的头部字段。
    2. 根据不同的网络层需求配置源/目的MAC地址、源/目的IP地址、UDP端口号等。
  3. 发送过程

    1. 前导码及帧起始界定符:在PRE_DATA状态下,发送7字节的前导码(0x55)和1字节的帧起始界定符(0xD5)。
    2. 以太网头部:在ETH_HEAD状态下,发送包含目标和源MAC地址以及类型字段的以太网头部(共14字节)。
    3. IP头部:在IP_HEAD状态下,发送包括版本号、首部长度、总长度、TTL、协议类型、源/目的IP地址在内的IP头部(通常为20字节)。
    4. UDP头部与数据:在UDP_DATA状态下,发送UDP头部(8字节)及用户数据。
    5. CRC校验值:最后,在CRC状态下,发送4字节的CRC校验值。此值由外部CRC计算模块提供,并在此过程中反向输出。
  4. 控制信号

    1. udp_tx_start: 启动UDP数据包发送过程。
    2. udp_gmii_tx_en: 控制GMII接口上的数据传输使能。
    3. 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

主要逻辑:

  1. 状态机控制

    模块采用多状态机结构依次处理接收流程:

    1. 检测前导码与帧起始界定符;
    2. 解析以太网头部,判断是否为IP协议;
    3. 解析IP头部,判断是否为本机IP且上层协议为UDP;
    4. 解析UDP头部;
    5. 提取UDP有效数据;
    6. 接收完成后进入空闲状态等待下一帧。
  2. 数据帧过滤机制:自动过滤非目标MAC地址、非目标IP地址或非UDP协议的数据包,仅接收合法且发往本机的UDP数据。

  3. 数据输出控制:在接收到UDP数据段时,输出数据(udp_rx_data)并拉高数据有效信号(udp_rx_data_en),一个完整数据包接收结束后产生完成信号(udp_rx_done)。

  4. 错误检测机制:在各解析阶段进行基本校验,若发现异常则置位错误标志并跳过当前帧,保障系统稳定性。

该 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

主要逻辑:

  • 时钟与复位控制

    1. 使用 gmii_txc(125MHz)作为主时钟,确保所有操作与时序同步;
    2. 复位信号 rst_n 低电平有效,用于将模块恢复到初始状态。
  • 启动条件判断

    1. 当检测到外部按键信号 key_flag 高电平时,主动发起ARP请求(type = 0);
    2. 若接收到ARP请求并完成解析(arp_rx_done 为高且 arp_rx_type 为0),则自动回复ARP应答(type = 1);
    3. 其他情况下保持不发送状态。
  • 输出控制信号

    1. arp_tx_start:当满足发送条件时拉高,通知发送模块开始发送;
    2. 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

主要逻辑:

  • 多路选择机制

    1. 当 arp_gmii_tx_en(ARP发送使能)为高电平时,优先选择ARP模块输出的数据和使能信号;
    2. 否则选择UDP模块输出的数据和使能信号;
    3. 实现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

主要逻辑:

  • 参数化设计

    1. M:定义按键有效电平(高电平或低电平有效);
    2. delay:设定消抖计数阈值,决定消抖时间(例如对应20ms);
  • 消抖机制

    1. 当检测到按键输入 key 处于有效电平(由 M 决定)时,启动内部计数器;
    2. 若按键持续稳定在有效电平达到预设时间(delay),则判定为有效按键;
    3. 否则计数器清零,避免误触发;
    4. 在计数接近完成时(如 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标准设置

  • 功能描述

    1. 将顶层模块中定义的各个端口信号绑定到FPGA芯片的具体物理引脚;
    2. 设置每个引脚的输入输出电平标准为 LVCMOS33(即3.3V LVTTL兼容电平);
    3. 确保外部接口如RGMII、复位按键、控制信号等能正确连接至外围电路或PHY芯片。
  • 主要配置内容

    1. RGMII时钟信号(rgmii_rxc_0rgmii_txc_0)分别绑定到引脚 L16 和 J14
    2. 复位信号 rst_n_0 绑定到 U19
    3. 接收数据信号 rgmii_rxd_0[3:0] 分别绑定到 L20K19J18J20
    4. 发送数据信号 rgmii_data_0[3:0] 分别绑定到 N16J19H20N15
    5. 控制信号 rgmii_rx_ctrl_0 和 rgmii_tx_ctrl_0 分别绑定到 L17 和 K14
    6. 按键输入 key_0 和 key_1 分别绑定到 M20 和 M19

5.2 时钟约束设置

  • 功能描述

    1. 定义系统主时钟的频率与时序特性,便于时序分析工具进行建立/保持时间检查;
    2. 保证高速通信接口(如RGMII)在125MHz时钟下的稳定运行。
  • 主要配置示例

create_clock -period 8.000 -name rgmii_rxc_0 -waveform {0.000 4.000} [get_ports rgmii_rxc_0]
  1. 表示 rgmii_rxc_0 为周期8ns(即频率125MHz)的时钟信号;
  2. 高电平持续时间为4ns,占空比为50%;
  3. 是整个以太网通信系统的关键时序参考源。

5.3 应用说明

该约束文件通过合理的引脚分配和时钟约束,适用于基于Xilinx FPGA平台的以太网通信系统开发,具体说明

  1. 可确保RGMII接口与外部PHY芯片之间的时序匹配;
  2. 提高系统稳定性,避免因信号延迟不一致导致的数据采样错误;
  3. 支持后续时序分析与优化,提升设计的可移植性与工程维护效率。

在实际的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地址,它们在整个仿真过程中保持不变,分别为40c2ba578b3fc0a80003
  • 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_enrst_n在整个仿真过程中保持稳定,确保了数据传输的正常进行。

4. 网络通信相关参数

  • MAC地址和IP地址的固定值表明这是一个特定设备在网络中的唯一标识。
  • UDP数据的传输过程和相关控制信号的变化,反映了网络通信协议的具体实现细节。

通过这张波形仿真图,可以详细地了解数字电路在模拟环境下的行为,对于调试和验证设计具有重要意义。

6.2 电路图

RTL ANALYSIS > Open Elaborated Design

从图中可以看到以下信息:

  1. 输入缓冲器(IBUF):图中有多个标记为IBUF的三角形符号,它们代表输入缓冲器。这些缓冲器用于接收外部输入信号,并将其传递到内部逻辑电路中。例如,key_0_IBUF_instkey_1_IBUF_inst等都是输入缓冲器实例。

  2. 输出缓冲器(OBUF):图中也有几个标记为OBUF的三角形符号,它们代表输出缓冲器。这些缓冲器用于将内部逻辑电路的输出信号传输到外部。例如,rgmii_data_0[3:0]_OBUF_instrgmii_tx_ctrl_0_OBUF_inst等都是输出缓冲器实例。

  3. 模块实例:图中有一个名为arp_top_i的矩形框,这代表一个顶层模块实例。这个模块包含了多个输入和输出端口,如key_0key_1rgmii_rx_ctrl_0等。这些端口与输入缓冲器和输出缓冲器相连,形成了完整的信号路径。

  4. 信号线:绿色的线条表示信号的连接关系。通过观察这些线条,可以追踪信号从输入到输出的完整路径。

  5. 统计信息:图的顶部显示了当前设计的一些统计信息,包括“16 Cells”、“15 I/O Ports”和“30 Nets”。这些信息分别表示设计中包含的单元数量、输入/输出端口数量以及网络(即信号线)的数量。

总的来说,这张原理图展示了FPGA设计中的信号流向和逻辑结构,帮助工程师理解和调试电路的行为。


七、网络测试

7.1 测试工具

这里我们使用一款网络协议分析工具:WiresharkWireshark 是一款功能强大、广受欢迎的开源网络协议分析工具,帮助网络管理员、安全专家和开发人员深入了解网络流量,诊断网络问题,确保网络安全。它提供实时数据捕获、支持数百种协议的强大过滤器、多种协议解析、支持多种捕获文件格式(如 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 就是我们的广播,就是我们发送的请求包,具体含义

  1. ff ff ff ff ff ff是目的MAC地址,以广播的形式进行发送;
  2. 后面的 00 11 22 33 44 55 是我们板子的MAC地址;
  3. 08 06 是我们的协议类型,这里表示ARP协议;
  4. 00 01 是以太网类型;08 00 是协议类型;
  5. 06 是MAC地址长度;04 是IP地址长度;
  6. 00 01 是数据包类型,OP;
  7. 22 33 44 55 是电脑源MAC地址;
  8. c0 a8 00 03 是电脑源IP地址;
  9. ff ff ff ff ff 是目的MAC地址;
  10. c0 a8 00 02 是目的IP地址;
  11. 最后是00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00占位18个零。

这就是ARP协议,以太网帧,这一帧发送出去所包含的数据,如图所示:

我们给电脑一个请求包之后,电脑会给我们一个应答包,就是将我们电脑的相关数据发送给FPGA:

  1. 00 11 22 33 44 55目的MAC地址,也就是我们的FPGA;
  2. 90 2E 16 DB C5 19是源MAC地址,也就是我们电脑的MAC地址;可以看到电脑的MAC地址与发送的应答包源MAC地址,保持一致。
  3. 其它同样......

这里是ARP协议,以太网帧,在接收数据包后,响应的应答包所包含的数据,如图所示:

这里我们可以通过Win+R输入cmd打开命令提示符窗口,然后再输入以下命令,查看交互状态和交互信息:

arp -a

第一个接口是WALN相关信息;第二个接口是FPGA板子的相关信息,有这个地址就说明电脑和板子建立好了连接,可以交互,如图所示:


八、注意事项

在基于上述模块构建和实现网络通信系统的过程中,有一些关键的注意事项需要特别关注,以确保系统的稳定性、兼容性和可扩展性。

  1. 时序约束与同步设计
    网络通信模块通常涉及高速数据传输,尤其在RGMII接口等模块中对时钟同步要求极高。在FPGA或ASIC实现中,必须合理设置时序约束,避免跨时钟域导致的数据不稳定或亚稳态问题。

  2. 协议标准一致性
    ARP、UDP等模块应严格遵循IEEE或IETF定义的标准协议格式与行为规范,确保与其他设备之间的互操作性,特别是在字段顺序、校验和计算等方面不能有偏差。

  3. 资源占用与优化平衡
    在硬件实现中,各模块会消耗一定的逻辑资源(如LUT、FF、BRAM等),需根据目标平台性能进行合理评估与优化,尤其是在多通道或多协议并行处理时,更应注意资源分配和带宽管理。

  4. 调试与验证机制
    建议为每个模块提供完善的测试接口或状态寄存器,便于在线调试和故障排查。同时,在仿真阶段应覆盖各种边界条件和异常场景(如丢包、错序、超时等),提升系统的鲁棒性。

  5. 可扩展性与模块化设计
    模块之间应保持良好的接口隔离和功能解耦,以便未来扩展其他协议(如TCP、ICMP、IP分片重组等)时能够灵活接入,降低集成成本。

  6. 安全与过滤机制
    在实际应用中,应考虑添加必要的数据过滤与安全机制,例如ARP欺骗检测、非法IP或端口过滤等,防止恶意攻击或无效流量对系统造成影响。

综上所述,在开发和部署该网络通信系统时,不仅要关注各个模块的功能实现,还需从整体架构、性能优化、安全性及可维护性等多个维度综合考量,确保系统在复杂网络环境中的高效稳定运行。


九、本文总结

本文围绕网络通信系统中的核心模块进行了分类、介绍与功能实现,涵盖了地址解析、物理接口转换、传输层协议处理以及各类辅助控制模块。这些模块共同构建了一个完整的网络数据处理架构,实现了从底层硬件交互到高层协议解析的全流程通信能力。整个系统主要分为几个关键功能区:地址解析、接口转换、传输协议处理以及辅助控制。

  1. 首先,地址解析部分实现了ARP协议的功能,负责IP地址与MAC地址之间的映射,确保数据能够准确无误地发送至目标设备。这一过程对于网络通信至关重要,因为它解决了不同网络层之间的地址识别问题。
  2. 其次,接口转换模块专注于物理层接口间的适配和转换工作,特别是RGMII到GMII的转换,使得不同的硬件设备能够在物理层面上实现互联互通。这对于保证网络数据流在不同设备间顺利传输具有重要意义。
  3. 再者,传输协议处理模块主要关注于UDP协议的实现,它提供了轻量级的、不可靠的用户数据报文传输服务。该模块确保了应用层数据可以高效地在网络中进行传输,并且根据需要对数据报文进行构建和解析。
  4. 最后,辅助模块提供了多种支持功能,如控制逻辑管理、信号处理等,它们虽然不直接参与数据的发送和接收,但对于系统的稳定运行不可或缺。这些模块帮助协调各个核心模块的工作,提供必要的管理和优化功能。

这个网络协议处理系统通过各个模块间的紧密协作,实现了从底层硬件接口到高层协议处理的全方位覆盖,从而支持了复杂而高效的网络通信。此外,除了上述列出的模块外,实际系统中还可能存在其他辅助模块,比如时钟管理模块、复位管理模块等,它们共同协作以保证整个系统的稳定运行。每个模块都专注于特定的任务,通过彼此之间的协调合作来实现复杂网络通信协议栈的功能。


十、更多操作

完整FPGA系列,请看

FPGA系列,文章目录https://blog.csdn.net/weixin_65793170/article/details/144185217?spm=1001.2014.3001.5502https://blog.csdn.net/weixin_65793170/article/details/144185217?spm=1001.2014.3001.5502

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北城笑笑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值