有项目要用以太网,考虑到项目周期和目前广泛应用的TCPIP协议栈,我选择了lwip,根据以前公司
有的407的板子历程,我选择了网络实验4 RAW_TCP客户端实验历程作为入门学习,基于裸编环境,
现在把学习到的心得记录如下,希望能够帮助需要的人。至于lwip的细节部分,请看
《STM32F4 LWIP开发手册_V2.1.pdf》
《LwIP协议栈的设计与实现_中文译稿.pdf》
《LwIP协议栈源码详解.pdf》
rawapi.txt 这篇入门重点看
《LwIP应用指南 V0.01.pdf》这篇入门重点看,介绍几个回调函数的意义
资料历程下载地址:https://download.csdn.net/download/wuhenyouyuyouyu/11119079
Additionaly, memory (de-)allocation functions may be
called from multiple threads (not ISR!) with NO_SYS=0
since they are protected by SYS_LIGHTWEIGHT_PROT and/or
semaphores.
此外,NO_SYS=0时候(这个时候有操作系统),释放内存函数
可以被多线程(不是ISR,即中断服务)调用,因为它们被
SYS_LIGHTWEIGHT_PROT和或semaphores保护。
Only since 1.3.0, if SYS_LIGHTWEIGHT_PROT is set to 1
and LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT is set to 1,
pbuf_free() may also be called from another thread or
an ISR (since only then, mem_free - for PBUF_RAM - may
be called from an ISR: otherwise, the HEAP is only
protected by semaphores).
仅从1.3.0开始,如果SYS_LIGHTWEIGHT_PROT设置为1
和LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT设置为1,
pbuf_free()也可以从另一个线程调用
一个ISR(因为只有这样,mem_free - 对于PBUF_RAM - 可能
从ISR调用:否则,HEAP只是
受信号量保护)。
一、应用到的资源
详细的资源请看官方历程,这里只是介绍大概。
1、PYH芯片:LAN8720;
2、MCU:STM32F407ZGT6;
3、MCU资源:
1)内部RAM:110K,初始化为内存池
//mem1内存参数设定,mem1完全处于内部SRAM里面
#define MEM1_BLOCK_SIZE 32 //内存块大小为32字节
#define MEM1_MAX_SIZE 100*1024 //最大管理内存 110k
#define MEM1_ALLOC_TABLE_SIZE MEM1_MAX_SIZE/MEM1_BLOCK_SIZE //内存表大小
2)心跳定时器,历程在没有开启操作系统情况下,是提供精确软延时(看历程是死查询);
//延时nms
//nms:0~65535
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
3)定时器3,提供10ms的中断,作为lwip的心跳定时器;
TIM3_Int_Init(999,839); //100khz的频率,计数1000为10ms
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
lwip_localtime +=10; //加10
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
这里没必要单独占用一个timer中断,浪费。一些和时间先关的,都可以放到这个中断里面。
其他的按键、屏什么的,可以忽略。
4、lwip相关
1)初始化: lwip_comm_init()
2)lwip的poll回调函数:lwip_periodic_handle()
3)TCP Client的demo:tcp_client_test()
4)以太网的接受中断: lwip_pkt_handle()
二、初始化
初始化看具体的lwip_comm_init()函数,这里需要注意的是里面的初始化都是采用阻塞式的,
并且在不插网线情况下是初始化失败的。
如果要用裸编方式,即lwip的RAW编程模式,则必须解决阻塞问题。
看下阻塞的地方:
LAN8720_Init()的
LAN8720_RST=0; //硬件复位LAN8720
delay_ms(50);
LAN8720_RST=1; //复位结束
ETH_MACDMA_Config()的
while (ETH_GetSoftwareResetStatus() == SET);//等待软件重启网络完成
ETH_Init()的
1)
/* Delay to assure PHY reset */
_eth_delay_(PHY_RESET_DELAY);
2)
就是如下的地方,一直在死等网线被插入,超时报错。
/* We wait for linked status... */
do
{
timeout++;
} while (!(ETH_ReadPHYRegister(PHYAddress, PHY_BSR) & PHY_Linked_Status) && (timeout < PHY_READ_TO));
3)
/* Wait until the auto-negotiation will be completed */
do
{
timeout++;
} while (!(ETH_ReadPHYRegister(PHYAddress, PHY_BSR) & PHY_AutoNego_Complete) && (timeout < (uint32_t)PHY_READ_TO));
4)
/* Delay to assure PHY configuration */
_eth_delay_(PHY_CONFIG_DELAY);
硬件底层的:
ETH_ReadPHYRegister()
do
{
timeout++;
tmpreg = ETH->MACMIIAR;
} while ((tmpreg & ETH_MACMIIAR_MB) && (timeout < (uint32_t)PHY_READ_TO));
ETH_WritePHYRegister()
/* Check for the Busy flag */
do
{
timeout++;
tmpreg = ETH->MACMIIAR;
} while ((tmpreg & ETH_MACMIIAR_MB) && (timeout < (uint32_t)PHY_WRITE_TO));
我测试了下这里寄存器操作大概在timeout=380次左右,并且和网线插拔无关。这块我没有改
,因为改动起来,影响太大。所有调用的地方都需要改。因此除了ETH_ReadPHYRegister()和
ETH_WritePHYRegister(),其它所有函数都要改写为状态机。
三、lwip_periodic_handle()
这个是Lwip要求的周期性的调用函数,工程里面做的有点乱,其思路用该是放到main里面,然后
不停插延时标志,根据各个延时标志调用想用函数。
//LWIP轮询任务
void lwip_periodic_handle()
{
#if LWIP_TCP
//每250ms调用一次tcp_tmr()函数
if (lwip_localtime - TCPTimer >= TCP_TMR_INTERVAL)
{
TCPTimer = lwip_localtime;
tcp_tmr();
}
#endif
//ARP每5s周期性调用一次
if ((lwip_localtime - ARPTimer) >= ARP_TMR_INTERVAL)
{
ARPTimer = lwip_localtime;
etharp_tmr();
}
#if LWIP_DHCP //如果使用DHCP的话
//每500ms调用一次dhcp_fine_tmr()
if (lwip_localtime - DHCPfineTimer >= DHCP_FINE_TIMER_MSECS)
{
DHCPfineTimer = lwip_localtime;
dhcp_fine_tmr();
if ((lwipdev.dhcpstatus != 2)&&(lwipdev.dhcpstatus != 0XFF))
{
lwip_dhcp_process_handle(); //DHCP处理
}
}
//每60s执行一次DHCP粗糙处理
if (lwip_localtime - DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS)
{
DHCPcoarseTimer = lwip_localtime;
dhcp_coarse_tmr();
}
#endif
}
模板的工程是到处调用(while(1)循环的地方都要调用),由于改为了FSM,我的工程只在一处调用
main()的while(1)里面:
while(1){
#ifndef __DEBUG
WDG_CLEAR(); //Çå¹·
#endif
SYS_CALL_SUB_FSM(); //ÄÚºË
user_tcpip_process(); //lwipÈÎÎñ
}
然后:
void user_tcpip_process(void)
{
tcpip_process(); //TCPIP Á¬½Ó·þÎñÆ÷ÈÎÎñ
if(CHECK_TCP_CLIENT_FLAG(TCP_CLIENT_FLAG_LWIP_INIT)){
lwip_periodic_handle(); //lwip¶¨Ê±ÈÎÎñ
}
}
这样思路就比较清晰了。
四、tcp_client_test()
这个函数是实现创建TCP客户端的核心,具体的实现看源码,我只讲个大概。
tcppcb=tcp_new(); //创建一个新的pcb
if(tcppcb) //创建成功
{
IP4_ADDR(&rmtipaddr,lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);
tcp_connect(tcppcb,&rmtipaddr,TCP_CLIENT_PORT,tcp_client_connected); //连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数
}else res=1;
这段就创建了一个TCP控制块,并且用它去连接一个服务器,这个函数是立即返回,并且不报告连接是否
成功,那么如何判断是否连接成功了呢?如果连接成功了,那么就会调用我们传入的回调函数tcp_client_connected
来看下它的实现:
//lwIP TCP连接建立后调用回调函数
err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
{
struct tcp_client_struct *es=NULL;
if(err==ERR_OK)
{
es=(struct tcp_client_struct*)mem_malloc(sizeof(struct tcp_client_struct)); //申请内存
if(es) //内存申请成功
{
es->state=ES_TCPCLIENT_CONNECTED;//状态为连接成功
es->pcb=tpcb;
es->p=NULL;
tcp_arg(tpcb,es); //使用es更新tpcb的callback_arg
tcp_recv(tpcb,tcp_client_recv); //初始化LwIP的tcp_recv回调功能
tcp_err(tpcb,tcp_client_error); //初始化tcp_err()回调函数
tcp_sent(tpcb,tcp_client_sent); //初始化LwIP的tcp_sent回调功能
tcp_poll(tpcb,tcp_client_poll,1); //初始化LwIP的tcp_poll回调功能
tcp_client_flag|=1<<5; //标记连接到服务器了
err=ERR_OK;
}else
{
tcp_client_connection_close(tpcb,es);//关闭连接
err=ERR_MEM; //返回内存分配错误
}
}else
{
tcp_client_connection_close(tpcb,0);//关闭连接
}
return err;
}
如果连接成功,那么就会初始化所有的callback函数,并且初始化了一个成功标志。这里看到有个申请内存的动作,
大小是一个应用层的tcp_client_struct大小,并且当做参数传递给了tcp块,
tcp_arg(tpcb,es); //使用es更新tpcb的callback_arg
这里一样要注意,close时候,如果已经连接了服务器,那么就要在close时候传入指针释放掉,否则内存泄漏;
如果没有连接成功,则close时候传入NULL。我感觉这样做很麻烦,一不小心就内存泄漏了,用一个static变量,
更合适。
再看看tcp_client_test()怎么处理的:
delay_ms(2);
t++;
if(t==200)
{
if((tcp_client_flag&1<<5)==0)//未连接上,则尝试重连
{
tcp_client_connection_close(tcppcb,0);//关闭连接
tcppcb=tcp_new(); //创建一个新的pcb
if(tcppcb) //创建成功
{
tcp_connect(tcppcb,&rmtipaddr,TCP_CLIENT_PORT,tcp_client_connected);//连接到目的地址的指定端口上,当连接成功后回调tcp_client_connected()函数
}
}
t=0;
LED0=!LED0;
}
如果2*200=400ms没有连接上,则重现发起连接,但是连接前必须先调用
tcp_client_connection_close(tcppcb,0);//关闭连接
看看它的实现:
//关闭与服务器的连接
void tcp_client_connection_close(struct tcp_pcb *tpcb, struct tcp_client_struct * es)
{
//移除回调
tcp_abort(tpcb);//终止连接,删除pcb控制块
tcp_arg(tpcb,NULL);
tcp_recv(tpcb,NULL);
tcp_sent(tpcb,NULL);
tcp_err(tpcb,NULL);
tcp_poll(tpcb,NULL,0);
if(es)mem_free(es);
tcp_client_flag&=~(1<<5);//标记连接断开了
}
es参数作用上面接过来,小心内存泄漏即可,(刚开始,我没仔细看,就内存泄漏了)。
这里清除了连接标志(直接清除,不管是否连接到服务器,省掉了判断)。
到此为止,客户端连接到服务器讲解完毕。
五、发送
demo程序是怎么发送的呢?看tcp_client_test(),有如下语句:
if(key==KEY0_PRES)//KEY0按下了,发送数据
{
tcp_client_flag|=1<<7;//标记要发送数据
}
来看看哪里检测这个标志了:
//lwIP tcp_poll的回调函数
err_t tcp_client_poll(void *arg, struct tcp_pcb *tpcb)
{
err_t ret_err;
struct tcp_client_struct *es;
es=(struct tcp_client_struct*)arg;
if(es!=NULL) //连接处于空闲可以发送数据
{
if(tcp_client_flag&(1<<7)) //判断是否有数据要发送
{
es->p=pbuf_alloc(PBUF_TRANSPORT, strlen((char*)tcp_client_sendbuf),PBUF_POOL); //申请内存
pbuf_take(es->p,(char*)tcp_client_sendbuf,strlen((char*)tcp_client_sendbuf)); //将tcp_client_sentbuf[]中的数据拷贝到es->p_tx中
tcp_client_senddata(tpcb,es);//将tcp_client_sentbuf[]里面复制给pbuf的数据发送出去
tcp_client_flag&=~(1<<7); //清除数据发送标志
if(es->p)pbuf_free(es->p); //释放内存
}else if(es->state==ES_TCPCLIENT_CLOSING)
{
tcp_client_connection_close(tpcb,es);//关闭TCP连接
}
ret_err=ERR_OK;
}else
{
tcp_abort(tpcb);//终止连接,删除pcb控制块
ret_err=ERR_ABRT;
}
return ret_err;
}
在tcp_client_poll()里面查询了,OK,看看哪里调用了tcp_client_poll(),
在tcp_client_connected()里面tcp_poll(tpcb,tcp_client_poll,1); //初始化LwIP的tcp_poll回调功能
设置为了lwip的poll函数,那好,看看tcp_poll()
void
tcp_poll(struct tcp_pcb *pcb, tcp_poll_fn poll, u8_t interval)
{
LWIP_ASSERT("invalid socket state for poll", pcb->state != LISTEN);
#if LWIP_CALLBACK_API
pcb->poll = poll;
#else /* LWIP_CALLBACK_API */
LWIP_UNUSED_ARG(poll);
#endif /* LWIP_CALLBACK_API */
pcb->pollinterval = interval;
}
再接着看:
#define TCP_EVENT_POLL(pcb,ret) \
do { \
if((pcb)->poll != NULL) \
(ret) = (pcb)->poll((pcb)->callback_arg,(pcb)); \
else (ret) = ERR_OK; \
} while (0)
在tcp_slowtmr()里面:
/* We check if we should poll the connection. */
++prev->polltmr;
if (prev->polltmr >= prev->pollinterval) {
prev->polltmr = 0;
LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: polling application\n"));
tcp_active_pcbs_changed = 0;
TCP_EVENT_POLL(prev, err);
if (tcp_active_pcbs_changed) {
goto tcp_slowtmr_start;
}
/* if err == ERR_ABRT, 'prev' is already deallocated */
if (err == ERR_OK) {
tcp_output(prev);
}
}
再看:
void
tcp_tmr(void)
{
/* Call tcp_fasttmr() every 250 ms */
tcp_fasttmr();
if (++tcp_timer & 1) {
/* Call tcp_tmr() every 500 ms, i.e., every other timer
tcp_tmr() is called. */
tcp_slowtmr();
}
}
再看,在void lwip_periodic_handle()里面
#if LWIP_TCP
//每250ms调用一次tcp_tmr()函数
if (lwip_localtime - TCPTimer >= TCP_TMR_INTERVAL)
{
TCPTimer = lwip_localtime;
tcp_tmr();
}
#endif
至此我们总算理清了发送关系。这里采用250ms查询发送标志,然后发送数据,实际应用要改为立即调用发送数据,
增加实时性。
六、接收
先看接受的回调函数:
//lwIP tcp_recv()函数的回调函数
err_t tcp_client_recv(void *arg,struct tcp_pcb *tpcb,struct pbuf *p,err_t err)
{
u32 data_len = 0;
struct pbuf *q;
struct tcp_client_struct *es;
err_t ret_err;
LWIP_ASSERT("arg != NULL",arg != NULL);
es=(struct tcp_client_struct *)arg;
if(p==NULL)//如果从服务器接收到空的数据帧就关闭连接
{
es->state=ES_TCPCLIENT_CLOSING;//需要关闭TCP 连接了
es->p=p;
ret_err=ERR_OK;
}else if(err!= ERR_OK)//当接收到一个非空的数据帧,但是err!=ERR_OK
{
if(p)pbuf_free(p);//释放接收pbuf
ret_err=err;
}else if(es->state==ES_TCPCLIENT_CONNECTED) //当处于连接状态时
{
if(p!=NULL)//当处于连接状态并且接收到的数据不为空时
{
memset(tcp_client_recvbuf,0,TCP_CLIENT_RX_BUFSIZE); //数据接收缓冲区清零
for(q=p;q!=NULL;q=q->next) //遍历完整个pbuf链表
{
//判断要拷贝到TCP_CLIENT_RX_BUFSIZE中的数据是否大于TCP_CLIENT_RX_BUFSIZE的剩余空间,如果大于
//的话就只拷贝TCP_CLIENT_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
if(q->len > (TCP_CLIENT_RX_BUFSIZE-data_len)) memcpy(tcp_client_recvbuf+data_len,q->payload,(TCP_CLIENT_RX_BUFSIZE-data_len));//拷贝数据
else memcpy(tcp_client_recvbuf+data_len,q->payload,q->len);
data_len += q->len;
if(data_len > TCP_CLIENT_RX_BUFSIZE) break; //超出TCP客户端接收数组,跳出
}
tcp_client_flag|=1<<6; //标记接收到数据了
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
pbuf_free(p); //释放内存
ret_err=ERR_OK;
}
}else //接收到数据但是连接已经关闭,
{
tcp_recved(tpcb,p->tot_len);//用于获取接收数据,通知LWIP可以获取更多数据
es->p=NULL;
pbuf_free(p); //释放内存
ret_err=ERR_OK;
}
return ret_err;
}
再看tcp_client_connected()的tcp_recv(tpcb,tcp_client_recv); //初始化LwIP的tcp_recv回调功能
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;
}
在tcp_impl.h中
#define TCP_EVENT_RECV(pcb,p,err,ret) \
do { \
if((pcb)->recv != NULL) { \
(ret) = (pcb)->recv((pcb)->callback_arg,(pcb),(p),(err));\
} else { \
(ret) = tcp_recv_null(NULL, (pcb), (p), (err)); \
} \
} while (0)
tcp_input()中
if (recv_data != NULL) {
LWIP_ASSERT("pcb->refused_data == NULL", pcb->refused_data == NULL);
if (pcb->flags & TF_RXCLOSED) {
/* received data although already closed -> abort (send RST) to
notify the remote host that not all data has been processed */
pbuf_free(recv_data);
tcp_abort(pcb);
goto aborted;
}
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {
goto aborted;
}
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {
pcb->refused_data = recv_data;
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \"full\"\n"));
}
}
在ip_input(struct pbuf *p, struct netif *inp)
#if LWIP_RAW
/* raw input did not eat the packet? */
if (raw_input(p, inp) == 0)
#endif /* LWIP_RAW */
{
switch (IPH_PROTO(iphdr)) {
#if LWIP_UDP
case IP_PROTO_UDP:
#if LWIP_UDPLITE
case IP_PROTO_UDPLITE:
#endif /* LWIP_UDPLITE */
snmp_inc_ipindelivers();
udp_input(p, inp);
break;
#endif /* LWIP_UDP */
#if LWIP_TCP
case IP_PROTO_TCP:
snmp_inc_ipindelivers();
tcp_input(p, inp);
break;
#endif /* LWIP_TCP */
在netif_init()
netif_add(&loop_netif, &loop_ipaddr, &loop_netmask, &loop_gw, NULL, netif_loopif_init, ip_input);
在lwip_init(void)有netif_init();lwip_init()被lwip_comm_init()调用。
咦,ip_input0哪里调用了?接着分析
netif_add(struct netif *netif, ip_addr_t *ipaddr, ip_addr_t *netmask,
ip_addr_t *gw, void *state, netif_init_fn init, netif_input_fn input)
有netif->input = input;
在ethernetif_input()中有:
//网卡接收数据(lwip直接调用)
//netif:网卡结构体指针
//返回值:ERR_OK,发送正常
// ERR_MEM,发送失败
err_t ethernetif_input(struct netif *netif)
{
err_t err;
struct pbuf *p;
p=low_level_input(netif);
if(p==NULL) return ERR_MEM;
err=netif->input(p, netif);
if(err!=ERR_OK)
{
LWIP_DEBUGF(NETIF_DEBUG,("ethernetif_input: IP input error\n"));
pbuf_free(p);
p = NULL;
}
return err;
}
//当接收到数据后调用
void lwip_pkt_handle(void)
{
//从网络缓冲区中读取接收到的数据包并将其发送给LWIP处理
ethernetif_input(&lwip_netif);
}
//以太网中断服务函数
void ETH_IRQHandler(void)
{
while(ETH_GetRxPktSize(DMARxDescToGet)!=0) //检测是否收到数据包
{
lwip_pkt_handle();
}
ETH_DMAClearITPendingBit(ETH_DMA_IT_R);
ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS);
}
好了,至此我们把接收逻辑也搞清楚了。网上有的帖子说接受回调是查询方式的,其实这个主要是看tcpip_input()的调用
方式,即lwip_pkt_handle()的调用方式决定,如果是查询的,那么考虑关键代码区就会少很多,如果是中断方式,那么APP
层就要考虑临界代码区保护了。
七、一些修改的心得
1、lwip_comm_init()初始化时候,如果初始化失败,那么可以隔一段时间再初始化,我设置的1分钟,没必要一直初始化,
可能网线没插呢;
2、在此强调tcp_client_connection_close时候,注意内存泄漏;
3、判断网线插拔
if((!(ETH_ReadPHYRegister(LAN8720_PHY_ADDRESS, PHY_BSR) & PHY_Linked_Status))){
//TRACE_INFO("ÍøÏ߶Ͽª =%X \r\n",s_ptTcpPcb->callback_arg);
tcp_client_connection_close(s_ptTcpPcb,s_ptTcpPcb->callback_arg);//¹Ø±ÕÁ¬½Ó
#if LWIP_DHCP
s_tState = FSM_USER_TCPIP_DHCP;
#else
s_tState = FSM_USER_TCPIP_CREATE_TCP_CLIENT;
#endif
TRACE_INFO("网线断开......\r\n");
break;
}
也没必要每次否判断,只要比心跳时间短,就可以了;
4、lwip_periodic_handle()在初始化失败后,没必须调用,无意义;
5、裸编情况下,一定要改为非阻塞模式,否则严重影响实时性;
6、ping命令的时候不能丢包。
发送和接收时候的申请和释放内存分析,请看Lwip pbuf分析
<----------------------------------------------------------------------------------------------------------------------------------->
2019.09.20
今天在做lwip的功能模块是否启用,要灵活可配置。主要实现的思路如下:
1、Lwip的要重新初始化;
2、lwip的内核的内存怎么回收、释放;
3、接收的数据释放;
4、发送的内存释放;
以上几点要保证,否则内存泄漏。鉴于以上思路比较麻烦,如果你的lwip只有一个网卡,使用
比较简单,可以采用如下思路:
1、lwip用到的所有的内存全部是独立的,和其他逻辑不共用;
2、重新初始化所有的内存池;
上面这个思路实现就很简单了,但是在测试中发Lwip重启几次,就进入一个死循环,出不来了。
随后追踪到这个函数:
为什么?lwip如果只初始化一次,肯定没有问题,因为测试了很长时间了。那么还要从重启里面去找,也不是内存泄漏。
我直觉是某个链表没有清掉,思路是把Lwipi用到的全局变量也全部初始化,最后测试,问题解决。