基于FPGA的UDP回环实验(二)----ARP模块

1.首先来介绍以下 ARP 协议原理与报文格式

1.1 ARP 协议的作用

  • ARP 请求(ARP Request) :以广播形式发送,询问"谁的 IP 是 192.168.1.10,请把你的 MAC 地址告诉我"。

  • ARP 应答(ARP Reply) :以单播形式回复,"我是 192.168.1.10,我的 MAC 地址是 XX:XX:XX:XX:XX:XX"。

在这个实验回环项目中,ARP模块主要有两部分(接受和发送)组成:

  1. arp_rx(接收模块):解析输入的 GMII 接收数据,校验目的 IP 是否匹配本机 IP。如果是 ARP 请求,触发发送模块回复;如果是 ARP 应答,更新目标 MAC 寄存器。

  2. arp_tx(发送模块):根据触发信号(回复应答或发送请求),按照 ARP 报文格式封装数据并输出到 GMII 总线(或交给 MAC 层整合)。

  3. arp(顶层模块):例化 RX 和 TX,控制仲裁与状态信号传递。

  4. crc(校验模块),用于crc字节数据的计算(单独写的)。

1.2 ARP 报文格式

ARP 报文嵌在以太网帧(Ethernet II 格式)的数据 payload 中:

|------------|---------------|---------|-------------------------------------|
| 字节偏移 | 字段名称 | 字节数 | 描述 / 固定值 |
| 以太网首部 | 目的 MAC 地址 | 6 Byte | 广播时为 FF:FF:FF:FF:FF:FF,单播时为目标 MAC |
| | 源 MAC 地址 | 6 Byte | 本机 MAC 地址 |
| | 帧类型 (Type) | 2 Byte | 16'h0806(代表 ARP 协议) |
| ARP 净荷 | 硬件类型 | 2 Byte | 16'h0001(代表以太网) |
| | 协议类型 | 2 Byte | 16'h0800(代表 IPv4) |
| | 硬件地址长度 | 1 Byte | 8'h06(MAC 地址长 6 字节) |
| | 协议地址长度 | 1 Byte | 8'h04(IP 地址长 4 字节) |
| | 操作码 (OP Code) | 2 Byte | 16'h0001(请求) / 16'h0002(应答) |
| | 发送方 MAC | 6 Byte | 发送者的 MAC |
| | 发送方 IP | 4 Byte | 发送者的 IP |
| | 接收方 MAC | 6 Byte | 请求时填 00:00:00:00:00:00,应答时填请求方的 MAC |
| | 接收方 IP | 4 Byte | 接收者的 IP |
| 填充位 | Padding | 18 Byte | 补齐至以太网最小帧长(数据区至少 46 字节,总帧 60/64 字节) |

接下来通过代码来介绍ARP模块的原理,以arp_rx.v为例:

首先是接口的定义,主要包含三个部分


第一部分:模块的引脚与参数定义

1. 参数定义 (Parameters)
  • BOARD_MAC:开发板的本地 MAC 地址(默认 00:11:22:33:44:55),用于比对目的 MAC 地址。

  • BOARD_IP:开发板的本地 IP 地址(默认 192.168.1.10),用于比对目的 IP 地址。

  • ETH_TPYE :以太网帧类型常量 16'h0806,代表该数据包是 ARP 协议

2. 引脚定义 (Ports)

|-----------------|--------|----|------------------------------------------------|
| 引脚名称 | I/O 类型 | 位宽 | 功能描述 |
| clk | Input | 1 | 系统时钟/GMII 接收时钟(通常为 125MHz) |
| rst_n | Input | 1 | 系统异步复位信号(低电平有效) |
| gmii_rx_dv | Input | 1 | GMII 接收数据有效信号(高电平时代表 gmii_rxd 上的字节有效) |
| gmii_rxd | Input | 8 | GMII 接收到的 8 位并行数据总线 |
| arp_rx_done | Output | 1 | ARP 接收并解析完成脉冲信号(高电平维持 1 个周期) |
| arp_rx_type | Output | 1 | 解析出的 ARP 类型(0: ARP 请求 request;1: ARP 应答 reply) |
| src_mac | Output | 48 | 解析出的发送方(源)MAC 地址 |
| src_ip | Output | 32 | 解析出的发送方(源)IP 地址 |

3. 核心内部寄存器 (Internal Registers)
  • cur_state / next_state:当前状态与下一状态寄存器(使用独热码 5-bit 编码)。

  • skip_en:状态正常跳转使能信号。当某个状态顺利解析完指定字节后置 1 1 个周期,通知状态机跳转到下一状态。

  • error_en:解析错误使能信号。当校验 MAC、IP 或协议类型失败时置 1,强行跳入错误/结束状态。

  • cnt:多功能计数器,用于在各个状态中对接收的字节进行计数。

  • des_mac_t / src_mac_t:目的/源 MAC 地址临时移位拼接寄存器。

  • des_ip_t / src_ip_t:目的/源 IP 地址临时移位拼接寄存器。

  • eth_type / op_data:以太网类型与 ARP 操作码拼接寄存器。

    //arp_rx.v
    module arp_rx
    #(
    parameter BOARD_MAC = 48'h00_11_22_33_44_55,
    parameter BOARD_IP = {8'd192,8'd168,8'd1,8'd10}
    )
    (
    input clk , //时钟信号
    input rst_n , //复位信号,低电平有效

    复制代码
      input                gmii_rx_dv , //GMII输入数据有效信号
      input        [7:0]   gmii_rxd   , //GMII输入数据
      output  reg          arp_rx_done, //ARP接收完成信号
      output  reg          arp_rx_type, //ARP接收类型 0:请求  1:应答
      output  reg  [47:0]  src_mac    , //接收到的源MAC地址
      output  reg  [31:0]  src_ip       //接收到的源IP地址
      );

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

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

    //reg define
    reg [4:0] cur_state ;
    reg [4:0] next_state;

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

第二部分:状态机的跳转和状态机的定义。

复制代码
//状态机:复位从开始状态执行,否则cur_state <= next_state,进入下一个状态
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cur_state <= st_idle;  
    else
        cur_state <= next_state;
end

//组合逻辑
//这里的skip_en 和error_en 可以视作是标志位: 执行成功,跳到下一状态/执行失败,跳出状态,重新循环
always @(*) begin
    next_state = st_idle;
    case(cur_state)
        st_idle : begin                     //等待接收前导码
            if(skip_en) 
                next_state = st_preamble;
            else
                next_state = st_idle;    
        end
        st_preamble : begin                 //接收前导码
            if(skip_en) 
                next_state = st_eth_head;
            else if(error_en) 
                next_state = st_rx_end;    
            else
                next_state = st_preamble;   
        end
        st_eth_head : begin                 //接收以太网帧头
            if(skip_en) 
                next_state = st_arp_data;
            else if(error_en) 
                next_state = st_rx_end;
            else
                next_state = st_eth_head;   
        end  
        st_arp_data : begin                  //接收ARP数据
            if(skip_en)
                next_state = st_rx_end;
            else if(error_en)
                next_state = st_rx_end;
            else
                next_state = st_arp_data;   
        end                  
        st_rx_end : begin                   //接收结束
            if(skip_en)
                next_state = st_idle;
            else
                next_state = st_rx_end;          
        end
        default : next_state = st_idle;
    endcase                                          
end    

第三部分:也是最重要的部分,数据包的解析逻辑。

在这之前我们要重温报文的组成 ,因为本质上数据包的解析就是像剥洋葱一样,确定好外层的正确性之后步步深入。

在刚才介绍的arp报文接收之前,我们还需要发送如下字段来确定硬件的同步:

|----------------------------|--------|--------------------|--------|------------------|--------------------------------|
| 报文层级 | 字节偏移 | 字段名称 | 字节数 | 描述 / 固定值 | FPGA 开发注意要点 |
| 物理层首部<br>(PHY Layer) | 0 ~ 6 | 前导码 (Preamble) | 7 Byte | 7 x 8'h55 | 7 个 01010101 方波,用于 PHY 芯片时钟同步。 |
| | 7 | 帧首定界符 (SFD) | 1 Byte | 1 Byte 8'hD5 | 标志物理层同步结束,下一字节进入以太网帧。 |

然后是我们熟悉的ARP报文的组成:

|------------|----------|-------------------|---------|--------------------------------------------------|-------------------------------|
| 以太网首部 | 8 ~ 13 | 目的 MAC 地址 | 6 Byte | 广播请求时为 FF:FF:FF:FF:FF:FF<br>单播应答时为目标 MAC | 接收时校验是否为本机 MAC 或广播 MAC。 |
| | 14 ~ 19 | 源 MAC 地址 | 6 Byte | 发送者的 MAC 地址 | 接收时提取存入 src_mac。 |
| | 20 ~ 21 | 帧类型 (Type) | 2 Byte | 16'h0806(代表 ARP 协议) | 若非 0x0806(如 IP 包 0x0800),则丢弃。 |
| ARP 净荷 | 22 ~ 23 | 硬件类型 | 2 Byte | 16'h0001(代表以太网) | 固定值。 |
| | 24 ~ 25 | 协议类型 | 2 Byte | 16'h0800(代表 IPv4 协议) | 固定值。 |
| | 26 | 硬件地址长度 | 1 Byte | 8'h06(MAC 地址长度 6 字节) | 固定值。 |
| | 27 | 协议地址长度 | 1 Byte | 8'h04(IP 地址长度 4 字节) | 固定值。 |
| | 28 ~ 29 | 操作码 (OP Code) | 2 Byte | 16'h0001 (ARP 请求)<br>16'h0002 (ARP 应答) | 区分当前是收到"询问"还是"回复"。 |
| | 30 ~ 35 | 发送方 MAC 地址 | 6 Byte | 发送者的 MAC 地址 | 对方的真实 MAC 地址。 |
| | 36 ~ 39 | 发送方 IP 地址 | 4 Byte | 发送者的 IP 地址 | 对方的 IP 地址。 |
| | 40 ~ 45 | 接收方 MAC 地址 | 6 Byte | 请求时填 00:00:00:00:00:00<br>应答时填请求方的 MAC | 接收方 MAC。 |
| | 46 ~ 49 | 接收方 IP 地址 | 4 Byte | 接收者的 IP 地址 | 接收时校验是否与 BOARD_IP 匹配。 |
| 填充位 | 50 ~ 67 | 填充位 (Padding) | 18 Byte | 全 0 填充 (18 x 8'h00) | 补齐以太网最小载荷(46 字节)要求。 |
| 校验和 | 68 ~ 71 | FCS (CRC32) | 4 Byte | 循环冗余校验和 | 部分硬件 PHY/MAC 自动追加/校验。 |

这里我们还要补充一点内容,可以看到以太网和ARP 净荷中都存在两个MAC地址,下面我来区分一下:

假设 PC(192.168.1.100)向 FPGA(192.168.1.10)发起 ARP 请求:

|-----------------|-------------|-----------------------------|---------------------------------------------------------------------------------------------------|
| 位置 | 字段名称 | 填写的 MAC 地址 | 为什么要这样填? |
| 以太网首部 (信封) | 目的 MAC | FF:FF:FF:FF:FF:FF (广播) | 告诉局域网内所有交换机和网卡:"这是一个广播包,需要找到MAC地址。" |
| | 源 MAC | AA:BB:CC:DD:EE:FF (PC的MAC) | 告诉局域网网卡:"这是 PC 发出来的。" |
| ARP 净荷 (信件) | 发送方 MAC | AA:BB:CC:DD:EE:FF (PC的MAC) | 告诉 FPGA 的 ARP 模块:"我是 192.168.1.100,我的 MAC 是这个,请记在你的 ARP 缓存里。" |
| | 接收方 MAC | 00:00:00:00:00:00 (全 0) | 因为 PC 根本不知道 FPGA 的 MAC 是什么,所以这里填全 0(这才是 PC 要问的问题:谁是 192.168.1.10,你的 MAC 是多少?这部分要在arp_tx进行覆写)。 |

现在我们来看代码,解释都附在代码的上部。

复制代码
always @(posedge clk or negedge rst_n) begin

//如果是复位状态,把所有存储变量,标志位变量置为0.

    if(!rst_n) begin
        skip_en <= 1'b0;
        error_en <= 1'b0;
        cnt <= 5'd0;
        des_mac_t <= 48'd0;
        des_ip_t <= 32'd0;
        src_mac_t <= 48'd0;
        src_ip_t <= 32'd0;        
        eth_type <= 16'd0;
        op_data <= 16'd0;
        rx_done_t <= 1'b0;
        arp_rx_type <= 1'b0;
        src_mac <= 48'd0;
        src_ip <= 32'd0;
    end
    else begin
        skip_en <= 1'b0;
        error_en <= 1'b0;  
        rx_done_t <= 1'b0;
        case(next_state)
//输入有效信号为1,同时检测到第一个8'h55
            st_idle : begin                                 
                if((gmii_rx_dv == 1'b1) && (gmii_rxd == 8'h55)) 
                    skip_en <= 1'b1;
            end
//(通过cnt计数)再检测是否后面6个字节是否都为8'h55,然后检测下一个字节是否为8'hd5,都通过将skip_en 置为1,如果任意一个不通过error_en 置为1。cnt清零
            st_preamble : begin
                if(gmii_rx_dv) begin                         //解析前导码
                    cnt <= cnt + 5'd1;
                    if((cnt < 5'd6) && (gmii_rxd != 8'h55)) 
                        error_en <= 1'b1;
                    else if(cnt==5'd6) begin
                        cnt <= 5'd0;
                        if(gmii_rxd==8'hd5)                  
                            skip_en <= 1'b1;
                        else
                            error_en <= 1'b1;    
                    end  
                end  
            end
//首先通过6次移位+拼接,来让6字节(48bit位)des_mac_t全部更新为目的 MAC 地址(8 ~ 13位)
//然后判断MAC地址是否为开发板MAC地址或者公共地址->如果不是给我的板子MAC地址发送(单播)也不是广播的话,error_en 置为1,这里要知道这是PC给FPGA发送的报文。
//这里跳过了14 ~ 19字节因为源MAC地址在以太网帧头和ARP报文内部都出现了,我们选择拿取后面那个,当
//(cnt == 5'd12)读取并存储帧类型(20 ~ 21位),最后校验这个字段是否为ARP协议,如果是,skip_en 置为1,跳转到下个状态。cnt清零
            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)
                            && (des_mac_t != 48'hff_ff_ff_ff_ff_ff))           
                            error_en <= 1'b1;
                    end
                    else if(cnt == 5'd12) 
                        eth_type[15:8] <= gmii_rxd;         
                    else if(cnt == 5'd13) begin
                        eth_type[7:0] <= gmii_rxd;
                        cnt <= 5'd0;
                        if(eth_type[15:8] == ETH_TPYE[15:8]  //判断是否为ARP协议
                            && gmii_rxd == ETH_TPYE[7:0])
                            skip_en <= 1'b1; 
                        else
                            error_en <= 1'b1;                       
                    end        
                end  
            end
//按顺序(跳过了硬件类型等4个字段)分别解析存储数据包中的操作码(28 ~ 29位),发送方 MAC 地址(30 ~ 35位),发送方 IP 地址(36 ~ 39位),判断操作码是请求还是应答,然后把发送者(PC)的MAC和IP地址覆写到src_mac 和src_ip 中作为输出,给其他模块使用
            st_arp_data : begin
                if(gmii_rx_dv) begin
                    cnt <= cnt + 5'd1;
                    if(cnt == 5'd6) 
                        op_data[15:8] <= gmii_rxd;           //操作码       
                    else if(cnt == 5'd7)
                        op_data[7:0] <= gmii_rxd;
                    else if(cnt >= 5'd8 && cnt < 5'd14)      //源MAC地址
                        src_mac_t <= {src_mac_t[39:0],gmii_rxd};
                    else if(cnt >= 5'd14 && cnt < 5'd18)     //源IP地址
                        src_ip_t<= {src_ip_t[23:0],gmii_rxd};
                    else if(cnt >= 5'd24 && cnt < 5'd28)     //目标IP地址
                        des_ip_t <= {des_ip_t[23:0],gmii_rxd};
                    else if(cnt == 5'd28) begin
                        cnt <= 5'd0;
                        if(des_ip_t == BOARD_IP) begin       //判断目的IP地址和操作码
                            if((op_data == 16'd1) || (op_data == 16'd2)) begin
                                skip_en <= 1'b1;
                                rx_done_t <= 1'b1;
                                src_mac <= src_mac_t;
                                src_ip <= src_ip_t;
                                src_mac_t <= 48'd0;
                                src_ip_t <= 32'd0;
                                des_mac_t <= 48'd0;
                                des_ip_t <= 32'd0;
                                if(op_data == 16'd1)         
                                    arp_rx_type <= 1'b0;     //ARP请求
                                else
                                    arp_rx_type <= 1'b1;     //ARP应答
                            end
                            else
                                error_en <= 1'b1;
                        end 
                        else
                            error_en <= 1'b1;
                    end
                end                                
            end
            st_rx_end : begin     
                cnt <= 5'd0;
                //单包数据接收完成   
                if(gmii_rx_dv == 1'b0 && skip_en == 1'b0)
                    skip_en <= 1'b1; 
            end    
            default : ;
        endcase                                                        
    end
end

//输出arp_rx_done信号
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        arp_rx_done <= 1'b0;
    else
        arp_rx_done <= rx_done_t;
end

endmodule

至此接收完毕,下面我们来看发送的代码,理解完接收的代码,发送也是类似的:同样是状态机,同样是在不同的状态进行解包,只不过发送时可能会覆盖新的值然后进行发送。

首先是arp_tx模块的引脚和模块定义:

复制代码
module arp_tx
#(
    parameter BOARD_MAC  = 48'h00_11_22_33_44_55,  
    parameter BOARD_IP   = {8'd192,8'd168,8'd1,8'd10},    
    parameter  DES_MAC   = 48'hff_ff_ff_ff_ff_ff,
    parameter  DES_IP    = {8'd192,8'd168,8'd1,8'd102}
)
( 
    input                clk        , //时钟信号
    input                rst_n      , //复位信号,低电平有效
    
    input                arp_tx_en  , //ARP发送使能信号
    input                arp_tx_type, //ARP发送类型 0:请求  1:应答
    input        [47:0]  des_mac    , //发送的目标MAC地址
    input        [31:0]  des_ip     , //发送的目标IP地址
    input        [31:0]  crc_data   , //CRC校验数据
    input         [7:0]  crc_next   , //CRC下次校验完成数据
    output  reg          tx_done    , //以太网发送完成信号
    output  reg          gmii_tx_en , //GMII输出数据有效信号
    output  reg  [7:0]   gmii_txd   , //GMII输出数据
    output  reg          crc_en     , //CRC开始校验使能
    output  reg          crc_clr      //CRC数据复位信号 
);

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

localparam  ETH_TYPE     = 16'h0806 ; //以太网帧类型 ARP协议
localparam  HD_TYPE      = 16'h0001 ; //硬件类型 以太网
localparam  PROTOCOL_TYPE= 16'h0800 ; //上层协议为IP协议
//以太网数据最小为46个字节,不足部分填充数据
localparam  MIN_DATA_NUM = 16'd46   ;    

//reg define
reg  [4:0]  cur_state     ;
reg  [4:0]  next_state    ;
                          
reg  [7:0]  preamble[7:0] ; //前导码+SFD
reg  [7:0]  eth_head[13:0]; //以太网首部
reg  [7:0]  arp_data[27:0]; //ARP数据
                            
reg         tx_en_d0      ; //arp_tx_en信号延时
reg         tx_en_d1      ; 
reg         skip_en       ; //控制状态跳转使能信号
reg  [5:0]  cnt           ; 
reg  [4:0]  data_cnt      ; //发送数据个数计数器
reg         tx_done_t     ; 
                                
//wire define                   
wire        pos_tx_en     ; //arp_tx_en信号上升沿

然后是同样的状态机代码:

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

//状态机的状态转移
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cur_state <= st_idle;  
    else
        cur_state <= next_state;
end

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

然后是状态机中的逻辑

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

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

最后是crc校验模块的逻辑。crc32_d8.v:

复制代码
module crc32_d8(
    input                clk     ,  // 时钟信号
    input                rst_n   ,  // 复位信号,低电平有效
    input        [7:0]   data    ,  // 输入待校验的8位数据
    input                crc_en  ,  // CRC校验使能信号
    input                crc_clr ,  // CRC寄存器清零信号(初始化)
    output  reg  [31:0]  crc_data,  // 当前CRC校验结果
    output       [31:0]  crc_next   // 下一次CRC校验结果(组合逻辑输出)
    );

// 内部变量定义
wire [7:0] d;
wire [31:0] c;

// 输入数据和当前CRC值的位序调整
assign d = data;
assign c = crc_data;

// CRC32 8位并行查找/计算逻辑 (基于多项式 0x04C11DB7)
// 该逻辑是根据以太网标准算法展开的组合逻辑
assign crc_next[0] = c[24] ^ c[30] ^ d[0] ^ d[6];
assign crc_next[1] = c[24] ^ c[25] ^ c[30] ^ c[31] ^ d[0] ^ d[1] ^ d[6] ^ d[7];
assign crc_next[2] = c[24] ^ c[25] ^ c[26] ^ c[30] ^ c[31] ^ d[0] ^ d[1] ^ d[2] ^ d[6] ^ d[7];
assign crc_next[3] = c[25] ^ c[26] ^ c[27] ^ c[31] ^ d[1] ^ d[2] ^ d[3] ^ d[7];
assign crc_next[4] = c[24] ^ c[26] ^ c[27] ^ c[28] ^ c[30] ^ d[0] ^ d[2] ^ d[3] ^ d[4] ^ d[6];
assign crc_next[5] = c[24] ^ c[25] ^ c[27] ^ c[28] ^ c[29] ^ c[30] ^ c[31] ^ d[0] ^ d[1] ^ d[3] ^ d[4] ^ d[5] ^ d[6] ^ d[7];
assign crc_next[6] = c[25] ^ c[26] ^ c[28] ^ c[29] ^ c[30] ^ c[31] ^ d[1] ^ d[2] ^ d[4] ^ d[5] ^ d[6] ^ d[7];
assign crc_next[7] = c[24] ^ c[26] ^ c[27] ^ c[29] ^ c[31] ^ d[0] ^ d[2] ^ d[3] ^ d[5] ^ d[7];
assign crc_next[8] = c[0] ^ c[24] ^ c[25] ^ c[27] ^ c[28] ^ d[0] ^ d[1] ^ d[3] ^ d[4];
assign crc_next[9] = c[1] ^ c[25] ^ c[26] ^ c[28] ^ c[29] ^ d[1] ^ d[2] ^ d[4] ^ d[5];
assign crc_next[10] = c[2] ^ c[24] ^ c[26] ^ c[27] ^ c[29] ^ d[0] ^ d[2] ^ d[3] ^ d[5];
assign crc_next[11] = c[3] ^ c[24] ^ c[25] ^ c[27] ^ c[28] ^ d[0] ^ d[1] ^ d[3] ^ d[4];
assign crc_next[12] = c[4] ^ c[24] ^ c[25] ^ c[26] ^ c[28] ^ c[29] ^ c[30] ^ d[0] ^ d[1] ^ d[2] ^ d[4] ^ d[5] ^ d[6];
assign crc_next[13] = c[5] ^ c[25] ^ c[26] ^ c[27] ^ c[29] ^ c[30] ^ c[31] ^ d[1] ^ d[2] ^ d[3] ^ d[5] ^ d[6] ^ d[7];
assign crc_next[14] = c[6] ^ c[26] ^ c[27] ^ c[28] ^ c[30] ^ c[31] ^ d[2] ^ d[3] ^ d[4] ^ d[6] ^ d[7];
assign crc_next[15] = c[7] ^ c[27] ^ c[28] ^ c[29] ^ c[31] ^ d[3] ^ d[4] ^ d[5] ^ d[7];
assign crc_next[16] = c[8] ^ c[24] ^ c[28] ^ c[29] ^ d[0] ^ d[4] ^ d[5];
assign crc_next[17] = c[9] ^ c[25] ^ c[29] ^ c[30] ^ d[1] ^ d[5] ^ d[6];
assign crc_next[18] = c[10] ^ c[26] ^ c[30] ^ c[31] ^ d[2] ^ d[6] ^ d[7];
assign crc_next[19] = c[11] ^ c[27] ^ c[31] ^ d[3] ^ d[7];
assign crc_next[20] = c[12] ^ c[28] ^ d[4];
assign crc_next[21] = c[13] ^ c[29] ^ d[5];
assign crc_next[22] = c[14] ^ c[24] ^ d[0];
assign crc_next[23] = c[15] ^ c[24] ^ c[25] ^ c[30] ^ d[0] ^ d[1] ^ d[6];
assign crc_next[24] = c[16] ^ c[25] ^ c[26] ^ c[31] ^ d[1] ^ d[2] ^ d[7];
assign crc_next[25] = c[17] ^ c[26] ^ c[27] ^ d[2] ^ d[3];
assign crc_next[26] = c[18] ^ c[24] ^ c[27] ^ c[28] ^ c[30] ^ d[0] ^ d[3] ^ d[4] ^ d[6];
assign crc_next[27] = c[19] ^ c[25] ^ c[28] ^ c[29] ^ c[31] ^ d[1] ^ d[4] ^ d[5] ^ d[7];
assign crc_next[28] = c[20] ^ c[26] ^ c[29] ^ c[30] ^ d[2] ^ d[5] ^ d[6];
assign crc_next[29] = c[21] ^ c[27] ^ c[30] ^ c[31] ^ d[3] ^ d[6] ^ d[7];
assign crc_next[30] = c[22] ^ c[28] ^ c[31] ^ d[4] ^ d[7];
assign crc_next[31] = c[23] ^ c[29] ^ d[5];

// 寄存器逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        crc_data <= 32'hFFFFFFFF;       // 复位时初始化为全1
    else if (crc_clr)                   // 开始新的校验前清零/预置
        crc_data <= 32'hFFFFFFFF;
    else if (crc_en)                    // 使能时更新校验值
        crc_data <= crc_next;
end

endmodule

最后写一个顶层模块,把刚才的三个模块例化并将接口连接:

复制代码
//arp.v
module arp
#(
    parameter BOARD_MAC  = 48'h00_11_22_33_44_55,  
    parameter BOARD_IP   = {8'd192,8'd168,8'd1,8'd10},    
    parameter  DES_MAC   = 48'hff_ff_ff_ff_ff_ff,
    parameter  DES_IP    = {8'd192,8'd168,8'd1,8'd102}
)
(
    input                rst_n      , //复位信号,低电平有效
    //GMII接口
    input                gmii_rx_clk, //GMII接收数据时钟
    input                gmii_rx_dv , //GMII输入数据有效信号
    input        [7:0]   gmii_rxd   , //GMII输入数据
    input                gmii_tx_clk, //GMII发送数据时钟
    output               gmii_tx_en , //GMII输出数据有效信号
    output       [7:0]   gmii_txd   , //GMII输出数据          

    //用户接口
    output               arp_rx_done, //ARP接收完成信号
    output               arp_rx_type, //ARP接收类型 0:请求  1:应答
    output       [47:0]  src_mac    , //接收到目的MAC地址
    output       [31:0]  src_ip     , //接收到目的IP地址    
    input                arp_tx_en  , //ARP发送使能信号
    input                arp_tx_type, //ARP发送类型 0:请求  1:应答
    input        [47:0]  des_mac    , //发送的目标MAC地址
    input        [31:0]  des_ip     , //发送的目标IP地址
    output               tx_done      //以太网发送完成信号    
    );

//wire define
wire           crc_en  ; //CRC开始校验使能
wire           crc_clr ; //CRC数据复位信号 
wire   [7:0]   crc_d8  ; //输入待校验8位数据
wire   [31:0]  crc_data; //CRC校验数据
wire   [31:0]  crc_next; //CRC下次校验完成数据

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

assign  crc_d8 = gmii_txd;

//ARP接收模块    
arp_rx 
   #(
    .BOARD_MAC       (BOARD_MAC),         //参数例化
    .BOARD_IP        (BOARD_IP )
    )
   u_arp_rx(
    .clk             (gmii_rx_clk),
    .rst_n           (rst_n),

    .gmii_rx_dv      (gmii_rx_dv),
    .gmii_rxd        (gmii_rxd  ),
    .arp_rx_done     (arp_rx_done),
    .arp_rx_type     (arp_rx_type),
    .src_mac         (src_mac    ),
    .src_ip          (src_ip     )
    );                                           

//ARP发送模块
arp_tx
   #(
    .BOARD_MAC       (BOARD_MAC),         //参数例化
    .BOARD_IP        (BOARD_IP ),
    .DES_MAC         (DES_MAC  ),
    .DES_IP          (DES_IP   )
    )
   u_arp_tx(
    .clk             (gmii_tx_clk),
    .rst_n           (rst_n),

    .arp_tx_en       (arp_tx_en ),
    .arp_tx_type     (arp_tx_type),
    .des_mac         (des_mac   ),
    .des_ip          (des_ip    ),
    .crc_data        (crc_data  ),
    .crc_next        (crc_next[31:24]),
    .tx_done         (tx_done   ),
    .gmii_tx_en      (gmii_tx_en),
    .gmii_txd        (gmii_txd  ),
    .crc_en          (crc_en    ),
    .crc_clr         (crc_clr   )
    );     

//以太网发送CRC校验模块
crc32_d8   u_crc32_d8(
    .clk             (gmii_tx_clk),                      
    .rst_n           (rst_n      ),                          
    .data            (crc_d8     ),            
    .crc_en          (crc_en     ),                          
    .crc_clr         (crc_clr    ),                         
    .crc_data        (crc_data   ),                        
    .crc_next        (crc_next   )                         
    );

endmodule

完整的代码将所有的代码块按照顺序放在一起即可。

相关推荐
Light Gao1 小时前
企业级灰度发布技术方案
网络·数据库·oracle
今儿敲了吗1 小时前
CN——数据链路层(下)
网络·笔记
你怎么知道我是队长2 小时前
计算机网络入门指南:从OSI模型到网络设备
网络·计算机网络
K成长日志2 小时前
BLE链路层--比特流处理
网络·物联网·网络协议·蓝牙·iot·ble·无线
怪奇云呼军2 小时前
闪电智能VoiceAgent 如何管理呼入、接听、桥接和挂断状态?
java·前端·网络·数据库·人工智能
x_x-F2 小时前
DMA技术详解:解放CPU的数据搬运工
网络
xiaoxiangsiyan2 小时前
RHCE之2026年全套考点详解
运维·前端·网络·chrome·学习·rhce
腾科IT教育2 小时前
网络安全与网络空间安全:概念、产业与防护建议
网络·安全·web安全·网络安全·网络空间安全
0Kilobyte2 小时前
自建拨号服务器上网
网络