LWIP一句话记住就行:
一项工程,两份配置,三种内存分配,四套操作API,五步初始化,六个"数据流",七个数据结构
-------------------------------------------
从这一讲开始我们来剖析一下lwip_init()中那些子系统的初始化.
先上源码:
/**
* @ingroup lwip_nosys
* Initialize all modules.
* Use this in NO_SYS mode. Use tcpip_init() otherwise.
*/
void
lwip_init(void)
{
#ifndef LWIP_SKIP_CONST_CHECK
int a = 0;
LWIP_UNUSED_ARG(a);
LWIP_ASSERT("LWIP_CONST_CAST not implemented correctly. Check your lwIP port.", LWIP_CONST_CAST(void *, &a) == &a);
#endif
#ifndef LWIP_SKIP_PACKING_CHECK
LWIP_ASSERT("Struct packing not implemented correctly. Check your lwIP port.", sizeof(struct packed_struct_test) == PACKED_STRUCT_TEST_EXPECTED_SIZE);
#endif
/* Modules initialization */
stats_init();
#if !NO_SYS
sys_init();
#endif /* !NO_SYS */
// mem_init();
lwip_mem_init();
memp_init();
pbuf_init();
netif_init();
#if LWIP_IPV4
ip_init();
#if LWIP_ARP
etharp_init();
#endif /* LWIP_ARP */
#endif /* LWIP_IPV4 */
#if LWIP_RAW
raw_init();
#endif /* LWIP_RAW */
#if LWIP_UDP
udp_init();
#endif /* LWIP_UDP */
#if LWIP_TCP
tcp_init();
#endif /* LWIP_TCP */
#if LWIP_IGMP
igmp_init();
#endif /* LWIP_IGMP */
#if LWIP_DNS
dns_init();
#endif /* LWIP_DNS */
#if PPP_SUPPORT
ppp_init();
#endif
#if LWIP_TIMERS
sys_timeouts_init();
#endif /* LWIP_TIMERS */
}
把一些条件编译先摘掉:
void
lwip_init(void)
{
sys_init(); /* 初始化系统的锁 */
mem_init(); /* 初始化内存指针 */
memp_init(); /* 初始化内存池 */
sys_timeouts_init(); /* 定时任务 */
}
少了很多,清楚明了,但是条件编译有些还是需要分析的,特别是一些协议的初始化.那么不多说,我们还是逐一来分析吧.
关于LWIP_SKIP_CONST_CHECK没什么解释的,看到check一般就顾名思义吧,应该是做一些检查.
1.stats_init()
void
stats_init(void)
{
#ifdef LWIP_DEBUG
#if MEM_STATS
lwip_stats.mem.name = "MEM";
#endif /* MEM_STATS */
#endif /* LWIP_DEBUG */
}
好像也没有做什么重要的事情,就是赋值动作.lwip_stats结构体还是很复杂的,进去一看发现各种state的结构体,放在这里吧,不知道有啥用.待进一步研究.
struct stats_ lwip_stats;
/** lwIP stats container */
struct stats_ {
#if LINK_STATS
/** Link level */
struct stats_proto link;
#endif
#if ETHARP_STATS
/** ARP */
struct stats_proto etharp;
#endif
#if IPFRAG_STATS
/** Fragmentation */
struct stats_proto ip_frag;
#endif
#if IP_STATS
/** IP */
struct stats_proto ip;
#endif
#if ICMP_STATS
/** ICMP */
struct stats_proto icmp;
#endif
#if IGMP_STATS
/** IGMP */
struct stats_igmp igmp;
#endif
#if UDP_STATS
/** UDP */
struct stats_proto udp;
#endif
#if TCP_STATS
/** TCP */
struct stats_proto tcp;
#endif
#if MEM_STATS
/** Heap */
struct stats_mem mem;
#endif
#if MEMP_STATS
/** Internal memory pools */
struct stats_mem *memp[MEMP_MAX];
#endif
#if SYS_STATS
/** System */
struct stats_sys sys;
#endif
#if IP6_STATS
/** IPv6 */
struct stats_proto ip6;
#endif
#if ICMP6_STATS
/** ICMP6 */
struct stats_proto icmp6;
#endif
#if IP6_FRAG_STATS
/** IPv6 fragmentation */
struct stats_proto ip6_frag;
#endif
#if MLD6_STATS
/** Multicast listener discovery */
struct stats_igmp mld6;
#endif
#if ND6_STATS
/** Neighbor discovery */
struct stats_proto nd6;
#endif
#if MIB2_STATS
/** SNMP MIB2 */
struct stats_mib2 mib2;
#endif
};
回到lwip_init()主线上来.
2.sys_init()
受NO_SYS宏控制,目前是个空函数,暂时也不管.
3.lwip_mem_init()
/**
* Zero the heap and initialize start, end and lowest-free
*/
void
lwip_mem_init(void)
{
struct mem *mem;
LWIP_ASSERT("Sanity check alignment",
(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);
/* align the heap */
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
/* initialize the start of the heap */
mem = (struct mem *)(void *)ram;
mem->next = MEM_SIZE_ALIGNED;
mem->prev = 0;
mem->used = 0;
/* initialize the end of the heap */
ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);
ram_end->used = 1;
ram_end->next = MEM_SIZE_ALIGNED;
ram_end->prev = MEM_SIZE_ALIGNED;
MEM_SANITY();
/* initialize the lowest-free pointer to the start of the heap */
lfree = (struct mem *)(void *)ram;
MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);
if (sys_mutex_new(&mem_mutex) != ERR_OK) {
LWIP_ASSERT("failed to create mem_mutex", 0);
}
}
内存堆的初始化,初始化了ram和ram_end定义,一般来说,ram只能在ram到ram_end – 1区申请,因为ram_end总是used,如果ram_end一使用,就溢出,他是最后一个元素+1,永远不能用.
3.1定义一个mem结构体
显然是一个双向链表.
/**
* The heap is made up as a list of structs of this type.
* This does not have to be aligned since for getting its size,
* we only use the macro SIZEOF_STRUCT_MEM, which automatically aligns.
*/
struct mem {
/** index (-> ram[next]) of the next struct */
mem_size_t next;
/** index (-> ram[prev]) of the previous struct */
mem_size_t prev;
/** 1: this area is used; 0: this area is unused */
u8_t used;
#if MEM_OVERFLOW_CHECK
/** this keeps track of the user allocation size for guard checks */
mem_size_t user_size;
#endif
};
3.2对齐操作
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
3.3初始化堆的start和end
4.memp_init()
/**
* Initializes lwIP built-in pools.
* Related functions: memp_malloc, memp_free
*
* Carves out memp_memory into linked lists for each pool-type.
*/
void
memp_init(void)
{
u16_t i;
/* for every pool: */
for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {
memp_init_pool(memp_pools[i]);
#if LWIP_STATS && MEMP_STATS
lwip_stats.memp[i] = memp_pools[i]->stats;
#endif
}
#if MEMP_OVERFLOW_CHECK >= 2
/* check everything a first time to see if it worked */
memp_overflow_check_all();
#endif /* MEMP_OVERFLOW_CHECK >= 2 */
}
内存池初始化,这个就不展开讲了,回去看看第四讲,提示一下name,type,size,desc四个主要元素.能想起来吗?
5.pbuf_init()
目前也是空函数
6.netif_init()
重要的东西来了,前面也应提到过了,本来计划单独拿出来的.但是放在这里还是系统些.虽然本篇篇幅会相对较长.
void
netif_init(void)
{
#if LWIP_HAVE_LOOPIF
#if LWIP_IPV4
#define LOOPIF_ADDRINIT &loop_ipaddr, &loop_netmask, &loop_gw,
ip4_addr_t loop_ipaddr, loop_netmask, loop_gw;
IP4_ADDR(&loop_gw, 127, 0, 0, 1);
IP4_ADDR(&loop_ipaddr, 127, 0, 0, 1);
IP4_ADDR(&loop_netmask, 255, 0, 0, 0);
#else /* LWIP_IPV4 */
#define LOOPIF_ADDRINIT
#endif /* LWIP_IPV4 */
#if NO_SYS
netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, ip_input);
#else /* NO_SYS */
netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, tcpip_input);
#endif /* NO_SYS */
#if LWIP_IPV6
IP_ADDR6_HOST(loop_netif.ip6_addr, 0, 0, 0, 0x00000001UL);
loop_netif.ip6_addr_state[0] = IP6_ADDR_VALID;
#endif /* LWIP_IPV6 */
netif_set_link_up(&loop_netif);
netif_set_up(&loop_netif);
#endif /* LWIP_HAVE_LOOPIF */
}
6.1利用ip4_ADDR()函数设置默认网关,隐码,ip地址(就是回环地址).
6.2利用netif_add()函数添加回环接口到netif链表中.
我们以有OS的为例
netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, tcpip_input);
netif_add函数的原型太过庞大,这里我只分析一下参数
struct netif *netif_add(struct netif *netif,const ip4_addr_t *ipaddr, const ip4_addr_t *netmask, const ip4_addr_t *gw,void *state, netif_init_fn init, netif_input_fn input)
参数:添加的网卡接口,ip地址,隐码,网关,状态,网卡接口初始化回调函数,网卡数据接收回调函数
关于网卡接口也就是netif结构体,这里也需要展开来讲:
/** Generic data structure used for all lwIP network interfaces.
* The following fields should be filled in by the initialization
* function for the device driver: hwaddr_len, hwaddr[], mtu, flags */
struct netif {
#if !LWIP_SINGLE_NETIF
/** pointer to next in linked list */
struct netif *next;
#endif
#if LWIP_IPV4
/** IP address configuration in network byte order */
ip_addr_t ip_addr;
ip_addr_t netmask;
ip_addr_t gw;
#endif /* LWIP_IPV4 */
#if LWIP_IPV6
/** Array of IPv6 addresses for this netif. */
ip_addr_t ip6_addr[LWIP_IPV6_NUM_ADDRESSES];
/** The state of each IPv6 address (Tentative, Preferred, etc).
* @see ip6_addr.h */
u8_t ip6_addr_state[LWIP_IPV6_NUM_ADDRESSES];
#if LWIP_IPV6_ADDRESS_LIFETIMES
/** Remaining valid and preferred lifetime of each IPv6 address, in seconds.
* For valid lifetimes, the special value of IP6_ADDR_LIFE_STATIC (0)
* indicates the address is static and has no lifetimes. */
u32_t ip6_addr_valid_life[LWIP_IPV6_NUM_ADDRESSES];
u32_t ip6_addr_pref_life[LWIP_IPV6_NUM_ADDRESSES];
#endif /* LWIP_IPV6_ADDRESS_LIFETIMES */
#endif /* LWIP_IPV6 */
/** This function is called by the network device driver
* to pass a packet up the TCP/IP stack. */
netif_input_fn input;
#if LWIP_IPV4
/** This function is called by the IP module when it wants
* to send a packet on the interface. This function typically
* first resolves the hardware address, then sends the packet.
* For ethernet physical layer, this is usually etharp_output() */
netif_output_fn output;
#endif /* LWIP_IPV4 */
/** This function is called by ethernet_output() when it wants
* to send a packet on the interface. This function outputs
* the pbuf as-is on the link medium. */
netif_linkoutput_fn linkoutput;
#if LWIP_IPV6
/** This function is called by the IPv6 module when it wants
* to send a packet on the interface. This function typically
* first resolves the hardware address, then sends the packet.
* For ethernet physical layer, this is usually ethip6_output() */
netif_output_ip6_fn output_ip6;
#endif /* LWIP_IPV6 */
#if LWIP_NETIF_STATUS_CALLBACK
/** This function is called when the netif state is set to up or down
*/
netif_status_callback_fn status_callback;
#endif /* LWIP_NETIF_STATUS_CALLBACK */
#if LWIP_NETIF_LINK_CALLBACK
/** This function is called when the netif link is set to up or down
*/
netif_status_callback_fn link_callback;
#endif /* LWIP_NETIF_LINK_CALLBACK */
#if LWIP_NETIF_REMOVE_CALLBACK
/** This function is called when the netif has been removed */
netif_status_callback_fn remove_callback;
#endif /* LWIP_NETIF_REMOVE_CALLBACK */
/** This field can be set by the device driver and could point
* to state information for the device. */
void *state;
#ifdef netif_get_client_data
void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA];
#endif
#if LWIP_NETIF_HOSTNAME
/* the hostname for this netif, NULL is a valid value */
const char* hostname;
#endif /* LWIP_NETIF_HOSTNAME */
#if LWIP_CHECKSUM_CTRL_PER_NETIF
u16_t chksum_flags;
#endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/
/** maximum transfer unit (in bytes) */
u16_t mtu;
#if LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES
/** maximum transfer unit (in bytes), updated by RA */
u16_t mtu6;
#endif /* LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES */
/** link level hardware address of this interface */
u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
/** number of bytes used in hwaddr */
u8_t hwaddr_len;
/** flags (@see @ref netif_flags) */
u8_t flags;
/** descriptive abbreviation */
char name[2];
/** number of this interface. Used for @ref if_api and @ref netifapi_netif,
* as well as for IPv6 zones */
u8_t num;
#if LWIP_IPV6_AUTOCONFIG
/** is this netif enabled for IPv6 autoconfiguration */
u8_t ip6_autoconfig_enabled;
#endif /* LWIP_IPV6_AUTOCONFIG */
#if LWIP_IPV6_SEND_ROUTER_SOLICIT
/** Number of Router Solicitation messages that remain to be sent. */
u8_t rs_count;
#endif /* LWIP_IPV6_SEND_ROUTER_SOLICIT */
#if MIB2_STATS
/** link type (from "snmp_ifType" enum from snmp_mib2.h) */
u8_t link_type;
/** (estimate) link speed */
u32_t link_speed;
/** timestamp at last change made (up/down) */
u32_t ts;
/** counters */
struct stats_mib2_netif_ctrs mib2_counters;
#endif /* MIB2_STATS */
#if LWIP_IPV4 && LWIP_IGMP
/** This function could be called to add or delete an entry in the multicast
filter table of the ethernet MAC.*/
netif_igmp_mac_filter_fn igmp_mac_filter;
#endif /* LWIP_IPV4 && LWIP_IGMP */
#if LWIP_IPV6 && LWIP_IPV6_MLD
/** This function could be called to add or delete an entry in the IPv6 multicast
filter table of the ethernet MAC. */
netif_mld_mac_filter_fn mld_mac_filter;
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */
#if LWIP_NETIF_USE_HINTS
struct netif_hint *hints;
#endif /* LWIP_NETIF_USE_HINTS */
#if ENABLE_LOOPBACK
/* List of packets to be queued for ourselves. */
struct pbuf *loop_first;
struct pbuf *loop_last;
#if LWIP_LOOPBACK_MAX_PBUFS
u16_t loop_cnt_current;
#endif /* LWIP_LOOPBACK_MAX_PBUFS */
#endif /* ENABLE_LOOPBACK */
};
有没有被吓到,反正我是被吓到了.所以做一下减法:
条件编译和ipv6的先不看
struct netif {
struct netif *next; //显然可以存在多网卡组成单向链表.
#if LWIP_IPV4
ip_addr_t ip_addr;
ip_addr_t netmask;
ip_addr_t gw;
#endif /* LWIP_IPV4 */
netif_input_fn input; //网卡数据接收回调函数,面向ip协议
netif_output_fn output; //网卡数据发送回调函数
netif_linkoutput_fn linkoutput; //网卡数据接收回调函数,(我理解为面向arp协议)
netif_status_callback_fn status_callback; //网卡启动/关闭回调函数
u16_t mtu; //最大数据传输单元
const char* hostname; //主机名
u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //mac地址,NETIF_MAX_HWADDR_LEN猜到应该是6
u8_t hwaddr_len; //mac地址长度
u8_t flags;
u8_t num; //网卡被用次数
...
}
这样一看就简单多了,结合数据传输理解也很容易想到这些成员变量.
6.3启动接口
netif_set_link_up(&loop_netif);
netif_set_up(&loop_netif);
netif_set_up是使能网卡,设置NETIF_FLAG_UP标志位,必须在网卡被使用前用户来调用
netif_set_link_up是当网卡链路层active时由网卡驱动来设置的,如,station关联上AP后就应该调用netif_set_link_up
7.ip_init()/etharp_init()/raw_init()
都是空函数
8.udp_init()/tcp_init()
这两个函数也简单,
udp_port = UDP_ENSURE_LOCAL_PORT_RANGE(LWIP_RAND());
tcp_port = TCP_ENSURE_LOCAL_PORT_RANGE(LWIP_RAND());
就做了一件事,限制端口号的范围
9.igmp_init()
/**
* Initialize the IGMP module
*/
void
igmp_init(void)
{
LWIP_DEBUGF(IGMP_DEBUG, ("igmp_init: initializing\n"));
IP4_ADDR(&allsystems, 224, 0, 0, 1);
IP4_ADDR(&allrouters, 224, 0, 0, 2);
}
10.dns_init()
这个也需要详细讲解一下
/**
* Initialize the resolver: set up the UDP pcb and configure the default server
* (if DNS_SERVER_ADDRESS is set).
*/
void
dns_init(void)
{
#ifdef DNS_SERVER_ADDRESS
/* initialize default DNS server address */
ip_addr_t dnsserver;
DNS_SERVER_ADDRESS(&dnsserver);
dns_setserver(0, &dnsserver);
#endif /* DNS_SERVER_ADDRESS */
LWIP_ASSERT("sanity check SIZEOF_DNS_QUERY",
sizeof(struct dns_query) == SIZEOF_DNS_QUERY);
LWIP_ASSERT("sanity check SIZEOF_DNS_ANSWER",
sizeof(struct dns_answer) <= SIZEOF_DNS_ANSWER_ASSERT);
LWIP_DEBUGF(DNS_DEBUG, ("dns_init: initializing\n"));
/* if dns client not yet initialized... */
#if ((LWIP_DNS_SECURE & LWIP_DNS_SECURE_RAND_SRC_PORT) == 0)
if (dns_pcbs[0] == NULL) {
dns_pcbs[0] = udp_new_ip_type(IPADDR_TYPE_ANY);
LWIP_ASSERT("dns_pcbs[0] != NULL", dns_pcbs[0] != NULL);
/* initialize DNS table not needed (initialized to zero since it is a
* global variable) */
LWIP_ASSERT("For implicit initialization to work, DNS_STATE_UNUSED needs to be 0",
DNS_STATE_UNUSED == 0);
/* initialize DNS client */
udp_bind(dns_pcbs[0], IP_ANY_TYPE, 0);
udp_recv(dns_pcbs[0], dns_recv, NULL);
}
#endif
#if DNS_LOCAL_HOSTLIST
dns_init_local();
#endif
}
10.1设置默认的dns服务器
关于这个,本人在调试过程中深有体会,移植好lwip后,直接ping ip地址一切正常,但是无论怎样就是解析不了域名,完整了跟了dns.c的代码,硬是没有找到哪里有问题,后来还是用ntp时发现的,ntp默认选择了阿里的一个服务器,于是灵机一动,dns当初好像没有这是默认服务器,中间也没有去指定.心累.
core/dns.c:92:#define DNS_SERVER_ADDRESS(ipaddr) (ip4_addr_set_u32(ipaddr, ipaddr_addr(“208.67.222.222”))) /*resolver1.opendns.com */
这样就ok了.
10.2设置dns_pcb控制块
初始化dns table
11.ppp_init()
点对点协议还没有去了解过,这里拿出来大家一起看看吧.
/************************************/
/*** PRIVATE FUNCTION DEFINITIONS ***/
/************************************/
/* Initialize the PPP subsystem. */
int ppp_init(void)
{
#if PPPOS_SUPPORT
LWIP_MEMPOOL_INIT(PPPOS_PCB);
#endif
#if PPPOE_SUPPORT
LWIP_MEMPOOL_INIT(PPPOE_IF);
#endif
#if PPPOL2TP_SUPPORT
LWIP_MEMPOOL_INIT(PPPOL2TP_PCB);
#endif
#if LWIP_PPP_API && LWIP_MPU_COMPATIBLE
LWIP_MEMPOOL_INIT(PPPAPI_MSG);
#endif
LWIP_MEMPOOL_INIT(PPP_PCB);
/*
* Initialize magic number generator now so that protocols may
* use magic numbers in initialization.
*/
magic_init();
return 0;
}
粗略看好像就是两个动作:初始化一些内存池子,然后初始化一个随机数.有什么用?鄙人也不清楚.emm
12.sys_timeouts_init()
/** Initialize this module */
void sys_timeouts_init(void)
{
size_t i;
/* tcp_tmr() at index 0 is started on demand */
for (i = (LWIP_TCP ? 1 : 0); i < LWIP_ARRAYSIZE(lwip_cyclic_timers); i++) {
/* we have to cast via size_t to get rid of const warning
(this is OK as cyclic_timer() casts back to const* */
sys_timeout(lwip_cyclic_timers[i].interval_ms, lwip_cyclic_timer, LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
}
}
最主要就是循环sys_timeout,而这个正是定时任务,网络应用里面很多定时应用.最关键肯定是lwip_cyclic_timer,第二个参数对应的是执行用的fn,然后是参数.
我们看看需要定时的任务有很多.
const int lwip_num_cyclic_timers = LWIP_ARRAYSIZE(lwip_cyclic_timers);
/** This array contains all stack-internal cyclic timers. To get the number of
* timers, use LWIP_ARRAYSIZE() */
const struct lwip_cyclic_timer lwip_cyclic_timers[] = {
#if LWIP_TCP
/* The TCP timer is a special case: it does not have to run always and
is triggered to start from TCP using tcp_timer_needed() */
{TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
#endif /* LWIP_TCP */
#if LWIP_IPV4
#if IP_REASSEMBLY
{IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
#endif /* IP_REASSEMBLY */
#if LWIP_ARP
{ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
#endif /* LWIP_ARP */
#if LWIP_DHCP
{DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},
{DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},
#endif /* LWIP_DHCP */
#if LWIP_AUTOIP
{AUTOIP_TMR_INTERVAL, HANDLER(autoip_tmr)},
#endif /* LWIP_AUTOIP */
#if LWIP_IGMP
{IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)},
#endif /* LWIP_IGMP */
#endif /* LWIP_IPV4 */
#if LWIP_DNS
{DNS_TMR_INTERVAL, HANDLER(dns_tmr)},
#endif /* LWIP_DNS */
#if LWIP_IPV6
{ND6_TMR_INTERVAL, HANDLER(nd6_tmr)},
#if LWIP_IPV6_REASS
{IP6_REASS_TMR_INTERVAL, HANDLER(ip6_reass_tmr)},
#endif /* LWIP_IPV6_REASS */
#if LWIP_IPV6_MLD
{MLD6_TMR_INTERVAL, HANDLER(mld6_tmr)},
#endif /* LWIP_IPV6_MLD */
#if LWIP_IPV6_DHCP6
{DHCP6_TIMER_MSECS, HANDLER(dhcp6_tmr)},
#endif /* LWIP_IPV6_DHCP6 */
#endif /* LWIP_IPV6 */
};
都是xxx_tmr()函数,前面鼓励过大家在调试时大胆的把所有DEBUG宏打开,如果这样做了,你应该就会看到很多xxx_tmr的打印.
比如TCP断开连接等等,都是需要定时器参与的.就知道这一点,足够了.
好了,这一期讲的有点儿多了,不过总算是瞎扯完了.还有很多细节的东西大家自己去挖掘吧!
-------------------------------------------
这期就到这里了,LWIP想怎么玩就怎么玩,我们下期再见.