lwip代码分析
lwIP(Lightweight IP)是一个为嵌入式系统设计的轻量级TCP/IP协议栈。它旨在为资源受限的环境提供完整的网络协议功能,同时保持低内存使用和代码大小。由于其模块化的设计,开发者可以根据需要选择包含或排除特定功能,以满足特定应用的资源要求。
Xilinx的lwIP是基于开源lwIP TCP/IP协议栈的一个适应版本,专门为Xilinx的硬件平台,如Zynq-7000和MicroBlaze,进行了优化和集成。Xilinx为其硬件平台提供了lwIP的库,使得开发者可以轻松地在其FPGA和SoC设计中实现网络通信功能。
以lwip TCP Perf Client
为例,这是一个fpga作为TCP Client,像TCP Server发送批量数据,并测试传输性能的例程。
TCP参数
先看几个TCP相关的参数
TCP_CONN_PORT
表示TCP的端口号,在Server中,需要指定该端口号,如果发现tcp一直不通,但ping是可以通的,多半原因是这个端口被占用了;TCP_SERVER_IP_ADDRESS
表示TCP Server的IP地址
FPGA的IP地址是在main.c
里面指定的:
如果TCP Server使用网络调试助手接收数据,设置如下:(需要注意,本地端口号应该是5001,跟代码中匹配)
main函数
main函数的内容如下:
int main(void)
{
struct netif *netif;
/* the mac address of the board. this should be unique per board */
unsigned char mac_ethernet_address[] = {
0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };
netif = &server_netif;
#if defined (__arm__) && !defined (ARMR5)
#if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || \
XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
ProgramSi5324();
ProgramSfpPhy();
#endif
#endif
/* Define this board specific macro in order perform PHY reset
* on ZCU102
*/
#ifdef XPS_BOARD_ZCU102
IicPhyReset();
#endif
init_platform();
xil_printf("\r\n\r\n");
xil_printf("-----lwIP RAW Mode TCP Client Application-----\r\n");
/* initialize lwIP */
lwip_init();
/* Add network interface to the netif_list, and set it as default */
if (!xemac_add(netif, NULL, NULL, NULL, mac_ethernet_address,
PLATFORM_EMAC_BASEADDR)) {
xil_printf("Error adding N/W interface\r\n");
return -1;
}
netif_set_default(netif);
/* now enable interrupts */
platform_enable_interrupts();
/* specify that the network if is up */
netif_set_up(netif);
assign_default_ip(&(netif->ip_addr), &(netif->netmask), &(netif->gw));
print_ip_settings(&(netif->ip_addr), &(netif->netmask), &(netif->gw));
xil_printf("\r\n");
/* print app header */
print_app_header();
/* start the application*/
start_application();
xil_printf("\r\n");
while (1) {
if (TcpFastTmrFlag) {
tcp_fasttmr();
TcpFastTmrFlag = 0;
}
if (TcpSlowTmrFlag) {
tcp_slowtmr();
TcpSlowTmrFlag = 0;
}
xemacif_input(netif);
transfer_data();
}
/* never reached */
cleanup_platform();
return 0;
}
在main函数中,首先就是定义各种网口接口相关的变量,并定义了MAC地址。
netif
这个netif
的指针,需要多关注一下。
在lwIP中,netif
(网络接口)是一个核心的结构体,它代表了一个网络接口,例如以太网接口、Wi-Fi接口等。netif
结构体用于定义和管理这些接口,使lwIP可以在多个接口上运行并进行路由决策。
具体来说,netif
结构体包括了以下几个主要的部分:
- 硬件地址:例如MAC地址。
- IP地址、子网掩码和网关:这些用于IP层的路由和地址决策。
- 状态标志:表示接口的状态,例如是否激活、是否为默认接口等。
- 输入和输出函数指针:这些函数用于处理从该接口接收到的数据包或向该接口发送数据包。
- 其他驱动特定的数据:例如用于DMA的描述符、缓冲区等。
当你在lwIP中添加一个新的网络接口时,你通常会初始化一个netif
结构体并使用netif_add()
函数将其添加到lwIP的接口列表中。这样,lwIP就可以开始在该接口上接收和发送数据包了。
简而言之,netif
是lwIP中用于表示和管理网络接口的关键结构体。
init_platform
在init_platform()
函数中,初始化定时器和中断。
接下来就是lwip的初始化,这三个初始化都是在platform的库里面写好的,直接调用就行。
xemac_add
后面xemac_add的原型如下,可以简单理解为设置网口的mac地址,此处没有设置IP的信息,可以看到传进去的参数都是NULL。
struct netif *
xemac_add(struct netif *netif,
ip_addr_t *ipaddr, ip_addr_t *netmask, ip_addr_t *gw,
unsigned char *mac_ethernet_address,
UINTPTR mac_baseaddr)
netif_set_default
netif_set_default
函数在lwIP中用于设置默认的网络接口。在一个系统中可能存在多个网络接口,但通常只有一个被视为默认接口。当lwIP需要发送数据包,但不知道应该通过哪个接口发送时,它会选择默认接口。
函数原型如下:
/**
* @ingroup netif
* Set a network interface as the default network interface
* (used to output all packets for which no specific route is found)
*
* @param netif the default network interface
*/
void
netif_set_default(struct netif *netif)
{
LWIP_ASSERT_CORE_LOCKED();
if (netif == NULL) {
/* remove default route */
mib2_remove_route_ip4(1, netif);
} else {
/* install default route */
mib2_add_route_ip4(1, netif);
}
netif_default = netif;
LWIP_DEBUGF(NETIF_DEBUG, ("netif: setting default interface %c%c\n",
netif ? netif->name[0] : '\'', netif ? netif->name[1] : '\''));
}
其中,netif
是你希望设置为默认的网络接口的指针。
当你调用这个函数时,传入的netif
结构体会被设置为默认网络接口。这意味着,除非有特定的路由决策指示其他接口,否则所有的出站数据包都会通过这个接口发送。
例如,如果你有一个以太网接口和一个Wi-Fi接口,并且你希望所有的通信默认通过Wi-Fi接口进行,那么你会在初始化Wi-Fi接口后调用netif_set_default
函数,并传入Wi-Fi接口的netif
结构体指针。
这个函数对于确保正确的网络通信行为非常重要,特别是在存在多个网络接口的系统中。
platform_enable_interrupts
这个函数就很容易理解了,就是使能中断,函数原型如下:
void platform_enable_interrupts()
{
/*
* Enable non-critical exceptions.
*/
Xil_ExceptionEnableMask(XIL_EXCEPTION_IRQ);
XScuTimer_EnableInterrupt(&TimerInstance);
XScuTimer_Start(&TimerInstance);
return;
}
netif_set_up
netif_set_up
函数在lwIP中用于激活一个网络接口。当你初始化一个网络接口并准备好开始接收和发送数据时,你需要调用这个函数来标记该接口为"up"状态。
函数原型如下:
void
netif_set_up(struct netif *netif)
{
LWIP_ASSERT_CORE_LOCKED();
LWIP_ERROR("netif_set_up: invalid netif", netif != NULL, return);
if (!(netif->flags & NETIF_FLAG_UP)) {
netif_set_flags(netif, NETIF_FLAG_UP);
MIB2_COPY_SYSUPTIME_TO(&netif->ts);
NETIF_STATUS_CALLBACK(netif);
#if LWIP_NETIF_EXT_STATUS_CALLBACK
{
netif_ext_callback_args_t args;
args.status_changed.state = 1;
netif_invoke_ext_callback(netif, LWIP_NSC_STATUS_CHANGED, &args);
}
#endif
netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV4 | NETIF_REPORT_TYPE_IPV6);
#if LWIP_IPV6
nd6_restart_netif(netif);
#endif /* LWIP_IPV6 */
}
}
其中,netif
是你希望激活的网络接口的指针。
当你调用netif_set_up
函数时,它会执行以下操作:
- 设置
netif
结构体中的flags
字段,标记该接口为"up"状态。 - 如果配置了lwIP的相关回调,例如
NETIF_STATUS_CALLBACK
,那么这些回调函数也会被触发,通知应用程序该接口的状态已经改变。
通常,在你完成网络接口的硬件初始化、分配了必要的资源,并确信接口已经准备好进行通信后,你会调用netif_set_up
函数。这样,lwIP就知道它可以开始在该接口上接收和发送数据包了。
相反地,如果你需要将一个接口标记为"down"状态,例如在接口遇到错误或需要进行维护时,你可以调用netif_set_down
函数。这会告诉lwIP停止在该接口上的通信,直到接口再次被设置为"up"状态。
assign_default_ip
从名字也可以看到出来,就是设置ip地址、Netmask和gate way
函数原型也非常直观,不做过多解释了
static void assign_default_ip(ip_addr_t *ip, ip_addr_t *mask, ip_addr_t *gw)
{
int err;
xil_printf("Configuring default IP %s \r\n", DEFAULT_IP_ADDRESS);
err = inet_aton(DEFAULT_IP_ADDRESS, ip);
if (!err)
xil_printf("Invalid default IP address: %d\r\n", err);
err = inet_aton(DEFAULT_IP_MASK, mask);
if (!err)
xil_printf("Invalid default IP MASK: %d\r\n", err);
err = inet_aton(DEFAULT_GW_ADDRESS, gw);
if (!err)
xil_printf("Invalid default gateway address: %d\r\n", err);
}
start_application
start_application
函数是一个启动网络应用的函数。在很多lwIP的示例应用中,这个函数被用来初始化和启动特定的网络应用,例如启动一个HTTP服务器、TCP客户端、UDP回声服务等。具体的功能和行为取决于应用的需求和设计。这个函数可能会初始化所需的网络资源,设置回调函数,并开始监听网络事件。
- 初始化变量:函数开始时,初始化了一些变量,如
err
用于错误处理,pcb
代表TCP控制块,remote_addr
用于存储远程服务器的IP地址,以及一个循环计数器i
。 - 设置远程服务器的IP地址:
- 如果启用了IPv6(
LWIP_IPV6==1
),则使用inet6_aton
函数将TCP_SERVER_IPV6_ADDRESS
字符串转换为IPv6地址格式并存储在remote_addr
中。 - 如果未启用IPv6,则使用
inet_aton
函数将TCP_SERVER_IP_ADDRESS
字符串转换为IPv4地址格式。
- 如果启用了IPv6(
- 检查IP地址的有效性:如果IP地址转换失败,函数会打印错误消息并返回。
- 创建TCP控制块(PCB):使用
tcp_new_ip_type
函数为客户端创建一个新的TCP控制块。 - 连接到远程服务器:使用
tcp_connect
函数尝试连接到远程服务器的指定IP地址和端口TCP_CONN_PORT
。如果连接成功,tcp_client_connected
回调函数将被注册,以便在连接建立后进行处理。 - 错误处理:如果在上述步骤中出现任何错误,函数会打印相应的错误消息并关闭TCP连接。
- 初始化发送缓冲区:为
send_buf
缓冲区填充数据,数据内容是0到9的数字字符。
总的来说,start_application
函数的主要目的是初始化一个TCP客户端,尝试连接到指定的远程服务器,并准备发送数据。
函数原型如下:
void start_application(void)
{
err_t err;
struct tcp_pcb* pcb;
ip_addr_t remote_addr;
u32_t i;
#if LWIP_IPV6==1
remote_addr.type= IPADDR_TYPE_V6;
err = inet6_aton(TCP_SERVER_IPV6_ADDRESS, &remote_addr);
#else
err = inet_aton(TCP_SERVER_IP_ADDRESS, &remote_addr);
#endif /* LWIP_IPV6 */
if (!err) {
xil_printf("Invalid Server IP address: %d\r\n", err);
return;
}
/* Create Client PCB */
pcb = tcp_new_ip_type(IPADDR_TYPE_ANY);
if (!pcb) {
xil_printf("Error in PCB creation. out of memory\r\n");
return;
}
err = tcp_connect(pcb, &remote_addr, TCP_CONN_PORT,
tcp_client_connected);
if (err) {
xil_printf("Error on tcp_connect: %d\r\n", err);
tcp_client_close(pcb);
return;
}
client.client_id = 0;
/* initialize data buffer being sent with same as used in iperf */
for (i = 0; i < TCP_SEND_BUFSIZE; i++)
send_buf[i] = (i % 10) + '0';
return;
}
tcp_fasttmr和tcp_slowtmr
在lwip的TCP视线中,快速定时器(tcp_fasttmr
)和慢速定时器(tcp_slowtmr
)都是为了TCP连接的维护而存在的,但它们关注的方面和执行频率是不同的。
-
运行频率:
- 快速定时器:通常每250毫秒被调用一次(这是默认值,但可以配置)。
- 慢速定时器:通常每500毫秒被调用一次(这也是默认值,但同样可以配置)。
-
关注的方面:
-
快速定时器 (
tcp_fasttmr
)主要关注:
- 重传管理:如果一个数据段没有得到确认,它会被重新发送。快速定时器负责处理这些重传。
- 延迟确认:TCP不会立刻确认每一个接收到的数据段,而是稍作延迟,以期待有数据可以与确认一同发送,从而减少网络的数据包数量。快速定时器可以触发这些延迟确认的发送。
-
慢速定时器 (
tcp_slowtmr
)主要关注:
- 连接的生命周期管理:例如,关闭那些已经结束但还没有完全关闭的连接。
- 持续活动检测:例如,检查长时间没有活动的连接,并可能发送探测数据段来检查对方是否仍然活跃。
- 超时管理:管理那些因为长时间没有响应而需要关闭的连接。
- 拥塞控制:调整窗口大小和其他与流量控制相关的参数。
-
简而言之,快速定时器主要关注与数据传输直接相关的事务,如重传和确认,而慢速定时器则更多地关注连接的维护、超时和流控制。
tcp_write
tcp_write
函数用于将数据排入到一个TCP连接的发送队列。它是应用程序与 lwIP
TCP层之间的一个关键接口,允许应用程序发送数据到其TCP连接。
以下是关于 tcp_write
函数的一些关键点:
- 非阻塞:与某些TCP/IP实现不同,
tcp_write
是非阻塞的。这意味着,如果当前没有足够的可用缓冲区来容纳你想发送的数据,函数将不会阻塞,而是返回一个错误。 - 排队,不是直接发送:当你调用
tcp_write
时,你实际上是将数据放入发送队列,而不是立即发送数据。真正的数据传输将在后续的lwIP
处理中进行,这可能涉及与其他TCP机制的交互,如拥塞控制。 - 参数:该函数通常接受以下参数:
pcb
:代表TCP连接的控制块。data
:指向要发送数据的指针。len
:要发送的数据的长度。flags
:与数据发送相关的标志。例如,TCP_WRITE_FLAG_COPY
表示应从应用程序的数据缓冲区复制数据(而不是直接引用)。
- 确认机制:使用
tcp_write
发送的数据将在对方确认收到之后才从发送队列中移除。这意味着,即使你已经调用了tcp_write
,你也需要确保你的应用程序继续处理(例如,通过调用tcp_output
或等待lwIP
的主循环)来确保数据被实际发送和确认。 - 合适的调用时间:为了避免不必要的网络拥塞和效率低下,建议在连接建立后或在接收到数据或发送缓冲区有可用空间时(通过相关的TCP回调函数)再调用
tcp_write
。
tcp_write
是 lwIP
的TCP API的一部分,与其他函数(如 tcp_connect
, tcp_listen
, tcp_close
等)一起,提供了完整的TCP功能。在使用它时,重要的是要理解其工作原理,以及与其他TCP操作的交互方式。
公众号:傅里叶的猫