跳转至

千兆以太网通信

千兆以太网通信笔记,包含内容较多,包括以太网基础知识、Wireshark抓包抓取绑定MAC地址、RGMII接收模块、RGMII发送模块、UDP协议栈架构编写、MAC接收模块、MAC发送模块、CRC校验模块、MAC顶层以及实例化CRC校验模块、mac to arp ip模块代码、ip接收与发送模块代码、UDP接收与发送模块代码、UDP回环实验仿真以及上班验证、Wireshark抓取arp数据包、arp协议层代码设计、ICMP协议层代码设计以及时序约束-跨时钟域约束、

然后通过使用以太网在LCD屏幕显示主机屏幕画面和ADC采集存储系统两个项目实验作为本节的结束(包括介绍VGA显示模块和ADC采集存储模块)。

后续会添加使用AXI4_DDR3对数据进行缓存处理,图像进行乒乓操作防止画面撕裂,以及图像处理和数字信号处理相关的内容。这是一个较大的工程项目。

祝愿本次项目我能够坚持下去并顺利完结,通过更新笔记内容对自己进行督促。

1. 千兆以太网

1.1 以太网基础知识

使用FPGA与主机通过以太网交互信息在工业上是一种非常常用的手段,得益于基于FPGA的以太网传输系统可以实现微秒级的延时,满足工业自动化中快速反馈和精确控制的需求,FPGA通过RJ45网口(我们主机和FPGA上插入网线的元件)与板载的PHY芯片(网络通信设备中的核心组件)与我们主机的RJ45和PHY芯片进行传输信息,在这期间涉及到组包和解包(OSI七层模型)的过程,

1.1.1 PHY芯片介绍

PHY芯片(Physical Layer Chip)是网络通信设备中的核心组件,用于实现OSI模型中物理层的功能,主要负责数据信号在物理介质(如双绞线、光纤等)上的传输和接收。以下是关于PHY芯片的详细介绍:

1. 功能
  • 信号转换:将数字信号转换为适合在物理介质上传输的模拟信号,并在接收端将模拟信号还原为数字信号。
  • 自动协商:支持自动协商功能,能够根据对端设备自动调整传输速率(如10Mbps、100Mbps、1Gbps等)和工作模式。
  • 时钟提取:从接收到的数据中提取时钟信号,以同步发送和接收。
  • 差分信号传输:支持差分信号传输,减少干扰和误码率。
  • 故障诊断:支持故障诊断和报告功能,帮助快速定位网络问题。
  • 节能功能:提供低功耗模式,降低设备能耗。
2. 工作原理

PHY芯片的工作流程主要包括:

  1. 将来自MAC层的并行数据转换为串行数据。
  2. 对数据进行编码(如4B/5B编码)并调制为模拟信号发送出去。
  3. 在接收端,将模拟信号解调为数字信号,解码后恢复为原始数据。
3. 技术规格
  • 传输速率:支持多种速率,包括10Mbps、100Mbps、1Gbps、2.5Gbps、5Gbps和10Gbps。
  • 接口类型:支持MII、RMII、GMII等接口,用于与MAC层通信。
  • 传输介质:支持双绞线、光纤等多种物理介质。
  • 低功耗设计:采用先进的低功耗架构,适合对功耗敏感的应用场景。
4. 应用场景

PHY芯片广泛应用于以下领域:

  • 数据中心网络:用于服务器、交换机和路由器之间的高速数据传输。
  • 企业局域网:连接计算机、打印机、IP电话等设备。
  • 家庭网络:支持家庭路由器和交换机。
  • 工业控制网络:用于传感器、执行器和控制器之间的通信。
  • 汽车电子:支持车载以太网,如辅助驾驶和液晶仪表盘。
5. 选型要点
  • 速率支持:根据应用需求选择支持特定速率的PHY芯片。
  • 功耗:优先选择低功耗设计的芯片。
  • 兼容性:确保PHY芯片与MAC层及其他网络设备兼容。
  • 可靠性:考虑芯片的抗干扰能力和稳定性。

总之,PHY芯片在网络通信中扮演着不可或缺的角色,其性能和功能直接影响网络的稳定性和传输效率

1.1.2 OSI七层模型介绍

以下图片解释了OSI七层模型的内容,在我们FPGA中只需要关注1,2,3,4层内容即可,即物理层、数据链路层、网络层和传输层。

OSI七层模型功能协议对照表
OSI层 功能 TCP/IP协议
应用层(Application layer) 文件传输、电子邮件、文件服务、虚拟终端 TFTP/HTTP/SNMP/FTP/SMTP/DNS/Telnet
表示层(Presentation layer) 数据格式化、代码转换、数据加密 DECnet NSP/LPP/XDP/IPX
会话层(Session layer) 解除或建立与其他接点的联系 SSL/TLS/DAP/RPC/
传输层(Transport layer) 提供端对端的接口 TCP/UDP
网络层(Network layer) 为数据包选择路由,控制子网运行,逻辑编址 IP/ICMP/RIP/OSPF/BGP/IGMP
数据链路层(Data link layer) 物理寻址,传输有地址的帧,错误检测功能 以太网、网卡、交换机、PPTP、ARP、ATMP等
物理层(Physical layer) 以二进制比特流数据形式在物理媒体上传输数据,机械电子接口 IEEE802/Ethernet v.2

小白FPGA讲义

传输方式,即通过用户数据,通过传输层后加入UDP头部,再通过网络层加入IP头部,然后通过数据链路层加入MAC头部,最后进入物理层将数据生成对应的比特流输出给主机。具体传输方式如下

小白FPGA讲义

1.1.3 GMII和RGMII接口介绍

GMII接口介绍

GMII(Gigabit Media Independent Interface,千兆媒体独立接口)是一种用于千兆以太网(1000Mbps)的接口标准,用于连接MAC(媒体访问控制)层和PHY(物理层)芯片。

主要特点
  1. 数据速率:支持1000Mbps的传输速率。
  2. 数据位宽:8位数据通道,工作时钟频率为125MHz。
  3. 信号线数量:共24根信号线,包括发送和接收数据线、控制信号线、时钟信号线和管理接口信号线。
  4. 兼容性:向下兼容10Mbps和100Mbps速率。
  5. 全双工操作:支持全双工通信。
  6. 管理接口:提供MDC(管理总线时钟)和MDIO(管理总线数据)信号,用于设备管理和配置。
应用场景

GMII接口主要用于千兆以太网设备,如交换机、路由器和网络接口卡,适用于对带宽要求较高的场景。

RGMII接口介绍

RGMII(Reduced Gigabit Media Independent Interface,精简千兆媒体独立接口)是GMII接口的简化版本,旨在减少引脚数量和降低功耗。

英特尔官网

主要特点
  1. 数据速率:支持10Mbps、100Mbps和1000Mbps的自适应速率。
  2. 数据位宽:4位数据通道,通过双倍数据速率(DDR)技术在时钟上升沿和下降沿传输数据
  3. 信号线数量:仅需12根信号线。
  4. 时钟频率
  5. 1000Mbps:125MHz。
  6. 100Mbps:25MHz。
  7. 10Mbps:2.5MHz。
  8. 信号复用:将TX_ER和TX_EN复用为TX_CTL,RX_ER和RX_DV复用为RX_CTL。
  9. 全双工操作:支持全双工通信。
  10. 管理接口:同样支持MDC和MDIO信号。
应用场景

RGMII接口广泛应用于对成本和功耗敏感的场景,如嵌入式设备、FPGA板卡和小型网络设备。

GMII vs RGMII
特性 MII RMII GMII RGMII
数据速率 10/100 Mbps 10/100 Mbps 10/100/1000 Mbps 10/100/1000 Mbps
数据位宽 4位 2位 8位 4位
信号线数量 16根 7根 24根 12根
时钟频率 25MHz(100Mbps)
2.5MHz(10Mbps)
50MHz(100Mbps)
2.5MHz(10Mbps)
125MHz(1000Mbps)
25MHz(100Mbps)
2.5MHz(10Mbps)
125MHz(1000Mbps,DDR)
25MHz(100Mbps)
2.5MHz(10Mbps)
传输模式 单数据速率(SDR) 单数据速率(SDR) 单数据速率(SDR) 双数据速率(DDR,1000Mbps)
单数据速率(SDR,10/100Mbps)
功耗 较高 较低 较低
应用场景 早期以太网设备,成本敏感 成本敏感、低功耗设备 高带宽需求设备 成本敏感、低功耗且支持千兆以太网
特点 信号线多,适合低速以太网 信号线少,简化设计 支持千兆以太网,兼容低速模式 信号线少,支持千兆以太网,低功耗

RGMII通过减少信号线数量和采用DDR技术,在保持高传输速率的同时降低了功耗和PCB布局的复杂性,注意,再RGMII和GMII接口模块图中,TXD和RXD一个是4为并行数据位宽(支持时钟上升沿和下降沿双边采样)一个是8位并行数据位宽(只支持时钟上升沿采样)

关于更多信息请参考:英特尔:5.1.7.1.2. RMII和RGMII PHY接口

以下举例说明RGMII接口的信号定义及其详细备注信息(MAC可以理解为FPGA板卡,PHY芯片可以理解为FPGA板载的PHY芯片,从MAC -> PHY则是FPGA输出给FPGA板载的PHY芯片):

信号名称 方向 备注信息
TX_CLK MAC → PHY 发送时钟信号,由MAC提供。在1000Mbps模式下为125MHz,100Mbps模式下为25MHz,10Mbps模式下为2.5MHz。
TX_CTL MAC → PHY 发送控制信号,用于传输数据有效(TXEN)和数据错误(TXERR)信息。在时钟上升沿表示TXEN,下降沿表示TXERR。
TXD3:0 MAC → PHY 发送数据信号,4位宽。在1000Mbps模式下,每个时钟周期内传输8位数据(DDR模式),上升沿传输TXD3:0,下降沿传输TXD7:4,数据位宽为4位。
RX_CLK PHY → MAC 接收时钟信号,由PHY提供。在1000Mbps模式下为125MHz,100Mbps模式下为25MHz,10Mbps模式下为2.5MHz。
RX_CTL PHY → MAC 接收控制信号,用于接收数据有效(RXDV)和数据错误(RXERR)信息。在时钟上升沿表示RXDV,下降沿表示RXERR。
RXD3:0 PHY → MAC 接收数据信号,4位宽。在1000Mbps模式下,每个时钟周期内传输8位数据(DDR模式),上升沿传输RXD3:0,下降沿传输RXD7:4,数据位宽为4位。
MDC MAC → PHY 管理总线时钟信号,用于设备管理和配置。
MDIO 双向 管理总线数据信号,用于设备管理和配置,支持双向通信。
说明:
  1. DDR模式:在1000Mbps模式下,TXD和RXD信号使用双倍数据速率(DDR)技术,在时钟的上升沿和下降沿分别传输数据。
  2. SDR模式:在10Mbps和100Mbps模式下,TXD和RXD信号使用单数据速率(SDR)技术,仅在时钟上升沿传输数据。
  3. 信号电平:通常为1.8V、2.5V或3.3V,具体取决于硬件设计。
  4. 时钟偏斜:为确保时序正确,TX_CLK和RX_CLK需要满足一定的时钟偏斜要求。
RGMII接口时序设计

RGMII的时序分为两种:延时模式和非延时模式,可以通过配置PHY芯片改变模式。用的比较多的模式是延时模式,一般PHY芯片默认配置为延时模式。

小白FPGA

小白FPGA

小白fpga

小白fpga

数据包结构

MAC数据包 字节大小
前导码 7byte 8‘h55(固定值)
SFD 1byte 8'hd5(固定值)
目的mac地址 6byte
源mac地址 6byte
类型/长度 2byte 小于1536表示长度,大于1536表示类型,arp:16'h0006,ip:16’h0800
MAC层数据 46byte-1500byte mac有效数据
FCS 48byte CRC校验
IP头部 字节大小
版本+首部长度 1byte 版本ipv4:4‘h4,首部长度:4’h5
服务类型 1byte 一般为8‘h0
总长度 2byte ip首部长度+ip层数据长度
标识 2byte 复位给0,发送一包数据自加1
标记+分段偏移2byte 2byte 标记:3bit。最高位保留位0;中间位为是否开启分段,0不开启,1开启;最低为表示是否存在下一个分段,0表示为最后一个分段,1表示还存在下一个分段,一般默认为3’b010
生存时间 1byte 表示以太网数据包可以中转进过多少路由器,每次进入一个路由器后,该值会减少1,直到减少到0就会丢包,win系统默认为8'h80
协议 1byte udp:8'd17, tcp:8'd6, icmp:8'd1
首部校验和 1byte
源ip地址 4byte
目的ip地址 4byte
UDP头部 字节大小
源端口号 2byte
目的端口号 2byte
udp长度 固定8byte+udp数据包长度
udp校验和 2byte
UDP、ICMP、TCP和ARP之间的关系

小白fpga

1.1.4 UDP首部校验和、IP首部校验和计算方法

udp和ip首部校验的计算方法是一模一样的,如下图所示

1.2 项目框架和时序图绘制

因为内容较多,图文讲解不易懂,所以请参考教程视频进行学习巩固。

1.3 RGMII接收模块代码编写:rgmii_recive.v

1.3.1 模块设计思路

在这个设计中,我们通过对接收到的信号进行多层缓冲、延迟控制以及 IDDR 原语将双边沿信号(rgmii)转换成单边沿信号(gmii),以便后续处理。具体操作如下:

首先,我们将phy_rgmii_rx_clk ,phy_regmii_rx_ctl,phy_rgmii_rx_data输入的信号经过IBUF (输入缓冲器),提高信号质量,避免信号在传输过程中的抖动和噪声。输出得到phy_rgmii_rx_clk_ibuf ,phy_regmii_rx_ctl_ibuf,phy_rgmii_rx_data_ibuf信号

接下来,将我们得到的phy_rgmii_rx_clk_ibuf 信号分别经过BUFIO(I/O时钟缓冲器)BUFG(全局时钟缓冲器)分别得到phy_rgmii_rx_clk_ibuf_bufiogmii_rx_clk,其中phy_rgmii_rx_clk_ibuf_bufio时钟信号线等待下一步处理,gmii_rx_clk则作为gmii作为时钟信号

IDELAYE2 原语用于输入信号的延迟控制,常用于高频信号的接收和处理。在该模块中,IDELAYE2 原语用于延迟接收到的 phy_rgmii_rx_ctlphy_rgmii_rx_data 信号,以减少信号的时序误差。具体来说,IDELAYE2 被用来对 phy_rgmii_rx_ctl_ibufphy_rgmii_rx_data_ibuf 进行延迟处理,确保数据和时钟信号的同步性,消除由于信号传输延迟或时钟偏差带来的问题。此处信号的输出命名为`phy_rgmii_rx_ctl_ibuf_delayphy_rgmii_rx_data_ibuf_delay

最后,确保接收到的信号足够稳定之后,将双边沿采样的RGMII数据通过IDDR(输入双倍速率触发器)转换成单边沿信号。在此设计中,IDDR 用于将经过延迟后的数据 (phy_rgmii_rx_ctl_ibuf_delayphy_rgmii_rx_data_ibuf_delay) 在每个时钟周期的上升沿和下降沿分别采样,输出 gmii_rx_data_vldgmii_rx_data_errorgmii_rx_data 信号。

下面给大家整理了这个模块中用到的原语的详细讲解文字,如果上面的内容存在疑惑,可以针对性的查阅下面的原语介绍的内容。

1.3.2 IBUF(输入缓冲器)原语的作用

IBUF可以想象成为一个“信号安检门”,主要作用是将外部信号“整理一下”,再“安全的”送进FPGA内部,总之,IBUF的作用就是让外部信号“安全、干净、整齐”的进入FPGA,为后续的处理做好准备,具体来说有一下几个作用:

  1. 信号“整形”:外部信号因为传输距离长或者干扰,变得“歪歪扭扭”,IBUF就像一个美颜相机一样,吧信号“修整”成整齐、清晰的样子,让FPGA内部的逻辑电路更好的识别。
  2. “保护门“:外部信号可能带有”杂质“(比如电压波动、噪声等),IBUF就像一个”过滤器“一样,将这些”杂质“过滤掉,只让干净的信号进入FPGA内部,保护内部电路不受损坏。
  3. ”信号放大器“:有时候我们的外部信号比较弱,IBUF可以把它”放大“一下,让它有足够的力量驱动FPGA内部的电路。
  4. ”信号隔离带“:IBUF就像一个“隔离带”,把外部信号和FPGA内部电路进行隔开,防止外部的“动荡”(比如电压波动)影响到内部的正常工作。

在FPGA设计中,IBUF通常可以由综合工具自动插入,与顶层的输入端口或者输入输出端口进行直接相连接,如果需要手动实例化IBUF,可以使用下面的方法:

  1. 首先在vivado中打开“Language Temlates”,搜索”ibuf“

  1. 在下面菜单中找到与自己板卡一致以及实际项目需要的选项,直接在我们的rtl程序中粘贴进去即可。不管是K7还是A7开发板,这个原语都是这样的,这里给出我们当前的示例:
// 单个端口通过IBUF
IBUF #(
      .IBUF_LOW_PWR("TRUE"),  // Low power (TRUE) vs. performance (FALSE) setting for referenced I/O standards
      .IOSTANDARD("DEFAULT")  // Specify the input I/O standard
   ) IBUF_inst (
      .O(O),     // Buffer output
      .I(I)      // Buffer input (connect directly to top-level port)
   );

// 多条数据线路通过IBUF写法:genvar,generate
genvar i_rx;//genvar是一种特殊的整型变量,用于在generate块中定义循环变量,只能在generate块中使用,不能在其他地方使用
generate 
    for(i_rx = 0; i_rx < 4; i_rx++) begin
        IBUF #(
          .IBUF_LOW_PWR("TRUE"),  // Low power (TRUE) vs. performance (FALSE) setting for referenced I/O standards
          .IOSTANDARD("DEFAULT")  // Specify the input I/O standard
       ) IBUF_inst (
          .O(O),     // Buffer output
          .I(I)      // Buffer input (connect directly to top-level port)
       );    
    end
endgenerate


1.3.3 BUFG(全局时钟缓冲器)、BUFIO(I/O时钟缓冲器)原语的作用

BUFGBUFIO是两种常用的时钟缓冲原语,它们在FPGA设计中具有重要作用,主要用于优化时钟信号的传输和分配

BUFG是一种全局时钟缓冲器,用于将时钟信号分配到FPGA内部的全局时钟网络。它具有以下特点:

  • 低延迟和低抖动BUFG的输出到达FPGA内部的I/O块(IOB)、逻辑块(CLB)、块RAM等的时钟延迟和抖动最小。
  • 高驱动能力:能够驱动整个FPGA芯片内的任何时钟点。
  • 应用场景:适用于需要全局分配的时钟信号,例如系统时钟。
   BUFG BUFG_inst (
      .O(O), // 1-bit output: Clock output.
      .I(I)  // 1-bit input: Clock input.
   );

BUFIO是用于I/O时钟网络的缓冲器,具有以下特点:

  • 独立于全局时钟资源BUFIO的时钟网络独立于全局时钟资源,适合采集源同步数据。
  • 低延时:在采集源同步I/O数据时,BUFIO可以提供非常小的延时,适合用于高速I/O接口。
  • 驱动范围限制BUFIO只能驱动同一时钟区域内的I/O块逻辑,不能驱动FPGA内部的逻辑块(CLB)。
   BUFIO BUFIO_inst (
      .O(O), // 1-bit output: Clock output (connect to I/O clock loads).
      .I(I)  // 1-bit input: Clock input (connect to an IBUF or BUFMR).
   );

使用建议:

  • 全局时钟分配:如果需要将时钟信号分配到整个FPGA芯片的逻辑资源,建议使用BUFG
  • 源同步I/O:对于需要采集源同步数据的I/O接口,建议使用BUFIO,并结合BUFG以实现最佳性能

1.3.4 IDELAY原语的作用

IDELAY原语是Xilinx FPGA中用于对输入信号进行延迟调整的模块,主要用于处理高速串行接口、源同步接口等场景中的信号对齐问题。它在FPGA设计中具有重要作用,尤其是在需要精确控制信号延迟的场合。在使用IDELAY原语时,需要同时实例化IDELAYCTRL这个原语,否则在综合的时候会报错。

  1. IDELAY原语的作用介绍

IDELAY原语的主要作用是对输入信号施加可配置的延迟,以实现以下功能:

  • 信号对齐:在高速串行接口或源同步接口中,输入信号和时钟信号之间可能存在相位偏差。通过IDELAY可以调整输入信号的延迟,使其与系统时钟对齐。
  • 延迟校准:在某些应用中,需要对输入信号的延迟进行动态调整,以补偿信号传输路径中的延迟变化。IDELAY可以实现这种动态延迟校准。
  • 消除亚稳态:在跨时钟域信号传输中,通过调整延迟可以减少亚稳态的影响。

  • IDELAY的类型

Xilinx FPGA提供了多种类型的延迟模块,常见的有:

  • IDELAY:基本的输入延迟模块。
  • IDELAYE2:增强型输入延迟模块,支持更多的功能和更灵活的配置。
  • IDELAYE3:进一步增强的输入延迟模块,适用于更复杂的延迟调整需求。

  • IDELAYE2的配置参数

IDELAYE2是常用的增强型输入延迟模块,它支持多种配置参数,以下是一些关键参数:

  • DELAY_SRC:指定延迟的输入源,可以是DATAIN(来自IDELAY模块的输入信号)或IDATAIN(来自用户逻辑的输入信号)。
  • DELAY_TYPE:指定延迟的类型,可以是:
  • FIXED:固定延迟。
  • VARIABLE:可变延迟。
  • VAR_LOAD:可变延迟,延迟值可以通过LDLD价值动态加载。
  • DELAY_VALUE:指定延迟的初始值,单位是ps(皮秒)。
  • REFCLK_FREQUENCY:参考时钟频率,单位是MHz
  • HIGH_PERFORMANCE_MODE:高性能量子延迟模式,可以提高延迟的精度。

  • 使用场景

  • 高速串行接口:在高速串行接口中,输入信号和时钟信号之间可能存在相位偏差。通过IDELAY可以调整输入信号的延迟,使其与系统时钟对齐。

  • 源同步接口:在源同步接口中,输入信号和时钟信号是同步传输的。通过IDELAY可以调整输入信号的延迟,以确保数据的正确采样。
  • 跨时钟域信号传输:在跨时钟域信号传输中,通过调整延迟可以减少亚稳态的影响。

  • 注意事项

  • 延迟范围IDELAY的延迟范围是有限的,通常在0ps1000ps之间。如果需要更大的延迟,可能需要结合其他延迟模块。

  • 参考时钟IDELAY需要一个参考时钟来控制延迟的更新。参考时钟的频率需要与设计中的时钟频率匹配。
  • 动态调整:在动态调整延迟时,需要确保延迟值的更新不会导致信号的不稳定。

  • IDELAYE2的实例化代码

   IDELAYE2 #(
      .CINVCTRL_SEL("FALSE"),          // Enable dynamic clock inversion (FALSE, TRUE)
      .DELAY_SRC("IDATAIN"),           // Delay input (IDATAIN, DATAIN)
      .HIGH_PERFORMANCE_MODE("FALSE"), // Reduced jitter ("TRUE"), Reduced power ("FALSE")
      .IDELAY_TYPE("FIXED"),           // FIXED, VARIABLE, VAR_LOAD, VAR_LOAD_PIPE
      .IDELAY_VALUE(0),                // Input delay tap setting (0-31)
      .PIPE_SEL("FALSE"),              // Select pipelined mode, FALSE, TRUE
      .REFCLK_FREQUENCY(200.0),        // IDELAYCTRL clock input frequency in MHz (190.0-210.0, 290.0-310.0).
      .SIGNAL_PATTERN("DATA")          // DATA, CLOCK input signal
   )
   IDELAYE2_inst (
      .CNTVALUEOUT(CNTVALUEOUT), // 5-bit output: Counter value output
       .DATAOUT(DATAOUT),         // 1-bit output: Delayed data output,延时后的输出信号
      .C(C),                     // 1-bit input: Clock input
      .CE(CE),                   // 1-bit input: Active high enable increment/decrement input
      .CINVCTRL(CINVCTRL),       // 1-bit input: Dynamic clock inversion input
      .CNTVALUEIN(CNTVALUEIN),   // 5-bit input: Counter value input
      .DATAIN(DATAIN),           // 1-bit input: Internal delay data input
      .IDATAIN(IDATAIN),         // 1-bit input: Data input from the I/O
      .INC(INC),                 // 1-bit input: Increment / Decrement tap delay input
      .LD(LD),                   // 1-bit input: Load IDELAY_VALUE input
      .LDPIPEEN(LDPIPEEN),       // 1-bit input: Enable PIPELINE register to load data input
      .REGRST(REGRST)            // 1-bit input: Active-high reset tap-delay input
   );

注意在这里,注意IDATAINDATAIN两个输入端口,我们实际上只需要将我们要延时的数据输入进一个端口即可,当我们的数据源处于FPGA内部时,将IDATAIN置0,将数据连接进DATAIN中,并且修改参数.DELAY_SRC("DATAIN"),。反之将反

  1. IDELAYCTRL原语实例化
   IDELAYCTRL IDELAYCTRL_inst (
      .RDY(RDY),       // 1-bit output: Ready output
      .REFCLK(REFCLK), // 1-bit input: Reference clock input
      .RST(RST)        // 1-bit input: Active high reset input
   );

1.3.5 IDDR原语的作用

IDDR(Input Double Data Rate)原语是Xilinx FPGA中用于处理双倍数据速率(DDR)信号的底层原语,其主要功能是将输入的双沿信号(DDR)转换为单沿信号(SDR),以便FPGA内部逻辑能够正确处理。

  1. 功能与作用

  2. 双沿信号转换IDDR能够将输入的双沿信号(在时钟上升沿和下降沿都传输数据)转换为单沿信号(仅在时钟上升沿或下降沿传输数据),从而将输入的DDR信号转换为FPGA内部可处理的SDR信号。

  3. 提高数据吞吐率:通过在每个时钟边沿捕获数据,IDDR可以在相同的时钟频率下实现双倍的数据传输速率。

  4. 工作模式

IDDR支持以下三种工作模式:下面有详细的模式介绍和时序图,注意观察Q1和Q2的数据排布,最常用的是第三个SAME_EDGE_PIPELINED

  1. OPPOSITE_EDGE:在时钟的上升沿捕获输入信号的上升沿数据,并在下降沿捕获输入信号的下降沿数据。捕获的数据分别输出到Q1Q2
  2. SAME_EDGE:在时钟的同一边沿(通常是上升沿)捕获输入信号的上升沿和下降沿数据。
  3. SAME_EDGE_PIPELINED:与SAME_EDGE类似,但数据输出会延迟一个时钟周期。

  4. 参数配置

  5. DDR_CLK_EDGE:设置工作模式,可选值为OPPOSITE_EDGESAME_EDGESAME_EDGE_PIPELINED

  6. INIT_Q1INIT_Q2:设置输出Q1Q2的初始值。
  7. SRTYPE:设置复位类型,可选值为SYNC(同步复位)或ASYNC(异步复位)。

  8. 端口说明

  9. Q1Q2:单沿数据输出,分别对应输入信号的上升沿和下降沿数据。

  10. C:时钟输入。
  11. CE:时钟使能信号,必须为高电平才能捕获数据。
  12. D:输入的双沿数据。
  13. RS:复位和置位信号。

实例化代码:

   IDDR #(
      .DDR_CLK_EDGE("OPPOSITE_EDGE"), // "OPPOSITE_EDGE", "SAME_EDGE" 
                                      //    or "SAME_EDGE_PIPELINED" 
      .INIT_Q1(1'b0), // Initial value of Q1: 1'b0 or 1'b1
      .INIT_Q2(1'b0), // Initial value of Q2: 1'b0 or 1'b1
      .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
   ) IDDR_inst (
      .Q1(Q1), // 1-bit output for positive edge of clock
      .Q2(Q2), // 1-bit output for negative edge of clock
      .C(C),   // 1-bit clock input
      .CE(CE), // 1-bit clock enable input
      .D(D),   // 1-bit DDR data input
      .R(R),   // 1-bit reset
      .S(S)    // 1-bit set
   );

1.3.6 RGMII接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-14 19:21:05
 * @LastEditTime: 2025-02-14 20:07:03
 * @FilePath: \rtl\rgmii_recive.v
 * @Description:添加信号的IBUF、BUFIO、BUFG、延时控制、IDDR原语,信号过BUF原语进行提高信号质量,过IDDR原语将rgmii双边沿信号转换位gmii单边沿信号
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */


module rgmii_recieve(
    input wire          sys_reset_n               ,
    input wire          idelay_refclk       ,

    input wire          phy_rgmii_rx_clk    ,
    input wire          phy_rgmii_rx_ctl    ,
    input wire [3:0]    phy_rgmii_rx_data   ,

    output wire         gmii_rx_clk         ,
    output wire         gmii_rx_data_vld    ,
    output wire         gmii_rx_data_error  ,
    output wire [7:0]   gmii_rx_data        
);
wire            phy_rgmii_rx_clk_ibuf       ;
wire            phy_rgmii_rx_ctl_ibuf       ;
wire  [3:0]     phy_rgmii_rx_data_ibuf      ;

wire            phy_rgmii_rx_clk_ibuf_bufio ;
wire            phy_rgmii_rx_ctl_ibuf_delay ;
wire  [3:0]     phy_rgmii_rx_data_ibuf_delay;

/*--------------------------------------------------*\
                       IBUF
\*--------------------------------------------------*/
//对phy_rgmii_rx_clk信号通过buf
   IBUF #(
      .IBUF_LOW_PWR("TRUE"),        // Low power (TRUE) vs. performance (FALSE) setting for referenced I/O standards
      .IOSTANDARD("DEFAULT")        // Specify the input I/O standard
   ) phy_rgmii_rx_clk_IBUF_inst (
      .O(phy_rgmii_rx_clk_ibuf),    // Buffer output
      .I(phy_rgmii_rx_clk)          // Buffer input (connect directly to top-level port)
   );
//对phy_rgmii_rx_ctl信号通过buf
   IBUF #(
      .IBUF_LOW_PWR("TRUE"),        // Low power (TRUE) vs. performance (FALSE) setting for referenced I/O standards
      .IOSTANDARD("DEFAULT")        // Specify the input I/O standard
   ) phy_rgmii_rx_ctl_IBUF_inst (
      .O(phy_rgmii_rx_ctl_ibuf),    // Buffer output
      .I(phy_rgmii_rx_ctl)          // Buffer input (connect directly to top-level port)
   );
//对phy_rgmii_rx_data信号通过buf
genvar i_rx;
generate
    for (i_rx = 0; i_rx < 4; i_rx = i_rx + 1) begin
        IBUF #(
            .IBUF_LOW_PWR("TRUE"),              // Low power (TRUE) vs. performance (FALSE) setting for referenced I/O standards
            .IOSTANDARD("DEFAULT")              // Specify the input I/O standard
        ) phy_rgmii_rx_data_IBUF_inst (
            .O(phy_rgmii_rx_data_ibuf[i_rx]),   // Buffer output
            .I(phy_rgmii_rx_data[i_rx])         // Buffer input (connect directly to top-level port)
        );
    end
endgenerate

/*--------------------------------------------------*\
                       BUFIO、BUFG
\*--------------------------------------------------*/
// 对时钟信号phy_rgmii_rx_clk_ibuf通过BUFIO和BUFG
   BUFIO phy_rgmii_rx_clk_ibuf_BUFIO_inst (
      .O(phy_rgmii_rx_clk_ibuf_bufio), // 1-bit output: Clock output (connect to I/O clock loads).
      .I(phy_rgmii_rx_clk_ibuf)  // 1-bit input: Clock input (connect to an IBUF or BUFMR).
   );

   BUFG phy_rgmii_rx_clk_ibuf_BUFG_inst (
      .O(gmii_rx_clk), // 1-bit output: Clock output
      .I(phy_rgmii_rx_clk_ibuf)  // 1-bit input: Clock input
   );

/*--------------------------------------------------*\
                IDELAYCTRL、IDELAYE2       
\*--------------------------------------------------*/
IDELAYCTRL idelay_refclk_IDELAYCTRL_inst (
    .RDY(),       // 1-bit output: Ready output
    .REFCLK(idelay_refclk), // 1-bit input: Reference clock input
    .RST(1'b0)        // 1-bit input: Active high reset input
   );
 IDELAYE2 #(
      .CINVCTRL_SEL("FALSE"),          // Enable dynamic clock inversion (FALSE, TRUE)
      .DELAY_SRC("IDATAIN"),           // Delay input (IDATAIN, DATAIN)
      .HIGH_PERFORMANCE_MODE("FALSE"), // Reduced jitter ("TRUE"), Reduced power ("FALSE")
      .IDELAY_TYPE("FIXED"),           // FIXED, VARIABLE, VAR_LOAD, VAR_LOAD_PIPE
      .IDELAY_VALUE(0),                // Input delay tap setting (0-31)
      .PIPE_SEL("FALSE"),              // Select pipelined mode, FALSE, TRUE
      .REFCLK_FREQUENCY(200.0),        // IDELAYCTRL clock input frequency in MHz (190.0-210.0, 290.0-310.0).
      .SIGNAL_PATTERN("DATA")          // DATA, CLOCK input signal
   )
   phy_rgmii_rx_ctl_ibuf_delay_IDELAYE2_inst (
      .CNTVALUEOUT(), // 5-bit output: Counter value output
      .DATAOUT(phy_rgmii_rx_ctl_ibuf_delay),         // 1-bit output: Delayed data output
      .C(0),                     // 1-bit input: Clock input
      .CE(0),                   // 1-bit input: Active high enable increment/decrement input
      .CINVCTRL(0),       // 1-bit input: Dynamic clock inversion input
      .CNTVALUEIN(0),   // 5-bit input: Counter value input
      .DATAIN(0),           // 1-bit input: Internal delay data input
      .IDATAIN(phy_rgmii_rx_ctl_ibuf),         // 1-bit input: Data input from the I/O
      .INC(0),                 // 1-bit input: Increment / Decrement tap delay input
      .LD(0),                   // 1-bit input: Load IDELAY_VALUE input
      .LDPIPEEN(0),       // 1-bit input: Enable PIPELINE register to load data input
      .REGRST(0)            // 1-bit input: Active-high reset tap-delay input
   );
genvar j_rx;
generate
    for (j_rx = 0; j_rx < 4; j_rx = j_rx + 1) begin

        IDELAYE2 #(
            .CINVCTRL_SEL("FALSE"),          // Enable dynamic clock inversion (FALSE, TRUE)
            .DELAY_SRC("IDATAIN"),           // Delay input (IDATAIN, DATAIN)
            .HIGH_PERFORMANCE_MODE("FALSE"), // Reduced jitter ("TRUE"), Reduced power ("FALSE")
            .IDELAY_TYPE("FIXED"),           // FIXED, VARIABLE, VAR_LOAD, VAR_LOAD_PIPE
            .IDELAY_VALUE(0),                // Input delay tap setting (0-31)
            .PIPE_SEL("FALSE"),              // Select pipelined mode, FALSE, TRUE
            .REFCLK_FREQUENCY(200.0),        // IDELAYCTRL clock input frequency in MHz (190.0-210.0, 290.0-310.0).
            .SIGNAL_PATTERN("DATA")          // DATA, CLOCK input signal
        )phy_rgmii_rx_data_ibuf_delay_IDELAYE2_inst (
            .IDATAIN(phy_rgmii_rx_data_ibuf[j_rx]),         // 1-bit input: Data input from the I/O      
            .DATAOUT(phy_rgmii_rx_data_ibuf_delay[j_rx]),         // 1-bit output: Delayed data output
            .CNTVALUEOUT(), // 5-bit output: Counter value output      
            .C(0),                     // 1-bit input: Clock input
            .CE(0),                   // 1-bit input: Active high enable increment/decrement input
            .CINVCTRL(0),       // 1-bit input: Dynamic clock inversion input
            .CNTVALUEIN(0),   // 5-bit input: Counter value input
            .DATAIN(0),           // 1-bit input: Internal delay data input
            .INC(0),                 // 1-bit input: Increment / Decrement tap delay input
            .LD(0),                   // 1-bit input: Load IDELAY_VALUE input
            .LDPIPEEN(0),       // 1-bit input: Enable PIPELINE register to load data input
            .REGRST(0)            // 1-bit input: Active-high reset tap-delay input
            );

    end
endgenerate

/*--------------------------------------------------*\
                       IDDR原语 
\*--------------------------------------------------*/
IDDR #(
      .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), // "OPPOSITE_EDGE", "SAME_EDGE" 
                                      //    or "SAME_EDGE_PIPELINED" 
      .INIT_Q1(1'b0), // Initial value of Q1: 1'b0 or 1'b1
      .INIT_Q2(1'b0), // Initial value of Q2: 1'b0 or 1'b1
      .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
   ) gmii_rx_data_vld_IDDR_inst (
      .Q1(gmii_rx_data_vld), // 1-bit output for positive edge of clock
      .Q2(gmii_rx_data_error_xor), // 1-bit output for negative edge of clock
      .C(phy_rgmii_rx_clk_ibuf_bufio),   // 1-bit clock input
      .CE(1), // 1-bit clock enable input
      .D(phy_rgmii_rx_ctl_ibuf_delay),   // 1-bit DDR data input
      .R(0),   // 1-bit reset
      .S(0)    // 1-bit set
   );

genvar q_rx;
generate

    for (q_rx = 0; q_rx < 4; q_rx = q_rx + 1) begin

        IDDR #(
            .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), // "OPPOSITE_EDGE", "SAME_EDGE"                                 //    or "SAME_EDGE_PIPELINED" 
            .INIT_Q1(1'b0), // Initial value of Q1: 1'b0 or 1'b1
            .INIT_Q2(1'b0), // Initial value of Q2: 1'b0 or 1'b1
            .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
        ) gmii_rx_data_IDDR_inst (
            .Q1(gmii_rx_data[q_rx]), // 1-bit output for positive edge of clock
            .Q2(gmii_rx_data[q_rx + 4]), // 1-bit output for negative edge of clock
            .C(phy_rgmii_rx_clk_ibuf_bufio),   // 1-bit clock input
            .CE(1), // 1-bit clock enable input
            .D(phy_rgmii_rx_data_ibuf_delay[q_rx]),   // 1-bit DDR data input
            .R(0),   // 1-bit reset
            .S(0)    // 1-bit set
   );

   end
endgenerate
endmodule

1.3.7 代码仿真代码与验证

1.4 RGMII发送模块代码编写:rgmii_send.v

1.4.1 模块设计思路

这个模块是RGMII发送数据模块,查看上面的模块框图可以看出来,它接收来自上游的数据(GMII 格式)。该模块的功能主要是将GMII格式的时钟和数据转换成RGMII格式的时钟和数据传出。

与RGMII接收模块类似,发送模块也是利用了FPGA中的一些基本原语来处理时钟和数据的转换的,主要用到了ODDR (输出双数据率触发器)OBUF (输出缓冲器)两个原语,这两个原语的介绍在下面也进行的详细的解释。

这个模块比较简单,不需要通过BUF原语多次处理时钟和其他信号确保时序的稳定了,因为这是发送模块,这些数据都是FPGA内向外部传输的。

我们只需要通过ODDR原语 将 gmii_tx_data_vld 信号(GMII 数据有效标志)转换为 RGMII 的 phy_rgmii_tx_ctl 信号。将 gmii_tx_dataGMII 数据(8 位)转换为 4 位的 phy_rgmii_tx_dataRGMII 数据。

1.4.2 ODDR原语的作用

Vivado 中的 ODDR 原语(Output Double Data Rate)是 FPGA 设计中用于实现 单数据速率(SDR)到双数据速率(DDR)转换 的关键组件。它通过时钟的 上升沿和下降沿 分别发送数据,从而将数据传输速率提升一倍。以下是关于 ODDR 原语的详细介绍:

1. ODDR 的核心作用

  • DDR 信号生成:将 FPGA 内部逻辑的单数据速率信号转换为双数据速率信号,满足高速接口(如 DDR 存储器、LVDS、HDMI 等)的时序要求。
  • 时钟域对齐:通过同步时钟边沿输出数据,确保数据与外部设备时钟严格对齐。
  • 资源优化:直接调用 FPGA 的专用硬件资源(如 I/O 触发器),避免通用逻辑资源浪费。

2.ODDR 的结构与信号定义

ODDR 原语的接口通常包含以下关键信号:

信号名 方向 描述
C 输入 主时钟,驱动数据传输的基准时钟。
D1 输入 在时钟 上升沿 锁存并输出的数据。
D2 输入 在时钟 下降沿 锁存并输出的数据。
Q 输出 DDR 输出信号,交替输出 D1 和 D2 的值。
CE 输入 时钟使能信号(可选),高电平时允许数据锁存。
R 输入 同步复位信号(可选),复位输出为初始值。
S 输入 同步置位信号(可选),置位输出为高电平。

3. ODDR 的工作模式

ODDR 支持两种模式,通过参数 DDR_CLK_EDGE 配置:

(1) OPPOSITE_EDGE 模式

  • 行为:D1 在时钟 上升沿 被锁存,D2 在 下降沿 被锁存。
  • 输出时序: https://www.xilinx.com/content/dam/xilinx/support/documentation/ip_documentation/ug471_7Series_SelectIO.pdf
  • 时钟上升沿 → 输出 D1。
  • 时钟下降沿 → 输出 D2。
  • 适用场景:传统 DDR 接口设计,需严格匹配外部设备时序。

(2) SAME_EDGE 模式

  • 行为:D1 和 D2 均在时钟 上升沿 被锁存,但 D2 的值会延迟半个周期输出。
  • 输出时序: https://www.xilinx.com/content/dam/xilinx/support/documentation/ip_documentation/ug471_7Series_SelectIO.pdf
  • 时钟上升沿 → 输出 D1。
  • 下一时钟上升沿 → 输出 D2(内部延迟半周期)。
  • 优势:简化 FPGA 内部逻辑时序设计,避免跨时钟域问题。

4. ODDR 的典型应用场景

  1. DDR 存储器接口
  2. 与 DDR SDRAM 通信时,需通过 ODDR 生成符合 JEDEC 标准的 DQS 和 DQ 信号。
  3. 高速串行接口
  4. 如 LVDS、HDMI 的时钟和数据通道,需 DDR 信号提升传输效率。
  5. 时钟转发(Clock Forwarding)
  6. 将 FPGA 内部时钟通过 DDR 模式输出,供外部芯片同步使用。
  7. 数据对齐
  8. 在源同步接口(如 Camera Link)中,对齐数据与随路时钟。

5.实例化代码

ODDR #(
    .DDR_CLK_EDGE("OPPOSITE_EDGE"), // 工作模式:OPPOSITE_EDGE 或 SAME_EDGE
    .INIT(1'b0),                    // 输出初始值(0 或 1)
    .SRTYPE("SYNC")                 // 复位/置位类型:SYNC(同步)或 ASYNC(异步)
) ODDR_inst (
    .Q(Q),   // DDR 输出信号
    .C(clk), // 基准时钟(需与外部设备时钟同源)
    .CE(1'b1), // 时钟使能(常接高电平)
    .D1(data_rise), // 上升沿数据
    .D2(data_fall), // 下降沿数据
    .R(1'b0), // 复位(通常禁用)
    .S(1'b0)  // 置位(通常禁用)
);

6. 关键配置参数详解

  • DDR_CLK_EDGE 决定数据锁存边沿模式,需根据外部设备时序要求选择。
  • INIT 初始化输出值,通常设为 0 或 1,需与接口空闲状态一致。
  • SRTYPE 复位/置位信号的同步方式:
  • SYNC:复位/置位与时钟同步。
  • ASYNC:复位/置位异步生效(可能导致时序违例)。

1.4.3 RGMII发送模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-14 19:21:29
 * @LastEditTime: 2025-02-14 20:08:08
 * @FilePath: \rtl\rgmii_send.v
 * @Description:添加信号的OBUF、ODDR原语
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

module rgmii_send(
    input wire          sys_reset_n              ,
    input wire          gmii_tx_clk        ,
    input wire          gmii_tx_data_vld   ,
    input wire [7:0]    gmii_tx_data       ,

    output wire         phy_rgmii_tx_clk   ,
    output wire         phy_rgmii_tx_ctl   ,
    output wire [3:0]   phy_rgmii_tx_data  
);

wire       rgmii_tx_ctl;
wire [3:0] rgmii_tx_data;

/*--------------------------------------------------*\
                       ODDR原语
\*--------------------------------------------------*/
   ODDR #(
      .DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE" 
      .INIT(1'b0),    // Initial value of Q: 1'b0 or 1'b1
      .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
   ) ODDR_ctl(
      .Q(rgmii_tx_ctl),   // 1-bit DDR output
      .C(gmii_tx_clk),   // 1-bit clock input
      .CE(1), // 1-bit clock enable input
      .D1(gmii_tx_data_vld), // 1-bit data input (positive edge)
      .D2(gmii_tx_data_vld ), // 1-bit data input (negative edge) //注意不要掉了异或
      .R(0),   // 1-bit reset
      .S(0)    // 1-bit set
   );
genvar i_tx;
generate
    for (i_tx = 0; i_tx < 4; i_tx = i_tx + 1) begin

        ODDR #(
            .DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE" 
            .INIT(1'b0),    // Initial value of Q: 1'b0 or 1'b1
            .SRTYPE("SYNC") // Set/Reset type: "SYNC" or "ASYNC" 
        ) ODDR_data(
            .Q(rgmii_tx_data[i_tx]),   // 1-bit DDR output
            .C(gmii_tx_clk),   // 1-bit clock input
            .CE(1), // 1-bit clock enable input
            .D1(gmii_tx_data[i_tx]), // 1-bit data input (positive edge)
            .D2(gmii_tx_data[i_tx + 4]), // 1-bit data input (negative edge)
            .R(0),   // 1-bit reset
            .S(0)    // 1-bit set
        );

    end
endgenerate


/*--------------------------------------------------*\
                       OBUF原语
\*--------------------------------------------------*/
 OBUF OBUF_clk (
      .O(phy_rgmii_tx_clk), // 1-bit output: Buffer output (connect directly to top-level port)
      .I(gmii_tx_clk)  // 1-bit input: Buffer input
   );

   OBUF OBUF_ctl (
      .O(phy_rgmii_tx_ctl), // 1-bit output: Buffer output (connect directly to top-level port)
      .I(rgmii_tx_ctl)  // 1-bit input: Buffer input
   );

genvar j_tx;
generate
    for (j_tx = 0; j_tx < 4; j_tx = j_tx + 1) begin

        OBUF OBUF_data 
        (
            .O(phy_rgmii_tx_data[j_tx]), // 1-bit output: Buffer output (connect directly to top-level port)
            .I(rgmii_tx_data[j_tx])  // 1-bit input: Buffer input

        );      
    end
endgenerate

endmodule

1.4.4 代码仿真与验证

1.5 UDP协议栈架构设计及时钟规划

1.6 MAC接收模块代码编写:mac_receivev.v

1.6.1 模块设计思路

以太网帧结构如下:

| 前导码(7B) | SFD(1B) | 目标MAC(6B) | 源MAC(6B) | 类型(2B) | 数据(46-1500B) | CRC(4B) |

首先,我们再次梳理整体项目的思路,我们的数据是从PC机中经过rgmii_receive模块将数据从rgmii格式转换到gmii格式后,将数据传递给mac_receive模块对以太网帧进行解析并去掉包头(包括前导码、SFD、目标MAC、源MAC、类型)后传递给下一层ip_receive模块的。所以在这个模块中,核心的功能应该有下面几点:

  1. 接收来自rgmii_receive的数据
  2. 完成以太网帧解析、校验和有效数据的传递
  3. 有效数据的需要经过SFD、CRC校验后传输以及跨时钟域处理,可以通过FIFO对数据的缓存来实现

具体来说:

  1. 对以太网帧前导码、目标MAC地址、帧类型字段的提取
  2. 前导码/SFD校验、MAC地址过滤、CRC校验
  3. 跨时钟域传输,通过双时钟FIFO实现从PHY时钟域到系统时钟域的数据传输
  4. 将数据存储在FIFO中,当校验通过后,数据有效信号再拉高表示数据没有问题

关于这个模块的接口信息如上图所示:

  • 输入sys_clksys_reset_n系统时钟和复位信号

  • 输入来自phy_rx_clkphy_rx_reset 来自phy芯片的时钟和复位信号

  • 输入来自rgmii_recieve模块的gmii_rx_data_vldgmii_rx_data 的数据有效信号和数据信号
  • 输入来自crc32_d8模块的计算结果的数据
  • 输出处理后的数据,包括mac_rx_data_vld(数据有效信号),mac_rx_data_last(数据结束标识信号),mac_rx_data(输出数据信号),mac_rx_frame_type(以太网帧类型字段信号),传递给mac_to_arp_ip模块进行针对不同的以太网帧字段类型选择不同的处理方式

1.6.2 状态机逻辑讲解

关于状态机的内容,相比看到这里的读者对状态机的写法还是无比熟悉了,在这里我想再次给大家重新梳理一下状态机的一些知识点和笔试面试中常见的问法,大家若是感觉自己已经很熟练了,可以跳过这部分内容。

FPGA是并行执行的,如果说我们想要处理一个前后顺序的事件该怎么办呐,这时候就需要我们的状态机来完成了。状态机的基本类型分下面两类:

  • Moore型状态机:输出仅由当前状态决定,与输入无关。
  • Mealy型状态机:输出由当前状态和输入共同决定。

在我们要完成一个完整的状态机逻辑之前,需要先设计绘制状态转移图,明确转移条件和跳转关系,

此外,完整的状态机应该包含状态寄存器下一状态逻辑输出逻辑这三个部分组成,下面我们一步一步来编写一个简单的状态机demo。

1.6.2.1状态机的Verilog实现步骤
(1)状态定义

使用parameter定义所有可能的状态,通常推荐独热码(One-Hot)或二进制编码:

// 示例:4个状态(独热码)
parameter IDLE  = 4'b0001,
          STATE1 = 4'b0010,
          STATE2 = 4'b0100,
          STATE3 = 4'b1000;
(2)状态寄存器

用时序逻辑(always @(posedge clk)更新当前状态:

reg [3:0] current_state, next_state;

always @(posedge clk or posedge reset) begin
    if (reset) 
        current_state <= IDLE; // 复位到初始状态
    else 
        current_state <= next_state;
end
(3)下一状态逻辑

用组合逻辑(always @*)根据输入和当前状态计算下一状态:

always @(*) begin
    case (current_state)
        IDLE: 
            if (start) next_state = STATE1;
            else      next_state = IDLE;
        STATE1: 
            if (input_a) next_state = STATE2;
            else         next_state = IDLE;
        STATE2: 
            next_state = STATE3; // 无条件跳转
        STATE3: 
            if (done)   next_state = IDLE;
            else        next_state = STATE3;
        default: 
            next_state = IDLE; // 避免锁存器
    endcase
end
(4)输出逻辑
  • Moore型:输出仅依赖当前状态:

verilog always @(*) begin case (current_state) IDLE: output = 0; STATE1: output = 1; STATE2: output = 0; STATE3: output = 1; endcase end

  • Mealy型:输出依赖当前状态和输入:

verilog always @(*) begin if (current_state == STATE1 && input_a) output = 1; else output = 0; end

在笔试面试中常见的问题就是自动贩卖机和交通信号灯的状态机编写,下面我们通过梳理实现思路,绘制状态转移图和编写模块代码和仿真代码进行实验

1.6.2.2 应用案例1:自动贩卖机示例

输入信号有:钱的投入金额、商品选择

输出信号有:不同商品出货信号

状态机状态:等待投币,投入硬币、选择商品、出货状态、恢复状态

选择商品:用户可以选择商品(product1product2product3)。只有在选择商品后,才会进入 COIN_INPUT 状态进行投入硬币。

投入硬币:在 COIN_INPUT 状态,系统会根据用户选择的商品判断是否足够支付。如果足够,进入 DISPENSE 状态,进行商品出货。如果余额不足,系统会继续等待硬币投入

交易完成:跳转到恢复等待,然后跳转到初始状态

模块代码和仿真代码如下:

module vending_machine(
    input clk,              // 时钟信号
    input reset,            // 复位信号
    input coin_5,           // 5元硬币投入
    input coin_10,          // 10元硬币投入
    input coin_25,          // 25元硬币投入
    input product1,         // 选择商品1
    input product2,         // 选择商品2
    input product3,         // 选择商品3
    output reg dispense_product1,   // 商品1出货信号
    output reg dispense_product2,   // 商品2出货信号
    output reg dispense_product3    // 商品3出货信号
);

parameter IDLE       = 3'b000,
          SELECT     = 3'b001,
          COIN_INPUT = 3'b010,
          DISPENSE   = 3'b011,
          RESTORE    = 3'b100;

reg [2:0] current_state, next_state;
reg [7:0] current_balance;
reg [7:0] product1_price = 50;  // 商品1价格:50元
reg [7:0] product2_price = 75;  // 商品2价格:75元
reg [7:0] product3_price = 100; // 商品3价格:100元

// 状态寄存器
always @(posedge clk or posedge reset) begin
    if (reset)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 当前余额更新逻辑
always @(posedge clk or posedge reset) begin
    if (reset)
        current_balance <= 0;
    else if (current_state == COIN_INPUT) begin
        if (coin_5) current_balance <= current_balance + 5;
        if (coin_10) current_balance <= current_balance + 10;
        if (coin_25) current_balance <= current_balance + 25;
    end
end

// 下一状态逻辑
always @(*) begin
    case (current_state)
        IDLE: next_state = SELECT;
        SELECT: next_state = (product1 || product2 || product3) ? COIN_INPUT : SELECT;
        COIN_INPUT: next_state = (product1 && current_balance >= product1_price) ? DISPENSE : 
                                 (product2 && current_balance >= product2_price) ? DISPENSE : 
                                 (product3 && current_balance >= product3_price) ? DISPENSE : COIN_INPUT;
        DISPENSE: next_state = RESTORE;
        RESTORE: next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 输出逻辑
always @(*) begin
    dispense_product1 = 0;
    dispense_product2 = 0;
    dispense_product3 = 0;

    case (current_state)
        IDLE: begin
            dispense_product1 = 0;
            dispense_product2 = 0;
            dispense_product3 = 0;
        end
        SELECT: begin
            dispense_product1 = 0;
            dispense_product2 = 0;
            dispense_product3 = 0;
        end
        COIN_INPUT: begin
            dispense_product1 = 0;
            dispense_product2 = 0;
            dispense_product3 = 0;
        end
        DISPENSE: begin
            if (product1) dispense_product1 = 1;
            else if (product2) dispense_product2 = 1;
            else if (product3) dispense_product3 = 1;
        end
        RESTORE: begin
            dispense_product1 = 0;
            dispense_product2 = 0;
            dispense_product3 = 0;
        end
    endcase
end

endmodule

module vending_machine_tb;

    reg clk;
    reg reset;
    reg coin_5;
    reg coin_10;
    reg coin_25;
    reg product1;
    reg product2;
    reg product3;
    wire dispense_product1;
    wire dispense_product2;
    wire dispense_product3;

    // 实例化自动贩卖机模块
    vending_machine vm (
        .clk(clk),
        .reset(reset),
        .coin_5(coin_5),
        .coin_10(coin_10),
        .coin_25(coin_25),
        .product1(product1),
        .product2(product2),
        .product3(product3),
        .dispense_product1(dispense_product1),
        .dispense_product2(dispense_product2),
        .dispense_product3(dispense_product3)
    );

    // 时钟生成
    always begin
        #5 clk = ~clk;  // 10ns周期的时钟
    end

    // 仿真过程
    initial begin
        // 初始化信号
        clk = 0;
        reset = 0;
        coin_5 = 0;
        coin_10 = 0;
        coin_25 = 0;
        product1 = 0;
        product2 = 0;
        product3 = 0;

        // 复位
        reset = 1;
        #10 reset = 0;

        // 投入硬币
        coin_5 = 1; #10 coin_5 = 0;
        coin_10 = 1; #10 coin_10 = 0;
        coin_25 = 1; #10 coin_25 = 0;

        // 选择商品1
        product1 = 1; #10 product1 = 0;

        // 继续选择商品2
        product2 = 1; #10 product2 = 0;

        // 查看输出
        #20;

        // 停止仿真
        $stop;
    end
endmodule

1.6.2.3 应用案例2:交通信号灯示例

在完成这个代码的时候,我们先对其进行分析一下,设计一个十字路口的交通信号灯控制器,东西方向南北方向交替通行,一个方向绿灯/黄灯时,另一个方向必须为红灯,此外绿灯亮30秒 → 黄灯亮5秒 → 红灯亮。状态转移图如下所示:

模块代码和仿真代码如下:

module traffic_light (
    input      clk,
    input      reset,
    output reg ew_red,
    output reg ew_yellow,
    output reg ew_green,
    output reg ns_red,
    output reg ns_yellow,
    output reg ns_green
);

parameter IDLE      = 3'b000,
          EW_GREEN  = 3'b001,
          EW_YELLOW = 3'b010,
          NS_GREEN  = 3'b011,
          NS_YELLOW = 3'b100;

reg [2:0] current_state, next_state;
reg [5:0] timer;

// 状态寄存器
always @(posedge clk or posedge reset) begin
    if (reset)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 定时器逻辑
always @(posedge clk or posedge reset) begin
    if (reset)
        timer <= 0;
    else begin
        if (timer == 0)
            timer <= (current_state == EW_GREEN)  ? 30 :
                     (current_state == EW_YELLOW) ? 5  :
                     (current_state == NS_GREEN)  ? 30 :
                     (current_state == NS_YELLOW) ? 5  : 0;
        else
            timer <= timer - 1;
    end
end

// 下一状态逻辑
always @(*) begin
    case (current_state)
        IDLE:      next_state = EW_GREEN;
        EW_GREEN:  next_state = (timer == 0) ? EW_YELLOW : EW_GREEN;
        EW_YELLOW: next_state = (timer == 0) ? NS_GREEN  : EW_YELLOW;
        NS_GREEN:  next_state = (timer == 0) ? NS_YELLOW : NS_GREEN;
        NS_YELLOW: next_state = (timer == 0) ? EW_GREEN  : NS_YELLOW;
        default:   next_state = IDLE;
    endcase
end

// 输出逻辑
always @(*) begin
    ew_red = 0; ew_yellow = 0; ew_green = 0;
    ns_red = 0; ns_yellow = 0; ns_green = 0;

    case (current_state)
        IDLE: begin
            ew_red = 1;
            ns_red = 1;
        end
        EW_GREEN: begin
            ew_green = 1;
            ns_red   = 1;
        end
        EW_YELLOW: begin
            ew_yellow = 1;
            ns_red    = 1;
        end
        NS_GREEN: begin
            ns_green = 1;
            ew_red   = 1;
        end
        NS_YELLOW: begin
            ns_yellow = 1;
            ew_red    = 1;
        end
    endcase
end

endmodule
module tb_traffic_light();
    reg clk, reset;
    wire ew_red, ew_yellow, ew_green;
    wire ns_red, ns_yellow, ns_green;

    traffic_light uut (
        .clk(clk),
        .reset(reset),
        .ew_red(ew_red),
        .ew_yellow(ew_yellow),
        .ew_green(ew_green),
        .ns_red(ns_red),
        .ns_yellow(ns_yellow),
        .ns_green(ns_green)
    );

    initial begin
        clk = 0;
        reset = 1;
        #10 reset = 0; // 复位后释放
    end

    always #5 clk = ~clk; // 10ns周期(假设1秒=10ns)

    initial begin
        $monitor("Time=%0t: EW=[R:%b Y:%b G:%b], NS=[R:%b Y:%b G:%b]",
                 $time, ew_red, ew_yellow, ew_green, ns_red, ns_yellow, ns_green);
        #500 $finish;
    end
endmodule

1.6.3 深入理解FIFO的底层资源(设置FIFO的位宽和深度即类型)

在 Vivado 中,FIFO IP 核的底层资源实现与 FPGA 的物理架构密切相关,主要涉及 Block RAM(BRAM)、分布式 RAM(Distributed RAM)、移位寄存器(SRL)以及逻辑资源(LUT/FF)。

首先,FIFO的类型可以分为同步FIFO异步FIFO两大类:

  • 同步FIFO:读写共享同一个时钟,无需跨时钟域
  • 异步FIFO:读写时钟独立,除了存储数据,还有跨时钟域处理的用途

接着,我们区分一下常听说的的BRAM、DRAM以

  • Block RAM:强制使用 BRAM,适合大容量或高频率场景。BRAM是FPGA有限的BRAM硬件资源,分为18k和32k,用一个少一个。在配置BRAM的宽度和深度的时候,一般建议选择将数据补零后占满当前资源量的最大值,充分利用BRAM资源。

1个18k的BRAM能存储多少数据呐?

1个18k的BRAM总共能存储18432bit = 9(宽度)*2048(深度) = 18(宽度) * 1024(深度) = 32(宽度) * 512(深度)

一个32k的BRAM能存储多少数据呐

示例:1024x32 FIFO → 总存储量 = 32,768b,需 1 块 36K BRAM(36,864b),相当于两个18k的BRAM级联得到。

  • Distributed RAM:强制使用 LUT,适合浅 FIFO 或低延迟需求。它消耗的是FPGA的逻辑资源,所以当数据量大的时候,对FPGA逻辑资源很不友好,所以适合存储像命令一类的数据

1.6.4 GMII输入信号延迟处理逻辑

1.6.5 校验逻辑实现(前导码、SFD、MAC地址、crc数据校验内容)

mac_receive模块作为数据从phy芯片传向FPGA做的第一次实际处理的模块,在这个模块中数据处理的逻辑毋庸置疑是最重要的内容,这里讲解校验逻辑的实现,只有当所有的校验通过后,我们的数据才会作为一个真正有效的数据向下传递,否则则会判定为非法帧而被过滤。

我们需要再次回忆这个模块接收到的数据到底是一个什么样的结构组成,我给大家绘制了一张图像,一目了然:

1.前导码和帧开始符是固定的,为7个0x55紧跟着1个0xd5 2.目的MAC地址指明帧的接受者 3.源MAC地址指明帧的发送者 4.以太网类型,指示帧的类型,比如0x0800表示该帧是IP数据包,0x0806表示该帧是ARP协议数据包 5.数据和填充就是所承载的数据包,跟前面以太网类型对应。 6.帧校验序列是一个32位的循环校验码(FCS)。   每一个设备都有一个不同的MAC地址,当一个设备A发送一个以太网帧,源MAC地址是自己的MAC地址,目的MAC地址如果是0xffffff,此时就是广播,所有与之连接的设备都会收到该帧,如果目的MAC地址是一个独特的MAC地址,那么本地MAC地址与之相同的设备将会接收到该以太网帧,然后通过判断以太网帧类型,进行下一步数据包解析。 那么下面我们来详细说明每一个校验逻辑(关于CRC校验逻辑,请参考下面的内容单独讲)

1.6.6 以太网数据帧分离(剥离有效数据和mac包头)

剥离接收到的以太网头部,包括前导码(7Byte)、SFD(1Byte)、目标MAC地址(6Byte)、源MAC地址(6Byte)、Type和Length(2Byte)。

通过使用字节计数器进行剥离数据,下面是详细的代码逻辑文件

1.6.7 MAC层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-10 22:03:40
 * @LastEditTime: 2025-02-17 07:09:17
 * @FilePath: \rtl\mac_receive.v
 * @Description: 以太网MAC接收模块,负责处理GMII接口数据,进行CRC校验,分离MAC头和有效数据,并跨时钟域传输:
                1. 接收GMII数据流,解析前导码、目标MAC地址、帧类型
                2. CRC校验(可选,设置CRC_CHECK_EN为高时开启crc校验)
                3. 使用双时钟FIFO实现跨时钟域传输
                4. 输出有效数据、帧类型和校验结果
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved. 
 * ****************************************************************************************
 */
// 程序存在问题,在实例化这个模块时,crc端口没有被实际连接在crc32_d8模块中,在此端口中crc32_d8的交互信号方向存在疑惑

module mac_receive#(
    parameter LOCAL_MAC_ADDR    = 48'hffffffffffff, // 本地MAC地址,用于匹配接收的MAC地址
    parameter CRC_CHECK_EN      = 1'b1              // CRC校验使能,1为启用,0为禁用
)(
    input               sys_clk             , // 系统接收时钟
    input               phy_rx_clk          , // phy芯片接收端时钟
    input               phy_rx_reset        , // phy芯片接收端复位信号
    input               sys_reset_n         , // 系统复位信号

    /*-------与rgmii_recieve模块交互信号--------*/
    input               gmii_rx_data_vld    , // GMII数据有效信号
    input [7:0]         gmii_rx_data        , // GMII接收数据

    /*-------与mac_to_arp_ip模块的交互信号--------*/
    output reg          mac_rx_data_vld     , // 输出数据有效
    output reg          mac_rx_data_last    , // 输出数据结束标志
    output reg [7:0]    mac_rx_data         , // 输出数据内容
    output reg [15:0]   mac_rx_frame_type   , // 以太网帧类型字段

    /*-------与rx_crc32_d8模块交互信号--------*/
    output reg          rx_crc_din_vld      , // CRC计算数据有效
    output reg [7:0]    rx_crc_din          , // CRC计算输入数据
    output reg          rx_crc_done         , // CRC计算完成信号
    input [31:0]        rx_crc_dout           // CRC计算结果输入

);
/*--------------------------------------------------*\
                       状态机信号定义
\*--------------------------------------------------*/
reg [2:0] cur_status            ; // 当前状态
reg [2:0] nxt_status            ; // 下一状态
localparam RX_IDLE = 3'b000     ; // 空闲状态:等待帧开始
localparam RX_PRE = 3'b001      ; // 前导码处理:读取帧信息
localparam RX_DATA = 3'b010     ; // 数据传输:输出有效数据
localparam RX_END = 3'b100      ; // 结束状态:完成以太网数据帧处理

/*--------------------------------------------------*\
                       接收MAC信号
\*--------------------------------------------------*/
reg [15:0]  rx_frame_type       ; // 帧类型字段(2字节)
reg [47:0]  rx_target_mac       ; // 目标MAC地址(6字节)
reg [10:0]  rx_cnt              ; // 接收计数器:统计有效数据字节数
reg [55:0]  rx_preamble         ; // 存储前导码(7字节)
reg         rx_preamble_chack   ; // 前导码校验结果,正确情况下应为7byte的0x55(固定值,参考我们的文档)
reg         rx_sfd_chack        ; // SFD校验结果(第7字节为0xD5)正确情况下应为1byte的0xd5(固定值,参考我们的文档
reg         rx_target_mac_chack ; // 目标MAC校验(本地MAC或广播地址)
reg         frame_fifo_crc      ; 
reg         rx_crc_chack        ; // CRC校验结果
reg         rx_wr_vld           ;

/*--------------------------------------------------*\
                       打拍信号:用于时序对齐
\*--------------------------------------------------*/
reg         gmii_rx_data_vld_d0     ;
reg         gmii_rx_data_vld_d1     ;
reg         gmii_rx_data_vld_d2     ;
reg         gmii_rx_data_vld_d3     ;
reg         gmii_rx_data_vld_d4     ;
reg [7:0]   gmii_rx_data_d0         ;
reg [7:0]   gmii_rx_data_d1         ;
reg [7:0]   gmii_rx_data_d2         ;
reg [7:0]   gmii_rx_data_d3         ;
reg [7:0]   gmii_rx_data_d4         ;

wire        rx_data_last            ;
reg         rx_data_last_d0         ;
reg         rx_data_last_d1         ;
reg         rx_data_last_d2         ;
reg         rx_data_last_d3         ;
reg         rx_data_last_d4         ;
reg         rx_data_last_d5         ;
reg         rx_data_last_d6         ;

/*--------------------------------------------------*\
                       FIFO端口信号
\*--------------------------------------------------*/
// 帧信息FIFO:存储帧类型和CRC校验结果
reg [16:0]  frame_din              ; // 帧FIFO数据输入
reg         frame_wren             ; // 帧FIFO写使能
wire [16:0] frame_dout             ; // 帧FIFO数据输出
reg         frame_rden             ; // 帧FIFO读使能
wire        frame_wrfull           ; // 帧FIFO写满标志
wire        frame_rdempty          ; // 帧FIFO读空标志
wire [3:0]  frame_wrcount          ; // 帧FIFO写计数
wire [3:0]  frame_rdcount          ; // 帧FIFO读计数

// 数据FIFO:存储有效载荷数据和结束标志
reg [8:0]   data_din               ; // 数据FIFO数据输入
reg         data_wren              ; // 数据FIFO写使能
wire [8:0]  data_dout              ; // 数据FIFO数据输出
reg         data_rden              ; // 数据FIFO读使能
wire        data_wrfull            ; // 数据FIFO写满标志
wire        data_rdempty           ; // 数据FIFO读空标志
wire [11:0] data_wrcount           ; // 数据FIFO写计数
wire [11:0] data_rdcount           ; // 数据FIFO读计数


//数据结束检测:利用有效信号的下降沿检测
assign rx_data_last = ~gmii_rx_data_vld & gmii_rx_data_vld_d0;


// GMII输入信号延迟处理(5级延迟用于时序对齐)
always @(posedge phy_rx_clk) begin
    gmii_rx_data_vld_d0 <= gmii_rx_data_vld;
    gmii_rx_data_vld_d1 <= gmii_rx_data_vld_d0;
    gmii_rx_data_vld_d2 <= gmii_rx_data_vld_d1;
    gmii_rx_data_vld_d3 <= gmii_rx_data_vld_d2;
    gmii_rx_data_vld_d4 <= gmii_rx_data_vld_d3;

    gmii_rx_data_d0     <= gmii_rx_data;
    gmii_rx_data_d1     <= gmii_rx_data_d0;
    gmii_rx_data_d2     <= gmii_rx_data_d1;
    gmii_rx_data_d3     <= gmii_rx_data_d2; 
    gmii_rx_data_d4     <= gmii_rx_data_d3; 

    rx_data_last_d0     <= rx_data_last;
    rx_data_last_d1     <= rx_data_last_d0;
    rx_data_last_d2     <= rx_data_last_d1;
    rx_data_last_d3     <= rx_data_last_d2;
    rx_data_last_d4     <= rx_data_last_d3; 
    rx_data_last_d5     <= rx_data_last_d4;     
    rx_data_last_d6     <= rx_data_last_d5; 
end

/*--------------------------------------------------*\
            rx_cnt:接收计数器,统计有效数据字节数
\*--------------------------------------------------*/
// 在phy_rx_clk时钟下进行计数,
always @(posedge phy_rx_clk) begin
    if (phy_rx_reset) 
        rx_cnt <= 0;
    else if (gmii_rx_data_vld_d4) //打5拍计数
        rx_cnt <= rx_cnt + 1;
    else 
        rx_cnt <= 0;
end


/*--------------------------------------------------*\
    校验preamble、sfd、mac addr
\*--------------------------------------------------*/
// 所有校验通过后,数据才会被后续模块处理,确保非法帧被过滤  
// 前导码移位寄存器的实现
always @(posedge phy_rx_clk) begin
    if (gmii_rx_data_vld_d4 && rx_cnt < 7) // rx_cnt < 7 对应以太网前导码的 7 个 0x55
        rx_preamble <= {rx_preamble[47:0], gmii_rx_data_d4}; // 左移拼接新字节
    else 
        rx_preamble <= rx_preamble; // 保持当前值
end
// 前导码校验逻辑
always @(posedge phy_rx_clk) begin
    if (rx_preamble == 56'h5555_5555_5555_55) 
        rx_preamble_chack <= 1'b1; // 前导码正确
    else 
        rx_preamble_chack <= 1'b0; // 前导码错误
end

// SFD校验逻辑
// rx_cnt == 7 表示第 8 个有效数据字节(前导码占 7 字节,SFD 是第 8 字节)
always @(posedge phy_rx_clk) begin
    if (rx_cnt == 7 && gmii_rx_data_d4 == 8'hd5) 
        rx_sfd_chack <= 1'b1; // SFD正确
    else 
        rx_sfd_chack <= 1'b0; // SFD错误
end


// 目标MAC地址移位寄存器的实现
// rx_cnt > 7 && rx_cnt < 14 表示第 9-14 字节(SFD 后紧跟 6 字节目标MAC地址)
always @(posedge phy_rx_clk) begin
    if (gmii_rx_data_vld_d4 && rx_cnt > 7 && rx_cnt < 14) 
        rx_target_mac <= {rx_target_mac[39:0], gmii_rx_data_d4}; // 右移拼接新字节
    else 
        rx_target_mac <= rx_target_mac; // 保持当前值
end

// 目标MAC地址校验逻辑
always @(posedge phy_rx_clk) begin
    if (rx_target_mac == LOCAL_MAC_ADDR || rx_target_mac == 48'hffffffffffff) 
        rx_target_mac_chack <= 1'b1; // 匹配本地MAC或广播地址
    else 
        rx_target_mac_chack <= 1'b0; // 不匹配
end
/*--------------------------------------------------*\
                       CRC校验模块
\*--------------------------------------------------*/
// 提取数值后传递给crc32_d8模块,
generate
    if (CRC_CHECK_EN == 0) begin
        // 禁用CRC校验时,直接标记为通过
        always @(posedge phy_rx_clk) begin
            rx_crc_chack <= 1'b1; // 强制通过
        end
    end else if (CRC_CHECK_EN == 1) begin
        reg         crc_en;     // CRC计算使能
        reg [31:0]  rc_crc;     // 接收端提取的CRC值

        // CRC计算使能控制
        always @(posedge phy_rx_clk) begin
            if (rx_cnt == 7)         // 第8字节(目标MAC开始)
                crc_en <= 1;         // 启动CRC计算
            else if (rx_data_last)   // 数据结束时
                crc_en <= 0;         // 停止计算
        end

        // 连接CRC模块的接口
        always @(posedge phy_rx_clk) begin
            rx_crc_din_vld <= crc_en;       // 有效信号
            rx_crc_din     <= gmii_rx_data_d4; // 输入数据(对齐后)
            rx_crc_done    <= rx_data_last_d6; // 计算完成信号(延迟6周期)
        end

        // 接收端CRC提取(从数据尾部)
        always @(posedge phy_rx_clk) begin
            // 在最后4个周期(rx_data_last_d0~d3)提取CRC值
            if (rx_data_last_d0 || rx_data_last_d1 || rx_data_last_d2 || rx_data_last_d3) 
                rc_crc <= {gmii_rx_data_d4, rc_crc[31:8]}; // 小端转换
        end

        // CRC校验结果比较
        always @(posedge phy_rx_clk) begin
            if (rx_data_last_d5) // 延迟到CRC计算完成
                rx_crc_chack <= (rc_crc == rx_crc_dout); // 校验结果
        end
    end
endgenerate

/*--------------------------------------------------*\
                       数据写入FIFO
\*--------------------------------------------------*/
// 帧类型提取(第21-22字节)
always @(posedge phy_rx_clk) begin
    if (gmii_rx_data_vld_d4 && rx_cnt > 19 && rx_cnt < 22) 
        rx_frame_type <= {rx_frame_type[7:0], gmii_rx_data_d4}; // 拼接为16位
    end


// 帧信息写入frame_fifo(CRC结果 + 帧类型)
always @(posedge phy_rx_clk) begin
    if (rx_data_last_d6 && rx_preamble_chack && rx_sfd_chack && rx_target_mac_chack) begin
        frame_din  <= {rx_crc_chack, rx_frame_type}; // [16:0] = {1bit CRC, 16bit Type}
        frame_wren <= 1'b1;
    end
    else begin
        frame_din  <= frame_din;
        frame_wren <= 1'b0;
    end 
end

// 有效数据写入data_fifo(数据 + 结束标志)
always @(posedge phy_rx_clk) begin
    if (rx_preamble_chack && rx_sfd_chack && rx_target_mac_chack && gmii_rx_data_vld_d4 && rx_cnt == 21) 
       rx_wr_vld <= 1'b1; 
    else if (rx_data_last) 
       rx_wr_vld <= 1'b0;  
end

always @(posedge phy_rx_clk) begin
    data_wren <= rx_wr_vld;
    data_din  <= {rx_data_last,gmii_rx_data_d4}; // [8:0] = {1bit Last, 8bit Data}
end
/*--------------------------------------------------*\
                       状态机控制
\*--------------------------------------------------*/
// 时序逻辑:状态机当前状态寄存器
always @(posedge sys_clk or negedge sys_reset_n) begin
    if (!sys_reset_n) begin
        cur_status <= RX_IDLE; // 异步复位,状态机回复到空闲状态
    end else begin
        cur_status <= nxt_status; // 同步更新到下一状态
    end
end

// 组合逻辑:计算下一状态
always @(*) begin
    // 默认下一状态保持当前状态
    nxt_status = cur_status; 

    // 在组合逻辑中处理下一状态转换
    case (cur_status)
        RX_IDLE: begin
            if (~frame_rdempty) begin
                nxt_status = RX_PRE; // 如果帧FIFO不为空,进入前导码状态
            end
        end
        RX_PRE: begin
            nxt_status = RX_DATA; // 前导码状态后进入数据状态
        end
        RX_DATA: begin
            if (data_rden && data_dout[8]) begin
                nxt_status = RX_END; // 数据读取完成且遇到包尾时,进入结束状态
            end
        end
        RX_END: begin
            nxt_status = RX_IDLE; // 结束状态后回到空闲状态
        end
        default: begin
            nxt_status = RX_IDLE; // 默认(非法)状态,回到空闲
        end
    endcase
end

/*--------------------------------------------------*\
                       FIFO读数据
\*--------------------------------------------------*/
// 组合逻辑:控制帧FIFO读使能信号
always @(*) begin
    frame_rden = (cur_status == RX_PRE); // 仅在前导码状态时使能帧FIFO读取
end

always @(posedge sys_clk) begin
    if (frame_rden) begin
        mac_rx_frame_type <= frame_dout[15:0];
        frame_fifo_crc    <= frame_dout[16];
    end
end

always @(posedge sys_clk) begin
    if (cur_status == RX_PRE) 
        data_rden <= 1'b1;
    else if (cur_status == RX_DATA && data_rden && data_dout[8]) 
        data_rden <= 1'b0;
    else 
        data_rden <= data_rden;
end

always @(posedge sys_clk) begin
    if (data_rden && frame_fifo_crc) begin
        mac_rx_data      <= data_dout[7:0];
        mac_rx_data_last <= data_dout[8];
        mac_rx_data_vld  <= 1'b1;
    end
    else begin
        mac_rx_data      <= 0;
        mac_rx_data_last <= 0;
        mac_rx_data_vld  <= 0;
    end 
end

/*--------------------------------------------------*\
                       FIFO实例化
\*--------------------------------------------------*/
fifo_w17xd16 frame_fifo (
  .rst(phy_rx_reset),                      // input wire rst
  .wr_clk(phy_rx_clk),                // input wire wr_clk
  .rd_clk(sys_clk),                // input wire rd_clk
  .din(frame_din),                      // input wire [16 : 0] din
  .wr_en(frame_wren),                  // input wire wr_en
  .rd_en(frame_rden),                  // input wire rd_en
  .dout(frame_dout),                    // output wire [16 : 0] dout
  .full(frame_wrfull),                    // output wire full
  .empty(frame_rdempty),                  // output wire empty
  .rd_data_count(frame_rdcount),  // output wire [3 : 0] rd_data_count
  .wr_data_count(frame_wrcount)  // output wire [3 : 0] wr_data_count
);

fifo_w9xd4096 data_fifo (
  .rst(phy_rx_reset),                      // input wire rst
  .wr_clk(phy_rx_clk),                // input wire wr_clk
  .rd_clk(sys_clk),                // input wire rd_clk
  .din(data_din),                      // input wire [8 : 0] din
  .wr_en(data_wren),                  // input wire wr_en
  .rd_en(data_rden),                  // input wire rd_en
  .dout(data_dout),                    // output wire [8 : 0] dout
  .full(data_wrfull),                    // output wire full
  .empty(data_rdempty),                  // output wire empty
  .rd_data_count(data_rdcount),  // output wire [11 : 0] rd_data_count
  .wr_data_count(data_wrcount)  // output wire [11 : 0] wr_data_count
);

endmodule

1.6.8 mac_to_arp_ip.v模块编写

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-17 16:14:13
 * @LastEditTime: 2025-02-17 16:27:58
 * @FilePath: rtl/mac_to_arp_ip.v
 * @Description:核心作用是根据以太网帧类型字段,将MAC层接收的数据分发至上层协议处理模块(ARP/IP)
 1. 协议符合时直通链路,协议不符合时数据静默清零
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module mac_to_arp_ip(
    /*-------系统接口--------*/
    input           sys_clk             ,
    input           sys_reset_n         ,

    /*-------MAC接收接口--------*/
    input           mac_rx_data_vld     ,
    input           mac_rx_data_last    ,
    input [7:0]     mac_rx_data         ,
    input [15:0]    mac_rx_frame_type   ,  

    /*-------IP协议接口--------*/
    output reg      ip_rx_data_vld      ,
    output reg      ip_rx_data_last     ,
    output reg [7:0]ip_rx_data          ,

    /*-------ARP协议接口--------*/
    output reg      arp_rx_data_vld     ,
    output reg      arp_rx_data_last    ,
    output reg [7:0]arp_rx_data             
 );
/*--------------------------------------------------*\
                       协议类型识别机制
\*--------------------------------------------------*/
// ARP协议:0x0806
// IPv4协议:0x0800
// IPv6协议:0x86DD(当前设计未实现)
localparam ARP_TYPE = 16'h0806;  // ARP协议类型值(IEEE标准定义)
localparam IP_TYPE  = 16'h0800;  // IPv4协议类型值

/*--------------------------------------------------*\
                       IP数据通道控制
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (mac_rx_frame_type == IP_TYPE) begin
        ip_rx_data_vld  <=  mac_rx_data_vld;
        ip_rx_data_last <=  mac_rx_data_last;
        ip_rx_data      <=  mac_rx_data;
    end
    else begin
        ip_rx_data_vld  <=  0;
        ip_rx_data_last <=  0;
        ip_rx_data      <=  0;  
    end     
end

/*--------------------------------------------------*\
                       ARP数据通道控制
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (mac_rx_frame_type == ARP_TYPE) begin
        arp_rx_data_vld  <=  mac_rx_data_vld;
        arp_rx_data_last <=  mac_rx_data_last;
        arp_rx_data      <=  mac_rx_data;
    end
    else begin
        arp_rx_data_vld  <=  0;
        arp_rx_data_last <=  0;
        arp_rx_data      <=  0;  
    end     
end

 endmodule

1.6.8 代码仿真与验证

1.7 MAC发送模块代码编写:mac_send.v

MAC发送代码模块的主要功能将数据添加MAC头部,包含前导码、MAC头部、源MAC地址、目标MAC地址,类型与长度以及CRC校验。最后将数据传递给rgmii_send.v模块进行发送给phy芯片,最后将数据传输到外部。

在这个工程里面除了刚刚讲到的组包和CRC校验的过程,还涉及到系统时钟域sys_clk跨域到PHY时钟域phy_tx_clk。

1.7.1 添加MAC头部

数据添加MAC头部时,与前面接收MAC数据包并刨析MAC数据头部原理时相似的,使用一个字节计数器,在状态机位TX_DATA的时候字节计数器激活,每一个phy时钟上升沿计数器加一用来计数。

使用case语句来对每一个字节位控制发送的数据位是什么,比如在下面的代码中,0-6发送的字节位8‘h55,在第七位发送8’hd5等等,原理很简单。

always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        tx_cnt <= 0;
    else if (cur_status == TX_DATA) 
        tx_cnt <= tx_cnt + 1; // 在数据阶段递增计数器
    else 
        tx_cnt <= 0; // 其他状态复位计数器
end
always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        send_data <= 0;
    else 
        case(tx_cnt)
            0,1,2,3,4,5,6  : send_data <= 8'h55; //// 前导码(7字节0x55)+ 帧起始符(1字节0xD5)
            7              : send_data <= 8'hd5; //帧起始定界符

            8              : send_data <= send_target_mac_cdc[47:40]; // 目标MAC(6字节) 高位先发,9-13依次发送后续字节
            9              : send_data <= send_target_mac_cdc[39:32];
            10             : send_data <= send_target_mac_cdc[31:24];
            11             : send_data <= send_target_mac_cdc[23:16];
            12             : send_data <= send_target_mac_cdc[15:8];
            13             : send_data <= send_target_mac_cdc[7:0];

            14             : send_data <= LOCAL_MAC_ADDR[47:40]; // 源MAC(6字节)
            15             : send_data <= LOCAL_MAC_ADDR[39:32];
            16             : send_data <= LOCAL_MAC_ADDR[31:24];
            17             : send_data <= LOCAL_MAC_ADDR[23:16];
            18             : send_data <= LOCAL_MAC_ADDR[15:8];
            19             : send_data <= LOCAL_MAC_ADDR[7:0];                                              

            20             : send_data <= mac_type[15:8]; // 类型字段(2字节)
            21             : send_data <= mac_type[7:0];  
            default        : send_data <= data_dout[7:0]; // 有效载荷(从data_fifo读取)
        endcase 
end

1.7.2 跨时钟域处理

在进行跨时钟域处理时,同样使用异步FIFO缓存数据的方法对数据进行跨时钟域处理。使用位宽为64位,深度位16位的的fifo缓存帧信息,用位宽为9位,深度为4096的fifo缓存以太网有效数据,

1.7.3 CRC校验(循环冗余校验)

在数据传输过程中,无论传输系统设计的多么完美,差错总会存在,这种差错可能是在传输链路中信号收到干扰导致一位数据从0突变成1,通过本教程,读者可深入理解 CRC32 的原理、硬件实现细节,并掌握该模块的正确使用方法,下面是详细的内容:

1.7.3.1 什么是 CRC?

CRC(Cyclic Redundancy Check,循环冗余校验)是一种数据检错算法,用于检测或校验数据传输/存储过程中是否发生错误。

  • 核心思想:通过多项式除法,生成固定长度的校验码(如32位),附加到数据末尾。接收方通过重新计算校验码验证数据完整性。
  • 特点
  • 检错能力强(可检测单bit、双bit、突发错误等)
  • 计算效率高(硬件实现简单)
  • 广泛用于以太网、磁盘存储、ZIP压缩等场景。
  • 特性
  • 生成32位校验码
  • 初始值为 0xFFFFFFFF
  • 输出结果取反(~)并反转位序(LSB first → MSB first)

1 发送端流程

  1. 数据输入:将待发送的以太网帧数据(包括前导码、MAC头、负载)逐字节输入模块。
  2. CRC附加:数据输入完成后,将 crc_dout 附加到帧末尾。

2 接收端校验

  1. 重新计算 CRC:接收数据时,重新计算 CRC 值(包括接收的 CRC 字段)。
  2. 校验判定:若最终 CRC 值不为 0xC704DD7B(以太网 Magic Value),则判定数据错误。
1.7.3.2 CRC 计算原理

1 模二除法(核心算法) CRC 的本质是模二多项式除法:

  • 输入数据 视为一个二进制多项式(如 0xA310100011x^7 + x^5 + x + 1)。
  • 生成多项式 为固定值(如CRC32多项式)。
  • 计算过程:数据多项式除以生成多项式,余数即为 CRC 值。

2 硬件实现(移位寄存器) 传统实现方式:

  • 串行计算:逐比特移位异或。
  • 并行优化:通过组合逻辑预计算多个比特的异或关系,提升速度(如8位并行计算)。

3 并行化推导 并行计算的本质是预判多bit输入对寄存器的影响。

  • 数学推导: 根据多项式,列出每个寄存器位在8次移位后的表达式。 例如,若输入8位数据 D[7:0],则新的寄存器值 CRC_new[31:0]CRC_old[31:0]D[7:0] 的异或组合。
  • 组合逻辑展开: 通过数学展开,得到每个寄存器位的更新方程(如代码中的 crc_data[0] = ...)。
1.7.3.3模块代码解析

1 输入处理

// 输入字节位序反转(MSB→LSB)
assign crc_din_r = {crc_din[0], crc_din[1], ..., crc_din[7]};
  • 原因:CRC计算从LSB开始处理,而输入数据通常是MSB在前。

2 组合逻辑计算 代码中 crc_data[31:0] 的每个位均由异或表达式生成,例如:

assign crc_data[0] = crc_din_r[6] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[30];
  • 解释: 根据多项式推导,crc_data[0] 的值由输入数据位和当前寄存器位的异或组合决定。

3 时序逻辑更新

always @(posedge sys_clk) begin
  if (!sys_reset_n) 
    crc_dout_r <= 32'hffff_ffff; // 初始值
  else if (crc_done) 
    crc_dout_r <= 32'hffff_ffff; // 复位
  else if (crc_din_vld)
    crc_dout_r <= crc_data;       // 更新CRC值
end
  • 关键点
  • 初始值和复位值均为 0xFFFFFFFF(IEEE 802.3要求)。
  • 每个有效输入字节触发一次寄存器更新。

4 输出处理

assign crc_dout = ~{反转位序(crc_dout_r)};
  • 步骤
  • 反转寄存器位序(crc_dout_r[0]crc_dout[31])。
  • 对结果取反(~)。
1.7.3.4完整计算流程

1 数据输入阶段 假设输入数据为 0x01, 0x02, 0x03, 0x04

  • 步骤
  • 复位寄存器(crc_done=1)。
  • 依次输入字节,每个字节持续一个时钟周期(crc_din_vld=1)。
  • 最后一个字节输入后,crc_dout 输出最终校验码。

2 计算过程示例

时钟周期 操作 寄存器值(crc_dout_r)
0 复位 0xFFFFFFFF
1 输入 0x01 更新为 CRC(0x01)
2 输入 0x02 更新为 CRC(0x01,0x02)
3 输入 0x03 更新为 CRC(0x01,0x02,0x03)
4 输入 0x04 更新为最终值
5 输出 crc_dout 取反并反转位序
1.7.3.5 总结

1 以太网帧结构

复制

| 前导码 | 目标MAC | 源MAC | 类型 | 数据 | CRC32 |
  • CRC位置:附加在数据字段末尾。

2 模块集成

  • 发送端
  • 将数据逐字节输入模块。
  • 数据发送完成后,附加 crc_dout 到帧末尾。
  • 接收端
  • 重新计算 CRC,若结果不为 0xC704DD7B(以太网“Magic Value”),则判定数据错误。
1.7.3.6 实际使用时的便捷方法

1. 验证方法

  • 软件对比: 使用 Python 计算同一数据的 CRC32:
import binascii
data = bytes([0x01, 0x02, 0x03, 0x04])
crc = binascii.crc32(data) & 0xFFFFFFFF
print(hex(crc)) # 输出应为模块计算结果

2. 实用工具

用Verilog算CRC已经有了两个比较不错的网站,可以在线生成Verilog代码,如:

  • OutputLogic.com » CRC Generator

  • Generator for CRC HDL code

它对应的开源库:GitHub - mbuesch/crcgen: Generator for CRC HDL code (VHDL, Verilog, MyHDL)

1.7.3.7 模块代码解析

该模块采用经典的 并行 CRC 计算架构,由以下核心部分组成:

  1. 输入位序处理
  2. 组合逻辑计算核心
  3. 时序控制与寄存器更新
  4. 输出结果后处理

1 模块接口

module crc32_d8(
    input        sys_clk,        // 系统时钟
    input        sys_reset_n,    // 同步复位(低有效)
    input        crc_din_vld,    // 输入数据有效
    input [7:0]  crc_din,        // 8位输入数据
    input        crc_done,       // 计算完成信号(复位寄存器)
    output [31:0] crc_dout       // CRC32 结果(小端序+取反)
);

2 关键逻辑实现

代码段 功能说明
输入位序反转 将输入字节从 MSB-first 转为 LSB-first:crc_din_r[7:0] = {crc_din[0], crc_din[1], ..., crc_din[7]}
组合逻辑计算 32位并行计算,通过预推导的异或表达式更新 CRC 值(代码中 crc_data[31:0]
时序逻辑更新 在时钟上升沿更新寄存器值,支持复位和流式输入
输出处理 结果取反并反转位序:crc_dout = ~{crc_dout_r[0], crc_dout_r[1], ..., crc_dout_r[31]}
1.7.3.8 完整代码工程
/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-14 18:08:12
 * @LastEditTime: 2025-02-15 16:30:28
 * @FilePath: \rtl\crc32_d8.v
 * @Description:基于IEEE 802.3标准的CRC32校验码生成器(8位并行计算)
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

module crc32_d8(
    input        sys_clk,         // 系统时钟
    input        sys_reset_n,       // 同步复位(高有效)

    // CRC计算控制接口
    input        crc_din_vld, // 输入数据有效信号
    input [7:0]  crc_din,     // 输入数据(8位并行)
    input        crc_done,    // 计算完成信号(复位CRC寄存器)

    // CRC输出接口
    output [31:0] crc_dout    // CRC32计算结果(小端序+取反)
);

/*--------------------------------------------------*\
                       内部信号定义
\*--------------------------------------------------*/
wire [7:0]  crc_din_r;      // 位序调整后的输入数据(LSB first)
reg  [31:0] crc_dout_r;     // CRC计算结果寄存器
wire [31:0] crc_data;       // 组合逻辑计算中间值

/*--------------------------------------------------*\
                       输出结果处理
\*--------------------------------------------------*/
// 最终输出处理:按以太网标准要求
// 1. 寄存器值取反(~)
// 2. 位序反转([0]→[31])
assign crc_dout = ~{
    crc_dout_r[0],  crc_dout_r[1],  crc_dout_r[2],  crc_dout_r[3], 
    crc_dout_r[4],  crc_dout_r[5],  crc_dout_r[6],  crc_dout_r[7],
    crc_dout_r[8],  crc_dout_r[9],  crc_dout_r[10], crc_dout_r[11],
    crc_dout_r[12], crc_dout_r[13], crc_dout_r[14], crc_dout_r[15],
    crc_dout_r[16], crc_dout_r[17], crc_dout_r[18], crc_dout_r[19],
    crc_dout_r[20], crc_dout_r[21], crc_dout_r[22], crc_dout_r[23],
    crc_dout_r[24], crc_dout_r[25], crc_dout_r[26], crc_dout_r[27],
    crc_dout_r[28], crc_dout_r[29], crc_dout_r[30], crc_dout_r[31]
};

/*--------------------------------------------------*\
                       输入数据位序处理 
\*--------------------------------------------------*/
// 将输入字节位序反转(MSB→LSB)
// 原因:CRC计算从数据LSB开始处理
assign crc_din_r = {
    crc_din[0], crc_din[1], crc_din[2], crc_din[3],
    crc_din[4], crc_din[5], crc_din[6], crc_din[7]
};

/*--------------------------------------------------*\
                        CRC组合逻辑计算
\*--------------------------------------------------*/
// 根据CRC32多项式推导的并行计算逻辑
// 多项式: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
assign   crc_data[0] = crc_din_r[6] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[30];
assign   crc_data[1] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[2] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[3] = crc_din_r[7] ^ crc_din_r[3] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[27] ^ crc_dout_r[31];
assign   crc_data[4] = crc_din_r[6] ^ crc_din_r[4] ^ crc_din_r[3] ^ crc_din_r[2] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[26] ^ crc_dout_r[27] ^ crc_dout_r[28] ^ crc_dout_r[30];
assign   crc_data[5] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[3] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[27] ^ crc_dout_r[28] ^ crc_dout_r[29] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[6] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[28] ^ crc_dout_r[29] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[7] = crc_din_r[7] ^ crc_din_r[5] ^ crc_din_r[3] ^ crc_din_r[2] ^ crc_din_r[0] ^ crc_dout_r[24] ^ crc_dout_r[26] ^ crc_dout_r[27] ^ crc_dout_r[29] ^ crc_dout_r[31];
assign   crc_data[8] = crc_din_r[4] ^ crc_din_r[3] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[0] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[27] ^ crc_dout_r[28];
assign   crc_data[9] = crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_dout_r[1] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[28] ^ crc_dout_r[29];
assign   crc_data[10] = crc_din_r[5] ^ crc_din_r[3] ^ crc_din_r[2] ^ crc_din_r[0] ^ crc_dout_r[2] ^ crc_dout_r[24] ^ crc_dout_r[26] ^ crc_dout_r[27] ^ crc_dout_r[29];
assign   crc_data[11] = crc_din_r[4] ^ crc_din_r[3] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[3] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[27] ^ crc_dout_r[28];
assign   crc_data[12] = crc_din_r[6] ^ crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[4] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[28] ^ crc_dout_r[29] ^ crc_dout_r[30];
assign   crc_data[13] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[5] ^ crc_din_r[3] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_dout_r[5] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[27] ^ crc_dout_r[29] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[14] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[4] ^ crc_din_r[3] ^ crc_din_r[2] ^ crc_dout_r[6] ^ crc_dout_r[26] ^ crc_dout_r[27] ^ crc_dout_r[28] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[15] = crc_din_r[7] ^ crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[3] ^ crc_dout_r[7] ^ crc_dout_r[27] ^ crc_dout_r[28] ^ crc_dout_r[29] ^ crc_dout_r[31];
assign   crc_data[16] = crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[0] ^ crc_dout_r[8] ^ crc_dout_r[24] ^ crc_dout_r[28] ^ crc_dout_r[29];
assign   crc_data[17] = crc_din_r[6] ^ crc_din_r[5] ^ crc_din_r[1] ^ crc_dout_r[9] ^ crc_dout_r[25] ^ crc_dout_r[29] ^ crc_dout_r[30];
assign   crc_data[18] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[2] ^ crc_dout_r[10] ^ crc_dout_r[26] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[19] = crc_din_r[7] ^ crc_din_r[3] ^ crc_dout_r[11] ^ crc_dout_r[27] ^ crc_dout_r[31];
assign   crc_data[20] = crc_din_r[4] ^ crc_dout_r[12] ^ crc_dout_r[28];
assign   crc_data[21] = crc_din_r[5] ^ crc_dout_r[13] ^ crc_dout_r[29];
assign   crc_data[22] = crc_din_r[0] ^ crc_dout_r[14] ^ crc_dout_r[24];
assign   crc_data[23] = crc_din_r[6] ^ crc_din_r[1] ^ crc_din_r[0] ^ crc_dout_r[15] ^ crc_dout_r[24] ^ crc_dout_r[25] ^ crc_dout_r[30];
assign   crc_data[24] = crc_din_r[7] ^ crc_din_r[2] ^ crc_din_r[1] ^ crc_dout_r[16] ^ crc_dout_r[25] ^ crc_dout_r[26] ^ crc_dout_r[31];
assign   crc_data[25] = crc_din_r[3] ^ crc_din_r[2] ^ crc_dout_r[17] ^ crc_dout_r[26] ^ crc_dout_r[27];
assign   crc_data[26] = crc_din_r[6] ^ crc_din_r[4] ^ crc_din_r[3] ^ crc_din_r[0] ^ crc_dout_r[18] ^ crc_dout_r[24] ^ crc_dout_r[27] ^ crc_dout_r[28] ^ crc_dout_r[30];
assign   crc_data[27] = crc_din_r[7] ^ crc_din_r[5] ^ crc_din_r[4] ^ crc_din_r[1] ^ crc_dout_r[19] ^ crc_dout_r[25] ^ crc_dout_r[28] ^ crc_dout_r[29] ^ crc_dout_r[31];
assign   crc_data[28] = crc_din_r[6] ^ crc_din_r[5] ^ crc_din_r[2] ^ crc_dout_r[20] ^ crc_dout_r[26] ^ crc_dout_r[29] ^ crc_dout_r[30];
assign   crc_data[29] = crc_din_r[7] ^ crc_din_r[6] ^ crc_din_r[3] ^ crc_dout_r[21] ^ crc_dout_r[27] ^ crc_dout_r[30] ^ crc_dout_r[31];
assign   crc_data[30] = crc_din_r[7] ^ crc_din_r[4] ^ crc_dout_r[22] ^ crc_dout_r[28] ^ crc_dout_r[31];
assign   crc_data[31] = crc_din_r[5] ^ crc_dout_r[23] ^ crc_dout_r[29];

/*--------------------------------------------------*\
                       时序逻辑部分
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        // 复位时初始化CRC寄存器为全1(标准要求)
        crc_dout_r <= 32'hffff_ffff;
    else if (crc_done) 
        // 计算完成时复位寄存器,准备下一次计算
        crc_dout_r <= 32'hffff_ffff;
    else if (crc_din_vld)
        // 有效数据输入时更新CRC值
        crc_dout_r <= crc_data;
    else 
        // 保持当前值
        crc_dout_r <= crc_dout_r;
end


endmodule
1.7.3.9 代码仿真与验证
module tb_crc32_d8();
    reg sys_clk, sys_reset_n;
    reg crc_din_vld;
    reg [7:0] crc_din;
    reg crc_done;
    wire [31:0] crc_dout;

    // 实例化模块
    crc32_d8 u_crc (
        .sys_clk(sys_clk),
        .sys_reset_n(sys_reset_n),
        .crc_din_vld(crc_din_vld),
        .crc_din(crc_din),
        .crc_done(crc_done),
        .crc_dout(crc_dout)
    );

    initial begin
        sys_clk = 0;
        forever #5 sys_clk = ~sys_clk;
    end

    initial begin
        sys_reset_n = 0;
        crc_din_vld = 0;
        crc_din = 0;
        crc_done = 0;
        #20;
        sys_reset_n = 1;

        // 输入数据 0x00
        crc_din_vld = 1;
        crc_din = 8'h00;
        #10;

        // 输入数据 0x01
        crc_din = 8'h01;
        #10;

        // 输入数据 0x02
        crc_din = 8'h02;
        #10;

        // 输入数据 0x03
        crc_din = 8'h03;
        #10;

        crc_din_vld = 0;
        #20;

        // 输出 CRC 值
        $display("CRC Result: 0x%h", crc_dout);
        $finish;
    end
endmodule
1.7.3.10 关键问题回答

Q1:为何初始值为 0xFFFFFFFF

  • 标准要求:IEEE 802.3 规定 CRC 寄存器初始化为全1,以提高错误检测能力。

Q2:输出为何要取反并反转位序?

  • 兼容性:确保接收方能正确识别校验结果,符合以太网帧格式规范。

Q3:组合逻辑表达式能否自动生成?

  • 可以:使用 Python 脚本(如用户提供的代码)基于多项式自动推导,但本模块为专用设计选择手动优化。

Q4:如何处理长数据流?

  • 流式处理:连续输入数据字节,每个周期更新 CRC 值,crc_done 用于复位寄存器以开始新计算。

1.7.4 MAC层发送模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-15 14:38:41
 * @LastEditTime: 2025-02-15 17:17:47
 * @FilePath: rtl/mac_send
 * @Description:以太网mac层数据发送模块,主要功能如下:
 1. 组包:根据以太网协议封装MAC帧,包含前导码、MAC头部、有效数据、CRC校验
 2. 跨时钟域处理:用户时钟(clk)与PHY时钟(phy_tx_clk)间的数据传输
 3. CRC校验计算:实时计算并附加CRC32校验码
 4. ARP交互支持:动态更新目标MAC地址
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module mac_send #(
    parameter     LOCAL_MAC_ADDR    = 48'hffffffffffff,
    parameter     TARGET_MAC_ADDR   = 48'hffffffffffff

 )(
    input               sys_clk         , // 系统时钟信号
    input               phy_tx_clk      , // phy芯片发送时钟

    input               phy_tx_reset    , // phy芯片复位信号
    input               sys_reset_n       , // 系统复位信号

    /*-------与rgmii_send模块交互信号--------*/
    output reg          gmii_tx_data_vld, // 输出给gmii数据有效信号
    output reg  [7:0]   gmii_tx_data    , // 输出给gmii数据信号

    /*-------与ip_send模块交互信号--------*/
    input               mac_tx_data_vld , // 输入mac的数据有效信号
    input               mac_tx_data_last, // 输入mac的数据结束标志
    input       [7:0]   mac_tx_data     , // 输入mac的数据信号
    input       [15:0]  mac_tx_frame_type, // 输入mac的帧类型(如0x0800=IP, 0x0806=ARP)
    input       [15:0]  mac_tx_length   , // 输入mac的数据长度

    /*-------查询arp链表的mac地址--------*/
    input       [47:0]  rd_arp_list_mac , // 查询到的MAC地址
    input               rd_arp_list_mac_vld , // 查询结果有效信号

    /*-------与tx_crc32_d8模块交互信号--------*/
    output reg          tx_crc_din_vld  , // 输出给CRC有效标志信号
    output      [7:0]   tx_crc_din      , // 输出给CRC的数据
    output reg          tx_crc_done     , // 输出给CRC计算完成信号
    input       [31:0]  tx_crc_dout       // CRC计算结果
 );
/*--------------------------------------------------*\
                       状态机信号定义
\*--------------------------------------------------*/
reg [2:0] cur_status, nxt_status;
localparam TX_IDLE = 3'b000;  // 空闲状态
localparam TX_PRE  = 3'b001;  // 前导码阶段
localparam TX_DATA = 3'b010;  // 数据发送阶段
localparam TX_END  = 3'b100;  // 结束阶段(发送CRC)

/*--------------------------------------------------*\
                       FIFO信号定义
\*--------------------------------------------------*/
// frame_fifo:存储目标MAC+帧类型(64位位宽)
reg  [63:0]  frame_din      ; // 输入数据:{目标MAC(48b), 类型(16b)}
reg          frame_wren     ; // 写入使能(用户侧)
wire [63:0]  frame_dout     ; // 输出数据(PHY侧)
reg          frame_rden     ; // 读取使能(PHY侧)
wire         frame_wrfull   ;
wire         frame_rdempty  ;
wire [3:0]   frame_wrcount  ;
wire [3:0]   frame_rdcount  ;

// data_fifo:存储有效数据+结束标志(9位位宽)
reg  [8:0]   data_din       ; // 输入数据:{结束标志(1b), 数据(8b)}
reg          data_wren      ; // 写入使能(用户侧)
wire [8:0]   data_dout      ; // 输出数据(PHY侧)
reg          data_rden      ; // 读取使能(PHY侧)
wire         data_wrfull    ;
wire         data_rdempty   ;
wire [11:0]  data_wrcount   ;
wire [11:0]  data_rdcount   ;

/*--------------------------------------------------*\
                       内部信号
\*--------------------------------------------------*/
reg [10:0] tx_cnt           ; // 发送字节计数器(控制数据组装)
reg [7:0]  send_data        ; // 组装后的发送数据
reg        send_data_en     ; // 发送数据有效
reg        send_data_last   ; // 发送结束标志
reg [7:0]  crc_data         ; // CRC校验数据
reg        crc_data_en      ; // CRC数据有效
reg [2:0]  crc_cnt          ; // CRC字节计数器(4字节分四次发送)
reg [15:0] mac_type         ; // 存储帧类型(跨时钟域同步后)
reg [47:0] send_target_mac  ; // 当前目标MAC(用户侧)
reg [47:0] send_target_mac_cdc; // 同步后的目标MAC(PHY侧)


/*--------------------------------------------------*\
                       目标MAC地址更新逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        send_target_mac <= TARGET_MAC_ADDR; // 复位时初始化为广播地址
    // ARP响应有效时更新目标MAC
    else if (rd_arp_list_mac_vld) 
        send_target_mac <= rd_arp_list_mac;
    // 发送ARP帧时强制使用广播地址
    else if (mac_tx_data_vld && mac_tx_frame_type == 16'h0806)
        send_target_mac <= 48'hffff_ffff_ffff;
    else 
        send_target_mac <= send_target_mac; // 保持当前值
end

/*--------------------------------------------------*\
                       FIFO写入逻辑(用户侧)
\*--------------------------------------------------*/
// frame_fifo写入:在数据包结束时存入目标MAC和类型
always @(posedge sys_clk) begin
    frame_din  <= {send_target_mac, mac_tx_frame_type}; // 拼接48+16位
    frame_wren <= mac_tx_data_last; // 在包尾写入
end

// data_fifo写入:持续存入有效数据及结束标志
always @(posedge sys_clk) begin
    data_wren <= mac_tx_data_vld;
    data_din  <= {mac_tx_data_last, mac_tx_data}; // 最高位标记包尾
end

/*--------------------------------------------------*\
                       状态机转换逻辑
\*--------------------------------------------------*/
always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) cur_status <= TX_IDLE;
    else cur_status <= nxt_status;
end

always @(*) begin
    nxt_status = cur_status; // 默认保持当前状态
    if (phy_tx_reset) nxt_status = TX_IDLE;
    else case(cur_status)
        TX_IDLE: 
            if (!frame_rdempty) 
                nxt_status = TX_PRE; // FIFO非空则启动发送
            else
                nxt_status = cur_status;
        TX_PRE: 
            nxt_status = TX_DATA; // 前导码阶段仅持续1周期
        TX_DATA: 
            // 当CRC发送完成且是最后一个字节时结束
            if (crc_cnt==3 && crc_data_en) 
                nxt_status = TX_END;
            else
                nxt_status <= cur_status;
        TX_END: 
            nxt_status = TX_IDLE; // 结束状态返回空闲
        default : nxt_status <= TX_IDLE;
    endcase
end

/*--------------------------------------------------*\
                       数据组包逻辑
\*--------------------------------------------------*/
always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        tx_cnt <= 0;
    else if (cur_status == TX_DATA) 
        tx_cnt <= tx_cnt + 1; // 在数据阶段递增计数器
    else 
        tx_cnt <= 0; // 其他状态复位计数器
end
always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        send_data <= 0;
    else 
        case(tx_cnt)
            0,1,2,3,4,5,6  : send_data <= 8'h55; //// 前导码(7字节0x55)+ 帧起始符(1字节0xD5)
            7              : send_data <= 8'hd5; //帧起始定界符

            8              : send_data <= send_target_mac_cdc[47:40]; // 目标MAC(6字节) 高位先发,9-13依次发送后续字节
            9              : send_data <= send_target_mac_cdc[39:32];
            10             : send_data <= send_target_mac_cdc[31:24];
            11             : send_data <= send_target_mac_cdc[23:16];
            12             : send_data <= send_target_mac_cdc[15:8];
            13             : send_data <= send_target_mac_cdc[7:0];

            14             : send_data <= LOCAL_MAC_ADDR[47:40]; // 源MAC(6字节)
            15             : send_data <= LOCAL_MAC_ADDR[39:32];
            16             : send_data <= LOCAL_MAC_ADDR[31:24];
            17             : send_data <= LOCAL_MAC_ADDR[23:16];
            18             : send_data <= LOCAL_MAC_ADDR[15:8];
            19             : send_data <= LOCAL_MAC_ADDR[7:0];                                              

            20             : send_data <= mac_type[15:8]; // 类型字段(2字节)
            21             : send_data <= mac_type[7:0];  
            default        : send_data <= data_dout[7:0]; // 有效载荷(从data_fifo读取)
        endcase 
end

always @(*) begin
    frame_rden <= cur_status == TX_PRE;
end

always @(posedge phy_tx_clk) begin
    if (frame_rden) begin
        mac_type             <= frame_dout[15:0];
        send_target_mac_cdc  <= frame_dout[63:16];
    end
    else begin
        mac_type             <= mac_type;
        send_target_mac_cdc  <= send_target_mac_cdc;
    end
end

always @(posedge phy_tx_clk) begin
    if (cur_status == TX_DATA && tx_cnt == 21) 
        data_rden <= 1'b1;
    else if (data_rden && data_dout[8]) //读到last数据
        data_rden <= 1'b0;
    else 
        data_rden <= data_rden;
end

always @(posedge phy_tx_clk) begin
    if (data_rden && data_dout[8]) 
        send_data_last <= 1'b1;
    else 
        send_data_last <= 0;
end

always @(posedge phy_tx_clk) begin
    if (phy_tx_reset)
        send_data_en <= 0;
    else if (cur_status == TX_DATA && tx_cnt == 0) 
        send_data_en <= 1'b1;
    else if (send_data_last) 
        send_data_en <= 0;
    else 
        send_data_en <= send_data_en;
end

/*--------------------------------------------------*\
                       CRC处理逻辑
\*--------------------------------------------------*/
// CRC计算启动:从MAC头开始(tx_cnt=8)
always @(posedge phy_tx_clk) begin
    if (tx_cnt == 8) 
        tx_crc_din_vld <= 1'b1;
    else if (send_data_last) 
        tx_crc_din_vld <= 1'b0;
    else 
        tx_crc_din_vld <= tx_crc_din_vld;
end
// CRC数据输入(与发送数据同步)
assign tx_crc_din = send_data;

always @(posedge phy_tx_clk) begin
    if (crc_data_en && crc_cnt == 3) 
        tx_crc_done <= 1'b1;
    else 
        tx_crc_done <= 0;
end

always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        crc_cnt <= 0;
    else if (crc_data_en && crc_cnt == 3) 
        crc_cnt <= 0;
    else if (crc_data_en)
        crc_cnt <= crc_cnt + 1'b1;
    else 
        crc_cnt <= crc_cnt;
end

always @(posedge phy_tx_clk) begin
    if (phy_tx_reset)
        crc_data_en <= 0;
    else if (crc_data_en && crc_cnt == 3) 
        crc_data_en <= 0;
    else if (send_data_last) 
        crc_data_en <= 1;
    else 
        crc_data_en <= crc_data_en;
end

// CRC结果分四次发送(小端序)
always @(*) begin
    case(crc_cnt)
        0: crc_data = tx_crc_dout[7:0];   // 第1字节(LSB)
        1: crc_data = tx_crc_dout[15:8];
        2: crc_data = tx_crc_dout[23:16];
        3: crc_data = tx_crc_dout[31:24]; // 第4字节(MSB)
    endcase
end

/*--------------------------------------------------*\
                       GMII接口输出
\*--------------------------------------------------*/
always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        gmii_tx_data <= 0;
    else if (send_data_en) 
        gmii_tx_data <= send_data;
    else if (crc_data_en)
        gmii_tx_data <= crc_data;
    else  
        gmii_tx_data <= gmii_tx_data;  
end

always @(posedge phy_tx_clk) begin
    if (phy_tx_reset) 
        gmii_tx_data_vld <= 0;
    else 
        gmii_tx_data_vld <= crc_data_en | send_data_en;
end

/*--------------------------------------------------*\
                       例化FIFO
\*--------------------------------------------------*/
fifo_w64xd16 frame_fifo (
  .rst(phy_tx_reset),                      // input wire rst
  .wr_clk(sys_clk),                // input wire wr_clk
  .rd_clk(phy_tx_clk),                // input wire rd_clk
  .din(frame_din),                      // input wire [63 : 0] din
  .wr_en(frame_wren),                  // input wire wr_en
  .rd_en(frame_rden),                  // input wire rd_en
  .dout(frame_dout),                    // output wire [63 : 0] dout
  .full(frame_wrfull),                    // output wire full
  .empty(frame_rdempty),                  // output wire empty
  .rd_data_count(frame_rdcount),  // output wire [4 : 0] rd_data_count
  .wr_data_count(frame_wrcount)  // output wire [4 : 0] wr_data_count
);

fifo_w9xd4096 data_fifo (
  .rst(phy_tx_reset),                      // input wire rst
  .wr_clk(sys_clk),                // input wire wr_clk
  .rd_clk(phy_tx_clk),                // input wire rd_clk
  .din(data_din),                      // input wire [8 : 0] din
  .wr_en(data_wren),                  // input wire wr_en
  .rd_en(data_rden),                  // input wire rd_en
  .dout(data_dout),                    // output wire [8 : 0] dout
  .full(data_wrfull),                    // output wire full
  .empty(data_rdempty),                  // output wire empty
  .rd_data_count(data_rdcount),  // output wire [11 : 0] rd_data_count
  .wr_data_count(data_wrcount)  // output wire [11 : 0] wr_data_count
);

 endmodule

1.7.5 代码仿真与验证

1.8 IP接收模块代码编写:ip_receive.v

1.8.1 模块功能简介

在IP接收模块中,我们的主要任务仍然是能够正确接收来着MAC接收模块输出的数据,包括数据有效数据信号数据结束标志信号,以及能够讲数据解析出IP包头的字段,然后正确将数据传递给下一层UDP接收模块,包括数据有效信号数据信号数据结束信号以及数据长度信号,在这个模块中,需要判定一下我们的数据类型是UDP协议还是ICMP协议,针对不同的协议类型输出给不同的下一层处理模块(UDP接收和ICMP接收)。其中ICMP协议类型我们在后面会有详细的介绍。

这个模块仍然在系统时钟域中进行处理,不需要将系统时钟与PHY时钟域信号做跨时钟域处理。模块比较简单,在整个大的模块中扮演者一个中间层的角色,总结一下,我们这个模块的主要功能如下:

  1. IPv4头部解析:提取总长度、协议类型、目标IP地址等关键字段。
  2. 本地IP过滤:仅处理目标IP与本地IP匹配的数据包,提升系统安全性。
  3. 协议分发:根据协议类型(UDP/ICMP)将载荷数据路由至对应上层模块。

FPGA解析IP层数据包过程动画演示

Step 1: 接收数据流(字节逐个到达)
        ↓
        [0x45][0x00][0x00][0x3C][0x1A][0x2B]... 

Step 2: 通过计数器(rx_cnt)定位字段
        ↓
        rx_cnt=2 → 总长度高字节(0x00)
        rx_cnt=3 → 总长度低字节(0x3C)→ 总长度=60
        rx_cnt=9 → 协议类型(0x11 → UDP)
        rx_cnt=16~19 → 目标IP(0xC0A80102)

Step 3: 校验目标IP是否为本机IP
        ↓
        0xC0A80102 vs LOCAL_IP_ADDR → 匹配则转发

Step 4: 根据协议类型分发数据
        ↓
        UDP → 输出到udp_receive*
        ICMP → 输出到icmp_receive*

1.8.2核心功能实现

/*--------------------------------------------------*\
                       IP包头字段解析
\*--------------------------------------------------*/
// 总长度字段(第2-3字节,大端序)
always @(posedge sys_clk)begin
    case(rx_cnt)
        2:total_length[15:8] <= ip_rx_data;//第2字节-高字节
        3:total_length[7:0] <=ip_rx_data; //第3字节-低字节
    endcase
end

// 协议类型字段(第9字节)
always @(posedge sys_clk)begin
    if(rx_cnt==9)
        ip_protocol <= ip_rx_data;// 记录协议类型
    else
        ip_protocol <= ip_protocol;
end

// 目标IP地址字段(第16-19字节,大端序)
always @(posedge sys_clk)begin
    if(rx_cnt >=16 && rx_cnt <=19)
        rx_target_ip <= {rx_target_ip[23:0],ip_rx_data};//移位拼接
    else
        rx_target_ip <= rx_target_ip;
end

1.8.3 IP层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-15 19:18:20
 * @LastEditTime: 2025-02-15 20:49:58
 * @FilePath: rtl/ip_receive.v
 * @Description:IP数据包接收和解析模块,主要功能如下:
  1. 解析IPv4包头字段(总长度、协议类型、目标IP地址)
  2. 根据协议类型分发数据包到UDP或ICMP处理模块
  3. 支持本地IP地址过滤(仅处理目标IP为本机的数据包)
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module ip_receive#(
    parameter LOCAL_IP_ADDR = {8'd0,8'd0,8'd0,8'd0}
 )(
    /*-------系统接口--------*/
    input               sys_clk         ,
    input               sys_reset_n     ,

    /*-------MAC层接口--------*/
    input               ip_rx_data_vld  , // IP数据有效信号(与MAC解帧同步)
    input               ip_rx_data_last , // IP数据结束标志(包尾指示)
    input       [7:0]   ip_rx_data      , // IP数据字节(LSB对齐)

    /*-------UDP层接口--------*/
    output reg          udp_rx_data_vld , // UDP数据有效信号
    output reg          udp_rx_data_last, // UDP数据结束标志
    output reg [7:0]    udp_rx_data     , // UDP数据字节
    output reg [15:0]   udp_rx_length   , // UDP数据长度(IP载荷长度)


    /*-------ICMP接口--------*/
    output reg          icmp_rx_data_vld, // ICMP数据有效信号
    output reg          icmp_rx_data_last,// ICMP数据结束标志
    output reg [7:0]    icmp_rx_data    , // ICMP数据字节
    output reg [15:0]   icmp_rx_length    // ICMP数据长度(IP载荷长度)

 );
/*--------------------------------------------------*\
                       定义协议类型
\*--------------------------------------------------*/
localparam UDP_TYPE  = 8'd17; // UDP协议号(RFC 768)
localparam ICMP_TYPE = 8'd1;  // ICMP协议号(RFC 792)

/*--------------------------------------------------*\
                       内部寄存器
\*--------------------------------------------------*/
reg [10:0] rx_cnt; // 接收字节计数器
reg [31:0] rx_target_ip; //目标IP地址寄存器
reg [7:0]  ip_protocol; // 协议类型寄存器
reg [15:0] total_length; // IP包总长度寄存器

/*--------------------------------------------------*\
                       接收字节计数器逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if(!sys_reset_n)
        rx_cnt <= 11'd0;
    else if(ip_rx_data_vld)
        rx_cnt <= rx_cnt + 1'b1;
    else
        rx_cnt <= 11'd0;
end

/*--------------------------------------------------*\
                       IP包头字段解析
\*--------------------------------------------------*/
// 总长度字段(第2-3字节,大端序)
always @(posedge sys_clk)begin
    case(rx_cnt)
        2:total_length[15:8] <= ip_rx_data;//第2字节-高字节
        3:total_length[7:0] <=ip_rx_data; //第3字节-低字节
    endcase
end

// 协议类型字段(第9字节)
always @(posedge sys_clk)begin
    if(rx_cnt==9)
        ip_protocol <= ip_rx_data;// 记录协议类型
    else
        ip_protocol <= ip_protocol;
end

// 目标IP地址字段(第16-19字节,大端序)
always @(posedge sys_clk)begin
    if(rx_cnt >=16 && rx_cnt <=19)
        rx_target_ip <= {rx_target_ip[23:0],ip_rx_data};//移位拼接
    else
        rx_target_ip <= rx_target_ip;
end

/*--------------------------------------------------*\
                       UDP数据输出逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    // 触发条件:协议是udp+目标IP匹配+超过IP头长度(20字节)
    if(ip_protocol == UDP_TYPE && rx_target_ip == LOCAL_IP_ADDR && rx_cnt>=20)begin
        udp_rx_data_vld     <= ip_rx_data_vld;
        udp_rx_data_last    <= ip_rx_data_last;
        udp_rx_data         <= ip_rx_data;
        udp_rx_length       <= total_length;
    end
    else begin //非UDP数据时关闭输出
        udp_rx_data_vld     <= 0;
        udp_rx_data_last    <= 0;
        udp_rx_data         <= 0;
        udp_rx_length       <= 0;
    end
end

/*--------------------------------------------------*\
                       ICMP数据输出逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    // 触发条件:协议是ICMP+目标IP匹配+超过IP头长度
    if(ip_protocol == ICMP_TYPE && rx_target_ip == LOCAL_IP_ADDR && rx_cnt>=20)begin
        icmp_rx_data_vld    <= ip_rx_data_vld;
        icmp_rx_data_last   <= ip_rx_data_last;
        icmp_rx_data        <= ip_rx_data;
        icmp_rx_length      <= total_length - 20;
    end
    else begin
        icmp_rx_data_vld    <= 0;  // 非ICMP数据时关闭输出
        icmp_rx_data_last   <= 0;
        icmp_rx_data        <= 0;
        icmp_rx_length      <= 0;
    end 
end

 endmodule

1.8.4 代码仿真与验证

1.9 IP发送模块代码编写:ip_send.v

1.9.1 模块功能简介

该模块是FPGA以太网协议栈中的IP层发送核心单元,主要完成从FPGA中UDP数据发送模块发送的数据传递到IP发送模块中,将数据使用IPv4数据包的封装与发送,符合RFC 791规范。其核心功能包括:

  1. IPv4头部组装:生成版本、总长度、协议类型等字段,支持校验和计算。
  2. 协议支持:支持UDP(协议号17)和ICMP(协议号1)协议封装。
  3. 分片标识管理:自动生成递增的IP标识字段,用于分片重组。
  4. ARP查询触发:在发送前触发ARP请求,获取目标MAC地址。
  5. 数据对齐处理:通过延迟线实现IP头与载荷数据的时序对齐。

1.9.2 核心代码

核心代码分为两个内容:

首先是IP数据从UDP得到的数据添加IP头部并成功送出,在添加IP头部的时候需要对IP数据结构有一个清晰的认识,下面给大家绘制了一下字段结构图:

字节序号 | 字段说明                | 示例值(十六进制)
--------|------------------------|------------------
0       | 版本(4) + 头部长度(5)   | 0x45 → 4: IPv4, 5*4=20字节头
1       | 服务类型                | 0x00(通常忽略)
2-3     | **总长度**             | 0x00 0x3C → 60字节(IP包总长)
4-5     | 标识 (ID)              | 0x1A 0x2B(分片用)
6-7     | 标志 + 分片偏移         | 0x00 0x00(未分片)
8       | TTL                    | 0x40 → 64跳
9       | **协议类型**           | 0x11 → UDP(17)
10-11   | 头部校验和              | 0xA0 0xC0
12-15   | **源IP地址**           | 0xC0 0xA8 0x01 0x01 → 192.168.1.1
16-19   | **目标IP地址**         | 0xC0 0xA8 0x01 0x02 → 192.168.1.2
20-...  | 数据载荷               | UDP/ICMP数据开始

组包代码如下:

/*--------------------------------------------------*\
                       IP包头组装逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    case(tx_cnt)
        // [0] 版本号(4bit) + 头长度(4bit): IPv4(0x4), 5*4=20字节头(0x5)
        0 : ip_tx_data <= {4'h4, 4'h5}; 

        // [1] 服务类型(TOS),默认0
        1 : ip_tx_data <= 0; 

        // [2-3] 总长度(大端序)
        2 : ip_tx_data <= ip_tx_length[15:8]; // 高字节
        3 : ip_tx_data <= ip_tx_length[7:0];  // 低字节

        // [4-5] 标识字段(用于分片)
        4 : ip_tx_data <= package_id[15:8]; 
        5 : ip_tx_data <= package_id[7:0];

        // [6-7] 标志(3bit)+分片偏移(13bit):默认不分片
        6 : ip_tx_data <= {3'b010, 5'h0}; // 010表示允许分片
        7 : ip_tx_data <= 8'h0;

        // [8] 生存时间(TTL):默认128跳
        8 : ip_tx_data <= 8'h80;

        // [9] 协议类型(UDP/ICMP)
        9 : ip_tx_data <= tx_type_r;

        // [10-11] 头部校验和(大端序)
        10 : ip_tx_data <= ip_head_chack[15:8];
        11 : ip_tx_data <= ip_head_chack[7:0];

        // [12-15] 源IP地址(大端序)
        12 : ip_tx_data <= LOCAL_IP_ADDR[31:24];
        13 : ip_tx_data <= LOCAL_IP_ADDR[23:16];
        14 : ip_tx_data <= LOCAL_IP_ADDR[15:8];
        15 : ip_tx_data <= LOCAL_IP_ADDR[7:0];

        // [16-19] 目标IP地址(大端序)
        16 : ip_tx_data <= TARGET_IP_ADDR[31:24];
        17 : ip_tx_data <= TARGET_IP_ADDR[23:16];
        18 : ip_tx_data <= TARGET_IP_ADDR[15:8];
        19 : ip_tx_data <= TARGET_IP_ADDR[7:0];

        // [20+] 载荷数据(延迟对齐)
        default : ip_tx_data <= tx_data_delay;
    endcase
end

接着要对组包的数据进行一个IP头校验和的处理,IP头校验和是IPv4协议的核心字段所有IPv4数据包必须包含正确的头部校验和。如果校验失败我们会直接丢弃该数据包。若IP头校验缺失,错误数据包可能被错误路由或引发安全问题。

/*--------------------------------------------------*\
                       IP头校验和计算
\*--------------------------------------------------*/
// 校验和算法:反码求和再取反
// 计算步骤:
// 1. 将IP头按16位分组求和
// 2. 累加进位回卷
// 3. 结果取反
always @(posedge sys_clk) begin
    // 第一组:版本(4)+头长(4)+TOS(8) | 总长度(16)
    add0 <= 16'h4500 + ip_tx_length + package_id;

    // 第二组:标志(3)+偏移(13) | TTL(8)+协议(8)
    add1 <= 16'h4000 + {8'h80, tx_type_r} + LOCAL_IP_ADDR[31:16];

    // 第三组:源IP后半+目标IP
    add2 <= LOCAL_IP_ADDR[15:0] + TARGET_IP_ADDR[31:16] + TARGET_IP_ADDR[15:0];

    // 累加总和
    chack_sum <= add0 + add1 + add2;
end

// 校验和结果处理
always @(posedge sys_clk) begin
    if (!sys_reset_n)
        ip_head_chack <= 0;
    else if (tx_cnt == 5) 
        // 进位回卷:将32位和转换为16位
        ip_head_chack <= chack_sum[31:16] + chack_sum[15:0]; 
    else if (tx_cnt == 6)
        // 最终取反
        ip_head_chack <= ~ip_head_chack; 
end

1.9.3 IP层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-15 20:09:37
 * @LastEditTime: 2025-02-15 20:38:03
 * @FilePath: rtl/ip_send.v
 * @Description:IPv4数据包发送模块,主要功能如下:
  1. 组装完整IPv4包头(含校验和计算)
  2. 支持UDP/ICMP协议封装
  3. 自动生成IP标识字段(用于分片识别)
  4. 集成ARP查询触发机制
  5. 数据延迟对齐处理(IP头与载荷数据时序对齐)
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module ip_send #(
    parameter LOCAL_IP_ADDR = {8'd0,8'd0,8'd0,8'd0}, // 本地IP地址(需配置)
    parameter TARGET_IP_ADDR = {8'd0,8'd0,8'd0,8'd0}  // 目标IP地址(需配置)
 )(
    /*-------系统接口--------*/
    input               sys_clk         ,
    input               sys_reset_n     ,

    /*-------MAC层接口--------*/
    output reg          ip_tx_data_vld  , // IP数据有效信号
    output reg          ip_tx_data_last , // IP数据结束标志
    output reg [15:0]   ip_tx_length    , // IP包总长度(头+载荷)
    output reg [7:0]    ip_tx_data      , // IP数据字节

    /*-------发送端口--------*/
    input               tx_data_vld     , // 数据有效信号
    input               tx_data_last    , // 数据结束标志
    input [7:0]         tx_data         , // 数据字节
    input [15:0]        tx_length       , // 数据长度
    input [7:0]         tx_type         , // 协议类型(UDP=17,ICMP=1)

    /*-------ARP查询接口--------*/
    output reg          rd_arp_list_en  , // ARP查询使能
    output reg [31:0]   rd_arp_list_ip    // 查询的目标IP地址
 );
/*--------------------------------------------------*\
                       内部寄存器定义
\*--------------------------------------------------*/
reg [10:0] tx_cnt;        // 发送字节计数器(最大2048字节)
reg [15:0] package_id;    // IP标识字段(每包递增)
reg [15:0] ip_head_chack; // IP头校验和寄存器
reg [31:0] add0, add1, add2; // 校验和计算中间值
reg [31:0] chack_sum;     // 校验和累加结果
reg [7:0]  tx_type_r;     // 协议类型锁存
reg        tx_data_vld_d0, tx_data_vld_d1; // 有效信号延迟线(打拍)

/*--------------------------------------------------*\
                       移位寄存器接口
\*--------------------------------------------------*/
wire [7:0] tx_data_delay; // 延迟后的上层数据(对齐IP头时序)

/*--------------------------------------------------*\
                       协议类型定义
\*--------------------------------------------------*/
localparam UDP_TYPE  = 8'd17; // UDP协议号
localparam ICMP_TYPE = 8'd1;  // ICMP协议号

/*--------------------------------------------------*\
                       输入信号锁存
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    tx_data_vld_d0 <= tx_data_vld; // 一级延迟
    tx_data_vld_d1 <= tx_data_vld_d0; // 二级延迟
end

// 总长度计算:载荷长度+20字节IP头
always @(posedge sys_clk) begin
    if (tx_data_vld) 
        ip_tx_length <= tx_length + 20; // 存储总长度
end

// 协议类型锁存(保证包头字段一致性)
always @(posedge sys_clk) begin
    if (tx_data_vld) 
        tx_type_r <= tx_type; // 捕获当前协议类型
end

/*--------------------------------------------------*\
                       发送字节计数器
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        tx_cnt <= 0;// 同步复位
    else if (tx_cnt == ip_tx_length - 1) 
        tx_cnt <= 0;// 包尾复位
    else if (tx_data_vld || tx_cnt != 0)
        tx_cnt <= tx_cnt + 1;// 数据有效或计数中递增
end

/*--------------------------------------------------*\
                       输出控制信号生成
\*--------------------------------------------------*/
// 包尾信号生成(提前1周期)
always @(posedge sys_clk) begin
    if (tx_cnt == ip_tx_length - 1) 
        ip_tx_data_last <= 1;
    else 
        ip_tx_data_last <= 0;
end

// 数据有效信号生成(流水线控制)
always @(posedge sys_clk) begin
    if (ip_tx_data_last) 
        ip_tx_data_vld <= 0; // 包尾关闭有效信号
    else if (tx_data_vld) 
        ip_tx_data_vld <= 1; // 数据输入启动发送
end

/*--------------------------------------------------*\
                       IP包头组装逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    case(tx_cnt)
        // [0] 版本号(4bit) + 头长度(4bit): IPv4(0x4), 5*4=20字节头(0x5)
        0 : ip_tx_data <= {4'h4, 4'h5}; 

        // [1] 服务类型(TOS),默认0
        1 : ip_tx_data <= 0; 

        // [2-3] 总长度(大端序)
        2 : ip_tx_data <= ip_tx_length[15:8]; // 高字节
        3 : ip_tx_data <= ip_tx_length[7:0];  // 低字节

        // [4-5] 标识字段(用于分片)
        4 : ip_tx_data <= package_id[15:8]; 
        5 : ip_tx_data <= package_id[7:0];

        // [6-7] 标志(3bit)+分片偏移(13bit):默认不分片
        6 : ip_tx_data <= {3'b010, 5'h0}; // 010表示允许分片
        7 : ip_tx_data <= 8'h0;

        // [8] 生存时间(TTL):默认128跳
        8 : ip_tx_data <= 8'h80;

        // [9] 协议类型(UDP/ICMP)
        9 : ip_tx_data <= tx_type_r;

        // [10-11] 头部校验和(大端序)
        10 : ip_tx_data <= ip_head_chack[15:8];
        11 : ip_tx_data <= ip_head_chack[7:0];

        // [12-15] 源IP地址(大端序)
        12 : ip_tx_data <= LOCAL_IP_ADDR[31:24];
        13 : ip_tx_data <= LOCAL_IP_ADDR[23:16];
        14 : ip_tx_data <= LOCAL_IP_ADDR[15:8];
        15 : ip_tx_data <= LOCAL_IP_ADDR[7:0];

        // [16-19] 目标IP地址(大端序)
        16 : ip_tx_data <= TARGET_IP_ADDR[31:24];
        17 : ip_tx_data <= TARGET_IP_ADDR[23:16];
        18 : ip_tx_data <= TARGET_IP_ADDR[15:8];
        19 : ip_tx_data <= TARGET_IP_ADDR[7:0];

        // [20+] 载荷数据(延迟对齐)
        default : ip_tx_data <= tx_data_delay;
    endcase
end

/*--------------------------------------------------*\
                       IP标识字段管理
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n)
        package_id <= 0; // 复位清零
    else if (ip_tx_data_last)
        package_id <= package_id + 1; // 每包递增
end

/*--------------------------------------------------*\
                       IP头校验和计算
\*--------------------------------------------------*/
// 校验和算法:反码求和再取反
// 计算步骤:
// 1. 将IP头按16位分组求和
// 2. 累加进位回卷
// 3. 结果取反
always @(posedge sys_clk) begin
    // 第一组:版本(4)+头长(4)+TOS(8) | 总长度(16)
    add0 <= 16'h4500 + ip_tx_length + package_id;

    // 第二组:标志(3)+偏移(13) | TTL(8)+协议(8)
    add1 <= 16'h4000 + {8'h80, tx_type_r} + LOCAL_IP_ADDR[31:16];

    // 第三组:源IP后半+目标IP
    add2 <= LOCAL_IP_ADDR[15:0] + TARGET_IP_ADDR[31:16] + TARGET_IP_ADDR[15:0];

    // 累加总和
    chack_sum <= add0 + add1 + add2;
end

// 校验和结果处理
always @(posedge sys_clk) begin
    if (!sys_reset_n)
        ip_head_chack <= 0;
    else if (tx_cnt == 5) 
        // 进位回卷:将32位和转换为16位
        ip_head_chack <= chack_sum[31:16] + chack_sum[15:0]; 
    else if (tx_cnt == 6)
        // 最终取反
        ip_head_chack <= ~ip_head_chack; 
end

/*--------------------------------------------------*\
                        ARP查询触发逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) begin
        rd_arp_list_en <= 0;
        rd_arp_list_ip <= 0;
    end
    // 检测数据有效上升沿(启动ARP查询)
    else if (tx_data_vld_d0 && ~tx_data_vld_d1) begin
        rd_arp_list_en <= 1'b1;
        rd_arp_list_ip <= TARGET_IP_ADDR;
    end else begin
        rd_arp_list_en <= 0; // 单周期脉冲信号
    end
end

// ********** 数据延迟对齐模块 **********
// 功能:将上层数据延迟20拍,对齐IP头时序
c_shift_ram_0 ip_delay (
  .A(19),       // 延迟深度 = A+1 = 20拍
  .D(tx_data),   // 原始数据输入
  .CLK(sys_clk),     // 同步时钟
  .Q(tx_data_delay) // 延迟后输出
);

 endmodule

1.9.4 代码仿真与验证

1.10 UDP接收模块代码编写:udp_receive.v

1.10.1 UDP接收模块功能简介

在这一节中到了UDP模块的编写了,UDP模块是最接近数据与FPGA交互的模块。

FPGA在接收数据的时候,先经过remii_receive模块将rgmii格式的数据转换成gmii格式并经过多层BUFG稳定信号,再经过mac_receive模块将数据去掉mac头部,然后进入ip_receive模块去掉ip头部,将最后的有效数据传递给udp_receive模块去掉udp头部,最后就会被FPGA读取后执行指定的操作了。

在FPGA发送数据的时候,先将FPGA的数据经过udp_send模块添加头部,再进入ip_send添加IP头部,接着进入mac_send添加mac头部,最后经过regmii_send将gmii数据转成rgmii数据并经过多层BUFG用来稳定信号。

这个模块与之前模块的功能很相似,对传进的数据进行剥离数据头部留下有效信号,再将信号传递给下一层即可

在前面的几个模块中已经很详细的讲了如何对数据进行剥离包头,这里就不再赘述,下面给大家这个模块的代码工程

1.10.2 UDP层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-16 14:54:03
 * @LastEditTime: 2025-02-16 16:46:19
 * @FilePath: ttl/udp_receive.vp_receive.v
 * @Description:UDP数据包接收解析模块,主要功能如下:
  1. 过滤指定本地端口的UDP数据包
  2. 剥离UDP头部(8字节)后向下一层传递有效数据
  3. 自动计算有效数据长度
  4. 支持数据流连续传输控制
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module udp_receive#(
    parameter LOCAL_PORT = 16'h8060 // 监听本地端口,默认8060端口
 )(
    /*-------系统信号--------*/
    input           sys_clk             ,
    input           sys_reset_n         ,

    /*-------IP层接口--------*/
    input           udp_rx_data_vld     , // UDP数据有效信号
    input           udp_rx_data_last    , // UDP数据结束标志信号
    input [7:0]     udp_rx_data         , // UDP数据字节
    input [15:0]    udp_rx_length       , // UDP数据总长度

    /*-------应用层端口--------*/
    output reg      app_rx_data_vld     , // 应用数据有效信号
    output reg      app_rx_data_last    , // 应用数据结束标志信号
    output reg [7:0] app_rx_data        , // 应用数据字节
    output reg [15:0] app_rx_length       // 应用数据总长度
 );
/*--------------------------------------------------*\
                       内部信号定义
\*--------------------------------------------------*/
reg [10:0] rx_cnt;
reg [15:0] rx_target_port;

/*--------------------------------------------------*\
                       字节计数器逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) begin
        rx_cnt <= 0;
    end
    else if(udp_rx_data_vld)
        rx_cnt <= rx_cnt + 1'b1;
    else
        rx_cnt <= 0;
end

/*--------------------------------------------------*\
                       目标端口捕获逻辑
\*--------------------------------------------------*/
/*
UDP包头结构(8字节):
0-1: 源端口 | 2-3: 目标端口 | 4-5: 长度 | 6-7: 校验和
*/
always @(posedge sys_clk)begin
    case (rx_cnt)
       2 : rx_target_port <= udp_rx_data; // 捕获高字节
       3 : rx_target_port <= udp_rx_data; // 捕获低字节
    endcase
end

/*--------------------------------------------------*\
                       应用数据输出逻辑
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    // 触发条件:端口匹配 + 超过UDP头(>=8字节)
    if ((LOCAL_PORT == rx_target_port) && (rx_cnt >= 8)) begin
        // 直通信号
        app_rx_data_vld  <= udp_rx_data_vld;  // 有效信号
        app_rx_data_last <= udp_rx_data_last; // 结束标志

        // 数据透传
        app_rx_data      <= udp_rx_data;      // 载荷数据

        // 长度计算:总长 - UDP头长度(8字节)
        app_rx_length    <= udp_rx_length - 8;
    end else begin
        // 非目标端口或头部数据时关闭输出
        app_rx_data_vld  <= 0;
        app_rx_data_last <= 0;
        app_rx_data      <= 0;
        app_rx_length    <= 0;
    end
end

 endmodule

1.10.3 代码仿真与验证

1.11 UDP发送模块代码编写:udp_send.v

1.11.1 UDP发送模块简介

这个模块的功能也比较简单,还是用来将数据拼接上UDP头部后输出给下一层

UDP头部格式如下:

0-1: 源端口 | 2-3: 目标端口 | 4-5: 长度 | 6-7: 校验和

1.11.2 UDP层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-16 15:36:47
 * @LastEditTime: 2025-02-16 16:57:58
 * @FilePath: rtl/udp_send.v
 * @Description:UDP数据发送模块,主要功能如下:
  1. 将用户提供的数据把包成符合UDP协议的数据报
  2. 模块内容自动生成UDP报头,处理数据发送逻辑(包括数据时序对齐,状态机管理和协议控制) 
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module udp_send #(
    parameter LOCAL_PORT    = 16'h0, // 本地端口号
    parameter TARGET_PORT   = 16'h0 // 目标端口号
 ) (
    /*-------系统信号--------*/
    input               sys_clk             ,
    input               sys_reset_n         ,

    /*-------与ip_send模块交互信号--------*/
    output reg          udp_tx_data_vld     , // UDP数据有效信号
    output reg          udp_tx_data_last    , // UDP数据结束标志信号
    output reg [7:0]    udp_tx_data         , // UDP数据
    output reg [15:0]   udp_tx_length       , // UDP数据总长度

    /*-------用户发送端口--------*/
    input               app_tx_data_vld     , // 用户数据有效信号
    input               app_tx_data_last    , // 用户数据结束标志信号
    input [7:0]         app_tx_data         , // 用户数据
    input [15:0]        app_tx_length       , // 用户数据总长度
    output              app_tx_req          , // 用户数据请求信号
    output              app_tx_ready          // 用户准备好信号,表示可以接收用户数据
 );
localparam  READY_CNT_MAX =  50; // 定义一个常数,用于控制app_tx_ready信号的间隔
/*--------------------------------------------------*\
                       内部信号定义
\*--------------------------------------------------*/
reg  [7:0]  ready_cnt          ;   // app_tx_ready信号控制相关的计数器
reg         app_tx_data_vld_r  ;   // 用户数据有效信号寄存器
reg         app_tx_data_last_r ;   // 用户数据末尾信号寄存器
reg  [7:0]  app_tx_data_r      ;   // 用户数据寄存器
reg  [15:0] app_tx_length_r    ;   // 用户数据长度寄存器
reg  [10:0] tx_cnt             ;   // 传输计数器,用于跟踪当前数据传输的进度
wire [7:0]  app_tx_data_delay  ;   // 用于存储延迟后的用户数据

/*--------------------------------------------------*\
                       用户发送请求信号
\*--------------------------------------------------*/
assign app_tx_req = app_tx_data_vld;   // 用户数据请求信号直接来自用户数据有效信号

/*--------------------------------------------------*\
                       打拍,采样用户数据
\*--------------------------------------------------*/
// 这部分逻辑用于在时钟边沿对用户数据信号进行采样和保持,确保数据稳定
always @(posedge sys_clk) begin
    if (app_tx_ready) begin
       app_tx_data_vld_r  <= app_tx_data_vld;   // 采样用户数据有效信号
       app_tx_data_last_r <= app_tx_data_last;  // 采样用户数据末尾信号
       app_tx_data_r      <= app_tx_data;       // 采样用户数据
    end
    else begin
       app_tx_data_vld_r  <= 0;                 // 在非准备状态下,置为无效
       app_tx_data_last_r <= 0;                 // 清除用户数据末尾信号
       app_tx_data_r      <= 0;                 // 清除用户数据
    end
end
// 对用户数据长度进行采样和保持
always @(posedge sys_clk) begin
    if (app_tx_data_vld && app_tx_ready) 
        app_tx_length_r <= app_tx_length;       // 在用户数据有效且准备好的情况下,采样用户数据长度
    else 
        app_tx_length_r <= app_tx_length_r;     // 保持当前长度
end

/*--------------------------------------------------*\
                       tx_cnt计数器:跟踪传输进度
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        tx_cnt <= 0;                            // 复位时,清零计数器
    else if (app_tx_length_r < 18 && tx_cnt == 25)  // 数据最小帧长为18 byte,不足时填充至 26 byte
        tx_cnt <= 0;                            // 当用户数据长度小于18,且 tx_cnt 达到25时复位
    else if (app_tx_length_r >= 18 && tx_cnt == app_tx_length_r + 7)
        tx_cnt <= 0;                            // 当用户数据长度大于等于18,且 tx_cnt 达到 app_tx_length_r +7时复位
    else if (app_tx_data_vld_r || tx_cnt != 0)
        tx_cnt <= tx_cnt + 1;                   // 用户数据有效或计数器非零时递增
    else 
        tx_cnt <= tx_cnt;                       // 保持计数器值
end

/*--------------------------------------------------*\
                       组包:生成UDP数据
\*--------------------------------------------------*/
always @(posedge sys_clk)begin
    if(!sys_reset_n)begin
        udp_tx_data <= 0; // 复位时发送数据清零
    end
    else begin
       case(tx_cnt)
            0 : udp_tx_data <= LOCAL_PORT[15:8];    // 发送本地端口的高字节
            1 : udp_tx_data <= LOCAL_PORT[7:0];     // 发送本地端口的低字节

            2 : udp_tx_data <= TARGET_PORT[15:8];   // 发送目标端口的高字节
            3 : udp_tx_data <= TARGET_PORT[7:0];    // 发送目标端口的低字节

            4 : udp_tx_data <= udp_tx_length[15:8]; // 发送UDP数据长度的高字节
            5 : udp_tx_data <= udp_tx_length[7:0];  // 发送UDP数据长度的低字节

            6 : udp_tx_data <= 0;                  // 填充两个字节的0
            7 : udp_tx_data <= 0;                  // 填充两个字节的0
            default : udp_tx_data <= app_tx_data_delay; // 发送用户数据
       endcase
    end
end
// 根据用户数据长度生成UDP数据长度
always @(posedge sys_clk) begin
    if (app_tx_data_vld_r && app_tx_length_r <= 18) 
        udp_tx_length <= 26;                      // 当用户数据长度小于18时,UDP数据总长度为26
    else if (app_tx_data_vld_r &&  app_tx_length_r > 18) 
        udp_tx_length <= app_tx_length_r + 8;     // 当用户数据长度大于18时,UDP数据总长度为用户数据长度加8
    else 
        udp_tx_length <= udp_tx_length;           // 保持UDP数据长度
end

// 控制udp_tx_data_vld信号,表示数据有效
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        udp_tx_data_vld <= 0;                     // 复位时,数据无效
    else if (udp_tx_data_last) 
        udp_tx_data_vld <= 0;                     // 数据末尾时,数据无效
    else if (app_tx_data_vld_r)
        udp_tx_data_vld <= 1;                     // 用户数据有效时,数据有效
    else 
        udp_tx_data_vld <= udp_tx_data_vld;       // 保持数据有效信号
end

// 控制udp_tx_data_last信号,表示数据末尾
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        udp_tx_data_last <= 0;                    // 复位时,数据非末尾
    else if (app_tx_length_r < 18 && tx_cnt == 25) 
        udp_tx_data_last <= 1'b1;                 // 当用户数据长度小于18,且tx_cnt=25时,数据末尾
    else if (app_tx_length_r >= 18 && tx_cnt == app_tx_length_r + 7)
        udp_tx_data_last <= 1'b1;                 // 当用户数据长度大于等于18,且tx_cnt=app_tx_length_r+7时,数据末尾
    else 
        udp_tx_data_last <= 0;                    // 其他情况为非末尾
end
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        ready_cnt <= 0;
    else if (ready_cnt == READY_CNT_MAX) 
        ready_cnt <= 0;
    else if (app_tx_data_last || ready_cnt != 0)
        ready_cnt <= ready_cnt + 1;
    else 
        ready_cnt <= ready_cnt;
end


// 逻辑复杂,下面是详细注释,我们的复位信号为低电平复位,当按下复位按钮时,sys_reset_n为低,取反后为高,则app_tx_ready被赋值为高电平,表示可以接收用户数据,
// 如果我们没有复位时,当ready_cnt计数到0的时候(或者处理完一组数据后),app_tx_ready被赋值给高电平,表示可以接收用户数据
// 当我们没有复位,并且有一组数据正在发送的时候(ready_cnt不为零),则app_tx_ready被赋值为低,表示没有准备好接收新的一组数据
assign app_tx_ready  = ~sys_reset_n ? 1'b1 : ready_cnt == 0;

/*--------------------------------------------------*\
                       数据延迟模块
\*--------------------------------------------------*/
// 使用移位寄存器对用户数据进行打拍,确保数据在正确的时间点进入发送通道
// 注意:如果A的值为7,udp_tx_data_delay 打拍为8拍
c_shift_ram_0 udp_delay (
  .A(7),      // input wire [5 : 0] A
  .D(app_tx_data_r),      // input wire [7 : 0] D
  .CLK(sys_clk),  // input wire CLK
  .Q(app_tx_data_delay)      // output wire [7 : 0] Q
);


 endmodule

1.11.3 代码仿真与验证

1.12 ARP介绍

在我们的程序设计中,arp需要三个模块组成,arp_receivearp_send,arp_dynamic_list分别是arp接收模块、arp发送模块、arp动态列表模块

小白FPGA

1.13 ARP接收模块代码编写:arp_receive.v

1.13.1 ARP层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-17 18:00:49
 * @LastEditTime: 2025-02-17 18:08:24
 * @FilePath: rtl/receive.v
 * @Description:用于将 IP 地址映射为物理网络地址(MAC地址),主要功能如下:
  1. 接收ARP请求数据包
  2. 解析其中的IP和MAC地址信息并判断是否需要生成ARP回复包
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */
module arp_receive #(
    parameter LOCAL_IP_ADDR = {8'd0,8'd0,8'd0,8'd0}  // 本地设备的IP地址,默认为0.0.0.0
)(
    /*-------接收时钟与复位信号--------*/
    input                   app_rx_clk,           // 接收时钟信号
    input                   app_tx_clk,           // 发送时钟信号
    input                   app_rx_reset,         // 接收复位信号,低有效
    input                   app_tx_reset,         // 发送复位信号,低有效

    /*-------接收到的ARP数据--------*/
    input                   arp_rx_data_vld,      // ARP接收数据有效信号
    input                   arp_rx_data_last,     // ARP接收数据最后一位标志
    input       [7:0]       arp_rx_data,          // ARP接收数据

    /*-------处理后的数据输出--------*/
    output reg              rx_source_vld,        // 源MAC地址和源IP地址有效标志
    output reg  [47:0]      rx_source_mac_addr,   // 源MAC地址
    output reg  [31:0]      rx_source_ip_addr,    // 源IP地址

    output reg              arp_reply_req         // ARP回复请求信号
);
/*--------------------------------------------------*\
                       内部寄存器信号定义
\*--------------------------------------------------*/
reg [15:0]      opcode;                // 操作码,1表示ARP请求,2表示ARP应答
reg [31:0]      rx_target_ip;          // 目标IP地址
reg             rx_target_ip_chack;    // 目标IP地址检查标志
reg [5:0]       rx_cnt;                // 接收计数器
reg [47:0]      source_mac_addr;       // 源MAC地址
reg [31:0]      source_ip_addr;        // 源IP地址
reg             arp_reply_active;      // ARP回复活动标志

/*--------------------------------------------------*\
                       rx_cnt:接收计数器逻辑
\*--------------------------------------------------*/
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        rx_cnt <= 0;  // 重置计数器
    else if (arp_rx_data_vld) 
        rx_cnt <= rx_cnt + 1;  // 如果接收到有效数据,计数器加1
    else 
        rx_cnt <= 0;  // 如果数据无效,计数器重置
end

/*--------------------------------------------------*\
                       操作码的拼接
\*--------------------------------------------------*/
// 根据接收计数器的值,拼接操作码。操作码在 ARP 包的第 6 位和第 7 位。
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        opcode <= 0;
    else if (rx_cnt == 6 || rx_cnt == 7) 
        opcode <= {opcode[7:0], arp_rx_data};  // 拼接操作码
    else 
        opcode <= opcode;  // 保持当前操作码
end

/*--------------------------------------------------*\
                       源MAC地址拼接
\*--------------------------------------------------*/
// 从第 8 位到第 13 位,接收并拼接源 MAC 地址
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        source_mac_addr <= 0;
    else if (rx_cnt >= 8 && rx_cnt <= 13) 
        source_mac_addr <= {source_mac_addr[39:0], arp_rx_data};  // 拼接源MAC地址
    else 
        source_mac_addr <= source_mac_addr;
end

/*--------------------------------------------------*\
                       目标IP地址的拼接
\*--------------------------------------------------*/
// 从第 24 位到第 27 位,接收并拼接目标 IP 地址。
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        rx_target_ip <= 0;
    else if (rx_cnt >= 24 && rx_cnt <= 27) 
        rx_target_ip <= {rx_target_ip[23:0], arp_rx_data};  // 拼接目标IP地址
    else 
        rx_target_ip <= rx_target_ip;
end

/*--------------------------------------------------*\
                       目标IP地址检查
\*--------------------------------------------------*/
// 检查接收到的目标 IP 地址是否是本地设备的 IP 地址。如果匹配,则需要生成 ARP 回复请求。
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        rx_target_ip_chack <= 0;
    else if (rx_target_ip == LOCAL_IP_ADDR) 
        rx_target_ip_chack <= 1'b1;  // 如果目标 IP 地址匹配本地地址,设置为 1
    else if (rx_target_ip != LOCAL_IP_ADDR)
        rx_target_ip_chack <= 1'b0;  // 如果目标 IP 地址不匹配本地地址,设置为 0
    else 
        rx_target_ip_chack <= rx_target_ip_chack;
end

/*--------------------------------------------------*\
                       生成ARP回复请求
\*--------------------------------------------------*/
// 如果目标 IP 地址匹配且操作码为 1(ARP 请求),则设置 arp_reply_active 为 1,表示需要生成 ARP 回复包。
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        arp_reply_active <= 0;
    else if (rx_target_ip_chack && opcode == 1) 
        arp_reply_active <= 1'b1;  // 收到ARP请求包时,激活ARP回复
    else if (!(rx_target_ip_chack && opcode == 1))
        arp_reply_active <= 0;    // 否则停止ARP回复
end

/*--------------------------------------------------*\
                       将ARP回复数据存入FIFO
\*--------------------------------------------------*/
// 如果接收到的数据是数据包的最后一位,生成 ARP 回复数据并将其写入 FIFO。
always @(posedge app_rx_clk) begin
    if (app_rx_reset) begin
        arp_wren <= 0;
        arp_din  <= 0;
    end
    else if (arp_rx_data_last) begin
        arp_wren <= 1'b1;
        arp_din  <= {arp_reply_active, source_mac_addr, source_ip_addr};  // 生成存储到FIFO的ARP回复数据
    end
    else begin
        arp_wren <= 0;
        arp_din  <= 0;
    end
end

/*--------------------------------------------------*\
                       跨时钟域数据读取
\*--------------------------------------------------*/
// 从 FIFO 中读取 ARP 回复数据并发送到上游模块或硬件。
always @(posedge app_tx_clk) begin
    if (app_tx_reset) begin
        rx_source_vld       <= 0;
        rx_source_mac_addr  <= 0;
        rx_source_ip_addr   <= 0;
        arp_reply_req       <= 0;
    end
    else if (arp_rden) begin
        rx_source_vld       <= 1'b1;
        arp_reply_req       <= arp_dout[80];  // 从 FIFO 中读取 ARP 回复请求信号
        rx_source_mac_addr  <= arp_dout[79:32]; // 从 FIFO 中读取源 MAC 地址
        rx_source_ip_addr   <= arp_dout[31:0];  // 从 FIFO 中读取源 IP 地址
    end  
    else begin
        rx_source_vld       <= 0;
        rx_source_mac_addr  <= 0;
        rx_source_ip_addr   <= 0;
        arp_reply_req       <= 0;
    end
end

/*--------------------------------------------------*\
                       实例化FIFO
\*--------------------------------------------------*/
fifo_w81xd16 arp_rx_fifo (
    .rst(app_rx_reset),        // 输入复位信号
    .wr_clk(app_rx_clk),       // 写时钟信号
    .rd_clk(app_tx_clk),       // 读时钟信号
    .din(arp_din),             // 输入数据
    .wr_en(arp_wren),          // 写使能信号
    .rd_en(arp_rden),          // 读使能信号
    .dout(arp_dout),           // 输出数据
    .full(arp_wrfull),         // FIFO满标志
    .empty(arp_rdempty)        // FIFO空标志
);

endmodule

1.13.2 代码仿真与验证

1.14 ARP发送模块代码编写:arp_send.v

1.14.1 ARP层发送模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-17 18:53:12
 * @LastEditTime: 2025-02-17 18:56:25
 * @FilePath: rtl/arp_send.v
 * @Description:
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */
 `timescale 1ns / 1ps

module arp_send #(
    parameter     LOCAL_MAC_ADDR    =  48'hffffffffffff,  // 本地MAC地址(默认广播地址)
    parameter     LOCAL_IP_ADDR     =  {8'd0,8'd0,8'd0,8'd0}   // 本地IP地址(默认0.0.0.0)
)(
    input           sys_clk,                    // 时钟信号
    input           sys_reset_n,                  // 复位信号
    input [47:0]    rx_source_mac_addr,     // 接收到的源MAC地址
    input [31:0]    rx_source_ip_addr,      // 接收到的源IP地址
    input           arp_reply_req,          // ARP应答请求信号
    output reg      arp_tx_data_vld,        // ARP数据有效信号
    output reg      arp_tx_data_last,       // ARP数据包最后一字节标志
    output reg [7:0] arp_tx_data,           // 当前发送的ARP数据
    output [15:0]   arp_tx_length           // ARP包的长度(固定为46)
);

    reg [5:0] tx_cnt;                       // 发送数据计数器
    reg [47:0] target_mac_addr;             // 目标MAC地址
    reg [31:0] target_ip_addr;              // 目标IP地址

    assign arp_tx_length = 46;  // ARP包的固定长度

    // 接收到ARP请求时,更新目标MAC地址和目标IP地址
    always @(posedge sys_clk) begin
        if (arp_reply_req && ~arp_tx_data_vld) begin
            target_mac_addr <= rx_source_mac_addr;
            target_ip_addr  <= rx_source_ip_addr;
        end
    end

    // 计数器tx_cnt控制数据的发送
    always @(posedge sys_clk) begin
        if (!sys_reset_n) 
            tx_cnt <= 0;
        else if (tx_cnt == 45) 
            tx_cnt <= 0;
        else if (arp_reply_req || tx_cnt != 0) 
            tx_cnt <= tx_cnt + 1;
    end

    // 发送数据有效信号控制
    always @(posedge sys_clk) begin
        if (!sys_reset_n) 
            arp_tx_data_vld <= 0;
        else if (arp_reply_req) 
            arp_tx_data_vld <= 1'b1;
        else if (arp_tx_data_last) 
            arp_tx_data_vld <= 1'b0;
    end

    // 数据包最后一字节标志
    always @(posedge sys_clk) begin
        if (!sys_reset_n) 
            arp_tx_data_last <= 0;
        else if (tx_cnt == 45) 
            arp_tx_data_last <= 1'b1;
        else 
            arp_tx_data_last <= 0;
    end

    // 根据计数器值发送各个字节的ARP数据
    always @(posedge sys_clk) begin
        if (!sys_reset_n) 
            arp_tx_data <= 0; 
        else begin
            case(tx_cnt)
                0   : arp_tx_data <= 0;                               // 硬件类型:以太网(0x0001)
                1   : arp_tx_data <= 1;                               // 协议类型:IP(0x0800)
                2   : arp_tx_data <= 8;                               // 硬件地址长度:6字节(MAC地址)
                3   : arp_tx_data <= 0;                               // 协议地址长度:4字节(IP地址)
                4   : arp_tx_data <= 6;                               // 操作码:请求(0x0001)
                5   : arp_tx_data <= 4;                               // 操作码(具体值)
                6   : arp_tx_data <= 0;
                7   : arp_tx_data <= 2;
                8   : arp_tx_data <= LOCAL_MAC_ADDR[47:40];           // 本机MAC地址(高字节)
                9   : arp_tx_data <= LOCAL_MAC_ADDR[39:32];
                10  : arp_tx_data <= LOCAL_MAC_ADDR[31:24];
                11  : arp_tx_data <= LOCAL_MAC_ADDR[23:16];
                12  : arp_tx_data <= LOCAL_MAC_ADDR[15:8];
                13  : arp_tx_data <= LOCAL_MAC_ADDR[7:0];            // 本机MAC地址(低字节)
                14  : arp_tx_data <= LOCAL_IP_ADDR[31:24];            // 本机IP地址(高字节)
                15  : arp_tx_data <= LOCAL_IP_ADDR[23:16];
                16  : arp_tx_data <= LOCAL_IP_ADDR[15:8];
                17  : arp_tx_data <= LOCAL_IP_ADDR[7:0];             // 本机IP地址(低字节)
                18  : arp_tx_data <= target_mac_addr[47:40];          // 目标MAC地址(高字节)
                19  : arp_tx_data <= target_mac_addr[39:32];
                20  : arp_tx_data <= target_mac_addr[31:24];
                21  : arp_tx_data <= target_mac_addr[23:16];
                22  : arp_tx_data <= target_mac_addr[15:8];
                23  : arp_tx_data <= target_mac_addr[7:0];           // 目标MAC地址(低字节)
                24  : arp_tx_data <= target_ip_addr[31:24];           // 目标IP地址(高字节)
                25  : arp_tx_data <= target_ip_addr[23:16];
                26  : arp_tx_data <= target_ip_addr[15:8];
                27  : arp_tx_data <= target_ip_addr[7:0];            // 目标IP地址(低字节)
                default : arp_tx_data <= 0;
            endcase
        end    
    end

endmodule

1.14.2 代码仿真与验证

1.15 ARP动态列表模块编写arp_dynamic_list.v

1.15.1 ARP动态列表代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-25 22:07:07
 * @LastEditTime: 2025-02-25 22:22:29
 * @FilePath: rtl/arp_dynamic_list.v
 * @Description:实现ARP协议中的IP-MAC地址动态映射表,用于存储和查询IP地址与MAC地址的对应关系。主要功能如下:
 1. 快速查询:通过并行比对实现IP地址到MAC地址的快速查找,使用并行比对代替软件遍历,查询速度仅需1个时钟周期

 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module arp_dynamic_list(
    input            sys_clk,        // 系统时钟
    input            sys_reset_n,    // 全局复位,低电平有效

    // 更新动态链表的写入端口信号
    input            wr_arp_en,      // ARP表写入使能,高有效
    input  [31:0]    wr_ip_addr,     // 待写入的32位IP地址
    input  [47:0]    wr_mac_addr,    // 待写入的48位MAC地址

    // 查询动态链表的读出端口信号
    input            rd_arp_en,      // ARP表读取使能,高有效
    input  [31:0]    rd_ip_addr,     // 待查询的32位IP地址
    output reg [47:0] rd_mac_addr,    // 查询到的48位MAC地址
    output reg       rd_mac_addr_vld  // MAC地址有效标志
);

// 参数定义
localparam SIZE = 4;     // ARP表深度,支持存储4个IP-MAC映射

// 寄存器定义
reg [31:0] ip_addr_buffer [SIZE-1:0];  // IP地址存储阵列
reg [47:0] mac_addr_buffer[SIZE-1:0];   // MAC地址存储阵列
reg [3:0]  pointer;                     // 循环写入指针(范围0-3)

//==========================================================================
// 主要逻辑部分
//==========================================================================

//---------------------------------------------------------------------
// 时钟同步处理:ARP表写入逻辑
// 功能:1. 复位初始化  2. 更新/添加IP-MAC映射
//---------------------------------------------------------------------
always @(posedge sys_clk) begin
    // 低电平复位初始化
    if (!sys_reset_n) begin 
        ip_addr_buffer[0]  <= 32'd0;    // 初始化IP存储阵列
        ip_addr_buffer[1]  <= 32'd0;
        ip_addr_buffer[2]  <= 32'd0;
        ip_addr_buffer[3]  <= 32'd0;    
        mac_addr_buffer[0] <= 48'd0;    // 初始化MAC存储阵列
        mac_addr_buffer[1] <= 48'd0;
        mac_addr_buffer[2] <= 48'd0;
        mac_addr_buffer[3] <= 48'd0;    
        pointer            <= 4'd0;     // 复位写入指针
    end

    // ARP表写入操作
    else if (wr_arp_en) begin
        case(wr_ip_addr)
            // 存在性检查:如果IP已存在,直接更新对应MAC地址
            ip_addr_buffer[0] : mac_addr_buffer[0] <= wr_mac_addr;
            ip_addr_buffer[1] : mac_addr_buffer[1] <= wr_mac_addr;
            ip_addr_buffer[2] : mac_addr_buffer[2] <= wr_mac_addr;
            ip_addr_buffer[3] : mac_addr_buffer[3] <= wr_mac_addr;

            // 新增条目:IP不存在时写入指针位置,并更新指针
            default : begin
                mac_addr_buffer[pointer] <= wr_mac_addr;  // 写入新MAC
                ip_addr_buffer [pointer] <= wr_ip_addr;  // 写入新IP
                // 指针循环递增(0->1->2->3->0...)
                pointer <= (pointer == 3'd3) ? 3'd0 : pointer + 3'd1;
            end
        endcase
    end
end

//---------------------------------------------------------------------
// 时钟同步处理:ARP表读取逻辑
// 功能:根据IP地址查询对应的MAC地址
//---------------------------------------------------------------------
always @(posedge sys_clk) begin
    if (!sys_reset_n) begin
        rd_mac_addr <= 48'd0;  // 复位时清除MAC输出
    end  
    else if (rd_arp_en) begin
        case(rd_ip_addr)  // 顺序查询四个存储单元
            ip_addr_buffer[0] : rd_mac_addr <= mac_addr_buffer[0];
            ip_addr_buffer[1] : rd_mac_addr <= mac_addr_buffer[1];
            ip_addr_buffer[2] : rd_mac_addr <= mac_addr_buffer[2];
            ip_addr_buffer[3] : rd_mac_addr <= mac_addr_buffer[3];   
            default :         rd_mac_addr <= 48'd0;  // 未命中返回0
        endcase
    end
end

//---------------------------------------------------------------------
// 时钟同步处理:有效信号生成
// 功能:在读取操作后生成单周期有效信号
//---------------------------------------------------------------------
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        rd_mac_addr_vld <= 1'b0;       // 复位时清除有效标志
    else 
        // 有效信号比读取操作延迟1个时钟周期
        rd_mac_addr_vld <= rd_arp_en;  // 读取使能直接作为有效信号
end

endmodule

1.16 ICMP协议介绍

说起icmp协议大家可能比较陌生,但是说到ping命令了,我想在座的同学们应该都再熟悉不过了吧。使用过ping命令的同学都知道,当我们不确定目标主题是否可以连通的时候,都可以拼一下目标。

比如我们想看一下我们的计算机能否与baidu.com联通,我们就可以执行一下ping baidu.com的命令。如果可以连通,那我们会看到这样的显示,表示可以连通,并且平均延迟为15毫秒。

今天咱们就一起来学习一下拼命令背后的协议icmp协议。ICMP协议简单来说,它的作用就是帮助网络管理员检测网络中发生的各种问题,然后根据问题原因做出诊断和解决。

1.16.1 ICMP协议分类

从大类上来分,它主要是两大块的功能。

一是询问报告,比如询问一下目标主机是否可以连通并做出回答,我们常用的Ping命令就是用这个功能来实现的。

二是用来做差错报告,比如网关发现目标网络不可谈,或者目标主机发现访问的UDP端口不可用,需要把错误报告给原主机。Tracroot这个工序就是用这个功能来实现的。

我们这个项目中ICMP协议就是只是用到第一个功能用来检查我们的电脑是否与FPGA开发板联通。

1.16.2. ICMP数据包的结构与实现流程

ICMP数据包包含以下几个部分:

  • ICMP头部:包括类型(Type)、代码(Code)、校验和(Checksum)、标识符(Identifier)和序列号(Sequence Number)。
  • 数据部分:通常用于携带额外的信息,如时间戳或用户数据。

1.17 ICMP接收模块代码编写:icmp_receive.v

1.17.1 ICMP层接收模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-17 17:23:58
 * @LastEditTime: 2025-02-17 17:35:19
 * @FilePath: rtl/icmp_receive.v
 * @Description:收到数据后通过FIFO缓存数据,实现跨时钟域处理,并且将接收到的数据包识别符和序列号提取出来并输出
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */
 module icmp_receive (
    /*-------用户端口信号--------*/
    input                   app_rx_clk;            // 接收时钟
    input                   app_tx_clk;            // 发送时钟
    input                   app_rx_reset;          // 接收复位信号
    input                   app_tx_reset;          // 发送复位信号

    /*-------ICMP输入端口信号--------*/
    input                   icmp_rx_data_vld;      // ICMP接收数据有效信号
    input                   icmp_rx_data_last;     // ICMP接收数据的最后一位标志
    input [7:0]             icmp_rx_data;          // ICMP接收数据
    input [15:0]            icmp_rx_length;        // ICMP接收数据包的长度

    /*-------ICMP输出端口信号--------*/
    output reg              icmp_reply_req;        // ICMP回复请求
    output reg [15:0]       icmp_rx_identify;      // ICMP包的标识符
    output reg [15:0]       icmp_rx_sequence;      // ICMP包的序列号
 );
/*--------------------------------------------------*\
                       内部寄存器和信号
\*--------------------------------------------------*/
reg [10:0]             rx_cnt;               // 接收计数器
reg [15:0]             rx_identify;          // 临时存储ICMP的标识符
reg [15:0]             rx_sequence;          // 临时存储ICMP的序列号
reg [7:0]              imcp_type;            // 临时存储ICMP类型
reg [31:0]             icmp_din;             // 用于FIFO的数据输入
reg                    icmp_wren;            // FIFO写使能信号
wire [31:0]            icmp_dout;            // FIFO数据输出
wire                   icmp_rden;            // FIFO读使能信号
wire                   icmp_wrfull;          // FIFO写满标志
wire                   icmp_rdempty;         // FIFO读空标志

/*--------------------------------------------------*\
                       接收计数器逻辑
\*--------------------------------------------------*/
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        rx_cnt <= 0;                         // 如果复位信号有效,则清零计数器
    else if (icmp_rx_data_vld) 
        rx_cnt <= rx_cnt + 1;               // 数据有效时,计数器递增
    else 
        rx_cnt <= 0;                         // 如果数据无效,则计数器清零
end

/*--------------------------------------------------*\
                       ICMP类型的提取
\*--------------------------------------------------*/
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        imcp_type <= 0;                  // 复位时清空ICMP类型
    else if (rx_cnt == 0) 
        imcp_type <= icmp_rx_data;       // 第一个字节(ICMP数据包的类型)
    else 
        imcp_type <= imcp_type;          // 保持原值
end

/*--------------------------------------------------*\
                       ICMP标识符提取
\*--------------------------------------------------*/
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        rx_identify <= 0;                // 复位时清空标识符
    else if (rx_cnt == 4 || rx_cnt == 5) 
        rx_identify <= {rx_identify[7:0], icmp_rx_data};  // 从第4、第5字节拼接标识符
    else 
        rx_identify <= rx_identify;      // 保持原值
end

/*--------------------------------------------------*\
                       ICMP序列号提取
\*--------------------------------------------------*/
always @(posedge app_rx_clk) begin
    if (app_rx_reset) 
        rx_sequence <= 0;                // 复位时清空序列号
    else if (rx_cnt == 6 || rx_cnt == 7) 
        rx_sequence <= {rx_sequence[7:0], icmp_rx_data};  // 从第6、第7字节拼接序列号
    else 
        rx_sequence <= rx_sequence;      // 保持原值
end

/*--------------------------------------------------*\
                       FIFO 存储 ICMP 数据
\*--------------------------------------------------*/
always @(posedge app_rx_clk) begin
    if (app_rx_reset) begin
        icmp_wren <= 0;                  // 复位时不写入FIFO
        icmp_din <= 0;                   // 清空FIFO输入数据
    end  
    else begin
        icmp_wren <= icmp_rx_data_last;  // 如果是最后一个数据字节,写入FIFO
        icmp_din <= {rx_identify, rx_sequence};  // 将标识符和序列号一起写入FIFO
    end
end

/*--------------------------------------------------*\
             跨时钟域处理(从接收时钟到发送时钟)
\*--------------------------------------------------*/
always @(posedge app_tx_clk) begin
    if (app_tx_reset) begin
        icmp_reply_req <= 0;            // 复位时不请求回复
        icmp_rx_identify <= 0;          // 清空ICMP标识符
        icmp_rx_sequence <= 0;          // 清空ICMP序列号
    end 
    else if (icmp_rden) begin
        icmp_reply_req <= 1;            // 如果FIFO读取有效数据,设置回复请求
        icmp_rx_identify <= icmp_dout[31:16];  // 提取标识符
        icmp_rx_sequence <= icmp_dout[15:0];   // 提取序列号
    end 
    else begin
        icmp_reply_req <= 0;            // 否则不请求回复
        icmp_rx_identify <= 0;          // 清空标识符
        icmp_rx_sequence <= 0;          // 清空序列号
    end     
end

/*--------------------------------------------------*\
                       FIFO 实例
\*--------------------------------------------------*/
fifo_w32xd16_async icmp_rx_fifo (
  .rst(app_rx_reset),        // 输入复位信号
  .wr_clk(app_rx_clk),       // 写时钟信号(接收时钟)
  .rd_clk(app_tx_clk),       // 读时钟信号(发送时钟)
  .din(icmp_din),            // 输入数据(标识符和序列号)
  .wr_en(icmp_wren),         // 写使能信号
  .rd_en(icmp_rden),         // 读使能信号
  .dout(icmp_dout),          // 输出数据(标识符和序列号)
  .full(icmp_wrfull),        // FIFO写满标志
  .empty(icmp_rdempty)       // FIFO读空标志
);

 endmodule

1.17.2 代码仿真与验证

1.18 ICMP发送模块代码编写:icmp_send.v

1.18.1 ICMP层发送模块代码

/*
 * ****************************************Copyright (c)***********************************
 * @Date: 2025-02-17 17:46:37
 * @LastEditTime: 2025-02-17 17:55:14
 * @FilePath: rtl/icmp_send.v
 * @Description:用于按照 ICMP 协议规则生成并回复 Echo 回复包,ICMP Echo 请求包和回复包的格式大致相同,区别主要在于其中的类型字段。主要功能如下:
 1. 接收并保存请求信息:当收到 ICMP 回复请求时,模块会从请求中提取标识符和序列号。
 2. 数据字节生成与发送:根据 ICMP 协议的格式(类型、代码、校验和、标识符、序列号等),模块将数据字节逐个生成并发送。
 3. 校验和计算:计算 ICMP 包的校验和,保证数据的完整性。
 4. 发送控制:通过计数器和状态信号控制数据的发送顺序,确保数据的正确发送。
 * Copyright (c) 2025 by 硅农公社, All Rights Reserved.
 *
 * 哔哩哔哩:https://space.bilibili.com/500610348?spm_id_from=333.1007.0.0
 * ****************************************************************************************
 */

 module icmp_send(
    /*-------系统端口信号--------*/
    input                   sys_clk;               // 输入时钟信号
    input                   sys_reset_n;             // 复位信号

    /*-------ICMP接收端口信号--------*/
    input                   icmp_reply_req;    // ICMP 回复请求信号
    input      [15:0]      icmp_rx_identify;  // 接收到的 ICMP 包标识符
    input      [15:0]      icmp_rx_sequence;  // 接收到的 ICMP 包序列号

    /*-------ICMP发送端口信号--------*/
    output reg              icmp_tx_data_vld;  // ICMP 发送数据有效信号
    output reg              icmp_tx_data_last; // ICMP 发送数据最后一位标志
    output reg [7:0]        icmp_tx_data;      // ICMP 发送数据字节
    output     [15:0]       icmp_tx_length;    // ICMP 发送数据包的长度
 );
 /*--------------------------------------------------*\
                        内部寄存器和信号
 \*--------------------------------------------------*/
reg [5:0]              tx_cnt;            // 发送计数器,用于控制数据发送的顺序
reg [15:0]             icmp_tx_identify;  // ICMP 回复包的标识符
reg [15:0]             icmp_tx_sequence;  // ICMP 回复包的序列号
reg [31:0]             icmp_tx_chack;     // ICMP 校验和

/*--------------------------------------------------*\
            处理 ICMP 回复请求,获取标识符和序列号
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) begin
        icmp_tx_identify <= 0;
        icmp_tx_sequence <= 0;
    end
    else if (icmp_reply_req && ~icmp_tx_data_vld) begin
        icmp_tx_identify <= icmp_rx_identify;   // 从接收到的数据包中获取标识符
        icmp_tx_sequence <= icmp_rx_sequence;   // 从接收到的数据包中获取序列号
    end
    else begin
        icmp_tx_identify <= icmp_tx_identify;   // 保持原值
        icmp_tx_sequence <= icmp_tx_sequence;   // 保持原值
    end 
end

/*--------------------------------------------------*\
                       发送计数器控制
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        tx_cnt <= 0;                            // 复位时,计数器归零
    else if (tx_cnt == 39)                      // 如果已经发送了40个字节(ICMP数据包长度)
        tx_cnt <= 0;                            // 重新开始
    else if (icmp_reply_req || tx_cnt != 0)     // 当有回复请求或计数器未清零时,计数器递增
        tx_cnt <= tx_cnt + 1;
    else 
        tx_cnt <= tx_cnt;                       // 保持计数器值
end

/*--------------------------------------------------*\
                       发送数据有效信号控制
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        icmp_tx_data_vld <= 0;                // 复位时,发送数据有效信号为 0
    else if (icmp_tx_data_last) 
        icmp_tx_data_vld <= 0;                // 如果是最后一个数据字节,数据无效
    else if (icmp_reply_req)
        icmp_tx_data_vld <= 1'b1;             // 当有 ICMP 回复请求时,数据有效
    else 
        icmp_tx_data_vld <= icmp_tx_data_vld; // 保持原值
end

/*--------------------------------------------------*\
                       发送数据最后一位标志控制
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        icmp_tx_data_last <= 0;                  // 复位时,最后一位标志为 0
    else if (tx_cnt == 39) 
        icmp_tx_data_last <= 1'b1;               // 发送完40个字节后,标志置为 1
    else 
        icmp_tx_data_last <= 0;                  // 其他情况下,标志置为 0
end

/*--------------------------------------------------*\
                       校验和计算
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        icmp_tx_chack <= 0;                         // 复位时,校验和为0
    else if (icmp_reply_req) 
        icmp_tx_chack <= icmp_rx_identify + icmp_rx_sequence;  // 初始校验和为标识符和序列号之和
    else if (tx_cnt == 1)
        icmp_tx_chack <= ~(icmp_tx_chack[31:16] + icmp_tx_chack[15:0]); // 计算校验和
    else 
        icmp_tx_chack <= icmp_tx_chack;             // 保持原值
end

/*--------------------------------------------------*\
                       数据字节生成
\*--------------------------------------------------*/
always @(posedge sys_clk) begin
    if (!sys_reset_n) 
        icmp_tx_data <= 0;
    else begin
        case(tx_cnt)
            0  : icmp_tx_data <= 0;                        // 类型字节(0表示应答)
            1  : icmp_tx_data <= 0;                        // 代码字节(默认为0)
            2  : icmp_tx_data <= icmp_tx_chack[15:8];      // 校验和的高字节
            3  : icmp_tx_data <= icmp_tx_chack[7:0];       // 校验和的低字节
            4  : icmp_tx_data <= icmp_tx_identify[15:8];   // 标识符的高字节
            5  : icmp_tx_data <= icmp_tx_identify[7:0];    // 标识符的低字节
            6  : icmp_tx_data <= icmp_tx_sequence[15:8];   // 序列号的高字节
            7  : icmp_tx_data <= icmp_tx_sequence[7:0];    // 序列号的低字节
            default : icmp_tx_data <= 0;                   // 默认值
        endcase
    end    
end


 endmodule

1.18.2 代码仿真与验证

2. 以太网数据回环实验

3. LCD屏幕驱动显示

3.1 VGA驱动显示

3.2DDR3图像缓存

3.3 基于DDR3存储显示LCD彩条

4. ADC采集存储系统

5. 基于UDP协议的实时画面显示

参考教程

笔记内容图片部分使用互联网图片,图片均会标明来源,如有侵权,请联系作者进行删除改正

致谢:

  1. 小白FPGA课程
  2. 哔哩哔哩:野火FPGA
  3. CSDN
  4. 博客园
  5. Kimi人工智能帮助编写文章内容
  6. 英特尔5.1.7.1.2. RMII和RGMII PHY接口
  7. DeepSeek | 深度求索
  8. 微信公众平台

附录1 PHY原理图即DataSheet说明

参考链接:136-第三十八讲-以太网数据回环实验(实战演练(一))_哔哩哔哩_bilibili

因为是原理图和手册的讲解,使用图文的方式很难给大家讲清楚里面的内容,请配合观看视频进行学习巩固。

米联客FPGA原理图截屏

参考手册下载地址:https://raw.githubusercontent.com/zikwq/Blog_Pic/main/new_migRealtek%20RTL8211F(D)(I)-CG%20DataSheet%201.9.pdf

附录2 以外网相关协议文档阅读