基于ZYNQ的TCP Client实现
参考文献
项目描述
TCP协议简述
TCP 三次握手
TCP 数据传输
TCP 四次挥手
PL端设计
PS端设计
下板测试
总结
参考文献
[1]、inner_peace8
[2]、米联客
项目描述
前面的文章我们已经讲解过基于FPGA的千兆以太网的实现,该协议主要使用的是UDP协议,文章中对UDP协议的理论部分也进行了详细的介绍,但是UDP协议是不稳定的协议类型,再进行精准传输的时候有所缺陷。与UDP相反,TCP协议可以保证数据的准确性,但是因为TCP协议的复杂性,目前还没有商用的纯逻辑TCP协议。但是在CPU中,TCP的协议已经非常完善,也有相应的开源团队开发API。本篇文章,我们将利用LWIP系统框架来实现ZYNQ的TCP传输协议。使用该系统框架我们甚至不需要熟悉TCP协议,只需要有前面基于FPGA的千兆以太网的实现的知识便可以正常使用TCP来传输数据协议。
工程描述:讲ZYNQ当作Client来进行与上位机通信,实现千兆网的循环测试。
本次实验所用到的软硬件环境如下:
1、VIVADO 2019.1
2、米联客MZ7015FA开发板
3、NetAssist网络调试助手
TCP协议简述
所谓协议,是指通信的双方,为了保证通信效果,特意在通信形式和内容上的一致协商。在生活中,协议处处可见,例如规定好的交通出行统一靠右行驶,这其实就是一种协议。在计算机世界中,计算机与计算机之间的沟通更加抽象复杂,为了保证能够各种应用场景下的正确通信,众多计算机世界中的协议应运而生。这其中最重要的当属TCP协议和Http协议。
所谓协议,是指通信的双方,为了保证通信效果,特意在通信形式和内容上的一致协商。在生活中,协议处处可见,例如规定好的交通出行统一靠右行驶,这其实就是一种协议。在计算机世界中,计算机与计算机之间的沟通更加抽象复杂,为了保证能够各种应用场景下的正确通信,众多计算机世界中的协议应运而生。这其中最重要的当属TCP协议和Http协议。
那么TCP数据协议为什么可以做到准确传输呢?主要是因为其三次握手和四次握手机制。以下机制的讲解来源于参考文献1——inner_peace8。
TCP 三次握手
TCP 三次握手就好比两个人在街上隔着50米看见了对方,但是因为雾霾等原因不能100%确认,所以要通过招手的方式相互确定对方是否认识自己。
张三首先向李四招手(syn),李四看到张三向自己招手后,向对方点了点头挤出了一个微笑(ack)。张三看到李四微笑后确认了李四成功辨认出了自己(进入estalished状态)。但是李四还有点狐疑,向四周看了一看,有没有可能张三是在看别人呢,他也需要确认一下。所以李四也向张三招了招手(syn),张三看到李四向自己招手后知道对方是在寻求自己的确认,于是也点了点头挤出了微笑(ack),李四看到对方的微笑后确认了张三就是在向自己打招呼(进入established状态)。
于是两人加快步伐,走到了一起,相互拥抱。
我们看到这个过程中一共是四个动作,张三招手–李四点头微笑–李四招手–张三点头微笑。其中李四连续进行了2个动作,先是点头微笑(回复对方),然后再次招手(寻求确认),实际上可以将这两个动作合一,招手的同时点头和微笑(syn+ack)。于是四个动作就简化成了三个动作,张三招手–李四点头微笑并招手–张三点头微笑。这就是三次握手的本质,中间的一次动作是两个动作的合并。
我们看到有两个中间状态,syn_sent和syn_rcvd,这两个状态叫着「半打开」状态,就是向对方招手了,但是还没来得及看到对方的点头微笑。syn_sent是主动打开方的「半打开」状态,syn_rcvd是被动打开方的「半打开」状态。客户端是主动打开方,服务器是被动打开方。
TCP 数据传输
TCP 数据传输就是两个人隔空对话,差了一点距离,所以需要对方反复确认听见了自己的话。
张三喊了一句话(data),李四听见了之后要向张三回复自己听见了(ack)。
如果张三喊了一句,半天没听到李四回复,张三就认为自己的话被大风吹走了,李四没听见,所以需要重新喊话,这就是tcp重传。
也有可能是李四听到了张三的话,但是李四向张三的回复被大风吹走了,以至于张三没听见李四的回复。张三并不能判断究竟是自己的话被大风吹走了还是李四的回复被大风吹走了,张三也不用管,重传一下就是。
既然会重传,李四就有可能同一句话听见了两次,这就是「去重」。「重传」和「去重」工作操作系统的网络内核模块都已经帮我们处理好了,用户层是不用关心的。
张三可以向李四喊话,同样李四也可以向张三喊话,因为tcp链接是「双工的」,双方都可以主动发起数据传输。不过无论是哪方喊话,都需要收到对方的确认才能认为对方收到了自己的喊话。
张三可能是个高射炮,一说连说了八句话,这时候李四可以不用一句一句回复,而是连续听了这八句话之后,一起向对方回复说前面你说的八句话我都听见了,这就是批量ack。但是张三也不能一次性说了太多话,李四的脑子短时间可能无法消化太多,两人之间需要有协商好的合适的发送和接受速率,这个就是「TCP窗口大小」。
网络环境的数据交互同人类之间的对话还要复杂一些,它存在数据包乱序的现象。同一个来源发出来的不同数据包在「网际路由」上可能会走过不同的路径,最终达到同一个地方时,顺序就不一样了。操作系统的网络内核模块会负责对数据包进行排序,到用户层时顺序就已经完全一致了。
TCP 四次挥手
TCP断开链接的过程和建立链接的过程比较类似,只不过中间的两部并不总是会合成一步走,所以它分成了4个动作,张三挥手(fin)——李四伤感地微笑(ack)——李四挥手(fin)——张三伤感地微笑(ack)。
之所以中间的两个动作没有合并,是因为tcp存在「半关闭」状态,也就是单向关闭。张三已经挥了手,可是人还没有走,只是不再说话,但是耳朵还是可以继续听,李四呢继续喊话。等待李四累了,也不再说话了,超张三挥了挥手,张三伤感地微笑了一下,才彻底结束了。
上面有一个非常特殊的状态time_wait,它是主动关闭的一方在回复完对方的挥手后进入的一个长期状态,这个状态标准的持续时间是4分钟,4分钟后才会进入到closed状态,释放套接字资源。不过在具体实现上这个时间是可以调整的。
它就好比主动分手方要承担的责任,是你提出的要分手,你得付出代价。这个后果就是持续4分钟的time_wait状态,不能释放套接字资源(端口),就好比守寡期,这段时间内套接字资源(端口)不得回收利用。
它的作用是重传最后一个ack报文,确保对方可以收到。因为如果对方没有收到ack的话,会重传fin报文,处于time_wait状态的套接字会立即向对方重发ack报文。
同时在这段时间内,该链接在对话期间于网际路由上产生的残留报文(因为路径过于崎岖,数据报文走的时间太长,重传的报文都收到了,原始报文还在路上)传过来时,都会被立即丢弃掉。4分钟的时间足以使得这些残留报文彻底消逝。不然当新的端口被重复利用时,这些残留报文可能会干扰新的链接。
4分钟就是2个MSL,每个MSL是2分钟。MSL就是maximium segment lifetime——最长报文寿命。这个时间是由官方RFC协议规定的。至于为什么是2个MSL而不是1个MSL,我还没有看到一个非常满意的解释。
四次挥手也并不总是四次挥手,中间的两个动作有时候是可以合并一起进行的,这个时候就成了三次挥手,主动关闭方就会从fin_wait_1状态直接进入到time_wait状态,跳过了fin_wait_2状态。
PL端设计
前面是TCP简单的理论部分,上面的文章也不是博主自己写的。熟悉博主博客的同学应该发现,我很少介绍理论部分,因为这一块都可以在书本上学到,书本上的理论知识比我写的要好太多。上面的介绍主要是为了防止我们接下来的代码太过突兀给出的,要想详细理解TCP协议的理论知识还是要看书本,这方面有许多经典课本。
我们的博客还是一个博客一个项目,因为要完成的功能都是PS侧做的事,所以我们PL端的设计没有任何代码只是例化了一个ZYNQ的IP,如下:
PS端设计
与前面SD卡的文章相同,这篇文章也用到了开源的系统框架,所以我们也需要设置BSP文件。
首先右击相应工程的bsp文件,选择Board Support Package Setting
然后点击相应的lwip
然后重新生成相应的bsp文件即可。
本例程使用 RAW API,即函数调用不依赖操作系统。传输效率也比 SOCKET API 高, (具体可参考 xapp1026)。
将 use_axieth_on_zynq 和 use_emaclite_on_zynq 设为 0。如下图所示。
修改 lwip_memory_options 设置,将 mem_size, memp_n_pbuf, mem_n_tcp_pcb, memp_n_tcp_seg 这 4 个参数
值设大,这样会提高 TCP 传输效率。如下图所示。
修改 pbuf_options 设置,将 pbuf_pool_size 设大,增加可用的 pbuf 数量,这样同样会提高 TCP 传输效率。如下
图所示。
修改 tcp_options 设置,将 tcp_snd_buf, tcp_wnd 参数设大,这样同样会提高 TCP 传输效率。如下图所示。
修改 temac_adapter_options 设置,将 n_rx_descriptors 和 n_tx_descriptors 参数设大。这样可以提高 zynq 内部 emac
dma 的数据迁移效率,同样能提高 TCP 传输效率。如下图所示。
需要手动修改 LWIP 库让网口芯片工作于 1000Mbps。
其余选项的参数默认即可,不用修改。点击 OK,重建 bsp。 一般情况下,修改完会自动更新,如果没有更新,手动更新一下,选中 bsp—>右键—> Re-generate BSP Sources。重新生成一下 BSP 包。上面进行这样设置的原因是为了增加lwip的缓存,进而提高千兆网的通信速度。
进行TCP通信的代码如下:
main.c函数
// *********************************************************************************
// Project Name : OSXXXX
// Author : zhangningning
// Email : nnzhang1996@foxmail.com
// Website : https://blog.csdn.net/zhangningning1996
// Module Name : main.c
// Create Time : 2020-06-16 14:39:12
// Editor : sublime text3, tab size (4)
// CopyRight(c) : All Rights Reserved
//
// *********************************************************************************
// Modification History:
// Date By Version Change Description
// -----------------------------------------------------------------------
// XXXX zhangningning 1.0 Original
//
// *********************************************************************************
#include
#include "xscugic.h"
#include "xparameters.h"
#include "sleep.h"
#include "xscutimer.h"
#include "lwip/err.h"
#include "lwip/tcp.h"
#include "lwip/init.h"
#include "lwipopts.h"
#include "netif/xadapter.h"
#include "lwipopts.h"
#include "lwip/priv/tcp_priv.h"
#include "tcp_transmission.h"
#define GIC_ID XPAR_PS7_SCUGIC_0_DEVICE_ID
#define TIMER_IRPT_INTR XPAR_SCUTIMER_INTR
#define TIMER_DEVICE_ID XPAR_XSCUTIMER_0_DEVICE_ID
#define TIMER_LOAD_VALUE 0x13D92D3F/8//1S
#define TCP_RXBUFFER_BASE_ADDR 0x10000000
#define PC_TCP_SERVER_PORT 5001
void TimerIntrHandler(void *CallBackRef);
int initimer();
int initSwIntr();
int inittcp(struct netif *netif);
int tcp_send_init();
err_t tcp_connected_callback(void *arg, struct tcp_pcb *tpcb, err_t err);
err_t tcp_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
static err_t tcp_sent_callback(void *arg, struct tcp_pcb *tpcb, u16_t len);
void send_received_data();
static XScuGic ScuGic;
static XScuGic_Config * ScuGicCfgPtr;
XScuTimer Timer;
XScuTimer_Config *Config;
volatile int TcpTmrFlag;
int flag;
int rec_cnt;
int main()
{
int status;
err_t err;
struct netif *netif, server_netif;
netif = &server_netif;
status = initSwIntr();
status = initimer();
status = inittcp(netif);
if(status != XST_SUCCESS){
return status;
}
tcp_send_init();
while(1){
if(TcpTmrFlag){
if(request_pcb->state == CLOSED || (request_pcb->state == SYN_SENT && request_pcb->nrtx == TCP_SYNMAXRTX)){
request_pcb = tcp_new();
if (!request_pcb) {
xil_printf("txperf: Error creating PCB. Out of Memory\r\n");
return -1;
}
//ip_set_option(request_pcb, SOF_REUSEADDR);
err = tcp_connect(request_pcb, &ipaddress, port, tcp_connected_callback);
if (err != ERR_OK) {
xil_printf("txperf: tcp_connect returned error: %d\r\n", err);
return err;
}
}
tcp_tmr();
TcpTmrFlag = 0;
}
xemacif_input(netif);//将MAC队列里的packets传输到你的LwIP/IP stack里
if(tcp_client_connected && flag == 1)
send_received_data();
}
return 0;
}
int initSwIntr(){
int status;
Xil_ExceptionInit();
ScuGicCfgPtr = XScuGic_LookupConfig(GIC_ID);
status = XScuGic_CfgInitialize(&ScuGic,ScuGicCfgPtr,ScuGicCfgPtr->CpuBaseAddress);
if(status != XST_SUCCESS){
return status;
}
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler,&ScuGic);
status = XScuGic_Connect(&ScuGic,TIMER_IRPT_INTR,(Xil_ExceptionHandler)TimerIntrHandler,&Timer);
if(status != XST_SUCCESS){
return status;
}
XScuGic_Enable(&ScuGic,TIMER_IRPT_INTR);
Xil_ExceptionEnable();
return XST_SUCCESS;
}
int initimer(){
int status;
Config = XScuTimer_LookupConfig(TIMER_DEVICE_ID);
status = XScuTimer_CfgInitialize(&Timer, Config, Config->BaseAddr);
XScuTimer_LoadTimer(&Timer, TIMER_LOAD_VALUE);
//自动装载
XScuTimer_EnableAutoReload(&Timer);
XScuTimer_Start(&Timer);
XScuTimer_EnableInterrupt(&Timer);//一定等定时器初始化好了之后再开始使能定时器中断
return status;
}
void TimerIntrHandler(void *CallBackRef){
XScuTimer *TimerInstancePtr = (XScuTimer *) CallBackRef;
XScuTimer_ClearInterruptStatus(TimerInstancePtr);
TcpTmrFlag = 1;
}
int inittcp(struct netif *netif){
struct ip4_addr ipaddr, netmask, gw;
unsigned char mac_ethernet_address[] = { 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };
IP4_ADDR(&ipaddr, 192, 168, 2, 10);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 2, 1);
lwip_init();
if (!xemac_add(netif, &ipaddr, &netmask, &gw, mac_ethernet_address, XPAR_XEMACPS_0_BASEADDR)) {
xil_printf("Error adding N/W interface\r\n");
return -1;
}
netif_set_default(netif);
netif_set_up(netif);
return XST_SUCCESS;
}
int tcp_send_init(){
err_t err;
tcp_rx_buffer = (u32 *)TCP_RXBUFFER_BASE_ADDR;
request_pcb = tcp_new();
if (!request_pcb) {
xil_printf("txperf: Error creating PCB. Out of Memory\r\n");
return -1;
}
IP4_ADDR(&ipaddress, 192, 168, 2, 26);
port = PC_TCP_SERVER_PORT;
err = tcp_connect(request_pcb, &ipaddress, port, tcp_connected_callback);
if (err != ERR_OK) {
xil_printf("txperf: tcp_connect returned error: %d\r\n", err);
return err;
}
return XST_SUCCESS;
}
err_t tcp_connected_callback(void *arg, struct tcp_pcb *tpcb, err_t err)
{
xil_printf("txperf: Connected to iperf server\r\n");
connected_pcb = tpcb;
tcp_arg(tpcb, NULL);
tcp_sent(tpcb, tcp_sent_callback);
tcp_recv(tpcb, tcp_recv_callback);
tcp_nagle_disable(tpcb);
if(!tcp_nagle_disabled(tpcb))
xil_printf("tcp nagle disable failed!\r\n");
tcp_client_connected = 1;
return ERR_OK;
}
err_t tcp_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
//err_t error;
struct pbuf *q;
u32 remain_length;
q = p;
flag = 1;
rec_cnt = q->tot_len;
if (p == NULL) {
tcp_close(tpcb);
xil_printf("tcp connection closed\r\n");
return ERR_OK;
}
if(q->tot_len > q->len)
{
remain_length = q->tot_len;
file_length = 0;
while(remain_length > 0)
{
memcpy(tcp_rx_buffer + file_length, q->payload, q->len);
file_length += q->len;
remain_length -= q->len;
q = q->next;
}
}
else
{
memcpy(tcp_rx_buffer, q->payload, q->len);
}
*tcp_rx_buffer = ntohl(*tcp_rx_buffer);
//xil_printf("tcp data come in!%d, %d, %08x\r\n", p->tot_len, p->len, *file);
tcp_recved(tpcb, p->tot_len);
pbuf_free(p);
return ERR_OK;
}
static err_t tcp_sent_callback(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
err_t err;
tcp_trans_done = 1;
err = tcp_output(tpcb);
if (err != ERR_OK) {
xil_printf("txperf: Error on tcp_output: %d\r\n",err);
return -1;
}
return ERR_OK;
}
void send_received_data()
{
err_t err;
struct tcp_pcb *tpcb = connected_pcb;
flag = 0;
if (!connected_pcb)
return;
err = tcp_write(tpcb,tcp_rx_buffer , rec_cnt, TCP_WRITE_FLAG_COPY & (~TCP_WRITE_FLAG_MORE));
if (err != ERR_OK) {
xil_printf("txperf: Error on tcp_write: %d\r\n", err);
connected_pcb = NULL;
return;
}
err = tcp_output(tpcb);
if (err != ERR_OK) {
xil_printf("txperf: Error on tcp_output: %d\r\n",err);
return;
}
packet_index++;
}
tcp_transmission.h头文件
#ifndef TCP_TRANSMISSION_H_
#define TCP_TRANSMISSION_H_
#include
#include "xadcps.h"
#include "xil_types.h"
#include "Xscugic.h"
#include "Xil_exception.h"
#define TCP_START_CMD 0xAA55FFA0
#define TCP_STOP_CMD 0xAA55FFB1
#define TCP_RESET_CMD 0xAA55FFC1
#define PC_TCP_SERVER_PORT 5001
#define HEADER_ID0 0xAA55AA55
#define HEADER_ID1 0xAA55AA55
#define HEADER_SIZE (16)
#define ADC_PACKET_LENGTH (16 * 1023)
#define TCP_PACKET_SIZE (ADC_PACKET_LENGTH + HEADER_SIZE)
#define TCP_RXBUFFER_BASE_ADDR 0x10000000
volatile int tcp_trans_start;
volatile int tcp_trans_reset;
unsigned first_trans_start;
volatile u32 packet_index;
volatile unsigned tcp_client_connected;
struct tcp_pcb *connected_pcb;
struct tcp_pcb *request_pcb;
volatile int tcp_trans_done;
volatile u32 file_length;
struct ip4_addr ipaddress;
u16_t port;
u32 *tcp_rx_buffer;
typedef struct packet_header
{
u32 ID0;
u32 ID1;
u32 frame_cnt;
u32 length;
}packet_header;
packet_header *header_p;
#endif
上面的代码便是进行TCP循环测试的代码,这里注意TCP的发送和接受都可以触发中断,但是UDP的只有接受可以触发中断。 上面的TCP协议使用到了我们前面讲解的定时器中断,因为TCP协议需要保证稳定的连接,每隔一定时间检测连接的稳定性。代码如下:
TCP协议初始化的代码:
由前面的握手协议可以看出,主动信号是由Client发起的,这一部分也填写了相应的主机的信息。所以这部分的初始化信号如下:
TCP中断服务函数如下:
TCP接收中断服务函数,这一部分的书写还是有点难度的,大家可以对比着学习一下,ZYNQ接收数据中断的处理方法,包括UART、USB等等, 代码如下:
TCP发送函数以及相应的发送中断:
这里博主不进行详细的介绍,想知道每个函数具体的含义可以从lwip官网进行相应的学习,上面对每隔api函数进行了大致的介绍,也有相应的历程,但是我们不需要对每个api函数都很了解,只需要我们可以跑起来相应的程序即可。
下板测试
我们利用NetAssist网络调试助手对其进行TCP循环测试,结果如下:
从上面可以看出,因为我们ZYNQ部分设置的是Client,所以我们的PC机设置的是Server。然后可以看到PC机发送数据与接收数据相一致,且测试了12000个数据,进而证明了我们实验的正确性。
总结
创作不易,认为文章有帮助的同学们可以关注、点赞、转发支持。为行业贡献及其微小的一部分。对文章有什么看法或者需要更近一步交流的同学,可以加入下面的群: