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中查询。如下:

  1. /** X.x.x: Major version of the stack */
  2. #define LWIP_VERSION_MAJOR 1U
  3. /** x.X.x: Minor version of the stack */
  4. #define LWIP_VERSION_MINOR 4U
  5. /** x.x.X: Revision of the stack */
  6. #define LWIP_VERSION_REVISION 1U

RT-Thread通过宏去指定使用哪个版本的lwIP,熟悉RT-Thread的朋友都知道一般都是使用scons工具(类linux下的make工具)生成项目工程文件(MDK工程、IAR工程等),因此在每个版本的文件夹中包含了一个SConscript文件,该文件中会依赖与相应的宏加入到工程文件中,以lwIP1.4.1中的SConscript为例:

  1. 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中包含很多设备,为了更简单的添加或者管理这些设备,使用面向对象的思想将设备抽象成了一个类,基于这个“设备类”,派生出不同类型的设备类,如:网络设备类、字符设备类、块设备类、音频设备类等等,它们的关系图如下:

RTT设备继承关系

除基类以外,其他继承自基类的类分别加上了与基类不同的属性和接口,比如设备类中就添加了基类没有的设备初始化,打开,关闭的接口和设备类型的属性。

有了这个概念接着说RT-Thread中设备的管理,RT-Thread中有一个数组,里面为每一种对象(信号、邮箱、设备、定时器)分配了一个链表(用结构体封装了),如下:

  1. struct rt_object_information
  2. {
  3. enum rt_object_class_type type; /**< object class type*/
  4. rt_list_t object_list; /**< object list */
  5. rt_size_t object_size; /**< object size */
  6. };
  7.  
  8. struct rt_object_information rt_object_container[RT_Object_Class_Unknown] =
  9. {
  10. /* initialize object container - thread */
  11. {
  12. RT_Object_Class_Thread, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Thread),
  13. sizeof(struct rt_thread)
  14. },
  15. #ifdef RT_USING_SEMAPHORE
  16. /* initialize object container - semaphore */
  17. {
  18. RT_Object_Class_Semaphore,
  19. _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Semaphore),
  20. sizeof(struct rt_semaphore)
  21. },
  22. #endif
  23. #ifdef RT_USING_MUTEX
  24. /* initialize object container - mutex */
  25. {
  26. RT_Object_Class_Mutex, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Mutex),
  27. sizeof(struct rt_mutex)
  28. },
  29. #endif
  30. #ifdef RT_USING_EVENT
  31. /* initialize object container - event */
  32. {
  33. RT_Object_Class_Event, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Event),
  34. sizeof(struct rt_event)
  35. },
  36. #endif
  37. #ifdef RT_USING_MAILBOX
  38. /* initialize object container - mailbox */
  39. {
  40. RT_Object_Class_MailBox, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MailBox),
  41. sizeof(struct rt_mailbox)
  42. },
  43. #endif
  44. #ifdef RT_USING_MESSAGEQUEUE
  45. /* initialize object container - message queue */
  46. {
  47. RT_Object_Class_MessageQueue,
  48. _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MessageQueue),
  49. sizeof(struct rt_messagequeue)
  50. },
  51. #endif
  52. #ifdef RT_USING_MEMHEAP
  53. /* initialize object container - memory heap */
  54. {
  55. RT_Object_Class_MemHeap, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MemHeap),
  56. sizeof(struct rt_memheap)
  57. },
  58. #endif
  59. #ifdef RT_USING_MEMPOOL
  60. /* initialize object container - memory pool */
  61. {
  62. RT_Object_Class_MemPool, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_MemPool),
  63. sizeof(struct rt_mempool)
  64. },
  65. #endif
  66. #ifdef RT_USING_DEVICE
  67. /* initialize object container - device */
  68. {
  69. RT_Object_Class_Device, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Device),
  70. sizeof(struct rt_device)
  71. },
  72. #endif
  73. /* initialize object container - timer */
  74. {
  75. RT_Object_Class_Timer, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Timer),
  76. sizeof(struct rt_timer)
  77. },
  78. #ifdef RT_USING_MODULE
  79. /* initialize object container - module */
  80. {
  81. RT_Object_Class_Module, _OBJ_CONTAINER_LIST_INIT(RT_Object_Class_Module),
  82. sizeof(struct rt_module)
  83. },
  84. #endif
  85. };

具体地讲,RT-Thread中使用一个链表来维护所有的设备,当需要往系统中注册设备时,需要将设备添加到对应的链表中(当然如何添加,RT-Thread提供了相应的接口)。如果对代码不了解,简单点的理解方式请看下图(图中并不对应实际的代码,代码中用的双向链表):

RTT网络设备管理

从图中可知,当系统需要操作网卡时,直接遍历这个链表即可。

RT-Thread lwIP有哪些变化

  • 上面提到过,将sys.c/h中的接口实现基本的移植工作就完成了,细心的读者可能会拿RT-Thread中lwIP这部分源码与lwIP官方的源码做一个对比,然后会发现RT-Thread增加一个“arch”目录,这部分代码主要实现了前面提到的信号量、互斥锁、邮箱等sys.h文件中的接口,另外RT-Thread根据其系统自身增加了lwIP的初始化工作,如下:
  1. /**
  2. * LwIP system initialization
  3. */
  4. void lwip_system_init(void)
  5. {
  6. rt_err_t rc;
  7. struct rt_semaphore done_sem;
  8.  
  9. /* set default netif to NULL */
  10. netif_default = RT_NULL;
  11.  
  12. // 初始化信号量
  13. rc = rt_sem_init(&done_sem, "done", 0, RT_IPC_FLAG_FIFO);
  14.  
  15. if (rc != RT_EOK)
  16. {
  17. LWIP_ASSERT("Failed to create semaphore", 0);
  18.  
  19. return;
  20. }
  21.  
  22. // 这是关键代码,调用sys_thread_new()创建lwIP线程,并回调tcpip_init_done_callback()初始化网卡设备的IP、子网掩码、网关,并设置系统中默认使用的网卡设备。如果查询到当前系统中没有网卡设备,则返回。(大家可能会有疑问,没有网卡设备怎么办?答:还会有其他的地方以添加网卡设备并初始化)。
  23. tcpip_init(tcpip_init_done_callback, (void *)&done_sem);
  24.  
  25. //等待tcpip_init_done_callback()初始化完成
  26. /* waiting for initialization done */
  27. if (rt_sem_take(&done_sem, RT_WAITING_FOREVER) != RT_EOK)
  28. {
  29. rt_sem_detach(&done_sem);
  30.  
  31. return;
  32. }
  33. // 将此信号量从系统的信号量对象链表中删除
  34. rt_sem_detach(&done_sem);
  35.  
  36. /* set default ip address */
  37. #if !LWIP_DHCP //如果未启用DHCP,即表示使用静态IP,则配置默认网卡的IP、子网掩码、网关
  38. if (netif_default != RT_NULL) //上面提到过,如果此时系统还未注册网卡设备,这部分代码也不执行。
  39. {
  40. struct ip_addr ipaddr, netmask, gw;
  41.  
  42. IP4_ADDR(&ipaddr, RT_LWIP_IPADDR0, RT_LWIP_IPADDR1, RT_LWIP_IPADDR2,
  43. RT_LWIP_IPADDR3);
  44. IP4_ADDR(&gw, RT_LWIP_GWADDR0, RT_LWIP_GWADDR1, RT_LWIP_GWADDR2,
  45. RT_LWIP_GWADDR3);
  46. IP4_ADDR(&netmask, RT_LWIP_MSKADDR0, RT_LWIP_MSKADDR1, RT_LWIP_MSKADDR2,
  47. RT_LWIP_MSKADDR3);
  48.  
  49. netifapi_netif_set_addr(netif_default, &ipaddr, &netmask, &gw);
  50. }
  51. #endif
  52. }

这段代码的解释通过注释的方式,大家请参照代码旁边的注释。

  • 单纯在RT-Thread中完成lwIP初始化和创建lwIP线程的工作还是不够的,因为要让协议栈与外界通信,系统必须可以收发数据,所以还需要硬件驱动的支持,这时牵扯到RT-Thread收发包的设计和网卡驱动。这部分的整体框架如下图:
    RTT收发包设计

由此可知,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双网口开发板网卡驱动例程为例,参考以下代码:

  1. #ifdef USING_MAC0
  2. /* set autonegotiation mode */
  3. fm3_emac_device0.phy_mode = EMAC_PHY_AUTO;
  4. fm3_emac_device0.FM3_ETHERNET_MAC = FM3_ETHERNET_MAC0;
  5. fm3_emac_device0.ETHER_MAC_IRQ = ETHER_MAC0_IRQn;
  6.  
  7. // OUI 00-00-0E FUJITSU LIMITED
  8. fm3_emac_device0.dev_addr[0] = 0x00;
  9. fm3_emac_device0.dev_addr[1] = 0x00;
  10. fm3_emac_device0.dev_addr[2] = 0x0E;
  11. /* set mac address: (only for test) */
  12. fm3_emac_device0.dev_addr[3] = 0x12;
  13. fm3_emac_device0.dev_addr[4] = 0x34;
  14. fm3_emac_device0.dev_addr[5] = 0x56;
  15.  
  16. fm3_emac_device0.parent.parent.init = fm3_emac_init;
  17. fm3_emac_device0.parent.parent.open = fm3_emac_open;
  18. fm3_emac_device0.parent.parent.close = fm3_emac_close;
  19. fm3_emac_device0.parent.parent.read = fm3_emac_read;
  20. fm3_emac_device0.parent.parent.write = fm3_emac_write;
  21. fm3_emac_device0.parent.parent.control = fm3_emac_control;
  22. fm3_emac_device0.parent.parent.user_data = RT_NULL;
  23.  
  24. fm3_emac_device0.parent.eth_rx = fm3_emac_rx;
  25. fm3_emac_device0.parent.eth_tx = fm3_emac_tx;
  26.  
  27. /* init tx buffer free semaphore */
  28. rt_sem_init(&fm3_emac_device0.tx_buf_free, "tx_buf0", EMAC_TXBUFNB,
  29. RT_IPC_FLAG_FIFO);
  30. // 关键代码,驱动向系统注册网卡设备
  31. eth_device_init(&(fm3_emac_device0.parent), "e0");
  32. #endif /* #ifdef USING_MAC0 */

eth_device_init()调用eth_device_init_with_flag()接口初始化网卡设备(为网卡添加名称,IP、子网掩码、网关,网卡设备使用的发包和收包接口函数等),并向系统注册网卡设备。到此,解释了一个现象:网卡驱动初始化和lwIP的初始化顺序互换并无影响。