zedboard第九课(LWIP standalone)

  • Post author:
  • Post category:其他


standalone的BSP,提供了LWIP141的源代码。对于APP来说,只需要关注LWIP的API函数即可。

NEWAPP中,LWIP ECHO SERVER这个工程样例,给我们搭建了使用LWIP的TCP服务的基本框架。我们基于这个框架进行情景分析。

整个工程基于RAW API来编写,所以是单进程的,但是仍然需要中断系统的支持,因为接收数据包,需要外设中断,然后在中断ISR中进行处理。

当外设接收到网络数据包时,发送一个中断,然后CPU进入中断处理,并调用LWIP的Callback来进行协议分解,数据提取,然后调用用户的Callback,进行数据处理。

当CPU需要向外发送网络数据包时,会调用LWIP的API进行数据封包,层层封包之后,LWIP会发送给外设,由外设把网络数据包发送出去。

也就是说,中断ISR主要负责接收网络包的处理,而用户程序负责发送网络数据包的处理。

接收和发送并不能同时进行,因为Callback处理接收时,不能同时处理发送封包。

这也是单进程的缺点。

整个LWIP,是基于SOD设计思想的。所以,我们在应用LWIP时,也要遵循SOD设计思想。

struct ip_addr,这个结构体,描述了IP地址的数据成员。

struct netif,这个结构体,描述了网络接口的硬件Resource。它包含了一个NETIF所需要的全部硬件属性,资源链接,以及操作函数入口。包括IPADDR,NETMASK,GW,HWADDR,MTU等硬件配置信息,以及netif_input()和netif_output()等函数指针。它还包含一些关联资源指针,用来关联其他的资源结构体,例如struct dhcp,struct pbuf等。

struct pbuf,这个结构体,用来描述缓冲包的硬件信息。包含了LEN,FLAG,TYPE等描述缓冲包的资源属性的信息,以及payload,这个关联资源指针,payload被定义为(void*),也就是被视为裸内存。

struct tcp_pcb,这个结构体,用来描述TCP数据包的硬件信息。

这些都是LWIP定义的,用户程序无需修改。

SI5324和SFP是ZCU706需要用到外设,在zedboard上并不需要。所以,IIC_access和IIC_phyreset也是不需要的。

zedboard需要GIC和Interrupt的支持,当然,还有Timer,这在init_platform()中体现出来。

echo这个文件,将APP需要的ECHOSERVER 的操作函数,集合在了一起。

这样,在Main中,调用所需要的ECHO函数,就可以实现基于LWIP的ECHOSERVER的功能。

main中,需要做的就是按照流程,配置和启动ECHOSERVER。

首先,要对LWIP协议栈进行初始化。这是LWIP协议栈中的init.c的函数。

struct ip_addr ipaddr, netmask, gw;
IP4_ADDR(&ipaddr,  192, 168,   1, 10);
	IP4_ADDR(&netmask, 255, 255, 255,  0);
	IP4_ADDR(&gw,      192, 168,   1,  1);
echo_netif = &server_netif;
lwip_init();

然后,要对zedboard的硬件外设进行配置。xemac_add(),这是xadaptor.c中的函数。zynq使用的是PS的MACPHY,所以这些函数在LWIP代码的contrib文件夹下面,xilinx对应的文件夹中。

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);

然后,使能中断系统,这样,网口外设可以接收数据包。这个函数在platform_zynq.c文件中。

platform_enable_interrupts();

然后,建立网口外设,让网口和交换机进行通信,使网口被交换机登记。

netif_set_up(echo_netif);

之后,就可以启动APP了。即ECHOSERVER。这个函数在echo.c中。按流程完成了如下工作,

新建一个tcp_pcb,绑定一个TCPPORT到这个TCP_PCB。设置TCP_PCB的参数。监听TCP_PCB,这是一个listen_point,监听成功后,LWIP会返回一个新的建立好连接的TCP_PCB。这是一个connect_point。为这个connect_point关联Callback。

start_application();

之后,用户程序进入后台业务工作流程内。xemacif_input()是协议中用来接收数据包的函数,将网口外设接收的数据包处理后,加入到pbuf中。

while (1) {
		if (TcpFastTmrFlag) {
			tcp_fasttmr();
			TcpFastTmrFlag = 0;
		}
		if (TcpSlowTmrFlag) {
			tcp_slowtmr();
			TcpSlowTmrFlag = 0;
		}
		xemacif_input(echo_netif);
		transfer_data();
	}

整体代码如下:

extern volatile int TcpFastTmrFlag;
extern volatile int TcpSlowTmrFlag;
static struct netif server_netif;
struct netif *echo_netif;
int main()
{
	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 */
#ifdef XPS_BOARD_ZCU102
	IicPhyReset();
#endif

	init_platform();

#if LWIP_DHCP==1
    ipaddr.addr = 0;
	gw.addr = 0;
	netmask.addr = 0;
#else
	/* 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);
#endif	
	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);

#if (LWIP_DHCP==1)
	/* Create a new DHCP client for this interface.
	 * Note: you must call dhcp_fine_tmr() and dhcp_coarse_tmr() at
	 * the predefined regular intervals after starting the client.
	 */
	dhcp_start(echo_netif);
	dhcp_timoutcntr = 24;

	while(((echo_netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
		xemacif_input(echo_netif);

	if (dhcp_timoutcntr <= 0) {
		if ((echo_netif->ip_addr.addr) == 0) {
			xil_printf("DHCP Timeout\r\n");
			xil_printf("Configuring default IP of 192.168.1.10\r\n");
			IP4_ADDR(&(echo_netif->ip_addr),  192, 168,   1, 10);
			IP4_ADDR(&(echo_netif->netmask), 255, 255, 255,  0);
			IP4_ADDR(&(echo_netif->gw),      192, 168,   1,  1);
		}
	}

	ipaddr.addr = echo_netif->ip_addr.addr;
	gw.addr = echo_netif->gw.addr;
	netmask.addr = echo_netif->netmask.addr;
#endif

	print_ip_settings(&ipaddr, &netmask, &gw);

	/* start the application (web server, rxtest, txtest, etc..) */
	start_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();
	}
  
	/* never reached */
	cleanup_platform();

	return 0;
}

来看看start_application这个函数具体怎么做的。

int start_application()
{
	struct tcp_pcb *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;
}

注意,这里最关键的就是tcp_accept,它绑定了Callback,它在echo.c文件中。

err_t accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
{
	static int connection = 1;

	/* set the receive callback for this connection */
	tcp_recv(newpcb, recv_callback);

	/* just use an integer number indicating the connection id as the
	   callback argument */
	tcp_arg(newpcb, (void*)(UINTPTR)connection);

	/* increment for subsequent accepted connections */
	connection++;

	return ERR_OK;
}

注意这里的编程技巧,这是C语言提供的一个灵活性,函数内的static变量。无论何处声明的static变量,都会被放到静态区中,所以虽然在函数内声明变量,也不是放在stack frame中。区别在于,函数内的static变量,编译时,会被自动添加函数标号作为前缀。由于有函数标号作为前缀,这也表明的变量的访问范围,所以即使这个变量放在静态区,也只能被这个函数访问,而不能被其他函数访问。

这是局部静态变量和全局静态变量的最大区别。

关键在于,为TCP_PCB设置了Callback,即recv_callback。

void
tcp_arg(struct tcp_pcb *pcb, void *arg)
{
  /* This function is allowed to be called for both listen pcbs and
     connection pcbs. */
  pcb->callback_arg = arg;
}
void
tcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv)
{
  LWIP_ASSERT("invalid socket state for recv callback", pcb->state != LISTEN);
  pcb->recv = recv;
}
void
tcp_sent(struct tcp_pcb *pcb, tcp_sent_fn sent)
{
  LWIP_ASSERT("invalid socket state for sent callback", pcb->state != LISTEN);
  pcb->sent = sent;
}

从这几个函数来看,他们的功能,只是简单的配置TCP_PCB的相关成员。

recv_callback,是当TCP数据包被接收后,TCP_PCB具体执行的Callback。这个函数位于echo.c文件中。此时的PBUF,是已经被LWIP协议处理后的TCP的数据包。

err_t recv_callback(void *arg, struct tcp_pcb *tpcb,
                               struct pbuf *p, err_t err)
{
	/* do not read the packet if we are not in ESTABLISHED state */
	if (!p) {
		tcp_close(tpcb);
		tcp_recv(tpcb, NULL);
		return ERR_OK;
	}

	/* indicate that the packet has been received */
	tcp_recved(tpcb, p->len);

	/* echo back the payload */
	/* in this case, we assume that the payload is < TCP_SND_BUF */
	if (tcp_sndbuf(tpcb) > p->len) {
		err = tcp_write(tpcb, p->payload, p->len, 1);
	} else
		xil_printf("no space in tcp_sndbuf\n\r");

	/* free the received pbuf */
	pbuf_free(p);

	return ERR_OK;
}

其中的关键操作就是tcp_write,它的作用是把payload裸内存,加入TCP_PCB的发送数据队列queue中。这个函数实现了ECHO功能。

当LWIP接收到数据包并处理后,会调用recv这个Callback,做一些用户特定的后续操作。

官方说明如下。

#if LWIP_CALLBACK_API
  /* Function to be called when more send buffer space is available. */
  tcp_sent_fn sent;
  /* Function to be called when (in-sequence) data has arrived. */
  tcp_recv_fn recv;
  /* Function to be called when a connection has been set up. */
  tcp_connected_fn connected;
  /* Function which is called periodically. */
  tcp_poll_fn poll;
  /* Function to be called whenever a fatal error occurs. */
  tcp_err_fn errf;
#endif /* LWIP_CALLBACK_API */

LWIP协议栈的运行时机,是由TIMER中断驱动的。可以简单理解为,LWIP运行在另外一个进程中。LWIP协议栈通过轮询NETIF,执行发送和接收任务。LWIP协议栈每次运行返回后,会让出CPU,从而让后台业务可以继续。

LWIP运行时,会逐个处理NETIF所关联的PBUF。所以用户所需要做的,就是把需要发送的数据提交到NETIF的queue中。等到LWIP运行时,再去处理queue。

Callback在LWIP每个运行时机,被调用执行。

当LWIP判断有数据包被接收时,会调用recv,希望由用户的Callback来进一步处理接收的数据包,然后将payload移出接收缓冲区。如果没有Callback,那么这个payload就被直接移出接收缓冲区了。

当LWIP在发送数据包后,判断有空闲的发送缓冲资源时,会调用sent,希望由用户的Callback来填充缓冲资源。如果没有Callback,那么这个缓冲资源就被闲置。

Callback机制,是multiple process编程思想的一种体现。

用户程序并不需要在自己的后台任务中控制Callback操作的运行时机,无需调用Callback。而前台任务则负责控制Callback操作的运行时机,根据条件来调用Callback。从而使Callback转入后台。

用户不需要关心Callback操作具体什么时候运行,只需要关心,当前台另一个进程需要Callback运行时,Callback究竟需要做哪些操作。这样,设计任务就简化为编写Callback的具体操作流程。



版权声明:本文为weixin_42418557原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。