收udp 会校验mac地址吗_UDP/IP硬件协议栈设计(八):封包

c853d949ac77ecb02e3208898c853015.png

前面已经完成UDP/IP硬件协议栈绝大部分设计了,“接下来发包,两转三转、四五六七八转”。

0831f1616eddd4d5525cff8410185d4f.png
“我常跟行家讲,周董是我的偶像”

经过前面的分类解析处理,最后需要发送的数据包只剩下了三类:ARP/ICMP/UDP,而ARP在处理当中已经完成了除了前导码和帧校验序列的封包,只剩padding部分(目前均填0处理),所以本文重点讲述ICMP和UDP帧的封包、以及最后三类帧封装称为完整以太网帧的处理过程,大致流程如下图所示。

c68458a71728ccc6ac6a8af8d55b660a.png
封包发送处理流程

三种协议数据按ARP>ICMP>UDP固定优先级进行仲裁,其中ICMP和UDP除了各自首部之外,均基于IP协议,所以笔者在封包处理时,这两种协议复用同样的逻辑完成MAC首部、IP首部的封装,然后各自完成本身协议首部和数据的封装,最后连同ARP一起复用前导码/帧校验序列逻辑完成完整以太网帧的封装,最后送入物理层中进行发送。

MAC首部

由于ICMP/UDP均采用IP协议,其MAC首部字段是一致的(6字节目的MAC地址、6字节源MAC地址、2字节的类型),而目的MAC地址会由于发送对象的不同而有所区别。

前面ARP表的设计中特意留了MAC/IP地址映射查询的接口,这里就可以用上了。

所以笔者设计的方式为:当ICMP/UDP需要发送时,提供目的IP地址,然后在ARP表中查询该IP地址是否有对应的MAC地址,如果有则取出作为目的MAC地址使用,如果不存在,目前的策略是直接丢弃该帧(当然,完备的方法是再产生ARP请求帧去查询映射关系)。

当查询到后就按大端模式填入MAC首部,这样就完成了MAC首部的封装,如下所示:

    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            mac_header_shift    <=  14*8'h0;
        end
        else if (mac_ip_ack_i) begin  //查询成功后将MAC首部各字段寄存
            mac_header_shift    <=  {ack_mac_i,local_mac_i,16'h0800};      //0800: ipv4
        end
        else if ((state_ns == ICMP_PROC) || (state_ns == UDP_PROC)) begin
            mac_header_shift    <=  {mac_header_shift[13*8-1:0],8'h0};
        end
    end

    assign mac_header           =   mac_header_shift[14*8-1:13*8];
    assign mac_header_valid     =   ((mac_ip_header_cnt >= 6'd1) && (mac_ip_header_cnt <= 6'd14))?1'b1:1'b0;

IP首部

IP首部相较于MAC首部,字段更多些,好在只要实现IPv4,且不实现分片等操作,所以只要获取其首部的20字节的字段就可以了,IP首部字段如下所示:

c4fb70e999b006e8dbfac52c889ee5a5.png
IP首部各字段定义

其中版本首部长度TOS标志片偏移生存时间源IP地址等可以固定,总长度协议目的IP地址等需要根据ICMP/UDP情况来确定,而标识字段需要每发送一帧数据就加一,IP首部校验和可参考校验一文的方法进行计算,这样一来IP首部的20字节可如下所示:

    //ip header info
    assign ip_version       =   4'b0100;    //IPv4
    assign ip_header_length =   4'b0101;    //20(5)
    assign service_type     =   8'h00;      //一般服务
    assign total_length     =   icmp_process?(16'd20 + icmp_length):(udp_process?(16'd20 + 16'd8 + udp_data_length):16'h0);
    assign identification   =   ip_identifier;
    assign ip_flag          =   3'b010;       //010: dont fragment
    assign fragment_offset  =   13'h0;
    assign time_to_live     =   8'h40;      //TTL 64
    assign protocol         =   icmp_process?8'h01:(udp_process?8'h11:8'h0);    //01: icmp;  11:udp
    assign ip_checksum      =   16'h0;
    assign src_ip           =   local_ip_i;
    assign dst_ip           =   icmp_process?icmp_dst_ip:(udp_process?udp_dst_ip:32'h0);

    //ip_identifier 每发一帧就加一
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            ip_identifier   <=  16'h0;
        end
        else if (icmp_process_end || udp_process_end) begin
            ip_identifier   <=  ip_identifier + 16'h1;
        end
    end

    //calc IP header checkusm
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            ip_checksum_clac_32_0   <=  32'h0;
            ip_checksum_clac_32_1   <=  32'h0;
            ip_checksum_clac_32     <=  32'h0;
            ip_checksum_clac_temp   <=  32'h0;
            ip_checksum_clac        <=  16'h0;
        end
        else begin
            ip_checksum_clac_32_0   <=  {ip_version,ip_header_length,service_type} + total_length + identification + {ip_flag,fragment_offset} + {time_to_live,protocol};
            ip_checksum_clac_32_1   <=  16'h0 + src_ip[31:16] + src_ip[15:0] + dst_ip[31:16] + dst_ip[15:0];
            ip_checksum_clac_32     <=  ip_checksum_clac_32_0 + ip_checksum_clac_32_1;
            ip_checksum_clac_temp   <=  ip_checksum_clac_32[31:16] + ip_checksum_clac_32[15:0];
            ip_checksum_clac        <=  ~{{15'h0,ip_checksum_clac_temp[16]}+ip_checksum_clac_temp[15:0]};
        end
    end

    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            ip_header_shift     <=  20*8'h0;
        end
        else if (mac_ip_header_cnt == 6'd12) begin  //完成MAC首部的封装就开始IP首部的封装
            ip_header_shift     <=  {   ip_version,
                                        ip_header_length,
                                        service_type,
                                        total_length,
                                        identification,
                                        ip_flag,
                                        fragment_offset,
                                        time_to_live,
                                        protocol,
                                        ip_checksum_clac,   //采用计算好的checksum
                                        src_ip,
                                        dst_ip
                                    };
        end
        else if (mac_ip_header_cnt > 6'd14) begin
            ip_header_shift     <=  {ip_header_shift[19*8-1:0],8'h0};
        end
    end

    assign ip_header        =   ip_header_shift[20*8-1:19*8];
    assign ip_header_valid  =   ((mac_ip_header_cnt >= 6'd15) && (mac_ip_header_cnt <= 6'd34))?1'b1:1'b0;

ICMP数据

在前文的ICMP处理中已经完成了ICMP首部和数据部分的封装,所以在这个模块中只需要将封装好的ICMP数据部分加在MAC首部、IP首部后面即可获得完整的MAC帧,如下所示:

    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            icmp_data_valid     <=  1'b0;
            icmp_data           <=  8'h0;
        end
        else begin
            icmp_data_valid     <=  icmp_process && (mac_header_valid || ip_header_valid || icmp_data_fifo_rd_ctrl);
            icmp_data           <=  (icmp_process&&mac_header_valid)?mac_header:
                                    (icmp_process&&ip_header_valid)?ip_header:
                                    (icmp_process&&icmp_tx_data_fifo_rd_en_o)?icmp_tx_data_fifo_dout_i:8'h0;
        end
    end

UDP首部

在分类一文中并未讲UDP首部的组成,所以这里熟悉UDP的首部后再进行封包,其首部如下所示:

132eecb0dc3ed1102c34f2846aaa42b7.png
UDP首部

其首部也简单,包括:2字节源端口2字节目的端口2字节UDP长度2字节的校验和

其中,源/目的端口和UDP数据的长度信息已经在前文的UDP发送模块中存入到状态FIFO当中了,注意UDP首部中的长度字段是需要加上首部的长度的,即UDP纯数据长度再加8字节。

重点的来了,UDP首部中的校验和计算,而校验和的具体计算过程可详见检验一文,曾说过UDP首部的校验和是需要加上伪首部UDP首部UDP数据的,而在前文UDP发送模块中已经完成了UDP数据部分的累加计算,所以这里只需构造伪首部和UDP首部进行累加再取反就可以计算得到,源码如下所示:

    //UDP header checksum
    assign udp_length           =   16'd8 + udp_data_length;
    assign udp_pseudo_header    =   {src_ip,dst_ip,8'h0,protocol,udp_length};     //UDP pseudo header include {src_ip,dst_pi,8'h0,protocol,udp_length}

    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            udp_checksum_clac_32_0  <=  32'h0;  //udp伪首部累加值
            udp_checksum_clac_32_1  <=  32'h0;  //udp首部累加值
            udp_checksum_clac_32    <=  32'h0;  //udp校验和计算所需的总累加值
            udp_checksum_clac_temp  <=  32'h0;  //循环累加
            udp_checksum_clac       <=  16'h0;  //udp首部校验和
        end
        else begin
            udp_checksum_clac_32_0  <=  udp_pseudo_header[12*8-1:10*8] + udp_pseudo_header[10*8-1:8*8] + udp_pseudo_header[8*8-1:6*8] + 
                                            udp_pseudo_header[6*8-1:4*8] + udp_pseudo_header[4*8-1:2*8] + udp_pseudo_header[2*8-1:0];
            udp_checksum_clac_32_1  <=  udp_src_port + udp_dst_port + udp_length + 16'h0;
            udp_checksum_clac_32    <=  udp_checksum_clac_32_0 + udp_checksum_clac_32_1 + udp_data_sum;     //udp pseudo header + udp header + udp data
            udp_checksum_clac_temp  <=  udp_checksum_clac_32[31:16] + udp_checksum_clac_32[15:0];
            udp_checksum_clac       <=  ~{{15'h0,udp_checksum_clac_temp[16]} + udp_checksum_clac_temp[15:0]};
        end
    end

这样一来,就完成了UDP首部各字段的获取,接下来就是按照大端模式封装UDP首部了,如下所示:

    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            udp_header_shift        <=  64'h0;
        end
        else if (udp_process && (mac_ip_header_cnt == 6'd34)) begin  //完成MAC首部、IP首部的封装则开始UDP首部
            udp_header_shift        <=  {udp_src_port,udp_dst_port,udp_length,udp_checksum_clac};
        end
        else if (mac_ip_header_cnt > 6'd34) begin
            udp_header_shift        <=  {udp_header_shift[7*8-1:0],8'h0};
        end
    end

    assign udp_header           =   udp_header_shift[8*8-1:7*8];
    assign udp_header_valid     =   ((mac_ip_header_cnt >= 6'd35) && (mac_ip_header_cnt <= 6'd42))?1'b1:1'b0;

UDP数据

前面已经完成了MAC首部、IP首部、UDP首部的封装,而UDP数据部分早已在UDP发送模块中准备好了,所以与ICMP数据部分的封装一样,将UDP的数据加在前面诸多首部的后面即可,如下所示:

    //udp fully frame
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            udp_data_valid      <=  1'b0;
            udp_data            <=  8'h0;
        end
        else begin
            udp_data_valid      <=  udp_process && (mac_header_valid || ip_header_valid || udp_header_valid || udp_data_fifo_rd_ctrl);
            udp_data            <=  (udp_process&&mac_header_valid)?mac_header:
                                    (udp_process&&ip_header_valid)?ip_header:
                                    (udp_process&&udp_header_valid)?udp_header:
                                    (udp_process&&udp_tx_data_fifo_rd_en_o)?udp_tx_data_fifo_dout_i:8'h0;
        end
    end

ARP补零

在ARP处理一文中已经完成了ARP应答帧的封装,但是只有42字节,低于以太网帧最小60字节(除去4字节FCS)的要求,所以在对ARP应答帧发送前先要进行补零操作。

补零,也就是在42字节的ARP报文后面填上18字节的“0”,使之达到以太网帧的传输要求,笔者偷懒起见就直接计数后进行补零,源码如下:

    //padding zero
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            arp_data_cnt        <=  8'b0;
            arp_padding_valid   <=  1'b0;
        end
        else if (state_ns == ARP_PROC) begin
            arp_data_cnt        <=  arp_data_cnt + 8'b1;
            arp_padding_valid   <=  ((arp_data_cnt < 8'd60) && (arp_data_cnt >= 8'd42))?1'b1:1'b0;
        end
        else begin
            arp_data_cnt        <=  8'b0;
            arp_padding_valid   <=  1'b0;
        end
    end

    //arp fully frame
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            arp_data_valid  <=  1'b0;
            arp_data        <=  8'h0;
        end
        else begin
            arp_data_valid  <=  arp_process && (arp_tx_fifo_rd_en_o || arp_padding_valid);
            arp_data        <=  (arp_process && arp_tx_fifo_rd_en_o)?arp_tx_fifo_dout_i[7:0]:8'h0;
        end
    end

前导码和帧校验序列(FCS)

通过前面的操作,已经完成了MAC帧的封装,但是要实际送入物理层中发送,还需要在头部加上8字节的前导码和尾部加上4字节的帧校验序列。

在千兆以太网中,8字节的前导码由7字节前同步码和1字节帧起始标志,其中7字节的前同步码均为0x55,而1字节的帧起始标志为0xd5,笔者采用的方式为对上面得到的MAC帧进行移位寄存操作,当开始发送的时候按字节产生7字节的0x55和1字节的0xd5,如下所示:

    //移位寄存
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            gmii_txd_dv_shift   <=  16'h0;
            gmii_txd_shift      <=  128'h0;
        end
        else begin
            gmii_txd_dv_shift   <=  {gmii_txd_dv_shift[14:0],gmii_txd_dv_i};
            gmii_txd_shift      <=  {gmii_txd_shift[15*8-1:0],gmii_txd_i};
        end
    end

    //preamble 8 bytes {55,55,55,55,55,55,55,d5}
    assign preamble_data_valid_temp =   (gmii_txd_dv_i & (~gmii_txd_dv_shift[7]));

    assign preamble_data_end        =   (gmii_txd_dv_shift[6] & (~gmii_txd_dv_shift[7]));

    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            preamble_data_valid     <=  1'b0;
        end
        else begin
            preamble_data_valid     <=  preamble_data_valid_temp;
        end
    end

   //产生前导码
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            preamble_data           <=  8'h0;
        end
        else if (preamble_data_valid_temp && (!preamble_data_end)) begin
            preamble_data           <=  8'h55;
        end
        else if (preamble_data_valid_temp && preamble_data_end) begin
            preamble_data           <=  8'hd5;
        end
        else begin
            preamble_data           <=  8'h0;
        end
    end

而构建的完整以太网数据帧的MAC帧部分从移位寄存器的第八位开始:

    //mac
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            mac_data_valid          <=  1'b0;
            mac_data                <=  8'h0;
        end
        else begin
            mac_data_valid          <=  gmii_txd_dv_shift[7];
            mac_data                <=  gmii_txd_dv_shift[7]?gmii_txd_shift[8*8-1:7*8]:8'h0;
        end
    end

最后尾部的4字节帧校验序列FCS可参见校验一文的方式产生:

    //calc crc32
    crc32_8 U_crc32_8(
        //system signal
        .clk                    (clk),
        .rst_n                  (rst_n),
       
        //input signal
        .din                    (gmii_txd_shift[8*8-1:7*8]),          
        .calc                   (gmii_txd_dv_shift[7]),     
       
        .init                   (1'b0),       
        .d_valid                (1'b1),   

        .crc_reg                (),
        .crc_valid_o            (crc_out_valid),
        .crc                    (crc_out)
    );

    //FCS 4 bytes
    always @ (posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            fcs_data_valid      <=  1'b0;
            fcs_data            <=  8'h0;
        end
        else begin
            fcs_data_valid      <=  crc_out_valid;
            fcs_data            <=  crc_out_valid?crc_out:8'h0;
        end
    end

最后将以上三部分采用“或”逻辑得到最终送入物理层发送的完整以太网数据帧:

    //output
    assign gmii_txd_with_preamble_o     =  preamble_data | mac_data | fcs_data;
    assign gmii_txd_dv_with_preamble_o  =  preamble_data_valid | mac_data_valid | fcs_data_valid;

可算是把整个弟弟协议栈处理过程的文章撸完了,其中很多逻辑可以进一步优化以节省资源,笔者时间受限就不做优化处理了,一切以能用为准。之后就是仿真验证和上板测试了,见证奇迹的时刻!

若有不足之处还望批评指正!


十二点过九分:UDP/IP硬件协议栈设计(九):仿真​zhuanlan.zhihu.com
4628808c1c2ee08047f98bfff7c62fb3.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值