前面已经完成UDP/IP硬件协议栈绝大部分设计了,“接下来发包,两转三转、四五六七八转”。
经过前面的分类解析处理,最后需要发送的数据包只剩下了三类:ARP/ICMP/UDP,而ARP在处理当中已经完成了除了前导码和帧校验序列的封包,只剩padding部分(目前均填0处理),所以本文重点讲述ICMP和UDP帧的封包、以及最后三类帧封装称为完整以太网帧的处理过程,大致流程如下图所示。
三种协议数据按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首部字段如下所示:
其中版本、首部长度、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的首部后再进行封包,其首部如下所示:
其首部也简单,包括: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