FPGA项目实战——以太网ARP测试

笔记根据正点原子官方教学视频第33讲

时间:2025/4/2

【第一期】手把手教你学领航者&启明星ZYNQ之FPGA开发篇【真人出镜】FPGA教学视频教程_哔哩哔哩_bilibili

 有错误的地方希望可以告诉博主

实验任务 

        使用FPGA开发板上的以太网接口,和上位机实现 ARP请求和应答的功能。当上位机发送ARP请求时,开发板返回ARP应答数据。当按下开发板的触摸按键时,开发板发送ARP请求,此时上位机返回应答数据。

程序框图

        首先是顶层模块 eth_arp_test 框图,输入信号和输出信号是根据 RGMII 接口的要求和触摸按键功能确定的,无论是从 PHY 芯片接收数据还是向 PHY 芯片发送数据,都需要使用 RGMII 接口(因为使用的开发板PHY芯片为 YT8531C,数据手册上指明了其与 MAC 模块的通信接口为 RGMII,下文有详细介绍)。

RGMII

         PLL 模块的框图,功能是将输入的系统时钟转变为 200MHz 的参考时钟给 IDELAY 模块以制造延迟。而需要 IDELAY 产生延迟的原因是 RGMII 的发送和接收在时序上有2ns的延迟。 

        RGMII和GMII相互转换模块,作用是适配不同的接口标准和数据传输需求。

         

        这里解释一下为什么要做这个转换,具体需要用到 FPGA 开发板的底板原理图,从下图中就可以看出此块开发板搭载的 PHY 芯片为 YT8531C,在数据手册中可以看到 YT8531C 对应的 MAC 接口为 RGMII(其实原理图上也可以看出)。将 RGMII 形式的信号转变为 GMII,有很多原因:一是很多 FPGA 的数据总线宽度为8位,适用于许多基本的配置模式和数据传输场景;二是如果直接使用 RGMII 的信号,所有数据处理模块均需要适配双沿时钟和半字节操作,导致设计复杂度和资源消耗大幅增加。

        在通用场景中,转换为 GMII 是更加具有性价比的方案。

        

        接下来是 ARP 控制模块,用于判断 ARP 的类型是请求还是应答 。

        最后是 ARP 解析和编码模块。

        本模块处于 FPGA 内部,故使用 GMII 接口信号。

RGMII 和 GMII 相互转换模块详细分析

实现思路解析

        整个模块划分为两个部分,一是 RGMII 转 GMII 模块,二是 GMII 转 RGMII 模块。

RGMII 转 GMII 功能实现

理论解释

         因为 RGMII 是双沿信号,所以需要采用 IDDR 对其进行双沿采样,并将RGMII的4位数据扩展为GMII的8位数据。IDDR 的工作模式选择使用相同沿的流水模式。再之前的文章中讲到过此工作模式(FPGA学习笔记——Xilinx原语-优快云博客),输入信号 D 将会采样给 Q1 和 Q2。框图中的 rgmii_rxd 便是信号 D,Q1 和 Q2 按高低位组合起来合成信号 gmii_rxd。

        上图就是该模块的简要描述。

        输入信号为: rgmii_rxc、rgmii_rx_ctl、rgmii_rxd[3:0]、idelay_clk(作为参考时钟输入到 IDELAY ,只不过上图未标明)。

        输出信号为:gmii_rx_clk、gmii_rx_dv、gmii_rxd[7:0]。

        如何确定这些信号的?

        从下图(左:GMII,右:RGMII)可以看出,FPGA 侧的输入一开始是 RGMII 接口信号,即下图右侧的 ETH_RXC、ETH_RXCTL、ETH_RXD[3:0],分别对应 rgmii_rxc、rgmii_rx_ctl、rgmii_rxd[3:0],另外还需要一个 idelay_clk 信号作为 IDELAY 的参考时钟。输出信号则来自下图左侧的 ETH_RXC、ETH_RXDV、ETH_RXD[7:0],分别对应 gmii_rx_clk、gmii_rx_dv、gmii_rxd[7:0],至于 ETH_RXER 是在本项目中不需要,如果需要的话也是可以加上的。

        

        讲解一下为什么会需要 IDELAY 模块。

        设计 IDELAY 的原因是为了增强代码的通用性,因为有一些 PHY 芯片内部不提供硬件延迟,所以我们需要在 FPGA 侧补上一个延迟。为什么需要这个延迟呢?看下图,这是正常带有硬件延迟的 PHY 芯片输出到 MAC 上的信号,RX_CLK 被延迟了2ns以便 FPGA 能在时钟边沿去处理 RXD 和 RX_CTRL。如果 PHY 芯片没有硬件延迟的话,就需要使用 IDELAY 来对数据信号和控制信号进行延迟,以便 FPGA 能在时钟边沿采集到。

        为什么不延迟时钟信号呢?因为延迟时钟信号会给系统带来不稳定因素,一般还是对其他信号进行延迟会比较安全。

        然后,解释一下为什么用到 BUFIO 和 BUFG。

        BUFIO 是一种专门用于驱动 FPGA 输入/输出(I/O)时钟的缓冲器,其主要作用是为 I/O 逻辑单元(如 IDDR、ODDR 等)提供低延迟的时钟信号。之所有 rgmii_rxc 会有一部分接到 BUFIO 是因为 BUFIO 与 BUFG 相比,在距离上更接近 IDDR,延迟会更小。 但 BUFIO 只能驱动 IO Block 里面的逻辑,不能驱动 CLB 里面的 LUT,REG 等逻辑,所以需要配合BUFG使用。

        如果单独使用 BUFG 时钟信号可能会受到较大的延迟和抖动影响,导致数据采样不准确。如果不使用 BUFIO,rgmii_rxc 的抖动和噪声可能会增加,影响时钟信号的稳定性,导致数据采样不准确。

代码解释

        对于以下部分例化模块,为什么有些接口或参数没有列出?因为这些参数或接口是有默认值的,或者在特定工作模式下有些端口不起作用。这里不再一一列举,只需了解省略原因即可。

        generate 和 genvar 实现循环例化,例化4次,分别处理4位 RGMII 数据。

        另一个可能混淆的点是:gmii_rxdv_t[0] 和 gmii_rxdv_t[1] 分别对应时钟周期的上升沿和下降沿采集到的 rgmii_rx_ctl。在 RGMII 中,rgmii_rx_ctl 在时钟的上升沿和下降沿同为高电平才有效,故采用相与的方式合成 gmii_rx_dv(复习 RGMII 可查看此文章:FPGA学习笔记——以太网-优快云博客)。

module rgmii_rx(
    input              idelay_clk  , //200Mhz时钟,IDELAY时钟
    
    //以太网RGMII接口
    input              rgmii_rxc   , //RGMII接收时钟
    input              rgmii_rx_ctl, //RGMII接收数据控制信号
    input       [3:0]  rgmii_rxd   , //RGMII接收数据    

    //以太网GMII接口
    output             gmii_rx_clk , //GMII接收时钟
    output             gmii_rx_dv  , //GMII接收数据有效信号
    output      [7:0]  gmii_rxd      //GMII接收数据   
    );

//parameter define
parameter IDELAY_VALUE = 0;

//wire define
wire         rgmii_rxc_bufg;     //全局时钟缓存
wire         rgmii_rxc_bufio;    //全局时钟IO缓存
wire  [3:0]  rgmii_rxd_delay;    //rgmii_rxd输入延时
wire         rgmii_rx_ctl_delay; //rgmii_rx_ctl输入延时
wire  [1:0]  gmii_rxdv_t;        //两位GMII接收有效信号 

//*****************************************************
//**                    main code
//*****************************************************

assign gmii_rx_clk = rgmii_rxc_bufg;
assign gmii_rx_dv = gmii_rxdv_t[0] & gmii_rxdv_t[1];  // rgmii_rx_ctl在时钟上升沿和下降沿同为高电平才有效,所以这里需要相与。

//全局时钟缓存
BUFG BUFG_inst (
  .I            (rgmii_rxc),     
  .O            (rgmii_rxc_bufg) 
);

//全局时钟IO缓存——BUFIO
BUFIO BUFIO_inst (
  .I            (rgmii_rxc),      
  .O            (rgmii_rxc_bufio) 
);

//输入延时控制
// Specifies group name for associated IDELAYs/ODELAYs and IDELAYCTRL
(* IODELAY_GROUP = "rgmii_rx_delay" *) 
IDELAYCTRL  IDELAYCTRL_inst (
    .RDY(),              // 本项目不需要检测 RDY 的输出,故悬空                
    .REFCLK(idelay_clk),         
    .RST(1'b0)                   
);

//rgmii_rx_ctl输入延时与双沿采样
(* IODELAY_GROUP = "rgmii_rx_delay" *) 
IDELAYE2 #(
  .IDELAY_TYPE     ("FIXED"),           
  .IDELAY_VALUE    (IDELAY_VALUE),      
  .REFCLK_FREQUENCY(200.0)              
)
u_delay_rx_ctrl (
  .CNTVALUEOUT     (),                  
  .DATAOUT         (rgmii_rx_ctl_delay),
  .C               (1'b0),              
  .CE              (1'b0),              
  .CINVCTRL        (1'b0),              
  .CNTVALUEIN      (5'b0),              
  .DATAIN          (1'b0),              
  .IDATAIN         (rgmii_rx_ctl),      
  .INC             (1'b0),              
  .LD              (1'b0),              
  .LDPIPEEN        (1'b0),              
  .REGRST          (1'b0)               
);

//输入双沿采样寄存器
IDDR #(
    .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"), 
                                        
    .INIT_Q1  (1'b0),                   
    .INIT_Q2  (1'b0),                   
    .SRTYPE   ("SYNC")                  
) u_iddr_rx_ctl (
    .Q1       (gmii_rxdv_t[0]),         
    .Q2       (gmii_rxdv_t[1]),         
    .C        (rgmii_rxc_bufio),        
    .CE       (1'b1),                   
    .D        (rgmii_rx_ctl_delay),     
    .R        (1'b0),                   
    .S        (1'b0)                    
);

//rgmii_rxd输入延时与双沿采样

genvar i;   // 循环例化4次
generate for (i=0; i<4; i=i+1)
    (* IODELAY_GROUP = "rgmii_rx_delay" *) 
    begin : rxdata_bus
        //输入延时           
        (* IODELAY_GROUP = "rgmii_rx_delay" *) 
        IDELAYE2 #(
          .IDELAY_TYPE     ("FIXED"),           
          .IDELAY_VALUE    (IDELAY_VALUE),      
          .REFCLK_FREQUENCY(200.0)              
        )
        u_delay_rxd (
          .CNTVALUEOUT     (),                  
          .DATAOUT         (rgmii_rxd_delay[i]),
          .C               (1'b0),              
          .CE              (1'b0),              
          .CINVCTRL        (1'b0),              
          .CNTVALUEIN      (5'b0),              
          .DATAIN          (1'b0),              
          .IDATAIN         (rgmii_rxd[i]),      
          .INC             (1'b0),              
          .LD              (1'b0),              
          .LDPIPEEN        (1'b0),              
          .REGRST          (1'b0)               
        );
        
        //输入双沿采样寄存器
        IDDR #(
            .DDR_CLK_EDGE("SAME_EDGE_PIPELINED"),
                                                
            .INIT_Q1  (1'b0),                   
            .INIT_Q2  (1'b0),                  
            .SRTYPE   ("SYNC")                  
        ) u_iddr_rxd (
            .Q1       (gmii_rxd[i]),            
            .Q2       (gmii_rxd[4+i]),          
            .C        (rgmii_rxc_bufio),        
            .CE       (1'b1),                   
            .D        (rgmii_rxd_delay[i]),     
            .R        (1'b0),                   
            .S        (1'b0)                    
        );
    end
endgenerate

endmodule

GMII 转 RGMII 功能实现

理论解释

         为什么会需要 ODDR?因为 ODDR 可以将两个单沿信号采样并输出为双沿信号。ODDR 的工作模式选择使用相同沿模式。这里没有对 gmii_txc 进行延迟是因为 PHY 芯片内部自带有硬件延迟。

        输入信号为:gmii_txc、gmii_tx_en、gmii_txd[7:0]。

        输出信号为:rgmii_txc、rgmii_tx_ctl、rgmii_txd[3:0]。

        如何确定这些信号的?

        从下图(左:GMII,右:RGMII)可以看出,FPGA 侧的输出一开始是 GMII 信号,即下图左侧的 ETH_TXC、ETH_TXEN、ETH_TXD[7:0],分别对应 gmii_txc、gmii_tx_en、gmii_txd,至于 ETH_TXER 没有出现是因为本项目不需要,如果有需要的话,也是可以加上的。输出信号则来自下图右侧的 ETH_TXC、ETH_TXCTL、ETH_TXD[3:0],分别对应 rgmii_txc、rgmii_tx_ctl、rgmii_txd[3:0]。

        

代码解释 

         根据理论解释实现即可。

module rgmii_tx(
    //GMII发送端口
    input              gmii_tx_clk , //GMII发送时钟    
    input              gmii_tx_en  , //GMII输出数据有效信号
    input       [7:0]  gmii_txd    , //GMII输出数据        
    
    //RGMII发送端口
    output             rgmii_txc   , //RGMII发送数据时钟    
    output             rgmii_tx_ctl, //RGMII输出数据有效信号
    output      [3:0]  rgmii_txd     //RGMII输出数据     
    );

//*****************************************************
//**                    main code
//*****************************************************

assign rgmii_txc = gmii_tx_clk;

//输出双沿采样寄存器 (rgmii_tx_ctl)
ODDR #(
    .DDR_CLK_EDGE  ("SAME_EDGE"),  
    .INIT          (1'b0),         
    .SRTYPE        ("SYNC")         
) ODDR_inst (
    .Q             (rgmii_tx_ctl), 
    .C             (gmii_tx_clk),  
    .CE            (1'b1),         
    .D1            (gmii_tx_en),   
    .D2            (gmii_tx_en),   
    .R             (1'b0),         
    .S             (1'b0)          
); 

genvar i;
generate for (i=0; i<4; i=i+1)
    begin : txdata_bus
        //输出双沿采样寄存器 (rgmii_txd)
        ODDR #(
            .DDR_CLK_EDGE  ("SAME_EDGE"),  
            .INIT          (1'b0),         
            .SRTYPE        ("SYNC")         
        ) ODDR_inst (
            .Q             (rgmii_txd[i]), 
            .C             (gmii_tx_clk),  
            .CE            (1'b1),         
            .D1            (gmii_txd[i]),  
            .D2            (gmii_txd[4+i]),
            .R             (1'b0),         
            .S             (1'b0)          
        );        
    end
endgenerate

endmodule

 RGMII 和 GMII 相互转换模块

         只需要实例化上述两个功能模块以及确定 IDELAY_VALUE 即可。

module gmii_to_rgmii(
    input              idelay_clk  , //IDELAY参考时钟,是由PLL输出的
    //以太网GMII接口,发送端和接收到都省略了err信号
    output             gmii_rx_clk , //GMII接收时钟
    output             gmii_rx_dv  , //GMII接收数据有效信号
    output      [7:0]  gmii_rxd    , //GMII接收数据
    
    output             gmii_tx_clk , //GMII发送时钟
    input              gmii_tx_en  , //GMII发送数据使能信号
    input       [7:0]  gmii_txd    , //GMII发送数据            
    //以太网RGMII接口   
    input              rgmii_rxc   , //RGMII接收时钟
    input              rgmii_rx_ctl, //RGMII接收数据控制信号
    input       [3:0]  rgmii_rxd   , //RGMII接收数据
    output             rgmii_txc   , //RGMII发送时钟    
    output             rgmii_tx_ctl, //RGMII发送数据控制信号
    output      [3:0]  rgmii_txd     //RGMII发送数据          
    );

//parameter define
parameter IDELAY_VALUE = 0;  //输入数据IO延时(如果为n,表示延时n*78ps),这里取0是因为使用的PHY芯片自带延迟。如果是别的不带延迟的PHY芯片就需要在这里做延迟

//*****************************************************
//**                    main code
//*****************************************************

assign gmii_tx_clk = gmii_rx_clk;

//RGMII接收
rgmii_rx 
    #(
     .IDELAY_VALUE  (IDELAY_VALUE)
     )
    u_rgmii_rx(
    .idelay_clk    (idelay_clk),    
    .gmii_rx_clk   (gmii_rx_clk),
    .rgmii_rxc     (rgmii_rxc   ),
    .rgmii_rx_ctl  (rgmii_rx_ctl),
    .rgmii_rxd     (rgmii_rxd   ),
    
    .gmii_rx_dv    (gmii_rx_dv ),
    .gmii_rxd      (gmii_rxd   )
    );

//RGMII发送
rgmii_tx u_rgmii_tx(
    .gmii_tx_clk   (gmii_tx_clk ),
    .gmii_tx_en    (gmii_tx_en  ),
    .gmii_txd      (gmii_txd    ),
              
    .rgmii_txc     (rgmii_txc   ),
    .rgmii_tx_ctl  (rgmii_tx_ctl),
    .rgmii_txd     (rgmii_txd   )
    );

endmodule

接口信号时序 

ARP 模块

        设计框图如下: 

        如何确定这些信号的? 

        因为这些数据是从 FPGA 内部而来,所以采取 GMII 接口的形式。而 ARP 模块既要接收从转换模块传输来的 GMII 接口数据,又要向转换模块发送 GMII 接口数据。所以 ARP 需要分为接收模块和发送模块,然后加上一个发送模块需要的 CRC 校验模块。

        首先考虑接收模块,需要输入信号有:gmii_rxc、gmii_rx_dv、gmii_rxd [7:0]、rst_n。

        然后考虑 CRC 校验模块(只对以太网帧头和数据部分进行校验)和发送模块,需要输入信号有:gmii_txc、arp_tx_en、arp_tx_type、des_mac、des_ip、rst_n。由于以太网帧有4Byte校验位,所以输入到的数据还需要传入到 CRC 校验模块中生成校验数据。所以 CRC 校验模块需要的输入信号为:gmii_txc、rst_n、crc_clr、crc_en、data [7:0]。其中 crc_en 用于控制CRC校验的开始。当该信号为高电平时,CRC校验模块开始对输入数据进行校验。 而 crc_clr 用于复位CRC校验模块的内部数据。当该信号为高电平时,CRC校验模块的内部数据会被重新置为初始值(一般是全1)。

        ARP 模块按照具体功能划分为:ARP 接收端、ARP 发送端、CRC 模块。

ARP 接收端

理论解释 

        输入信号为:gmii_rxc、gmii_rx_dv、gmii_rxd [7:0]、rst_n。

        输出信号为:arp_rx_done(接收完成标志)、arp_rx_type(接收到的类型:请求/应答)、src_ip [31:0]、src_mac [47:0]。

        以太网帧格式如下(详细解释可看:FPGA学习笔记——以太网-优快云博客):

        ARP 协议格式如下:

         根据以太网帧和 ARP 数据包的格式,我们可以将 ARP 接收端的工作划分为五个状态:空闲态、前导码+SFD解析状态、以太网帧头解析状态、ARP 数据包解析状态、接收结束状态。并以此来构建状态机。

         

模块时序 

         当 gmii_rx_dv 为高时,表示 rgmii_to_gmii 模块接收的信号有效,可以开始解析。

 代码实现
module arp_rx
  #(
    //开发板MAC地址 00-11-22-33-44-55
    parameter BOARD_MAC = 48'h00_11_22_33_44_55,  
    //开发板IP地址 192.168.1.10   
    parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10}      
    )
   (
    input                clk        ,   // 时钟信号
    input                rst_n      ,   // 复位信号,低电平有效                            
    input                gmii_rx_dv ,   // GMII输入数据有效信号
    input        [7:0]   gmii_rxd   ,   // GMII输入数据
    
    output  reg          arp_rx_done,   // ARP接收完成信号
    output  reg          arp_rx_type,   // ARP接收类型 0:请求  1:应答
    output  reg  [47:0]  src_mac    ,   // 接收到的源MAC地址
    output  reg  [31:0]  src_ip         // 接收到的源IP地址
    );

// 状态机代码
localparam  st_idle     = 5'b0_0001;    // 初始状态,等待接收前导码
localparam  st_preamble = 5'b0_0010;    // 接收前导码状态 
localparam  st_eth_head = 5'b0_0100;    // 接收以太网帧头
localparam  st_arp_data = 5'b0_1000;    // 接收ARP数据
localparam  st_rx_end   = 5'b1_0000;    // 接收结束 

localparam  ETH_TPYE    = 16'h0806;     // 以太网帧类型 ARP


reg    [4:0]   cur_state ;      // 当前状态
reg    [4:0]   next_state;      // 下一个状态                     
reg            skip_en   ;      // 控制状态跳转使能信号
reg            error_en  ;      // 解析错误使能信号
reg    [4:0]   cnt       ;      // 解析数据计数器
reg    [47:0]  des_mac_t ;      // 接收到的目的MAC地址
reg    [31:0]  des_ip_t  ;      // 接收到的目的IP地址
reg    [47:0]  src_mac_t ;      // 接收到的源MAC地址
reg    [31:0]  src_ip_t  ;      // 接收到的源IP地址
reg    [15:0]  eth_type  ;      // 以太网类型
reg    [15:0]  op_data   ;      // 操作码


always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cur_state <= st_idle;  
    else
        cur_state <= next_state;
end

//组合逻辑判断状态转移条件
always @(*) begin
    next_state = st_idle;
    
    case(cur_state)
        
        // 空闲阶段
        st_idle : begin                     
            if(skip_en) 
                next_state = st_preamble;   
            else
                next_state = st_idle;    
        end
        
        // 接收前导码
        st_preamble : begin                 
            if(skip_en) 
                next_state = st_eth_head;
            else if(error_en) 
                next_state = st_rx_end;    
            else
                next_state = st_preamble;   
        end
        
        // 接收以太网帧头
        st_eth_head : begin                 
            if(skip_en) 
                next_state = st_arp_data;
            else if(error_en) 
                next_state = st_rx_end;
            else
                next_state = st_eth_head;   
        end  
        
        // 接收ARP数据
        st_arp_data : begin                  
            if(skip_en)
                next_state = st_rx_end;
            else if(error_en)
                next_state = st_rx_end;
            else
                next_state = st_arp_data;   
        end  
        
        // 接收结束
        st_rx_end : begin                   
            if(skip_en)
                next_state = st_idle;
			//else if(gmii_rx_dv == 1'b0) 
            //    next_state = st_idle;
            else
                next_state = st_rx_end;          
        end
        
        default : next_state = st_idle;
        
    endcase                                          
end    

// 状态机各个状态下的操作
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        skip_en <= 1'b0;
        error_en <= 1'b0;
        cnt <= 5'd0;
        des_mac_t <= 48'd0;
        des_ip_t <= 32'd0;
        src_mac_t <= 48'd0;
        src_ip_t <= 32'd0;        
        eth_type <= 16'd0;
        op_data <= 16'd0;
        arp_rx_done <= 1'b0;
        arp_rx_type <= 1'b0;
        src_mac <= 48'd0;
        src_ip <= 32'd0;
    end
    else begin
    
        skip_en <= 1'b0;
        error_en <= 1'b0;  
        arp_rx_done <= 1'b0;
        
        case(next_state)
        
            // 空闲阶段
            st_idle : begin                                  
                if((gmii_rx_dv == 1'b1) && (gmii_rxd == 8'h55)) //检测到第一个8'h55
                    skip_en <= 1'b1;
				else;
            end
            
            // 解析前导码
            st_preamble : begin
                if(gmii_rx_dv) begin                         
                    cnt <= cnt + 5'd1;
                    if((cnt < 5'd6) && (gmii_rxd != 8'h55))  // 还有6个8'h55  
                        error_en <= 1'b1;
                    else if(cnt == 5'd6) begin
                        cnt <= 5'd0;
                        if(gmii_rxd==8'hd5)                  // 1个8'hd5
                            skip_en <= 1'b1;
                        else
                            error_en <= 1'b1;    
                    end
					else;
                end 
				else;
            end
            
            // 解析以太网帧头
            st_eth_head : begin
                if(gmii_rx_dv) begin
                    cnt <= cnt + 5'b1;
                    if(cnt < 5'd6) 
                        des_mac_t <= {des_mac_t[39:0], gmii_rxd};
                    else if(cnt == 5'd6) begin
                        if((des_mac_t != BOARD_MAC) //判断MAC地址是否为开发板MAC地址或者公共地址
                            && (des_mac_t != 48'hff_ff_ff_ff_ff_ff))           
                            error_en <= 1'b1;
                    end
                    else if(cnt == 5'd12) 
                        eth_type[15:8] <= gmii_rxd;          //以太网协议类型
                    else if(cnt == 5'd13) begin
                        eth_type[7:0] <= gmii_rxd;
                        cnt <= 5'd0;
                        if(eth_type[15:8] == ETH_TPYE[15:8]  //判断是否为ARP协议
                            && gmii_rxd == ETH_TPYE[7:0])
                            skip_en <= 1'b1; 
                        else
                            error_en <= 1'b1;                       
                    end 
					else;
                end  
            end
            
            // 解析ARP数据
            st_arp_data : begin
                if(gmii_rx_dv) begin
                    cnt <= cnt + 5'd1;
                    if(cnt == 5'd6) 
                        op_data[15:8] <= gmii_rxd;           //操作码       
                    else if(cnt == 5'd7)
                        op_data[7:0] <= gmii_rxd;
                    else if(cnt >= 5'd8 && cnt < 5'd14)      
                        src_mac_t <= {src_mac_t[39:0],gmii_rxd};    // 源MAC地址
                    else if(cnt >= 5'd14 && cnt < 5'd18)     
                        src_ip_t<= {src_ip_t[23:0],gmii_rxd};       // 源IP地址
                    else if(cnt >= 5'd24 && cnt < 5'd28)     
                        des_ip_t <= {des_ip_t[23:0],gmii_rxd};      // 目标IP地址
                    else if(cnt == 5'd28) begin
                        cnt <= 5'd0;    // ARP 解析完成
                        if(des_ip_t == BOARD_IP) begin       //判断目的IP地址和操作码
                            if((op_data == 16'd1) || (op_data == 16'd2)) begin
                                skip_en <= 1'b1;
                                arp_rx_done <= 1'b1;
                                src_mac <= src_mac_t;
                                src_ip <= src_ip_t;
                                src_mac_t <= 48'd0;
                                src_ip_t <= 32'd0;
                                des_mac_t <= 48'd0;
                                des_ip_t <= 32'd0;
                                if(op_data == 16'd1)         
                                    arp_rx_type <= 1'b0;     //ARP请求
                                else
                                    arp_rx_type <= 1'b1;     //ARP应答
                            end
                            else
                                error_en <= 1'b1;
                        end 
                        else
                            error_en <= 1'b1;
                    end
					else;
                end                                
            end
            
            st_rx_end : begin     
                cnt <= 5'd0;
                //单包数据接收完成   
                if(gmii_rx_dv == 1'b0 && skip_en == 1'b0)
                    skip_en <= 1'b1; 
				else;
            end    
            default : ;
        endcase                                                        
    end
end

endmodule

 ARP 发送端

 理论解释

输入含义输出含义
crc_data[31:0]校验位数据gmii_tx_enGMII发送使能
rst_n复位gmii_txd[7:0]GMII发送数据
crc_next[31:0]CRC 下次校验完成数据crc_enCRC 开始校验使能信号
gmii_txc发送时钟crc_clrCRC 数据复位信号
arp_tx_enARP发送使能tx_done以太网发送完成信号
arp_tx_type发送的ARP类型
des_mac目的MAC地址
des_ip目的IP地址

模块时序

代码实现

        以下是参数定义部分:

module arp_tx( 
    input                clk        , // 时钟信号
    input                rst_n      , // 复位信号,低电平有效
                                         
    input                arp_tx_en  , // ARP发送使能信号
    input                arp_tx_type, // ARP发送类型 0:请求  1:应答
    input        [47:0]  des_mac    , // 发送的目标MAC地址
    input        [31:0]  des_ip     , // 发送的目标IP地址
    input        [31:0]  crc_data   , // CRC校验数据
    input         [7:0]  crc_next   , // CRC下次校验完成数据
                                         
    output  reg          tx_done    , // 以太网发送完成信号
    output  reg          gmii_tx_en , // GMII输出数据有效信号
    output  reg  [7:0]   gmii_txd   , // GMII输出数据
    output  reg          crc_en     , // CRC开始校验使能
    output  reg          crc_clr      // CRC数据复位信号  
    );                                   

//开发板MAC地址 00-11-22-33-44-55
parameter BOARD_MAC = 48'h00_11_22_33_44_55;
//开发板IP地址 192.168.1.10
parameter BOARD_IP  = {8'd192,8'd168,8'd1,8'd10}; 
//目的MAC地址 ff_ff_ff_ff_ff_ff
parameter  DES_MAC   = 48'hff_ff_ff_ff_ff_ff;   // 作为请求包使用这个MAC地址,作为应答包使用接口传过来的MAC地址
//目的IP地址 192.168.1.102     
parameter  DES_IP    = {8'd192,8'd168,8'd1,8'd102};

// 状态机定义
localparam  st_idle      = 5'b0_0001;   // 初始状态,等待开始发送信号
localparam  st_preamble  = 5'b0_0010;   // 发送前导码+帧起始界定符
localparam  st_eth_head  = 5'b0_0100;   // 发送以太网帧头
localparam  st_arp_data  = 5'b0_1000;   // 发送ARP数据
localparam  st_crc       = 5'b1_0000;   // 发送CRC校验值


localparam  ETH_TYPE     = 16'h0806 ;   // 以太网帧类型 ARP协议
localparam  HD_TYPE      = 16'h0001 ;   // 硬件类型 以太网
localparam  PROTOCOL_TYPE= 16'h0800 ;   // 上层协议为IP协议

//以太网数据最小为46个字节,不足部分填充数据
localparam  MIN_DATA_NUM = 16'd46   ;    


reg  [4:0]  cur_state     ; 
reg  [4:0]  next_state    ;                    
reg  [7:0]  preamble[7:0] ;     // 前导码+SFD
reg  [7:0]  eth_head[13:0];     // 以太网首部
reg  [7:0]  arp_data[27:0];     // ARP数据      
                     
reg         tx_en_d0      ;     // arp_tx_en信号延时
reg         tx_en_d1      ;        
reg			tx_en_d2	  ;        

reg         skip_en       ;     // 控制状态跳转使能信号
reg  [5:0]  cnt           ;        
reg  [4:0]  data_cnt      ;     // 发送数据个数计数器
reg         tx_done_t     ;        
                                    
                    
wire        pos_tx_en     ;     // arp_tx_en信号上升沿

         以下代码对 arp_tx_en 信号进行处理:

assign  pos_tx_en = (~tx_en_d2) & tx_en_d1;
                           
//对arp_tx_en信号延时打拍两次,用于采arp_tx_en的上升沿
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        tx_en_d0 <= 1'b0;
        tx_en_d1 <= 1'b0;
		tx_en_d2 <= 1'b0;
    end    
    else begin
        tx_en_d0 <= arp_tx_en;
        tx_en_d1 <= tx_en_d0;
		tx_en_d2 <= tx_en_d1;
    end
end 

        这里解释一下为什么要通过对 arp_tx_en 信号延时打拍两次的方式来采集 arp_tx_en 的上升沿。

        因为 arp_tx_en 是一个异步信号,它的变化可能不受当前时钟边沿的限制。直接对异步信号进行采样会导致亚稳态(当异步信号在时钟边沿附近变化时,采样结果处于不确定状态)和采样不准确(在高速时钟下,异步信号的毛刺或抖动可能被错误地采样为有效信号)的后果。

        通过寄存器链(tx_en_d0, tx_en_d1, tx_en_d2)可以将异步信号同步到当前时钟域中,可以显著降低亚稳态发生的概率,并提高系统的可靠性。

        

        状态机的跳转代码:

//(三段式状态机)同步时序描述状态转移
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cur_state <= st_idle;  
    else
        cur_state <= next_state;
end

//组合逻辑判断状态转移条件
always @(*) begin
    next_state = st_idle;
    case(cur_state)
    
        // 空闲状态
        st_idle : begin                     
            if(skip_en)                
                next_state = st_preamble;
            else
                next_state = st_idle;
        end                          
        
        // 发送前导码+帧起始界定符
        st_preamble : begin                 
            if(skip_en)
                next_state = st_eth_head;
            else
                next_state = st_preamble;      
        end
        
        // 发送以太网首部
        st_eth_head : begin                 
            if(skip_en)
                next_state = st_arp_data;
            else
                next_state = st_eth_head;      
        end
        
        // 发送ARP数据 
        st_arp_data : begin                                      
            if(skip_en)
                next_state = st_crc;
            else
                next_state = st_arp_data;      
        end
        
        // 发送CRC校验值
        st_crc: begin                       
            if(skip_en)
                next_state = st_idle;
            else
                next_state = st_crc;      
        end
        
        default : next_state = st_idle;
        
    endcase
end  

        状态机在每个状态下的动作:

//时序电路描述状态输出,发送以太网数据
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        skip_en <= 1'b0; 
        cnt <= 6'd0;
        data_cnt <= 5'd0;
        crc_en <= 1'b0;
        gmii_tx_en <= 1'b0;
        gmii_txd <= 8'd0;
        tx_done_t <= 1'b0; 
        
        //初始化数组    
        //前导码 7个8'h55 + 1个8'hd5 
        preamble[0] <= 8'h55;                
        preamble[1] <= 8'h55;
        preamble[2] <= 8'h55;
        preamble[3] <= 8'h55;
        preamble[4] <= 8'h55;
        preamble[5] <= 8'h55;
        preamble[6] <= 8'h55;
        preamble[7] <= 8'hd5;
        //以太网帧头 
        eth_head[0] <= DES_MAC[47:40];      //目的MAC地址
        eth_head[1] <= DES_MAC[39:32];
        eth_head[2] <= DES_MAC[31:24];
        eth_head[3] <= DES_MAC[23:16];
        eth_head[4] <= DES_MAC[15:8];
        eth_head[5] <= DES_MAC[7:0];        
        eth_head[6] <= BOARD_MAC[47:40];    //源MAC地址
        eth_head[7] <= BOARD_MAC[39:32];    
        eth_head[8] <= BOARD_MAC[31:24];    
        eth_head[9] <= BOARD_MAC[23:16];    
        eth_head[10] <= BOARD_MAC[15:8];    
        eth_head[11] <= BOARD_MAC[7:0];     
        eth_head[12] <= ETH_TYPE[15:8];     //以太网帧类型
        eth_head[13] <= ETH_TYPE[7:0];      
        //ARP数据                           
        arp_data[0] <= HD_TYPE[15:8];       //硬件类型
        arp_data[1] <= HD_TYPE[7:0];
        arp_data[2] <= PROTOCOL_TYPE[15:8]; //上层协议类型
        arp_data[3] <= PROTOCOL_TYPE[7:0];
        arp_data[4] <= 8'h06;               //硬件地址长度,6
        arp_data[5] <= 8'h04;               //协议地址长度,4
        arp_data[6] <= 8'h00;               //OP,操作码 8'h01:ARP请求 8'h02:ARP应答
        arp_data[7] <= 8'h01;
        arp_data[8] <= BOARD_MAC[47:40];    //发送端(源)MAC地址
        arp_data[9] <= BOARD_MAC[39:32];
        arp_data[10] <= BOARD_MAC[31:24];
        arp_data[11] <= BOARD_MAC[23:16];
        arp_data[12] <= BOARD_MAC[15:8];
        arp_data[13] <= BOARD_MAC[7:0];
        arp_data[14] <= BOARD_IP[31:24];    //发送端(源)IP地址
        arp_data[15] <= BOARD_IP[23:16];
        arp_data[16] <= BOARD_IP[15:8];
        arp_data[17] <= BOARD_IP[7:0];
        arp_data[18] <= DES_MAC[47:40];     //接收端(目的)MAC地址
        arp_data[19] <= DES_MAC[39:32];
        arp_data[20] <= DES_MAC[31:24];
        arp_data[21] <= DES_MAC[23:16];
        arp_data[22] <= DES_MAC[15:8];
        arp_data[23] <= DES_MAC[7:0];  
        arp_data[24] <= DES_IP[31:24];      //接收端(目的)IP地址
        arp_data[25] <= DES_IP[23:16];
        arp_data[26] <= DES_IP[15:8];
        arp_data[27] <= DES_IP[7:0];
    end
    
    else begin
        skip_en <= 1'b0;
        crc_en <= 1'b0;
        gmii_tx_en <= 1'b0;
        tx_done_t <= 1'b0;
        
        case(next_state)
            
            // 空闲阶段的动作
            st_idle : begin
                if(pos_tx_en) begin
                    skip_en <= 1'b1;  
                    //如果目标MAC地址和IP地址已经更新,则发送正确的地址
                    if((des_mac != 48'b0) || (des_ip != 32'd0)) begin
                        eth_head[0] <= des_mac[47:40];
                        eth_head[1] <= des_mac[39:32];
                        eth_head[2] <= des_mac[31:24];
                        eth_head[3] <= des_mac[23:16];
                        eth_head[4] <= des_mac[15:8];
                        eth_head[5] <= des_mac[7:0];  
                        arp_data[18] <= des_mac[47:40];
                        arp_data[19] <= des_mac[39:32];
                        arp_data[20] <= des_mac[31:24];
                        arp_data[21] <= des_mac[23:16];
                        arp_data[22] <= des_mac[15:8];
                        arp_data[23] <= des_mac[7:0];  
                        arp_data[24] <= des_ip[31:24];
                        arp_data[25] <= des_ip[23:16];
                        arp_data[26] <= des_ip[15:8];
                        arp_data[27] <= des_ip[7:0];
                    end
					else;
                    if(arp_tx_type == 1'b0)
                        arp_data[7] <= 8'h01;            //ARP请求 
                    else 
                        arp_data[7] <= 8'h02;            //ARP应答
                end 
				else;
            end                                                                   
            st_preamble : begin                          //发送前导码+帧起始界定符
                gmii_tx_en <= 1'b1;
                gmii_txd <= preamble[cnt];
                if(cnt == 6'd7) begin                        
                    skip_en <= 1'b1;
                    cnt <= 1'b0;    
                end
                else    
                    cnt <= cnt + 1'b1;                     
            end
            st_eth_head : begin                          //发送以太网首部
                gmii_tx_en <= 1'b1;
                crc_en <= 1'b1;
                gmii_txd <= eth_head[cnt];
                if (cnt == 6'd13) begin
                    skip_en <= 1'b1;
                    cnt <= 1'b0;
                end    
                else    
                    cnt <= cnt + 1'b1;    
            end                    
            st_arp_data : begin                          //发送ARP数据  
                crc_en <= 1'b1;
                gmii_tx_en <= 1'b1;
                //至少发送46个字节
                if (cnt == MIN_DATA_NUM - 1'b1) begin    
                    skip_en <= 1'b1;
                    cnt <= 1'b0;
                    data_cnt <= 1'b0;
                end    
                else    
                    cnt <= cnt + 1'b1;  
                if(data_cnt <= 6'd27) begin
                    data_cnt <= data_cnt + 1'b1;
                    gmii_txd <= arp_data[data_cnt];
                end    
                else
                    gmii_txd <= 8'd0;                    //Padding,填充0
            end
            st_crc      : begin                          //发送CRC校验值
                gmii_tx_en <= 1'b1;
                cnt <= cnt + 1'b1;
                if(cnt == 6'd0)
                    gmii_txd <= {~crc_next[0], ~crc_next[1], ~crc_next[2],~crc_next[3],
                                 ~crc_next[4], ~crc_next[5], ~crc_next[6],~crc_next[7]};
                else if(cnt == 6'd1)
                    gmii_txd <= {~crc_data[16], ~crc_data[17], ~crc_data[18],
                                 ~crc_data[19], ~crc_data[20], ~crc_data[21], 
                                 ~crc_data[22],~crc_data[23]};
                else if(cnt == 6'd2) begin
                    gmii_txd <= {~crc_data[8], ~crc_data[9], ~crc_data[10],
                                 ~crc_data[11],~crc_data[12], ~crc_data[13], 
                                 ~crc_data[14],~crc_data[15]};                              
                end
                else if(cnt == 6'd3) begin
                    gmii_txd <= {~crc_data[0], ~crc_data[1], ~crc_data[2],~crc_data[3],
                                 ~crc_data[4], ~crc_data[5], ~crc_data[6],~crc_data[7]};  
                    tx_done_t <= 1'b1;
                    skip_en <= 1'b1;
                    cnt <= 1'b0;
                end   
				else;
            end                          
            default :;  
        endcase                                             
    end
end 

        状态机完成后的动态:

//发送完成信号及crc值复位信号
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        tx_done <= 1'b0;
        crc_clr <= 1'b0;
    end
    else begin
        tx_done <= tx_done_t;
        crc_clr <= tx_done_t;
    end
end

 CRC 模块

        CRC 模块由开源网站生成,因为不是本实验的重点内容,所以不过多描述。

接口信号时序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值