利用ZYNQ SOC快速打开算法验证通路(6)——LWIP实现千兆TCP/IP网络传输
一、前言
之前ZYNQ与PC之间的网络连接依赖于外接硬件协议栈芯片,虽然C驱动非常简单,但网络带宽受限。现采用LWIP+PS端MAC控制器+PHY芯片的通用架构。关于LWIP库,已经有很多现成的资料和书籍。其有两套API,一个是SOCKET,另一个是本例中要用到的RAW。RAW API理解起来较为复杂,整个程序基于中断机制运行,通过函数指针完成多层回调函数的执行。SOCKET API需要支持多线程操作系统的支持,也牺牲了效率,但理解和编程都较为容易。实际上SOCKET API是对RAW API的进一步封装。
二、LWIP Echo Server demo解读
首先打开Xilinx SDK自带的LwIP Echo Server demo.
1 /******************************************************************************
2 *
3 * Copyright (C) 2009 - 2014 Xilinx, Inc. All rights reserved.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * Use of the Software is limited solely to applications:
16 * (a) running on a Xilinx device, or
17 * (b) that interact with a Xilinx device through a bus or interconnect.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
22 * XILINX BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
24 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 * SOFTWARE.
26 *
27 * Except as contained in this notice, the name of the Xilinx shall not be used
28 * in advertising or otherwise to promote the sale, use or other dealings in
29 * this Software without prior written authorization from Xilinx.
30 *
31 ******************************************************************************/
32
33 #include <stdio.h>
34
35 #include "xparameters.h"
36
37 #include "netif/xadapter.h"
38
39 #include "platform.h"
40 #include "platform_config.h"
41 #if defined (__arm__) || defined(__aarch64__)
42 #include "xil_printf.h"
43 #endif
44
45 #include "lwip/tcp.h"
46 #include "xil_cache.h"
47
48 #if LWIP_DHCP==1
49 #include "lwip/dhcp.h"
50 #endif
51
52 /* defined by each RAW mode application */
53 void print_app_header();
54 int start_application();
55 int transfer_data();
56 void tcp_fasttmr(void);
57 void tcp_slowtmr(void);
58
59 /* missing declaration in lwIP */
60 void lwip_init();
61
62 #if LWIP_DHCP==1
63 extern volatile int dhcp_timoutcntr;
64 err_t dhcp_start(struct netif *netif);
65 #endif
66
67 extern volatile int TcpFastTmrFlag;
68 extern volatile int TcpSlowTmrFlag;
69 static struct netif server_netif;
70 struct netif *echo_netif;
71
72 void
73 print_ip(char *msg, struct ip_addr *ip)
74 {
75 print(msg);
76 xil_printf("%d.%d.%d.%d\n\r", ip4_addr1(ip), ip4_addr2(ip),
77 ip4_addr3(ip), ip4_addr4(ip));
78 }
79
80 void
81 print_ip_settings(struct ip_addr *ip, struct ip_addr *mask, struct ip_addr *gw)
82 {
83
84 print_ip("Board IP: ", ip);
85 print_ip("Netmask : ", mask);
86 print_ip("Gateway : ", gw);
87 }
88
89 #if defined (__arm__) && !defined (ARMR5)
90 #if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
91 int ProgramSi5324(void);
92 int ProgramSfpPhy(void);
93 #endif
94 #endif
95
96 #ifdef XPS_BOARD_ZCU102
97 #ifdef XPAR_XIICPS_0_DEVICE_ID
98 int IicPhyReset(void);
99 #endif
100 #endif
101
102 int main()
103 {
104 struct ip_addr ipaddr, netmask, gw;
105
106 /* the mac address of the board. this should be unique per board */
107 unsigned char mac_ethernet_address[] =
108 { 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };
109
110 echo_netif = &server_netif;
111 #if defined (__arm__) && !defined (ARMR5)
112 #if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
113 ProgramSi5324();
114 ProgramSfpPhy();
115 #endif
116 #endif
117
118 /* Define this board specific macro in order perform PHY reset on ZCU102 */
119 #ifdef XPS_BOARD_ZCU102
120 IicPhyReset();
121 #endif
122
123 init_platform();
124
125 #if LWIP_DHCP==1
126 ipaddr.addr = 0;
127 gw.addr = 0;
128 netmask.addr = 0;
129 #else
130 /* initliaze IP addresses to be used */
131 IP4_ADDR(&ipaddr, 192, 168, 1, 10);
132 IP4_ADDR(&netmask, 255, 255, 255, 0);
133 IP4_ADDR(&gw, 192, 168, 1, 1);
134 #endif
135 print_app_header();
136
137 lwip_init();//网络参数初始化
138
139 /* Add network interface to the netif_list, and set it as default */
140 if (!xemac_add(echo_netif, &ipaddr, &netmask,
141 &gw, mac_ethernet_address,
142 PLATFORM_EMAC_BASEADDR)) {
143 xil_printf("Error adding N/W interface\n\r");
144 return -1;
145 }
146 netif_set_default(echo_netif);
147
148 /* now enable interrupts */
149 platform_enable_interrupts();
150
151 /* specify that the network if is up */
152 netif_set_up(echo_netif);
153
154 #if (LWIP_DHCP==1)
155 /* Create a new DHCP client for this interface.
156 * Note: you must call dhcp_fine_tmr() and dhcp_coarse_tmr() at
157 * the predefined regular intervals after starting the client.
158 */
159 dhcp_start(echo_netif);
160 dhcp_timoutcntr = 24;
161
162 while(((echo_netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
163 xemacif_input(echo_netif);
164
165 if (dhcp_timoutcntr <= 0) {
166 if ((echo_netif->ip_addr.addr) == 0) {
167 xil_printf("DHCP Timeout\r\n");
168 xil_printf("Configuring default IP of 192.168.1.10\r\n");
169 IP4_ADDR(&(echo_netif->ip_addr), 192, 168, 1, 10);
170 IP4_ADDR(&(echo_netif->netmask), 255, 255, 255, 0);
171 IP4_ADDR(&(echo_netif->gw), 192, 168, 1, 1);
172 }
173 }
174
175 ipaddr.addr = echo_netif->ip_addr.addr;
176 gw.addr = echo_netif->gw.addr;
177 netmask.addr = echo_netif->netmask.addr;
178 #endif
179
180 print_ip_settings(&ipaddr, &netmask, &gw);//打印关键网络参数
181
182 /* start the application (web server, rxtest, txtest, etc..) */
183 start_application();//设置回调函数,这些函数在特定事件发生时以函数指针的方式被调用
184
185 /* receive and process packets */
186 while (1) {
187 if (TcpFastTmrFlag) {//发送处理,如差错重传,通过定时器置位标志位
188 tcp_fasttmr();
189 TcpFastTmrFlag = 0;
190 }
191 if (TcpSlowTmrFlag) {
192 tcp_slowtmr();
193 TcpSlowTmrFlag = 0;
194 }
195 xemacif_input(echo_netif);//连续接收数据包,并将数据包存入LWIP
196 transfer_data();//空函数
197 }
198
199 /* never reached */
200 cleanup_platform();
201
202 return 0;
203 }
echo
整体流程为:初始化LWIP、添加网络接口(MAC)、使能中断、设置回调函数。最终进入主循环,内部不断检测定时器中断标志位,当标志位TcpFastTmrFlag或TcpSlowTmrFlag为1则调用相应的处理函数,完成超时重传等任务。接下来查看回调函数的设置:
int start_application()
{
struct tcp_pcb *pcb;//protocol control block 简称PCB
err_t err;
unsigned port = 7;
/* create new TCP PCB structure */
pcb = tcp_new();
if (!pcb) {
xil_printf("Error creating PCB. Out of Memory\n\r");
return -1;
}
/* bind to specified @port */
err = tcp_bind(pcb, IP_ADDR_ANY, port);
if (err != ERR_OK) {
xil_printf("Unable to bind to port %d: err = %d\n\r", port, err);
return -2;
}
/* we do not need any arguments to callback functions */
tcp_arg(pcb, NULL);
/* listen for connections */
pcb = tcp_listen(pcb);
if (!pcb) {
xil_printf("Out of memory while tcp_listen\n\r");
return -3;
}
/* specify callback to use for incoming connections */
tcp_accept(pcb, accept_callback);
xil_printf("TCP echo server started @ port %d\n\r", port);
return 0;
}
start_application
创建PCB(protocol control block)建立连接、绑定IP地址和端口号、监听请求,最后tcp_accept函数用于指定当监听到连接请求时调用的函数accept_callback。进入该函数内部查看:
1 err_t accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
2 {
3 static int connection = 1;
4
5 /* set the receive callback for this connection */
6 tcp_recv(newpcb, recv_callback);
7
8 /* just use an integer number indicating the connection id as the
9 callback argument */
10 tcp_arg(newpcb, (void*)(UINTPTR)connection);
11
12 /* increment for subsequent accepted connections */
13 connection++;
14
15 return ERR_OK;
16 }
accept_callback
内部主要通过tcp_recv函数来指定当收到TCP包后调用的函数recv_callback。我们再次观察其内容:
1 err_t recv_callback(void *arg, struct tcp_pcb *tpcb,
2 struct pbuf *p, err_t err)
3 {
4 /* do not read the packet if we are not in ESTABLISHED state */
5 if (!p) {
6 tcp_close(tpcb);
7 tcp_recv(tpcb, NULL);
8 return ERR_OK;
9 }
10
11 /* indicate that the packet has been received */
12 tcp_recved(tpcb, p->len);
13
14 /* echo back the payload */
15 /* in this case, we assume that the payload is < TCP_SND_BUF */
16 if (tcp_sndbuf(tpcb) > p->len) {
17 err = tcp_write(tpcb, p->payload, p->len, 1);
18 } else
19 xil_printf("no space in tcp_sndbuf\n\r");
20
21 /* free the received pbuf */
22 pbuf_free(p);
23
24 return ERR_OK;
25 }
recv_callback
tcp_recved函数指示用来告知LWIP接收数据量,然后检测发送缓冲区是否足够容纳接收内容,若大于则调用tcp_write函数将接收数据写入发送缓冲区等待发送。综上,整体的调用流程为:tcp_accept -> accept_callback -> tcp_recv -> recv_callback -> tcp_recved和tcp_write。前四个用于接收,后两个用于发送。
函数解析完毕,之后改动上位机网络参数,使PC机IP地址与Board在同一网段内,这里设置为192.168.1.11.打开网络调试助手,设置PC为TCP Client。以下是ZYNQ串口打印及网络调试结果。
三、TCP Client Send data
现在我们来改动demo,设计一个客户端发送数据包的示例工程,功能是循环发送一个常数数组中数据到远程服务器。该工程参考米联客教程中相关章节内容。代码如下:
/****************************************************************************** * * Copyright (C) 2009 - 2014 Xilinx, Inc. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * Use of the Software is limited solely to applications: * (a) running on a Xilinx device, or * (b) that interact with a Xilinx device through a bus or interconnect. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * XILINX BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * Except as contained in this notice, the name of the Xilinx shall not be used * in advertising or otherwise to promote the sale, use or other dealings in * this Software without prior written authorization from Xilinx. * ******************************************************************************/ #include <stdio.h> #include "xparameters.h" #include "netif/xadapter.h" #include "platform.h" #include "platform_config.h" #if defined (__arm__) || defined(__aarch64__) #include "xil_printf.h" #endif #include "lwip/tcp.h" #include "xil_cache.h" #if LWIP_DHCP==1 #include "lwip/dhcp.h" #endif /* defined by each RAW mode application */ void print_app_header(); int client_application(); //int start_application(); //int transfer_data(); int send_data(); void tcp_fasttmr(void); void tcp_slowtmr(void); /* missing declaration in lwIP */ void lwip_init(); #if LWIP_DHCP==1 extern volatile int dhcp_timoutcntr; err_t dhcp_start(struct netif *netif); #endif extern volatile int TcpFastTmrFlag; extern volatile int TcpSlowTmrFlag; static struct netif server_netif; struct netif *echo_netif; void print_ip(char *msg, struct ip_addr *ip) { print(msg); xil_printf("%d.%d.%d.%d\n\r", ip4_addr1(ip), ip4_addr2(ip), ip4_addr3(ip), ip4_addr4(ip)); } void print_ip_settings(struct ip_addr *ip, struct ip_addr *mask, struct ip_addr *gw) { print_ip("Board IP: ", ip); print_ip("Netmask : ", mask); print_ip("Gateway : ", gw); } int main() { uint cycle = 0; struct ip_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 }; echo_netif = &server_netif; /* Define this board specific macro in order perform PHY reset on ZCU102 */ init_platform(); /* initliaze IP addresses to be used */ IP4_ADDR(&ipaddr, 192, 168, 1, 10); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); print_app_header(); lwip_init(); /* Add network interface to the netif_list, and set it as default */ if (!xemac_add(echo_netif, &ipaddr, &netmask, &gw, mac_ethernet_address, PLATFORM_EMAC_BASEADDR)) { xil_printf("Error adding N/W interface\n\r"); return -1; } netif_set_default(echo_netif); /* now enable interrupts */ platform_enable_interrupts(); /* specify that the network if is up */ netif_set_up(echo_netif); print_ip_settings(&ipaddr, &netmask, &gw); /* start the application (web server, rxtest, txtest, etc..) */ //start_application(); client_application(); /* receive and process packets */ while (1) { if (TcpFastTmrFlag) { tcp_fasttmr(); TcpFastTmrFlag = 0; } if (TcpSlowTmrFlag) { tcp_slowtmr(); TcpSlowTmrFlag = 0; } xemacif_input(echo_netif); //transfer_data(); if(cycle == 9999){ cycle = 0; send_data(); } else cycle++; } return 0; }
main
函数定义:
1 /* 2 * tcp_trans.c 3 * 4 * Created on: 2018年10月18日 5 * Author: s 6 */ 7 8 9 #include <stdio.h> 10 #include <string.h> 11 12 #include "lwip/err.h" 13 #include "lwip/tcp.h" 14 #include "lwipopts.h" 15 #include "xil_cache.h" 16 #include "xil_printf.h" 17 #include "sleep.h" 18 19 #define TX_SIZE 10 20 21 static struct tcp_pcb*connected_pcb = NULL; 22 unsigned client_connected = 0; 23 //静态全局函数 外部文件不可见 24 uint tcp_trans_done = 0; 25 26 u_char data[TX_SIZE] = {0,1,2,3,4,5,6,7,8,9}; 27 28 int send_data() 29 { 30 err_t err; 31 struct tcp_pcb *tpcb = connected_pcb; 32 33 if (!tpcb) 34 return -1; 35 36 //判断发送数据长度是否小于发送缓冲区剩余可用长度 37 if (TX_SIZE < tcp_sndbuf(tpcb)) { 38 //Write data for sending (but does not send it immediately). 39 err = tcp_write(tpcb, data, TX_SIZE, 1); 40 if (err != ERR_OK) { 41 xil_printf("txperf: Error on tcp_write: %d\r\n", err); 42 connected_pcb = NULL; 43 return -1; 44 } 45 46 //Find out what we can send and send it 47 err = tcp_output(tpcb); 48 if (err != ERR_OK) { 49 xil_printf("txperf: Error on tcp_output: %d\r\n",err); 50 return -1; 51 } 52 } 53 else 54 xil_printf("no space in tcp_sndbuf\n\r"); 55 56 return 0; 57 } 58 59 static err_t tcp_sent_callback(void *arg, struct tcp_pcb *tpcb,u16_t len) 60 { 61 tcp_trans_done ++; 62 return ERR_OK; 63 } 64 65 //tcp连接回调函数 设置为静态函数,外部文件不可见 66 static err_t tcp_connected_callback(void *arg, struct tcp_pcb *tpcb, err_t err) 67 { 68 /* store state */ 69 connected_pcb = tpcb; 70 71 /* set callback values & functions */ 72 tcp_arg(tpcb, NULL); 73 74 //发送到远程主机后调用tcp_sent_callback 75 tcp_sent(tpcb, tcp_sent_callback); 76 77 client_connected = 1; 78 79 /* initiate data transfer */ 80 return ERR_OK; 81 } 82 83 int client_application() 84 { 85 struct tcp_pcb *pcb; 86 struct ip_addr ipaddr; 87 err_t err; 88 unsigned port = 7; 89 90 /* create new TCP PCB structure */ 91 pcb = tcp_new(); 92 if (!pcb) { 93 xil_printf("Error creating PCB. Out of Memory\n\r"); 94 return -1; 95 } 96 97 /* connect to iperf tcp server */ 98 IP4_ADDR(&ipaddr, 192, 168, 1, 209);//设置要连接的主机的地址 99 100 //当连接到主机时,调用tcp_connected_callback 101 err = tcp_connect(pcb, &ipaddr, port, tcp_connected_callback); 102 if (err != ERR_OK) { 103 xil_printf("txperf: tcp_connect returned error: %d\r\n", err); 104 return err; 105 } 106 107 return 0; 108 }
tcp_trans
可以看出还是一样的套路,在client_application函数中设置回调函数。首先新建PCB,tcp_connect函数设定要连接远程服务器的IP地址和端口号,连接建立时将调用回调函数tcp_connected_callback。tcp_connected_callback内部tcp_sent函数用于指定当发送数据包完成后执行的tcp_sent_callback。tcp_sent_callback内部只利用tcp_trans_done变量计数发送次数。而真正的发送处理任务则交给主循环中的send_data。若处于连接状态,且发送缓冲区容量比带发送数据量大,则调用tcp_write将待发送数据写入发送缓冲区,之后调用tcp_output函数立即传输发送缓冲区内容。如果不调用tcp_output,LWIP会等待数据量达到一定值时一起发送来提高效率,是否调用tcp_output函数可根据具体需求而定。
接下来看下实验结果:
PC端正确接收到常数数组,实验无误。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
参考文献:
1 LWIP 无OS RAW-API 函数 – 专注的力量 – CSDN博客 https://blog.csdn.net/liang890319/article/details/8574603
2 解读TCP 四种定时器 – xiaofei0859的专栏 – CSDN博客 https://blog.csdn.net/xiaofei0859/article/details/52794576
3 米联 《ZYNQ SOC修炼秘籍》