提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
最近在大佬们的帮助下学习了TCP,并独立实现了TCP的数据回环实验,网上也基本没有FPGA关于TCP实现的记录,所以想打算写这篇文章,也算是对自己所学的总结和梳理。
本文只对TCP/IP理论基础进行简单介绍,主要记录工程的实现思路和方法,仅供参考,欢迎指出错误。
想必大家都很了解,所以就直接看看UDP和和TCP报文的区别吧。
TCP首部没有options字段时为20字节。建立连接和关闭连接主要靠的是报文序列号SEQ number 和确认应答号ACKnumber,分别在SYN位和ACK位为1时有效。
三次握手与四次挥手参考博文:作者:爱前端不爱恋爱。卧槽!牛皮了,头一次见有大佬把TCP/IP三次握手四次挥手解释的这么明白
URG | 紧急标志位,紧急指针有效 |
---|---|
ACK | 确认序列号有效 |
PSH | 表示尽快将信息推给应用层 |
RST | 重新连接 |
SYN | 连接 |
FIN | 关闭连接 |
Window Size | 可以理解为下一次可以收到数据报文的最大字节数 |
TCP Checksum | 为TCP伪首部 = Source IP+Destination IP + TCP header+TCP DATA的校验和,tcp checksum强制为0,不然就是套娃了 |
TCP Checksum 计算方法为TCP伪首部16位16位相加,如果结果大于16位继续高16位与低16位相加
状态机分为两条主线,一条是FPGA作为服务端的,一条是FPGA作为客户端的。转移条件与三次握手四次挥手条件一样。
下面只展示当FPGA作为服务端时的连接与关闭(因为在作为客户机时的关闭,作为服务端的上位机发送的第一条报文的与协议不同,不知是否是上位机bug),在串口软件上点击连接,显示连接成功,wireshark抓到了三条报文如图
上位机作为客户端首先发送了一条包含初始化序列号的SYN报文,FPGA回复SYN + ACK报文,客户端收到后再通知给服务端,自此连接成功。
关闭连接四次挥手,最初是FIN报文,最后FPGA状态机也恢复到IDLE 0001,关闭成功。
always @ (posedge sys_clk,negedge sys_rst_n)
if(!sys_rst_n) begin
tack <= 1'b0;
tack_seq <= 32'd0;
ack_start<= 1'b0;
end
else if(tx_done & tras_ready)
begin
tack <= 1'b0;
tack_seq <= 32'd0;
ack_start<= 1'b0;
end
else if(rec_pkt_done& tras_ready) //应答包不再次应答
if(rec_byte_num == 32'd0)
begin
tack <= 1'b0;
tack_seq <= 32'd0;
ack_start<= 1'b0;
end
else begin
tack <= 1'b1;
tack_seq <= rseq_num + rec_byte_num;
ack_start<= 1'b1;
end
应答模块比较简单,应答时ACK位为1,应答号为收到的序列号 + 接收到的字节数,表明下次发送的序列号从这个开始,要注意的是应答包不用应答,所以判断这个包收到的有效数据字节数rec_byte_num是否为0即可。
因为TCP接收方超时不应答发送方的话,发送方会重传几次。然而fifo读完后数据就不存在了,所以不能用fifo作为发送的缓存,只能用ram,具体代码如下
always @ (posedge clka,negedge rst_n)
if(!rst_n) send_done_flag <= 1'b0;
else if(time_500ms == CNT_MAX)
send_done_flag <= 1'b0;
else if(ACK) send_done_flag <= 1'b0;
else if(send_done) send_done_flag <= 1'b1;
always @ (posedge clka,negedge rst_n)
if(!rst_n) time_500ms <= 32'd0;
else if(ACK) time_500ms <= 32'd0;
else if(re_send)
time_500ms <= 32'd0;
else if(send_done_flag)
time_500ms <= time_500ms + 1'b1;
always @ (posedge clka,negedge rst_n)
if(!rst_n) re_send <= 1'b0;
else if(time_500ms == CNT_MAX && re_send_cnt < 4'd5)
re_send <= 1'b1;
else re_send <= 1'b0;
always @ (posedge clka,negedge rst_n)
if(!rst_n) re_send_cnt <= 4'd0;
else if(ACK) re_send_cnt <= 4'd0;
else if(re_send)
re_send_cnt <= re_send_cnt + 1'b1;
else re_send_cnt <= re_send_cnt;
always @ (posedge clka,negedge rst_n)
if(!rst_n) wr_addr <= 9'd0;
else if(send_done) wr_addr <= 9'd0;
else if(wea) wr_addr <= wr_addr + 1'b1;
always @ (posedge clka,negedge rst_n)
if(!rst_n) rd_addr <= 9'd0;
else if(send_done) rd_addr <= 9'd0;
else if(enb) rd_addr <= rd_addr + 1'b1;
如果发送完0.5s还是没接收应答就会再次发送,直至有应答或者发了四五次,说明需要排查问题了。
always @ (*) begin
if(!sys_rst_n)
next_state = IDLE;
else
case(cur_state)
IDLE : if(cnt == CNT_MAX)begin
if(tx_start_edge)
next_state = TX_START;
else if(tx_ack_edge)
next_state = TX_ACK;
else if(tx_data_en_edge)
next_state = TX_DATA;
else next_state = IDLE; end
else next_state = IDLE;
TX_START: if(tx_done)
next_state = IDLE;
else next_state = TX_START;
TX_ACK : if(tx_done)
next_state = IDLE;
else next_state = TX_ACK;
TX_DATA : if(tx_done)
next_state = IDLE;
else next_state = TX_DATA;
default : next_state = IDLE;
endcase
end
TX_START为三握四挥的时候的以太网发送,TX_ACK为应答时的以太网发送,TX_DATA为有效数据时候的发送,可以看出三握四挥的时优先级最高。
实现上面的几个模块就可以做到数据回环的实现了,当然前提是其他都没问题。
上位机发送数据是默认先应答,再回传。从仿真来看:
1是建立连接的三个包,2是发送数据,3是发送成功收到的应答包,4是数据回传过来,5是收到数据包的应答,仿真是没问题的。
如图上位机发送了一条数据,也收到了数据相同的数据,wireshark也抓到了两条应答包两条数据包。说明回环实验成功了。
以上结束,包括学习和实现断断续续用了一个月,调试了也花了许多时间,还是感觉相当不易,虽然只做了tcp的一些基础内容,但是互相通信也没有问题,今后有机会再好好完善吧,TCP/ip毕竟是个很复杂的东西,硬件全部实现也不可能,不然FPGA关于tcp这方面的东西也不会这么少。