RT-Thread中的lwIP
由于原版的lwIP更适合于在无操作系统的情况下运行,所以RT-Thread在移植lwIP的过程中根据RT-Thread的特点进行了适当调整。其结构如下图所示:
RT-Thread操作系统中的lwIP是从lwIP发布原始版本移植过来,然后添加了设备层以替换原来的驱动层。不同于原版,这里RT-Thread对于以太网数据的收发采用了独立的双线程(erx线程与etx线程)结构:
- erx线程用于以太网报文的接收──当以太网硬件设备收到网络报文产生中断时,中断服务例程将会通过邮箱的形式唤醒erx线程,让erx线程主动进行以太网报文收取过程,当erx线程收到有效网络报文后,它通过邮箱的形式通知给LwIP的主线程(tcp线程);
- tcp的发送操作则是通过邮箱的形式唤醒etx线程进行实际的以太网硬件写入。在正常情况下,erx线程和etx线程的优先级是相同的,用户可以根据自身实际要求进行微调以侧重接收或发送。
lwIP版本
RT-Thread lwIP包含三个版本,分别为:“1.3.2”,“1.4.1”,“2.0.2”,在RT-Thread 3.0版本中默认会选择“2.0.2”版本,lwIP的具体版本号信息可以在src/include/lwip/init.h中查询。如下:
- /** X.x.x: Major version of the stack */
- #define LWIP_VERSION_MAJOR 1U
- /** x.X.x: Minor version of the stack */
- #define LWIP_VERSION_MINOR 4U
- /** x.x.X: Revision of the stack */
- #define LWIP_VERSION_REVISION 1U
RT-Thread通过宏去指定使用哪个版本的lwIP,熟悉RT-Thread的朋友都知道一般都是使用scons工具(类linux下的make工具)生成项目工程文件(MDK工程、IAR工程等),因此在每个版本的文件夹中包含了一个SConscript文件,该文件中会依赖与相应的宏加入到工程文件中,以lwIP1.4.1中的SConscript为例:
- group = DefineGroup('LwIP', src, depend = ['RT_USING_LWIP', 'RT_USING_LWIP141'], CPPPATH = path)
大家可以看到加入该版本下的所有文件依赖与(RT_USING_LWIP、RT_USING_LWIP141)两个宏,这两个宏在RT-Thread源码的rtconfig.h中,这个文件与实际的项目(或者说BSP、开发板相关),点开“bsp”目录下任何一个文件夹都可以找到rtconfig.h,也可以由menuconfig配置后生成对应的rtconfig.h头文件。
RT-Thread 网络设备管理
RT-Thread有一套自己的设备框架,这里只作一个简单的描述,具体请参考《RT-Thread编程指南第六章—I/O设备管理》,可以在RT-Thread入门帖中找到。RT-Thread中包含很多设备,为了更简单的添加或者管理这些设备,使用面向对象的思想将设备抽象成了一个类,基于这个“设备类”,派生出不同类型的设备类,如:网络设备类、字符设备类、块设备类、音频设备类等等,它们的关系图如下:
除基类以外,其他继承自基类的类分别加上了与基类不同的属性和接口,比如设备类中就添加了基类没有的设备初始化,打开,关闭的接口和设备类型的属性。
有了这个概念接着说RT-Thread中设备的管理,RT-Thread中有一个数组,里面为每一种对象(信号、邮箱、设备、定时器)分配了一个链表(用结构体封装了),如下:
- struct rt_object_information
- {
- enum rt_object_class_type type; /**< object class type*/
- rt_list_t object_list; /**< object list */
- rt_size_t object_size; /**< object size */
- };
- struct rt_object_information rt_object_container[RT_Object_Class_Unknown] =
- {
- /* initialize object container - thread */
- {
- RT_Object_Class_Thread, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Thread),
- sizeof(struct rt_thread)
- },
- #ifdef RT_USING_SEMAPHORE
- /* initialize object container - semaphore */
- {
- RT_Object_Class_Semaphore,
- _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Semaphore),
- sizeof(struct rt_semaphore)
- },
- #endif
- #ifdef RT_USING_MUTEX
- /* initialize object container - mutex */
- {
- RT_Object_Class_Mutex, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Mutex),
- sizeof(struct rt_mutex)
- },
- #endif
- #ifdef RT_USING_EVENT
- /* initialize object container - event */
- {
- RT_Object_Class_Event, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Event),
- sizeof(struct rt_event)
- },
- #endif
- #ifdef RT_USING_MAILBOX
- /* initialize object container - mailbox */
- {
- RT_Object_Class_MailBox, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MailBox),
- sizeof(struct rt_mailbox)
- },
- #endif
- #ifdef RT_USING_MESSAGEQUEUE
- /* initialize object container - message queue */
- {
- RT_Object_Class_MessageQueue,
- _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MessageQueue),
- sizeof(struct rt_messagequeue)
- },
- #endif
- #ifdef RT_USING_MEMHEAP
- /* initialize object container - memory heap */
- {
- RT_Object_Class_MemHeap, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MemHeap),
- sizeof(struct rt_memheap)
- },
- #endif
- #ifdef RT_USING_MEMPOOL
- /* initialize object container - memory pool */
- {
- RT_Object_Class_MemPool, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MemPool),
- sizeof(struct rt_mempool)
- },
- #endif
- #ifdef RT_USING_DEVICE
- /* initialize object container - device */
- {
- RT_Object_Class_Device, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Device),
- sizeof(struct rt_device)
- },
- #endif
- /* initialize object container - timer */
- {
- RT_Object_Class_Timer, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Timer),
- sizeof(struct rt_timer)
- },
- #ifdef RT_USING_MODULE
- /* initialize object container - module */
- {
- RT_Object_Class_Module, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Module),
- sizeof(struct rt_module)
- },
- #endif
- };
具体地讲,RT-Thread中使用一个链表来维护所有的设备,当需要往系统中注册设备时,需要将设备添加到对应的链表中(当然如何添加,RT-Thread提供了相应的接口)。如果对代码不了解,简单点的理解方式请看下图(图中并不对应实际的代码,代码中用的双向链表):
从图中可知,当系统需要操作网卡时,直接遍历这个链表即可。
RT-Thread lwIP有哪些变化
- 上面提到过,将sys.c/h中的接口实现基本的移植工作就完成了,细心的读者可能会拿RT-Thread中lwIP这部分源码与lwIP官方的源码做一个对比,然后会发现RT-Thread增加一个“arch”目录,这部分代码主要实现了前面提到的信号量、互斥锁、邮箱等sys.h文件中的接口,另外RT-Thread根据其系统自身增加了lwIP的初始化工作,如下:
- /**
- * LwIP system initialization
- */
- void lwip_system_init(void)
- {
- rt_err_t rc;
- struct rt_semaphore done_sem;
- /* set default netif to NULL */
- netif_default = RT_NULL;
- // 初始化信号量
- rc = rt_sem_init(&done_sem, "done", 0, RT_IPC_FLAG_FIFO);
- if (rc != RT_EOK)
- {
- LWIP_ASSERT("Failed to create semaphore", 0);
- return;
- }
- // 这是关键代码,调用sys_thread_new()创建lwIP线程,并回调tcpip_init_done_callback()初始化网卡设备的IP、子网掩码、网关,并设置系统中默认使用的网卡设备。如果查询到当前系统中没有网卡设备,则返回。(大家可能会有疑问,没有网卡设备怎么办?答:还会有其他的地方以添加网卡设备并初始化)。
- tcpip_init(tcpip_init_done_callback, (void *)&done_sem);
- //等待tcpip_init_done_callback()初始化完成
- /* waiting for initialization done */
- if (rt_sem_take(&done_sem, RT_WAITING_FOREVER) != RT_EOK)
- {
- rt_sem_detach(&done_sem);
- return;
- }
- // 将此信号量从系统的信号量对象链表中删除
- rt_sem_detach(&done_sem);
- /* set default ip address */
- #if !LWIP_DHCP //如果未启用DHCP,即表示使用静态IP,则配置默认网卡的IP、子网掩码、网关
- if (netif_default != RT_NULL) //上面提到过,如果此时系统还未注册网卡设备,这部分代码也不执行。
- {
- struct ip_addr ipaddr, netmask, gw;
- IP4_ADDR(&ipaddr, RT_LWIP_IPADDR0, RT_LWIP_IPADDR1, RT_LWIP_IPADDR2,
- RT_LWIP_IPADDR3);
- IP4_ADDR(&gw, RT_LWIP_GWADDR0, RT_LWIP_GWADDR1, RT_LWIP_GWADDR2,
- RT_LWIP_GWADDR3);
- IP4_ADDR(&netmask, RT_LWIP_MSKADDR0, RT_LWIP_MSKADDR1, RT_LWIP_MSKADDR2,
- RT_LWIP_MSKADDR3);
- netifapi_netif_set_addr(netif_default, &ipaddr, &netmask, &gw);
- }
- #endif
- }
这段代码的解释通过注释的方式,大家请参照代码旁边的注释。
- 单纯在RT-Thread中完成lwIP初始化和创建lwIP线程的工作还是不够的,因为要让协议栈与外界通信,系统必须可以收发数据,所以还需要硬件驱动的支持,这时牵扯到RT-Thread收发包的设计和网卡驱动。这部分的整体框架如下图:
由此可知,RT-Thread中将lwIP应用起来主要包括三个核心步骤:1. 创建收发包线程,调用接口eth_system_device_init()。2. 提供网卡驱动,调用网卡初始化函数,注册网卡设备。(驱动不同相应的接口函数可能不同)3. 初始化lwIP,创建lwIP线程,调用接口lwip_sys_init()(实际调用的lwip_system_init())。
至此,三个步骤完成之后,应用层便可以直接与外界通讯。
RT-Thread lwIP相关代码补充说明
前面已经提及过lwip_system_init()中,当系统中没有网卡设备时,有一部分初始化工作(为网卡初始化IP、子网掩码、网关等)是不会进行的。此时lwIP线程已经创建,如果需要和外界通讯,那么必须为系统添加网卡设备,而在网卡驱动中,网卡设备初始化时,会向系统注册,此时网卡设备就添加到系统中了。以RT-Thread双网口开发板网卡驱动例程为例,参考以下代码:
- #ifdef USING_MAC0
- /* set autonegotiation mode */
- fm3_emac_device0.phy_mode = EMAC_PHY_AUTO;
- fm3_emac_device0.FM3_ETHERNET_MAC = FM3_ETHERNET_MAC0;
- fm3_emac_device0.ETHER_MAC_IRQ = ETHER_MAC0_IRQn;
- // OUI 00-00-0E FUJITSU LIMITED
- fm3_emac_device0.dev_addr[0] = 0x00;
- fm3_emac_device0.dev_addr[1] = 0x00;
- fm3_emac_device0.dev_addr[2] = 0x0E;
- /* set mac address: (only for test) */
- fm3_emac_device0.dev_addr[3] = 0x12;
- fm3_emac_device0.dev_addr[4] = 0x34;
- fm3_emac_device0.dev_addr[5] = 0x56;
- fm3_emac_device0.parent.parent.init = fm3_emac_init;
- fm3_emac_device0.parent.parent.open = fm3_emac_open;
- fm3_emac_device0.parent.parent.close = fm3_emac_close;
- fm3_emac_device0.parent.parent.read = fm3_emac_read;
- fm3_emac_device0.parent.parent.write = fm3_emac_write;
- fm3_emac_device0.parent.parent.control = fm3_emac_control;
- fm3_emac_device0.parent.parent.user_data = RT_NULL;
- fm3_emac_device0.parent.eth_rx = fm3_emac_rx;
- fm3_emac_device0.parent.eth_tx = fm3_emac_tx;
- /* init tx buffer free semaphore */
- rt_sem_init(&fm3_emac_device0.tx_buf_free, "tx_buf0", EMAC_TXBUFNB,
- RT_IPC_FLAG_FIFO);
- // 关键代码,驱动向系统注册网卡设备
- eth_device_init(&(fm3_emac_device0.parent), "e0");
- #endif /* #ifdef USING_MAC0 */
eth_device_init()调用eth_device_init_with_flag()接口初始化网卡设备(为网卡添加名称,IP、子网掩码、网关,网卡设备使用的发包和收包接口函数等),并向系统注册网卡设备。到此,解释了一个现象:网卡驱动初始化和lwIP的初始化顺序互换并无影响。