uIP学习笔记

原文链接:http://blog.csdn.net/xukai871105/article/details/17471865#comments

1.前言

最近半年的时间一直在学习应用嵌入式以太网。虽然学习的动机仅仅是玩玩,但是以太网真的深深吸引了我。这里我和各位分享一下uIP的使用经验。uIP是一个简单好用的嵌入式网络协议栈,易于移植且消耗的内存空间较少,非常适合学习和使用。可以肯定的说uIP是嵌入式以太网学习的好起点,但不一定是终点。uIP的功能远不如LwIP强大,但两者并没有孰优孰劣之分,uIP和LwIP的作者同为Adam Dunkels,LwIP开发较早uIP开发较晚,uIP经过这几年的发展从IPV4迁移到IPV6,最终可以适用于无线传感网络。总的来说,uIP是一个很好的起点,学好uIP可以迁移到LwIP,也可以迁移到uIPV6。
【 uIP官方代码】

2.搭建实验环境

先讲一下如何搭建实验环境。建议于把开发板接到路由器上,而调试使用的PC机通过有线或者无线接到路由器上,保证开发板和PC机接入同一个路由器。由于uIP不支持DHCP(不直接支持),所以需要保证开发板和PC位于相同的子网,开发板的IP地址、路由器地址和子网掩码都需要手动设定。设定之前最好看看调试PC机IP地址和路由器(网关)地址。例如调试PC机的IP地址如下图所示。
uIP学习笔记_第1张图片
图1 PC机IP地址

    路由器的IP地址为192.168.1.1。那么开发板的IP地址可以设定为 192.168.1.2到192.168.1.255。为保证你的调试万无一失,还是建议访问路由器,确认此时有哪些设备接入路由器,该步骤的主要功能是避免IP地址重复。
uIP学习笔记_第2张图片
图2 和路由器相连的以太网设备

3.硬件和软件说明

3.1 硬件环境

    奋斗开发板。
    奋斗开发板上有一片ENC28J60,ENC28J60通过SPI接口控制内部寄存器,并有中断输出接口。STM32通过SPI1和ENC28J60相连。具体接口如下:
    SPI1_MISO@PA6
    SPI1_MOSI@PA7
    SPI1_SCK@PA5
    ENC28J60_CS@PA4
    ENC28J60_INT@PA1
    由于SPI上同时挂载其他SPI从设备,所有初始化的过程中需要通过操作CS端口禁止其他SPI从设备。(别小看这步,调试的时候在这步花费了非常多的时间) 其他SPI从设备包括SST25VF016,CS端位于PC5;VS1003,CS端口位于PB12。
    其他说明
    串口调试位于UART1。有三个LED灯分别位于PB.5,PD.6和PD.3。如果您的开发板和有存在差别,请按照顺序修改相关IO口并打开相应RCC时钟。

3.2 软件说明

    工具链为EWARM 6.5。

4.网卡驱动

    网卡驱动采用ENC28J60。具体可参考论坛中的另一篇博文 【ENC28J60学习笔记】
    博文详细分析了如何使用ENC28J60,虽然ENC28J60使用复杂但是深入理解两点即可, 第一点如何通过SPI发送命令和数据第二点理解ENC28J60的缓冲区,在发送以太网和接受以太网数据包的过程中,ENC28J60会帮助用户做些额外的工作,例如 发送时自动填充SFD,在读取接收缓冲区数据时会包含若干状态信息,包括 数据包长度和CRC校验结果等。如果你比较“速食”可以跳过该部分内容,如果你比较“耐心”可以花点时间看看。其他的以太网驱动芯片或RF芯片也遵循相同的规律,可以做到触类旁通。

5.一个简单有效的定时器

    uIP协议栈处理过程需要一个定时配合,该定时器实际为一个软件定时器, 定时器帮助uIP处理若干周期性任务,例如处理TCP连接重传,定时更新ARP缓冲表等。设计定时器的方法很多,在这里推荐uIP原作者的timer模块。timer模块的原理类似于MCU硬件中的比较匹配原理,timer模块中有一个全部变量counter,每次MCU发生某个定时器中断时累加1,如果某个任务需要使用定时器服务,在该任务中声明一个timer(在该任务中为全局变量),并记录此时的counter值。判断溢出可查询当前的counter和被记录的counter的差值,如果差值超过间隔值那么软件定时器timer溢出(类似于发生比较匹配中断)。软件定时器的主要作用有两个。 第一,更新TCP或UDP连接,第二,更新ARP缓冲区(ARP表)。虽然uIP在功能上比LwIP简单的多,但是LwIP也有类似的部分(或者说完全一样)。
    详细代码如下:
[cpp]  view plain copy
  1. /* 引用头文件 *****************************************************************/  
  2. #include "timer.h"  
  3. #include "stm32f10x_it.h"  
  4. /* 私有数据类型 ***************************************************************/  
  5. /* 私有常数宏 *****************************************************************/  
  6. /* 私有动作宏 *****************************************************************/  
  7. /* 私有变量 *******************************************************************/  
  8. /* 全局变量 定时器计数值 */  
  9. uint16_t current_clock = 0;  
  10. /* 私有函数声明 ***************************************************************/  
  11. /* 私有函数 ******************************************************************/  
  12. /* 
  13. ******************************************************************************** 
  14. * 函 数 名: timer_config 
  15. * 功能说明: 使用systick作为时钟 
  16. * 参 数:无 
  17. * 返 回 值: 无 
  18. * 使用说明:可在上电初始化时调用 
  19. * 调用方法:timer_config() 
  20. ******************************************************************************** 
  21. */  
  22. void timer_config(void)  
  23. {  
  24.     /* Systick时钟每秒触发CLOCK_SECOND次 */  
  25.     if (SysTick_Config(SystemCoreClock / CLOCK_SECOND))  
  26.     {  
  27.         while (1);  
  28.     }  
  29. }  
  30. /* 
  31. ******************************************************************************** 
  32. * 函 数 名: SysTick_Handler 
  33. * 功能说明: Systick中断函数 
  34. * 参 数:无 
  35. * 返 回 值: 无 
  36. * 使用说明: 
  37. * 调用方法: 
  38. ******************************************************************************** 
  39. */  
  40. void SysTick_Handler(void)  
  41. {  
  42.     /* 时间标志累加 */  
  43.     current_clock++;  
  44. }  
  45. /* 
  46. ******************************************************************************** 
  47. * 函 数 名: clock_time 
  48. * 功能说明: 返回当前计数值 
  49. * 参 数:无 
  50. * 返 回 值: uint16_t current_clock 当前计数时间 
  51. * 使用说明: 
  52. * 调用方法:clock_time() 
  53. ******************************************************************************** 
  54. */  
  55. uint16_t clock_time(void)  
  56. {  
  57.     return current_clock;  
  58. }  
  59. /* 
  60. ******************************************************************************** 
  61. * 函 数 名: timer_set 
  62. * 功能说明: 设定定时器定时间隔时间 
  63. * 参 数:timer_typedef* ptimer 定时器结构体 
  64. * uint16_t interval 间隔时间 
  65. * 返 回 值: 无 
  66. * 使用说明: 
  67. * 调用方法:timer_set(&arp_timer,10000); 
  68. ******************************************************************************** 
  69. */  
  70. void timer_set(timer_typedef* ptimer,uint16_t interval)  
  71. {  
  72.     /* 设置时间间隔 */  
  73.     ptimer->interval = interval;  
  74.     /* 设置启动时间 */  
  75.     ptimer->start = clock_time();  
  76. }  
  77. /* 
  78. ******************************************************************************** 
  79. * 函 数 名: timer_reset 
  80. * 功能说明: 重新设定定时器 
  81. * 参 数:timer_typedef* ptimer 定时器结构体 
  82. * 返 回 值: 无 
  83. * 使用说明: 
  84. * 调用方法:timer_reset(&arp_timer); 
  85. ******************************************************************************** 
  86. */  
  87. void timer_reset(timer_typedef * ptimer)  
  88. {  
  89.     ptimer->start =ptimer->start + ptimer->interval;  
  90. }  
  91. /* 
  92. ******************************************************************************** 
  93. * 函 数 名: timer_expired 
  94. * 功能说明: 查询定时器是否溢出 
  95. * 参 数:timer_typedef* ptimer 定时器结构体 
  96. * 返 回 值: int8_t 定时器是否溢出 1 代表溢出 0代表未溢出 
  97. * 使用说明: 
  98. * 调用方法:timer_expired(&arp_timer) 
  99. ******************************************************************************** 
  100. */  
  101. int8_t timer_expired(timer_typedef* ptimer)  
  102. {  
  103.     /* 一定要装换为有符号数,进行数学比较时,多使用有符号数 */  
  104.     if((int16_t)(clock_time() - ptimer->start) >= (int16_t)ptimer->interval)  
  105.         return 1;  
  106.     else  
  107.         return 0;  
  108. }  

6.uIP基本结构与配置

6.1 uIP基本结构

    uIP的代码编写需要遵守一定的结构,而且这种结构最好保持稳定(保持不变)。这个结构主要做以下几个部分任务。
    1.获得以太网数据包
    2.处理ARP报文
    3.处理IP报文
    4.定期处理TCP和UDP连接
    5.定期更新ARP缓冲区
   
[cpp]  view plain copy
  1. // BUF指向uIP缓冲区 uip_eth_hdr为以太网首部结构体  
  2. // 6字节目标MAC地址 6字节源MAC地址 2字节类型  
  3. #define BUF ((struct uip_eth_hdr *)&uip_buf[0])   
  4. void GPIO_Config(void);  
  5.   
  6. int main(void)  
  7. {  
  8.     timer_typedef periodic_timer, arp_timer;  
  9.     uip_ipaddr_t ipaddr;  
  10.      
  11.     /* 设定查询定时器 ARP定时器 */  
  12.     timer_set(&periodic_timer, CLOCK_SECOND / 2);  
  13.     timer_set(&arp_timer, CLOCK_SECOND * 10);  
  14.      
  15.     GPIO_Config(); /* 禁止SPI其他设备,防止窜扰 */  
  16.      
  17.     timer_config(); /* 配置systic作为1ms中断 */  
  18.     BSP_ConfigSPI1();  
  19.      
  20.     /* 网卡初始化,ENC28J60,包括MAC地址初始化 */  
  21.     tapdev_init();  
  22.     /* UIP协议栈初始化 */  
  23.     uip_init();  
  24.     /* 设置本机IP地址 */  
  25.     uip_ipaddr(ipaddr, 192,168,1,15);  
  26.     uip_sethostaddr(ipaddr);  
  27.     /* 设置默认路由器IP地址 */  
  28.     uip_ipaddr(ipaddr, 192,168,1,1);  
  29.     uip_setdraddr(ipaddr);  
  30.     /* 设置网络掩码 */  
  31.     uip_ipaddr(ipaddr, 255,255,255,0);  
  32.     uip_setnetmask(ipaddr);  
  33.     /* 用户任务初始化 为TCP echo任务*/  
  34.     example1_init();  
  35.      
  36.     /* 初始化串口 重定义putchar */  
  37.     BSP_ConfigUSART1();  
  38.     /* 打印本机IP地址 */  
  39.     printf("\r\nuip start!\r\n");  
  40.     printf("ipaddr:192.168.1.15\r\n");  
  41.     /* 打印个人信息,呵呵*/  
  42.     printf("eID:xukai871105\r\r");  
  43.     printf("Email:[email protected]");  
  44.      
  45.     while (1)  
  46.     {  
  47.         /* 读取以太网数据包,返回数据长度 */  
  48.         uip_len = tapdev_read();  
  49.         if(uip_len > 0)  
  50.         {  
  51.             /* 收到IP数据包 */  
  52.             if(BUF->type == htons(UIP_ETHTYPE_IP))  
  53.             {  
  54.                 uip_arp_ipin();  
  55.                 uip_input();  
  56.                  
  57.                 if (uip_len > 0)  
  58.                 {  
  59.                     uip_arp_out();  
  60.                     tapdev_send();  
  61.                 }  
  62.             }  
  63.             /* 收到ARP数据包 */  
  64.             else if (BUF->type == htons(UIP_ETHTYPE_ARP))  
  65.             {  
  66.                 uip_arp_arpin();  
  67.                 if (uip_len > 0)  
  68.                 {  
  69.                     tapdev_send();  
  70.                 }  
  71.             }  
  72.         }  
  73.         /* 查询定时器是否超时 */  
  74.         if(timer_expired(&periodic_timer))  
  75.         {  
  76.             timer_reset(&periodic_timer);  
  77.             /* 测试使用,表现为LED灯闪烁 */  
  78.             GPIOB->ODR ^= GPIO_Pin_5;  
  79.             /* 查询并处理所有TCP连接*/  
  80.             for(uint8_t i = 0; i < UIP_CONNS; i++)  
  81.             {  
  82.                 uip_periodic(i);  
  83.                 if(uip_len > 0)  
  84.                 {  
  85.                     uip_arp_out();  
  86.                     tapdev_send();  
  87.                 }  
  88.             }  
  89.              
  90. #if UIP_UDP  
  91.             /* 查询并处理所有UDP连接*/  
  92.             for(uint8_t i = 0; i < UIP_UDP_CONNS; i++)  
  93.             {  
  94.                 uip_udp_periodic(i);  
  95.                 if(uip_len > 0)  
  96.                 {  
  97.                     uip_arp_out();  
  98.                     tapdev_send();  
  99.                 }  
  100.             }  
  101. #endif /* UIP_UDP */  
  102.             /* ARP定时是否溢出 */  
  103.             if (timer_expired(&arp_timer))  
  104.             {  
  105.                 timer_reset(&arp_timer);  
  106.                 uip_arp_timer();  
  107.             }  
  108.         }  
  109.     }  
  110. }  

   【简单说明】
     1.#define BUF ((struct uip_eth_hdr *)&uip_buf[0])
    指向uIP缓冲区,强制类型转化为uip_eth_hdr结构体,uip_eth_hdr即为以太网首部结构,6字节目标MAC地址 6字节源MAC地址 2字节类型。
     2. tapdev_init();tapdev_read();tapdev_send();
    三个函数为以太网操作函数,只有tapdev_read有返回值,其他函数即无输入参数也无返回参数。这三个函数便是ENC28J60操作的三个封装,ENC28J60发送或接收直接操作uIP的两个全局变量uip_buf和uip_len。
    具体代码如下:

[cpp]  view plain copy
  1. #include "tapdev.h"  
  2. #include "uip.h"  
  3. #include "uip_arp.h"  
  4. #include "enc28j60.h"  
  5. // MAC地址  
  6. struct uip_eth_addr uip_mac;  
  7. static unsigned char ethernet_mac[6] = {0x00,0x14,0x0B,0x3F,0x04,0xB1};  
  8. /* 
  9. ******************************************************************************** 
  10. * 函 数 名: tapdev_init 
  11. * 功能说明: 初始化网卡,并赋值MAC地址 
  12. * 参 数:无 
  13. * 返 回 值: 无 
  14. * 使用说明: 
  15. * 调用方法: 
  16. ******************************************************************************** 
  17. */  
  18. void tapdev_init(void)  
  19. {  
  20.     enc28j60_init(ethernet_mac); /*初始化enc28j60 赋值MAC地址*/  
  21.     for (uint8_t i = 0; i < 6; i++)  
  22.     {  
  23.         uip_mac.addr[i] = ethernet_mac[i];  
  24.     }  
  25.     uip_setethaddr(uip_mac); /* 设定uip mac地址*/  
  26. }  
  27. /* 
  28. ******************************************************************************** 
  29. * 函 数 名: tapdev_read 
  30. * 功能说明: 读取以太网数据,最大读取数据包为1500字节 
  31. * 参 数:无 
  32. * 返 回 值: 返回读取数据包大小 
  33. * 使用说明: 
  34. * 调用方法: 
  35. ******************************************************************************** 
  36. */  
  37. uint16_t tapdev_read(void)  
  38. {  
  39.     return enc28j60_packet_receive(uip_buf,1500);  
  40. }  
  41. /* 
  42. ******************************************************************************** 
  43. * 函 数 名: tapdev_send 
  44. * 功能说明: 发送以太网数据包 
  45. * 参 数:无 
  46. * 返 回 值: 无 
  47. * 使用说明:操作uIP全局变量 uip_buf和uip_len 
  48. * 调用方法: 
  49. ******************************************************************************** 
  50. */  
  51. void tapdev_send(void)  
  52. {  
  53.     enc28j60_packet_send(uip_buf,uip_len);  
  54. }  
  55.       

6.2 uIP配置部分

    IP地址配置
        IP地址设置包括,本地IP地址,网关地址和子网掩码。具体代码如下:
    MAC地址配置
        MAC的地址较为特殊,由于ENC28J60本身没有唯一的EUI-48(俗称MAC地址)地址,所以EUI-48地址需要手动配置。该地址不但应用于ENC28J60也应用于uIP。相关代码在上一小节已说明。   

6.3 uip-conf.h部分

        uip-conf部分说明三点
        1.如果不熟悉请保留默认参数,例如UIP_CONF_MAX_CONNECTIONS等
        2.如果设置UIP_CONF_LOGGING为1,请添加void uip_log(char *m){}
        3.必须包含用户任务头文件,且放在该头文件的最后。例如添加#include "example1.h"。这样做的主要目的是定义uip_tcp_appstate_t和UIP_APPCALL两个关键参数。
        具体代码如下:
[cpp]  view plain copy
  1. #ifndef __UIP_CONF_H  
  2. #define __UIP_CONF_H  
  3. #include <inttypes.h>  
  4. typedef uint8_t u8_t;  
  5. typedef uint16_t u16_t;  
  6. typedef unsigned short uip_stats_t;  
  7. /* 最大TCP连接数 */  
  8. #define UIP_CONF_MAX_CONNECTIONS 10  
  9. /* 最大端口监听数 */  
  10. #define UIP_CONF_MAX_LISTENPORTS 10  
  11. /* uIP 缓存大小*/  
  12. #define UIP_CONF_BUFFER_SIZE 1500  
  13. /* CPU字节顺序 */  
  14. #define UIP_CONF_BYTE_ORDER UIP_LITTLE_ENDIAN  
  15. /* 日志开关  */  
  16. #define UIP_CONF_LOGGING 1  
  17. /* UDP支持开关*/  
  18. #define UIP_CONF_UDP 0  
  19. /* UDP校验和开关 */  
  20. #define UIP_CONF_UDP_CHECKSUMS 1  
  21. /* uIP统计开关 */  
  22. #define UIP_CONF_STATISTICS 1  
  23. // 加入用户任务头文件,请修改  
  24. #include "example1.h"  
  25. #endif  

7.案例——最简单的TCP echo程序

    先来一个最简单的TCP程序。uIP作为server,IP地址为192.168.1.15。PC机做client,IP地址为192.168.1.10X。
    1.在网络调试助手中,选择以太网通信种类为client(表示PC机为Client),IP地址输入192.168.1.15,端口号输入1234。最后点击连接。
    2.在发送区域输入任意内容,点击发送数据。
    3.观察返回结果,是否和发送数据相同。
    为了实现该功能新建example1.c和example1.两个文件。代码如下:
[cpp]  view plain copy
  1. #include "example1.h"  
  2. #include "uip.h"  
  3. #include <string.h>  
  4. #include <stdio.h>  
  5. #include <stdint.h>  
  6. void example1_init(void)  
  7. {  
  8.     uip_listen(HTONS(1234));  
  9. }  
  10. void example1_appcall(void)  
  11. {  
  12.     if( uip_newdata() )  
  13.     {  
  14.         // 输出远程IP和端口号  
  15.         printf("remote ip addr:%d.%d.%d.%d\r\n",  
  16.                (uip_conn->ripaddr[0]) & 0X00ff,  
  17.                (uip_conn->ripaddr[0]) >> 8,  
  18.                (uip_conn->ripaddr[1]) & 0X00ff,  
  19.                (uip_conn->ripaddr[1]) >> 8  
  20.                    );  
  21.         printf("remote ip port:%d\r\n",HTONS(uip_conn->rport));  
  22.         // TCP ECHO  
  23.         uip_send(uip_appdata,uip_len);  
  24.     }  
  25. }  


uIP学习笔记_第3张图片

图3 TCP Echo实验结果
     代码做如下分析
    1.uip_listen(HTONS(1234));侦听1234端口,
    2.uip_newdata()即查询uip_buf中是否有新数据,如果返回1的话,表示接收到新数据。
    3.uip_send(uip_appdata,uip_len);uip_send为发送数据包函数
    4.uip_appdata指向用户数据,所谓用户数据即TCP负载数据,例如网络调试助手发送xukai871105,那么uip_appdata指向xukai871105.
    5.uip_len为用户数据长度,若串口调试助手发送xukai871105,那么uip_len为11。

8.wireshark网络包分析

    程序虽然简单,但是TCP通过过程还是可以好好分析一下的。通过wireshark软件抓取整个通信过程。
    其中192.168.1.102为调试PC机(下文简称PC机),192.168.1.15为uIP嵌入式开发板(下文简称uIP)。


图4 网络数据包分析
===================================================

 1.建立连接阶段

【36】PC机向uIP发送SYN,表示请求连接(点击网络调试助手的连接按钮)
【37】uIP向PC机返回ACK,同时发送SYN(注意若接收到SYN标志,必须返回ACK)
【38】PC机向uIP发送应答ACK,表示该次TCP连接成功。
===================================================

2.数据交换阶段

(负载数据包假定为1234)

【51】PC机向uIP发送1234,标志位PSH+ACK,表示该数据包需要立即处理,并需要应答
【52】uIP向PC机返回1234,标志位PSH+ACK,表示该数据包需要立即处理,并需要应答
【53】PC机返回应答,表示PC机接收到echo数据包。
此时数据交换完成,若在网络调试助手再次点击发送,便重复51到53部分。
===================================================

3.关闭连接部分

【65】PC机要求停止连接,发送FIN标志。(点击网络调试助手的关闭按钮)
【66】uIP返回FIN+ACK,表示同意结束本次TCP连接。
【67】PC机发送ACK,表示收到了uIP的FIN。(至此,TCP连接完全结束)
===================================================

10.总结

    1.掌握嵌入式以太网需要较多的背景知识,只能在实践的过程中一点一滴积累。回过头来想想自己的学习嵌入式以太网的经历, 多数时间多是在急躁和失望中度过。唯有耐心与细致并不断学习基础知识才可以把问题解决,最终把想法变成现实。
    2.uIP功能简单,但是易于使用。如果觉得uIP在实际中难以发挥作用的话,还有LwIP作为补充。虽然两者存在功能上的差异,但是TCP连接还是那几个——SYN、ACK、PSH、FIN标志位。LwIP提供套接字通信,这使得嵌入式以太网应用和PC机上的以太网应用变得极为相似。
    3.由于TCP协议属于运输层协议,TCP传输的内容本身并没有含义,这些被传输的数据需要被赋予含义才可以使用。从工业控制来说,MODBUS协议可以应用与TCP协议,并可以实现完善的检测与控制功能。 从其他应用来说,嵌入式系统可以提供HTTP通信、提供web service应用,通过解析JSON格式等手段实现更广泛的应用。
    最后感谢大家的关注,我一定继续努力。若有描述错误的地方请指出,定当更正。

11.推荐图书资料

    《基于IP的物联网架构、技术与应用》。图书作者之一adam dunkels为uIP和LwIP的作者,虽然uIP在书中只占非常小的一部分,但本书信息量较大,技术非常新颖。书中提到的PACHUBE即是在论坛打广告乐为物联的原型。
    《嵌入式Internet TCP IP基础、实现及应用》。本书的TCP IP部分介绍的非常详细,书中有实现嵌入式以太网的代码分析。本书的作者也设计了一套功能完善的TCP IP协议栈。结合书中前半部分的基础和中部的实现,会有非常大的收获。

12.其他网络资料

    第一次有学习嵌入式以太网的冲动便从淘宝上购入ENC28J60模块,卖家提供的源码为国外AVRNET项目的源码。如果耐心一点认真分析AVRNET项目的源代码,并不断修改实践,收获颇丰。顺着ARP、IP、ICMP、UDP、TCP写了几个帖子,算是自己对嵌入式以太网的第一个总结。在这里再次贴一下链接。
【 ARP部分】【 IP和ICMP部分】【 UDP部分】【 TCP部分】
    在这一系类帖子中,还欠了一个HTTP的帖子。通过大家的关注度我发现,ARP部分关注的人最少,因为这个离HTTP最远。包括我在内,得到网络模块ENC28J60的第一个反应就是如果实现网页(HTTP)控制LED灯,读取温度湿度数据。现在回过头来看看基础还是非常重要的。

13.完整代码

【工程代码】


你可能感兴趣的:(uIP学习笔记)