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的具体操作流程。