学会Zynq(18)TCP发送Hello World(client模式)

TCP的工作机制比UDP要复杂的多。本文介绍用TCP发送“Hello World”的实例,讨论程序设计中几个关键的问题。本文撰写思路假设您已经阅读了本系列前几篇与lwIP、UDP相关的文章,重复性语言不过多描述。本文TCP工作在client模式。TCP内容较多,后面的文章会逐步深入介绍。

SDK程序设计
按照前文方法,新建工程后启用lwIP 1.4.1库,其余配置都保持默认即可(使用RAW API)。使用lwIP需要启动中断系统,sys_intr.h和sys_intr.c文件代码与UDP实例中的相同。

此外lwIP要求每250ms调用一次tcp_tmr()函数(具体原因后面小节分析)因此我们要添加定时器资源。定时器使用方法参考本系列第10篇,这里不再讲述。

timer_intr.h文件代码如下:
#include
#include "xadcps.h"
#include "xil_types.h"
#include "Xscugic.h"
#include "Xil_exception.h"
#include "xscutimer.h"

extern volatile int TcpTmrFlag;

#define TIMER_DEVICE_ID XPAR_XSCUTIMER_0_DEVICE_ID
#define TIMER_IRPT_INTR XPAR_SCUTIMER_INTR

void Timer_start(XScuTimer *TimerPtr);
void Timer_Setup_Intr_System(XScuGic *GicInstancePtr,XScuTimer *TimerInstancePtr, u16 TimerIntrId);
int Timer_init(XScuTimer *TimerPtr,u32 Load_Value,u32 DeviceId);

timer_intr.c文件的代码如下:

#include "timer_intr.h"

volatile int TcpTmrFlag;

//---------------------------------------------------------
// 定时器中断处理函数
//---------------------------------------------------------
static void TimerIntrHandler(void *CallBackRef)
{

XScuTimer *TimerInstancePtr = (XScuTimer *) CallBackRef;
XScuTimer_ClearInterruptStatus(TimerInstancePtr);
TcpTmrFlag = 1;
}

//---------------------------------------------------------
// 启动定时器函数
//---------------------------------------------------------
void Timer_start(XScuTimer *TimerPtr)
{
XScuTimer_Start(TimerPtr);
}

//---------------------------------------------------------
// 定时器中断设置函数
//---------------------------------------------------------
void Timer_Setup_Intr_System(XScuGic *GicInstancePtr,XScuTimer *TimerInstancePtr, u16 TimerIntrId)
{
XScuGic_Connect(GicInstancePtr, TimerIntrId,
(Xil_ExceptionHandler)TimerIntrHandler, (void *)TimerInstancePtr);

XScuGic_Enable(GicInstancePtr, TimerIntrId);
XScuTimer_EnableInterrupt(TimerInstancePtr);
}

//---------------------------------------------------------
// 定时器初始化函数
//---------------------------------------------------------
int Timer_init(XScuTimer *TimerPtr,u32 Load_Value,u32 DeviceId)
{
XScuTimer_Config *TMRConfigPtr;
TMRConfigPtr = XScuTimer_LookupConfig(DeviceId);
XScuTimer_CfgInitialize(TimerPtr,TMRConfigPtr,TMRConfigPtr->BaseAddr);
XScuTimer_LoadTimer(TimerPtr, Load_Value);
XScuTimer_EnableAutoReload(TimerPtr);

return 1;
}

先看mian.c文件中的代码,看下RAW模式下使用TCP的架构与使用UDP有何不同:

//--------------------------------------------------
// blog.csdn.net/FPGADesigner
// copyright by CUIT Qi Liu
// Zynq Lwip TCP Communication Test Program
//--------------------------------------------------

#include "timer_intr.h"
#include "sys_intr.h"
#include "user_tcp.h"
#include "sleep.h"

#define TIMER_LOAD_VALUE XPAR_CPU_CORTEXA9_0_CPU_CLK_FREQ_HZ / 8 //0.25S

static XScuGic Intc; //GIC
static XScuTimer Timer;//timer
extern volatile unsigned tcp_client_connected;
extern int tcp_trans_cnt;

//--------------------------------------------------
// 中断与定时器初始化
//--------------------------------------------------
void System_Init()
{
Timer_init(&Timer,TIMER_LOAD_VALUE,TIMER_DEVICE_ID);
Init_Intr_System(&Intc); // initial DMA interrupt system
Setup_Intr_Exception(&Intc);
Timer_Setup_Intr_System(&Intc,&Timer,TIMER_IRPT_INTR);
Timer_start(&Timer);
TcpTmrFlag = 0;
}

//--------------------------------------------------
// 主程序
//--------------------------------------------------
int main(void)
{
struct netif *netif, server_netif; //用于lwIP网络接口的通用数据结构
struct ip_addr ipaddr, netmask, gw; //unsigned int

//开发板的MAC地址
unsigned char mac_ethernet_address[] = {0x00,0x0a,0x35,0x00,0x01,0x02};
System_Init();

netif = &server_netif;

//将4byte结构的IP地址转换为unsigned int
IP4_ADDR(&ipaddr, 192, 168, 1, 10); //IP地址(开发板)
IP4_ADDR(&netmask, 255, 255, 255, 0); //网络掩码
IP4_ADDR(&gw, 192, 168, 1, 1); //网关

lwip_init(); //初始化lwIP
//将网络接口添加到netif_list中
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); //启动网络接口
tcp_send_init(); //初始化TCP PCB

while(1) {
/* call tcp timer every 250ms */
if(TcpTmrFlag)
{
tcp_tmr();
TcpTmrFlag = 0;
}
xemacif_input(netif); //将MAC队列中的packets传输到lwIP栈中
if (tcp_client_connected) { //连接成功则发送数据
sleep(1);
send_data();
xil_printf("tran_cnt:%d\n\r", tcp_trans_cnt);
}
}
}

与UDP相同的lwIP配置流程代码中给出了详细注释。最大区别在于下面这部分:

while(1) {
/* call tcp timer every 250ms */
if(TcpTmrFlag)
{
tcp_tmr();
TcpTmrFlag = 0;
}
user_function(); //用户功能
}

别小瞧这个函数,tcp_tmr对TCP的稳定使用至关重要,可查看本文后面小节的测试。

user_tcp.h文件代码如下(为了避免混淆,尽量不要取名为lwIP库中已用过的udp.h/c和tcp.h/c):

#include
#include
#include "lwip/err.h"
#include "lwip/tcp.h"
#include "lwipopts.h"
#include "netif/xadapter.h"
#include "lwip/init.h"
#include "lwip/tcp_impl.h"
#include "xil_printf.h"

extern volatile unsigned tcp_client_connected;
extern int tcp_trans_cnt;

int tcp_send_init();
void send_data(void);

user_tcp.c文件代码如下:

#include "user_tcp.h"

#define SEND_SIZE 12

static struct tcp_pcb *connected_pcb = NULL;
volatile unsigned tcp_client_connected = 0;
int tcp_trans_cnt = 0;
char sendBuffer[12]="Hello World!";

//--------------------------------------------------
// TCP数据发送成功的回调函数
//--------------------------------------------------
static err_t tcp_sent_callback(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
tcp_trans_cnt++; //统计发送数据的次数
xil_printf("send int");
return ERR_OK;
}

//--------------------------------------------------
// TCP连接成功的回调函数
//--------------------------------------------------
static 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状态

tcp_nagle_disable(connected_pcb);
tcp_arg(tpcb, NULL); //指定应该传递回调函数的参数
//设置当TCP数据成功传递到远程主机时调用回调函数tcp_sent_callback
tcp_sent(tpcb, tcp_sent_callback);

tcp_client_connected = 1; //置1表示连接已建立
xil_printf("Connect Success.\r\n");
return ERR_OK;
}

//--------------------------------------------------
// TCP PCB初始化函数
//--------------------------------------------------
int tcp_send_init()
{
struct tcp_pcb *pcb;
struct ip_addr ipaddr;
err_t err;
u16_t port;

pcb = tcp_new(); //创建新的TCP PCB
if (!pcb) {
xil_printf("txperf: Error creating PCB. Out of Memory\r\n");
return -1;
}

IP4_ADDR(&ipaddr, 192, 168, 1, 100); //服务器的IP地址
port = 7; //服务器的默认端口
tcp_client_connected = 0;

//连接主机,连接建立后调用回调函数tcp_connected_callback
err = tcp_connect(pcb, &ipaddr, port, tcp_connected_callback);
if (err != ERR_OK) {
xil_printf("txperf: tcp_connect returned error: %d\r\n", err);
return err;
}
xil_printf("%d\r\n",err);

return 0;
}

//--------------------------------------------------
// TCP数据发送函数
//--------------------------------------------------
void send_data(void)
{
err_t err;
struct tcp_pcb *tpcb = connected_pcb;

if (!connected_pcb)
return;

err = tcp_write(tpcb, sendBuffer, SEND_SIZE, 3);
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;
}
}

本设计的TCP工作在客户端模式,将远程主机当作服务器,主动请求连接。TCP client和TCP server在lwIP中的连接流程和区别可参考本系列前面与lwIP相关的文章。

可以看到仅仅是TCP发送,就用到了两个回调函数。TCP的回调机制比UDP要复杂的多。下面我们分小节探讨下上面程序中的各种问题。

回调机制
在初始化TCP连接时,我们使用tcp_connect函数请求建立于服务器的连接,绑定了第一个回调函数tcp_connected_callback。这个回调函数会在服务器给出应答,也就是连接的确建立起来后才会调用。在这个回调函数中我们可以设置“xil_printf(“Connect Success.\r\n”);”这句代码,通过串口打印确认回调函数有没有被调用。

tcp_connect函数中又使用tcp_sent函数绑定了另一个函数tcp_sent_callback。我们要知道,这个回调函数并不是在TCP发送后就会马上被调用。它需要等主机返回应答信号,确认数据成功的发送到主机后才会调用。我们可以在tcp_sent_callback中计数,并通过“xil_printf(“send int”);”来查看是否进入了回调。main.c中每次发送时打印计数值。

测试结果如上图,注意并不是每次发送数据都进入了tcp_sent_callback回调函数。正确理解回调函数的调用机制,我们才能灵活使用。

TCP客户端连接
回顾下本系列第11篇中介绍过的TCP主动连接方法:

1. 调用pcb_new创建一个pcb。
2. (可选)调用tcp_arg将应用程序中特定的值于PCB关联在一起。
3. (可选)调用tcp_bind函数指定本地IP地址和端口
4. 调用tcp_connect函数。

对照本实例中的tcp_send_init函数,你会发现我们2、3步都没有设置,读者可以自行添加代码查看效果。如果没有使用tcp_bind绑定本地端口,那么与主机连接后会自动分配一个端口;本地IP地址使用main.c中设置了的地址。

在网络调试助手中,可以查看与TCP服务器连接的客户端IP地址和端口号。

tcp_tmr函数为何重要
如果各位实际使用时,把while循环中的tcp_tmr函数注释掉,或者不启动定时器,发现好像tcp也可以正常使用,确实连接成功也发送出去了呀?其实不然。我举几个例子,不知道您是否遇到过下列情况:

1. 连接到网络助手后,一开始工作正常,但一段时间后失常;
2. 已经连接到网络调试助手,再次下载程序(即再次连接),发现tcp_connect返回的也是ERR_OK,但就是没有进入连接回调函数;
3. 发送过程中偶尔出现tcp_write函数返回ERR_CONN(对应-13,表示未连接)。

这就是tcp工作不稳定的表现。lwIP的TCP内部也有定时器,维持其正常运作。我们只有周期性地(通常是250ms)调用tcp_tmr函数,才能调度TCP内部的定时器。

神奇的nagle算法
本实例中我们在连接回调函数中使用“tcp_nagle_disable(connected_pcb);”这条代码关闭了nagle算法。如果把这句代码注释掉,会出现如下情况:

一下子接收到一大片Hello World,且数量不一,这便是nagle算法的作用。如果我们要发送字节较短的数据:TCP/IP要为其加包,加包的字节比数据本身还多,还要进行校验和的计算;主机接收到包后再解包,检查校验和……这无疑是个效率低下的过程。

Nagle算法会自动连接许多小的消息,减少发送包的个数来增加网络效率。TCP/IP协议中无论发送多少数据,总需要在数据前面加上协议头;对方接收到数据也需要发送应答。Nagle算法尽可能发送大块数据,避免小数据块,从而充分利用网络带宽。

然而本例中我们不追求效率,只希望实现确确实实的是每秒发送一个Hello World,那我们就有必要关掉nagle算法。效果如下:

TCP内容较多,后面的文章会逐步深入介绍。

---------------------


文章来源:FPGADesigner的博客
*本文由作者授权转发,如需转载请联系作者本人

推荐阅读