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效率高。
选项名称 | 说明 | 默认值 |
---|---|---|
api_mode{RAW_API|SOCKET_API} | lwIP库的操作模式 | RAW_API |
socket_mode_thread_prio | lwIP线程的优先级,仅当使用Xilkernel的优先级模式时才有效 | 1 |
use_axieth_on_zynq | 如果要在Zynq中使用AxiEthernet软核IP,需要设置此选项来关闭MAC核(GigE) | 0表示使用GigE控制器;1表示使用AxiEthernet |
2.配置Xilinx适配器选项
axi_ethernetlite适配器(ethernetlite_adapter_options)相关的配置参数如下:
属性 | 说明 | 默认值 |
---|---|---|
sw_rx_fifo_size | 软件缓冲区大小,以EMAC和处理器之间接收数据的字节为单位 | 8192 |
sw_tx_fifo_size | 软件缓冲区大小,以处理器和EMAC之间发送数据的字节为单位 | 8192 |
axi_ethernet和GigE适配器(temac_adapter_options)的相关配置参数如下:
属性 | 默认值 | 说明 |
---|---|---|
n_tx_descriptors | 64 | 使用Tx描述符的数量,高性能系统增大此值 |
n_rx_descriptors | 64 | 使用Rx描述符的数量,高性能系统增大此值 |
n_tx_coalesce | 1 | 设置Tx中断合并 |
n_rx_coalesce | 1 | 设置Rx中断合并 |
tcp_rx_checksum_offload | false | 卸载(offload)TCP接收校验和计算 |
tcp_tx_checksum_offload | false | 卸载(offload)TCP发送校验和计算 |
tcp_ip_rx_checksum_ofload | false | 卸载(offload)TCP和IP接收校验和计算 |
tcp_ip_tx_checksum_ofload | false | 卸载(offload)TCP和IP发送校验和计算 |
phy_link_speed | AUTO | 由物理层自动协商链路速度,lwIP据此配置TEMAC/GigE,某些PHY可能不支持自动检测,此时这个值必须设置正确 |
temac_use_jumbo_frames_experimental | false | 使用TEMAC巨型帧(可达9k字节)。设置为true,TEMAC将允许传输和接收巨型帧。 |
3.配置内存选项
lwIP栈提供了不同种类的内存。当应用程序使用socket模式时,将使用不同的内存选项。所有可配置的内存选项都作为单独的类别提供。
属性 | 默认值 | 说明 |
---|---|---|
mem _size | 131072 | 可用堆内存的总大小(字节),如果应用程序需要从堆中使用大量内存应考虑增大此值 |
memp_n_pbuf | 16 | memp结构pbuf的数量,如果应用程序需要从ROM等静态内存中发送大量数据,考虑增大此值 |
memp_n_udp_pcb | 4 | UDP协议控制块的数量,每个活跃的UDP连接占用一个控制块 |
memp_n_tcp_pcb | 32 | 同上,同时活跃的TCP连接数 |
memp_n_tcp_pcb_listen | 8 | 监听TCP连接的数量 |
memp_n_tcp_seg | 256 | 同时排队的TCP段的数量 |
memp_n_sys_timeout | 8 | 同时活跃的超时(timeout)数量 |
memp_num_netbuf | 8 | netbufs类型的结构体实例的数量,仅在socket模式下可用 |
memp_num_netconn | 16 | netconns类型的结构体实例的数量,仅在socket模式下可用 |
memp_num_api_msg | 16 | api_msg类型的结构体实例的数量,仅在socket模式下可用 |
memp_num_tcpip_msg | 64 | TCP/IP msg结构体的数量,仅在socket模式下可用 |
4.pbuf_options
包缓冲区(Pbuf)跨TCP/IP栈的不同层,下面是lwip栈提供的pbuf内存选项,一般情况下无需修改该选项的默认值。
属性 | 默认值 | 说明 |
---|---|---|
pbuf_pool_size | 256 | pbuf池中的缓冲区数目,对于高性能系统,可以考虑设置更大的值 |
pbuf_pool_bufsize | 1700 | pbuf池中每个pbuf的大小,对于支持巨型帧的系统,要设置比巨型帧更大的值 |
pbuf_link_hlen | 16 | 分配给链路级header的字节数 |
5.arp_options
一般情况下无需修改该选项的默认值。
属性 | 默认值 | 说明 |
---|---|---|
arp_table_size | 10 | 缓存的硬件地址IP地址对数目 |
arp_queueing | 1 | 出站数据包在硬件地址解析期间会排队 |
6.lwip_ip_options
下表是下级菜单中的IP参数选项,一般情况下无需修改该选项的默认值。
属性 | 默认值 | 说明 |
---|---|---|
ip_forward | 0 | 1表示启用跨网络接口转发IP数据包的功能在单个网络接口上运行lwIP时设置为0 |
ip_options | 0 | 1表示允许使用IP选项0表示丢弃所有带IP选项的包 |
ip_reassembly | 1 | 重新组装传入的碎片IP数据包 |
ip_flag | 1 | 发送的IP数据包大小超过MTU时对其分段 |
ip_reass_max_pbufs | 128 | 重组的pbuf队列长度 |
ip_frag_max_mtu | 1500 | 任意接口的IP碎片缓冲区的的最大MTU |
ip_default_ttl | 255 | 传输层使用的全局默认TTL值 |
7.icmp_options
该选项只可以设置ICMP的TTL值。Zynq中的GigE核不支持使用ICMP。
8.igmp_options
lwIP支持IGMP协议,该选项没有下级菜单,设置为true可以启用IGMP协议。
9.udp_options
lwIP支持UDP协议,一般情况下无需修改该选项的默认值。
属性 | 默认值 | 说明 |
---|---|---|
lwip_udp | true | 设定是否需要UDP |
udp_ttl | 255 | UDP TTL值 |
10.tcp_options
lwIP支持TCP协议,一般情况下无需修改该选项的默认值。
属性 | 默认值 | 说明 |
---|---|---|
lwip_tcp | true | 设定是否需要TCP |
tcp_ttl | 255 | TCP |
tcp_wnd | 2048 | TCP窗口大小(字节) |
tcp_maxrtx | 12 | TCP最大重传值 |
tcp_synmaxrtx | 4 | TCP最大SYN重传值 |
tcp_queue_ooseq | 1 | 接收顺序错误的TCP队列段在内存不足的情况下可设为0 |
tcp_mss | 1460 | TCP最大段的大小 |
tcp_snd_buf | 8192 | TCP发送缓冲空间(字节) |
11.dhcp_options
lwIP支持DHCP协议,一般情况下无需修改该选项的默认值。
属性 | 默认值 | 说明 |
---|---|---|
lwip_dhcp | false | 设定是否需要DHCP |
Dhcp_does_arp_check | false | 设定ARP是否检查提供的地址 |
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()
这个函数为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)
这个函数为添加任何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)
该函数只在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);
然后,应用程序可以启动单独的线程来完成应用程序中特定的任务。xemacif_input_thread会接收中断处理程序处理的数据,将其传递给lwIP tcpip_thread。
5.xemacpsif_resetrx_on_no_rxdata
void xemacpsif_resetrx_on_no_rxdata(struct netif *netif)
该函数在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;
}