lwIP概述
lwIP是一个用于嵌入式系统的开源TCP/IP协议集,是一套可以独立运行的栈,无需依赖操作系统,但也可以与操作系统同时使用。lwIP提供了两套API(术语为A05PI),供用户选择:
- RAW API:直接访问核心的lwIP栈;
- Socket API:通过BSD socket风格的接口访问lwIP栈。
基于lwIP 1.4.1库版本,SDK提供了相应适配的库,称作lwip 141_v1_x。这个库为Ethernetlite、TEMAC、GigE、MAC核提供了适配器(adapter)。Ethernetlite和TEMAC核用于MicroBlaze系统;GigE控制器和MAC核用于Zynq。想在Xilinx FPGA环境下熟练使用lwIP,不仅要了解lwIP的API用法,还要掌握xilinx适配器的一些知识。
Xilinx中lwIP的使用可以参考xapp1026和UG650;lwIP的开发者主页为 http://savannah.nongnu.org/projects/lwip/ ;查阅lwip的相关知识:https://lwip.fandom.com/wiki/LwIP_Wiki。
设置硬件系统
lwIP支持的硬件系统包含的关键组件如下:
- 处理器:MicroBlaze、Zynq中的Cortex-A9、Zynq UltraScale+ MPSoC系统中的Cortex-A53和Cortex-R5;
- MAC:lwIP支持axi_ethernetlite、axi_ethernet、GigE控制器和MAC核;
- 定时器:基于lwIP RAW API的应用需要按周期间隔调用某些函数,可通过一个带定时器的中断处理器来实现。
- DMA:对于MicroBlaze,axi_ethernet核可以配置一个软DMA引擎或一个FIFO接口。对于Zynq,已经有嵌入的DMA,因此无需额外配置。
上图是一个MicroBlaze系统架构的示例,使用了带DMA的axi_ethernet核。
设置软件系统
把Vivado的硬件平台导入到SDK中时,默认是不包含lwIP库的,因此必须先做相应配置,编译lwIP库到应用程序中。步骤如下:
1.SDK中选择File->New->Xilinx Board Support Package,创建新的板级支持包。
2.设置工程名称和目录。Zynq系列选择FreeRTOS或裸机;MicroBlaze还可以选择XilKernel。点击Finish,弹出配置窗口,配置BSP。
3.选中lwip 141,窗口左侧会出现对lwip库做更详细配置的窗口。
4.配置完成后点击OK,SDK会自动构建包含lwIP的板级支持包。
lwIP的详细配置
lwIP提供了可配置的参数,SDK中可以改变这些参数值。可配置的选项可以分为两类:
- Xilinx适配器的相关选项:Xilinx适配器把这些控制设置用于以太网核;
- 基本lwIP选项:这些选项是lwIP库本身的一部分,包括用于TCP、UDP、IP等其它协议的参数。
1.定制lwIP API模式
lwip141_v1_x支持RAM API和Socket API。RAW API有更好的性能和更低的内存占用,但由于是基于回调机制的,因此不能与其它TCP 栈兼容;Socket API提供了一个BSD socket风格的接口,因此移植性很强,但在性能和内存需求方面没有RAW API效率高。
2.配置Xilinx适配器选项
axi_ethernetlite适配器(ethernetlite_adapter_options)相关的配置参数如下:
axi_ethernet和GigE适配器(temac_adapter_options)的相关配置参数如下:
3.配置内存选项
lwIP栈提供了不同种类的内存。当应用程序使用socket模式时,将使用不同的内存选项。所有可配置的内存选项都作为单独的类别提供。
4.pbuf_options
包缓冲区(Pbuf)跨TCP/IP栈的不同层,下面是lwip栈提供的pbuf内存选项,一般情况下无需修改该选项的默认值。
5.arp_options
一般情况下无需修改该选项的默认值。
6.lwip_ip_options
下表是下级菜单中的IP参数选项,一般情况下无需修改该选项的默认值。
7.icmp_options
该选项只可以设置ICMP的TTL值。Zynq中的GigE核不支持使用ICMP。
8.igmp_options
lwIP支持IGMP协议,该选项没有下级菜单,设置为true可以启用IGMP协议。
9.udp_options
lwIP支持UDP协议,一般情况下无需修改该选项的默认值。
10.tcp_options
lwIP支持TCP协议,一般情况下无需修改该选项的默认值。
11.dhcp_options
lwIP支持DHCP协议,一般情况下无需修改该选项的默认值。
12.stats_options
lwIP栈在设计上可以收集一些统计信息,比如使用的连接数、内存使用量、应用程序使用的信号量的数量。lwIP库提供了函数stats_display()来显示统计值。是否启用该功能在stats选项中设置。该选项下只有一个boolean类型的lwip_stats,默认为false。
13.debug_options
lwIP可以提供调试信息,debug_options下包括lwip_debug、ip_debug、tcp_debug、udp_debug、icmp_debug、igmp_debug、netif_debug、sys_debug、pbuf_debug几个选项,都是boolean类型,设置true/false来打开/关闭对应的调试功能。
软件API
lwIP库提供了两种不同的API:RAW mode和Socket mode。
RAW API基于回调机制,应用程序与TCP栈之间可以直接访问。因此没有额外的socket层,RAW API有优秀的性能表现,但不能与其它TCP栈兼容。此外,Xilinx适配器还为接收数据包提供了xemacif_input程序函数。必须经常调用此函数,将接收到的数据包从中断处理程序移动到lwIP栈。根据接受包的类型,lwIP回调相应的程序。
Socket API是一套BSD socket风格的API。该API提供了一个执行模型,它是一个阻塞的、“打开-读-写-关闭(open-read-write-close)”模型。使用Socket API和Xilinx适配器的应用程序需要生成一个单独的线程xemacif_input_thread。这个线程将接收到的数据包从中断处理程序移动到lwIP的tcpip_thread。必须使用lwIP的sys_thread_new API创建使用lwIP的应用程序线程。
Xilinx适配器提供了一些辅助函数,以简化lwIP API的使用,各函数简要说明如下:
1.lwip_init
void lwip_init()
- 1
这个函数为lwIP数据结构做了初始化,会替换对初始化状态、系统、内存、pbuf、ARP、IP、UDP、TCP的特定调用。
2.xemac_add
struct netif *xemac_add (struct netif *netif, struct ip_addr *ipaddr, struct ip_addr *netmask, struct ip_addr *gw, unsigned char *mac_ethernet_address, unsigned mac_baseaddr)
- 1
这个函数为添加任何Xilinx EMAC IP和GigE核提供了一个统一的接口。这个函数在lwIP的netif_add函数基础上封装的,用于初始化网络接口‘netif’,给定它的IP地址、网络掩码、网关的IP地址、6字节的以太网地址(MAC地址),以及axi_ethernetlite或axi_ethernet MAC核的基地址。
3.xemacif_input
void xemacif_input(struct netif *netif)
- 1
该函数只在RAW模式下可用。Xlinx lwIP适配器在中断模式下工作。接收中断处理程序从EMAC/GigE中将包数据移动并存储在队列中。xemacif_input函数从队列中取出这些包,传递给lwIP。因此在RAW mode下,需要使用这个程序。下面是一个简单示例:
while (1) {
/* receive packets */
xemacif_input(netif);
/* do application specific processing */
}
该程序会通过回调通知已经接收到的数据。
4.xemacif_input_thread
void xemacif_input_thread(struct netif *netif)
该函数只在Socket模式下可用。Socket模式中,应用程序必须启动一个单独的线程来接收输入包。这与RAW模式下的xemacif_input函数功能相同,只不过它驻留在独立的线程中。因此,任何lwIP socket模式下的应用程序都需要有类似如下的代码:
sys_thread_new(“xemacif_input_thread”, xemacif_input_thread, netif,
THREAD_STACK_SIZE, DEFAULT_THREAD_PRIO);
- 1
- 2
然后,应用程序可以启动单独的线程来完成应用程序中特定的任务。xemacif_input_thread会接收中断处理程序处理的数据,将其传递给lwIP tcpip_thread。
5.xemacpsif_resetrx_on_no_rxdata
void xemacpsif_resetrx_on_no_rxdata(struct netif *netif)
- 1
该函数在Raw模式和Socket模式下都可用,但只能用于Zynq系列的GigE控制器。GigE控制器上有一个与Rx路径有关的勘误表(errata)。该勘误表描述了当小数据包的Rx流量过大时,GigE的Rx路径完全没有响应的情况。这种情况很少发生,但发生时需要对控制器中的Rx逻辑进行软件重置。用户的应用程序必须周期性地调用这个函数,以确保Rx路径不会再超过100ms的时间内停止响应。
RAW API程序架构
使用RAW API的应用程序是单线程的,一般具有与下面伪代码类似的主架构:
int main()
{
struct netif *netif, server_netif;
struct ip_addr ipaddr, netmask, gw;
//板子的MAC地址,每个PHY都不同
unsigned char mac_ethernet_address[] =
{0x00, 0x0a, 0x35, 0x00, 0x01, 0x02};
lwip_init();
//把网络接口添加到netif_list, 并设为默认
if (!xemac_add(netif, &ipaddr, &netmask,
&gw, mac_ethernet_address, EMAC_BASEADDR)) {
printf(“Error adding N/W interface\n\r”);
return -1;
}
netif_set_default(netif);
platform_enable_interrupts(); //使能中断
netif_set_up(netif); //指定网络是否打开
start_application(); //启动应用程序,设置回调
//接收并处理包
while (1) {
xemacif_input(netif);
transfer_data(); //执行应用程序的特定功能
}
}
RAW API主要通过异步调用发送和接收的回调函数来工作。
Socket API程序架构
Socket模式下,基于Xilkernel的应用程序可以在Xilkernel软件平台的设置对话框中指定一个静态线程列表,这些线程会在Xilkernel启动时生成。假设main_thread()是一个设定的由Xilkernel启动的线程,在启动Xilkernel调度后,控制权会从应用程序中的“main”转移到这个线程。在main线程中,再创建一个线程(network_thread)来初始化MAC层。
对于基于FreeRTOS(Zynq-7000处理器系统)的应用程序,一旦控制权到达“main”,就会在启动调度程序之前创建一个带有main_thread()入口函数的任务。在FreeRTOS调度程序启动之后,控制权到达main_thread(),在这里进行lwIP的初始化。然后,应用程序再创建一个线程(network_thread)来初始化MAC层。
下面的伪代码展示了一个典型的Socket模式下的程序架构:
void network_thread(void *p)
{
struct netif *netif, server_netif;
struct ip_addr ipaddr, netmask, gw;
//板子的MAC地址,每个PHY都不同
unsigned char mac_ethernet_address[] =
{0x00, 0x0a, 0x35, 0x00, 0x01, 0x02};
netif = &server_netif;
//初始化使用的IP地址
IP4_ADDR(&ipaddr,192,168,1,10);
IP4_ADDR(&netmask,255,255,255,0);
IP4_ADDR(&gw,192,168,1,1);
//把网络接口添加到netif_list, 并设为默认
if (!xemac_add(netif, &ipaddr, &netmask,
&gw, mac_ethernet_address, EMAC_BASEADDR)) {
printf(“Error adding N/W interface\n\r”);
return;
}
netif_set_default(netif);
netif_set_up(netif); //指定网络是否打开
//启动包接收线程
sys_thread_new(“xemacif_input_thread”, xemacif_input_thread,
netif, THREAD_STACKSIZE, DEFAULT_THREAD_PRIO);
//启动应用程序线程
sys_thread_new(“httpd” web_application_thread, 0,
THREAD_STACKSIZE DEFAULT_THREAD_PRIO);
}
int main_thread()
{
//调用sys_thread_new前初始化lwIP
lwip_init();
//使用lwIP的所有线程都要用sys_thread_new()创建
sys_thread_new(“network_thread” network_thread,
NULL, THREAD_STACKSIZE DEFAULT_THREAD_PRIO);
return 0;
}