參考文獻
[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 : [email protected]
// 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 <stdio.h>
#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;
}
/*receive input packet and control command from emac*/
xemacif_input(netif);//將MAC隊列裏的packets傳輸到你的LwIP/IP stack裏
/* if connected to the server and received start command,
* start receiving data from PL through AXI DMA,
* then transmit the data to the PC using TCP
* */
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;
/* the mac address of the board. this should be unique per board */
unsigned char mac_ethernet_address[] = { 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };
/*local ip address*/
IP4_ADDR(&ipaddr, 192, 168, 2, 10);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 2, 1);
/*lwip library init*/
lwip_init();
/* Add network interface to the netif_list, and set it as default */
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);
/* specify that the network if is up */
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;
}
/* connect to tcp server */
IP4_ADDR(&ipaddress, 192, 168, 2, 26); /* tcp server address */
port = PC_TCP_SERVER_PORT; /* 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");
/* store state */
connected_pcb = tpcb;
/* set callback values & functions */
tcp_arg(tpcb, NULL);
tcp_sent(tpcb, tcp_sent_callback);
tcp_recv(tpcb, tcp_recv_callback);
/* disable nagle algorithm to ensure
* the last small segment of a ADC packet will be sent out immediately
* with no delay
* */
tcp_nagle_disable(tpcb);
if(!tcp_nagle_disabled(tpcb))
xil_printf("tcp nagle disable failed!\r\n");
tcp_client_connected = 1;
/* initiate data transfer */
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;
/* close socket if the peer has sent the FIN packet */
if (p == NULL) {
tcp_close(tpcb);
xil_printf("tcp connection closed\r\n");
return ERR_OK;
}
/*if received ip fragment packets*/
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;
/*go to next pbuf pointer*/
q = q->next;
}
}
/*if received no ip fragment packets*/
else
{
memcpy(tcp_rx_buffer, q->payload, q->len);
}
/*change the endian of received command*/
*tcp_rx_buffer = ntohl(*tcp_rx_buffer);
//xil_printf("tcp data come in!%d, %d, %08x\r\n", p->tot_len, p->len, *file);
/* tell lwip we've received the tcp packet */
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;
/* if tcp send buffer has enough space to hold the data we want to transmit from PL, then start tcp transmission*/
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頭文件
/*
* tcp_transmission.h
*
* Created on: 2017年3月13日
* Author: 201607062058
*/
#ifndef TCP_TRANSMISSION_H_
#define TCP_TRANSMISSION_H_
#include <stdio.h>
#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_TRANSMISSION_H_ */
上面的代碼便是進行TCP循環測試的代碼,這裏注意TCP的發送和接受都可以觸發中斷,但是UDP的只有接受可以觸發中斷。 上面的TCP協議使用到了我們前面講解的定時器中斷,因爲TCP協議需要保證穩定的連接,每隔一定時間檢測連接的穩定性。代碼如下:
TCP協議初始化的代碼:
由前面的握手協議可以看出,主動信號是由Client發起的,這一部分也填寫了相應的主機的信息。所以這部分的初始化信號如下:
TCP中斷服務函數如下:
TCP接收中斷服務函數,這一部分的書寫還是有點難度的,大家可以對比着學習一下,ZYNQ接收數據中斷的處理方法,包括UART、USB等等, 代碼如下:
TCP發送函數以及相應的發送中斷:
這裏博主不進行詳細的介紹,想知道每個函數具體的含義可以從lwip官網進行相應的學習,上面對每隔api函數進行了大致的介紹,也有相應的歷程,但是我們不需要對每個api函數都很瞭解,只需要我們可以跑起來相應的程序即可。
下板測試
我們利用NetAssist網絡調試助手對其進行TCP循環測試,結果如下:
從上面可以看出,因爲我們ZYNQ部分設置的是Client,所以我們的PC機設置的是Server。然後可以看到PC機發送數據與接收數據相一致,且測試了12000個數據,進而證明了我們實驗的正確性。
總結
創作不易,認爲文章有幫助的同學們可以關注、點贊、轉發支持。爲行業貢獻及其微小的一部分。對文章有什麼看法或者需要更近一步交流的同學,可以加入下面的羣: