关于uIP的移植以及部分特性解析和勘误
关于嵌入式网络的领域,uIP是一个值得去学习的轻量级协议栈,在我的理解里,uIP具有如下特点.
1.封装性好
封装性好体现在uIP它能做到的网络协议栈的底层所做的工作都给完成了,当然包括最基本的数据链路层和网络层,当然,物理层是不确定的,需要我们自己写驱动程序接口然后进行接合.整个完整的uIP只留给用户两个宏定义(另外一个为UIP_UDP_APPCALL)
从官方文档可以得知用户应该在编译程序的时候把这个宏定义给加上去,另外任何用户事件的发生都是通过该函数来执行的(新数据到来,连接轮询,tcp链接和断开, 超时等事件).我们进行的任何活动都只需要在里面进行就可以了.所以甚至我们可以直接把一些文件编译成lib文件,直接添加.因为这些文件时用户无关的,我们不需要关心他在干嘛(但是如果你想了解它的运行机制还是得单步跟踪的.).
2.可移植性强
移植很方便,这是我时隔半年又一次学习uIP的感受,差不多半个小时我就已经把uIP整个给搞定了.驱动的话你得在移植uIP之前就应该做完了.然后再配置一下时钟的话就差不多搞定了.
3.速度快
如果你认真的通读了uIP的官方说明文档,会发现作者把lightweight深入到编程的每一个细节,包括尽量减少函数的调用,能用宏定义编写的比较小的固定的函数进行都用宏定义来写,这样子虽然代码容量增大了,但是内存确实是会变的很小,这对于51这种小内存的主控尤其显得至关重要,毕竟再牛逼的51也就1K内存就已经很不错了,但是flash却是可以很大,32K用来搞定这个uIP那已经显得是搓搓有余了.所以这些机制不止节约了内存,同样也加快了速度,你可以通过看uIP_process这个函数来清晰了解到这个思想,虽然我不是很赞成这种做法,因为代码风格很乱,各种goto语句充斥着一个1000多行的主执行函数,那样子任何人也受不鸟,毕竟我是看你程序,而不是写,看到一半上面的思路已经断了.
闲话不多说,让我们开始uIP之旅吧,相信读完我的博文,你也可以轻松的在uIP上进行你的网络应用开发.
PART1:uIP移植
移植的话我就不多讲,这里贴上一个连接,大家照这样子做就行了,我说一些比较需要大家注意的地方,是我在移植中碰到的问题.
时钟机制:uip运行必不可少的就是时间机制,包括超时重传,定时轮询处理(这点在UDP主动发送数据和TCP主动发送数据和超时重传的时候非常重要,没有了timer那么uip就是一个只会ping和arp的傻瓜).
/** * A timer. * * This structure is used for declaring a timer. The timer must be set * with timer_set() before it can be used. * * \hideinitializer */ struct timer { clock_time_t start; clock_time_t interval; };
上面这个是uIP的时钟定义函数,它只实现一个功能,就是写入当前的系统运行时间start,然后还有多久超时interval.当然,这个可以通过已经做好的api实现
/*---------------------------------------------------------------------------*/ /** * Set a timer. * * This function is used to set a timer for a time sometime in the * future. The function timer_expired() will evaluate to true after * the timer has expired. * * \param t A pointer to the timer * \param interval The interval before the timer expires. * */ void timer_set(struct timer *t, clock_time_t interval) { t->interval = interval; t->start = clock_time(); }
clock_time()函数在下面贴出来的代码里面已经实现,这也是移植的一部分,该函数用来获取当前系统运行时间.
/*---------------------------------------------------------------------------*/ /** * Check if a timer has expired. * * This function tests if a timer has expired and returns true or * false depending on its status. * * \param t A pointer to the timer * * \return Non-zero if the timer has expired, zero otherwise. * */ int timer_expired(struct timer *t) { return (clock_time_t)(clock_time() - t->start) >= (clock_time_t)t->interval; }
这个函数则用来判断当前结构体所代表的单元是否已经超时,通过当前系统时间减去设定时候的时间是否大于interval间距来判断,如果超时就返回1,否则是返回0的.
最重要的就是这三个函数,当然,还有一些函数可以去timer.c里面慢慢阅读,在uip1.0里面注释都很清楚.
网卡驱动接口:
#include "uip.h" #include "enc28j60.h" /*---------------------------------------------------------------------------*/ void tapdev_init(unsigned char *my_mac) { enc28j60Init(my_mac); } /*---------------------------------------------------------------------------*/ unsigned int tapdev_read(void) { return enc28j60PacketReceive(UIP_CONF_BUFFER_SIZE,uip_buf); } /*---------------------------------------------------------------------------*/ void tapdev_send(void) { enc28j60PacketSend(uip_len,uip_buf); } /*---------------------------------------------------------------------------*/
这里我所用的网卡是enc28j60,当然,如果你用的是dm9000或者是rtl8019等等之类的显卡,只要测试到能接到数据包就行,剩下的一些网卡运行机制请在中断中实现,比如判断网线是否断开,或者网卡硬件缓冲区是否溢出或者发送异常接收异常等等.
tapdev_init是网卡初始化函数,它必须带入mac地址,顺便的,不仅要让网卡知道当前的本机mac地址,也应该让uip也知道自己的mac地址,移植中很容易忽略的问题.
代码如下:uip_ethaddr就是uip应该知道的mac地址.不然接收到的数据包会被无条件丢弃,除非他是广播包
for (i = 0; i < 6; i++) { uip_ethaddr.addr[i] = macaddr[i]; } printf("MAADR5 = 0x%x\r\n", enc28j60Read(MAADR5)); printf("MAADR4 = 0x%x\r\n", enc28j60Read(MAADR4)); printf("MAADR3 = 0x%x\r\n", enc28j60Read(MAADR3)); printf("MAADR2 = 0x%x\r\n", enc28j60Read(MAADR2)); printf("MAADR1 = 0x%x\r\n", enc28j60Read(MAADR1)); printf("MAADR0 = 0x%x\r\n", enc28j60Read(MAADR0));
#define UIP_UDP_APPCALL MY_UDP_APP
如果开启了UDP支持,那么必须把UDP应用程序接口给定义成自己要用的,比如如上:
void MY_UDP_APP(void) { switch (uip_udp_conn->rport) { case HTONS(6677): UDP_6677_APP(); break; case HTONS(67): dhcpc_appcall(); break; case HTONS(68): dhcpc_appcall(); break; case HTONS(53): resolv_appcall(); } }
定义完之后需要写udp数据分类,uip_udp_conn->rport进行解析可以得出remove端口的值,这个是外部发送过来的数据的端口号.因为UDP是运输层,所以通过上面这个简单的函数可以分类交给应用层进行解析.另外,因为有的CPU字节端序和网络字节序不一样,所以我们统一可以用HTONS来转换我们的顺序,这个函数会取决于你所设定的cpu端顺序.反正请谨记,如果是16bit的数据是从网络上传过来的,都必须HTONS函数调用下才能转化成我们程序能识别的数据.
TCP也是一样.就不赘述了.
uip_tcp_appstate_t:
这个也是编译的时候会碰到的问题,它说白了就是一个结构体重定义,在这里面有讲解到:
如果你没有使用任何关于他之中的tcp应用层程序,比如telnet,或者simple_web.那么这个函数可以直接定义成
typedef int uip_tcp_appstate_t.
没有任何意义,当然也可以定义成自己的结构体,那么他会关联到每个你所使用的tcp链接里面,你去使用和定义tcp连接,每个tcp连接的结构体数据都会独立.这样自己可用性会变强.
在定义这个时候,比如你在app.h定义该函数,必须包含关系如下
app.h <----- uipopt.h
conf.h <----- app.h
uip.c <----- conf.h
当调用uip.h就会自动定义进去了.
问题所在:根据官方文档:这个结构体被多次包含之后,会出现重复定义的状况,这是比较纠结的.如果按照我的水平理解,作者这样子做是不是还要改他的程序,不然smtp和telnet和web应用是不是无法同时开启,但是这就不符合我们的协议栈的设计初衷了.
对此,根据网上的一些方法,我总结如下方法:
1.这个是一个老外的问题解答,通过上述可知,我们可以把app...定义成一个最大的数组(完全取决于你的最大结构体的定义).
2.动态分配法,这个可能会花费的精力比较多点,那就是把app...更改成一个指针,然后每次有一个新的连接建立的时候,就要去申请一个结构体的空间,然后指针指向该结构体,连接完成后再释放,这个方法我感觉不可取,因为堆会照成碎片,到最后甚至没法申请到空间,那么程序就会崩溃,这是致命的.
3.放弃该app...,但是这个事没有办法了,我这边简单介绍下如何使用第一种方法:
因为该结构体是包含在uip.h的,然后空间申请是在uip.c里面的,所以,uip.c是决定该空间的最大关系者.
1 struct StrAppSta 2 { 3 unsigned char strappsta[maxsize]; 4 }; 5 6 typedef struct StrAppSta uip_tcp_appstate_t; 7 8 // 根据程序编译前的拷贝,在包含uip.h时候要先定义app. 9 #include "uip.h" 10 11 // 下面是uip.c的文件内容....... 12 // .......... 13 // .......
警告:在包含uip.h的时候都要注意,不要随意叠加包含,可能会照成如上错误.
其他有用到uip.h但是没有用到该结构体的可以如下定义
typedef int uip_tcp_appstate_t; #include "uip.h" // 下面other.c 的文件内容...... // ................ // ...........
在有用到uip.h的地方,请按如下方法定义:比如telnet
1 #ifndef __TELNETD_H__ 2 #define __TELNETD_H__ 3 4 void telnetd_appcall(void); 5 void telnetd_init(void); 6 7 #endif /* __TELNETD_H__ */
1 struct telnetd_state 2 { 3 char *lines[TELNETD_CONF_NUMLINES]; 4 char buf[TELNETD_CONF_LINELEN]; 5 char bufptr; 6 u8_t numsent; 7 u8_t state; 8 }; 9 10 typedef struct telnetd_state uip_tcp_appstate_t; 11 #include "uipopt.h" 12 #include "uip.h" 13 14 // 下面是内容............. 15 // ............. 16 // .....telnet.c
因为这个这个结构体说白了也就是一块内存,看我们是怎么去使用它而已,struct就模子,三角形的模子往豆腐一压豆腐就是三角形,圆形就是圆形豆腐,但是豆腐本质是不会变的,只是看你去怎么吃豆腐,怎么去把豆腐给切成不同的形状而已,同理,该切的豆腐也是在同一块地方的,所以不同担心会越界,因为结构体是按内存顺序排列的.
方法二比较复杂,也比较危险,我们可以做如下措施,那就是使用自己的内存管理函数,这个我可以把malloc函数去掉,写自己的mymalloc,并且要带有内存管理的,这个如何书写我不多述,可以参考ucGUI内存管理机制,之前写的那个不知道跑哪去了,不然可以给大家参考下.
uIP的移植和一些勘误和修改就做到这,还有问题可以直接联系我或者留言,我都会第一时间帮你解答.共同进步是吧.
PART2:uIP的UDP精讲(传输层挑选)
进行UDP传输的时候也是要定义一个接口
#define UIP_UDP_APPCALL MY_UDP_APP
然后示例代码如下
void MY_UDP_APP(void) { switch (uip_udp_conn->rport) { case HTONS(6677): UDP_6677_APP(); break; case HTONS(67): dhcpc_appcall(); break; case HTONS(68): dhcpc_appcall(); break; case HTONS(53): resolv_appcall(); } }
因为这个UDP总的用户接口,所以我们需要对数据包进行分类,上面也已经说了这个的具体工作原理.就不赘述了.
在下面,我做了一件简单的demo来证明我们的udp是可用的,poll是轮询函数,每当轮询到来的时候,就发送auto send字符串,另外呢,如果接收到新的数据就把数据送往串口,然后往udp端口送数据,数据为"receive the data!\r\n".待会给大家上图,结果很简单.
void UDP_6677_APP(void) { /* 判断当前状态 */ if (uip_poll()) { char *tmp_dat = "the auto send!\r\n"; uip_send((char *)tmp_dat,strlen(tmp_dat)); } if (uip_newdata()) { char *tmp_dat = "receive the data!\r\n"; /* 收到新数据 */ printf("%d",uip_len); printf("%s",(char *)uip_appdata); uip_send((char *)tmp_dat,strlen(tmp_dat)); } }
当然,免不了要做UDP初始化的,首先,我们需要在uip-conf.h中开启我们的udp支持,另外我们要在初始化完ip地址之后初始化端口,端口初始化函数如下
uip_ipaddr(ipaddr, 192, 168, 0, 149); c = uip_udp_new(&ipaddr, HTONS(6677)); if(c != NULL) { uip_udp_bind(c, HTONS(6677)); }
初始化完就可以正常使用了.
另外大家也一直想问udp是如何主动发送数据的,包括tcp,tcp需要在ack函数中发送数据,而udp呢,选择udp可是因为速度考虑,不然一个没有滑动窗口支持,没有捎带确认,没有选择确认的uip是没有办法达到所谓的tcp快速传数据的,所以udp当然就成为了首选.
如果认真看上面代码,可以发现,udp主动发送数据就是通过轮询函数来实现的,因为所有的app接口调用都要先调用process函数才能正常完成一整个发送过程,这就要求我们必须切个入口进来,入口嘛,就是poll.
那如何加快发送速度呢?简单,那就是把udp的轮询函数放到主函数的while循环第一层,那么循环次数加快,要发送什么数据没有呢?
PART3:uIP的TCP精讲(传输层挑选)
这里我仅对一个简单的demo进行分析:
telnet:
void telnetd_appcall(void) { static unsigned int i; if(uip_connected()) { /* tcp_markconn(uip_conn, &s);*/ for(i = 0; i < TELNETD_CONF_NUMLINES; ++i) { s.lines[i] = NULL; } s.bufptr = 0; s.state = STATE_NORMAL; shell_start(); } if(s.state == STATE_CLOSE) { s.state = STATE_NORMAL; uip_close(); return; } if(uip_closed() || uip_aborted() || uip_timedout()) { closed(); } if(uip_acked()) { acked(); } if(uip_newdata()) { newdata(); } if(uip_rexmit() || uip_newdata() || uip_acked() || uip_connected() || uip_poll()) { senddata(); } }
uipappcall函数入口点可以设定成telneted_appcall,每当process进来的时候,都会自动跳转到该函数.
在我们移植telnet的时候,需要先监听端口,当然,这部分都已经做好了,那就是
void telnetd_init(void) { uip_listen(HTONS(23)); memb_init(&linemem); shell_init(); }
它用到了里面的一个内存管理函数,因为时间有限,所以没有认真研究,另外他监听了一个端口,那就是uip_listen(HTONS(23)),这个函数是用来建立连接的,等轮训函数一到,那么该连接就会被建立.
从很多的if语句可以看出uip的用户程序状态机是通过标志位来设定的,我们通过回调函数来看到要执行什么动作都必须查询动作标志位才能执行,像比如超时,新数据到来,重发等等.
关于重发:大家可能会有这样的疑问,uIP不是只有一个缓冲区吗,那怎么实现重发,它这里有个机制,那就是等待重发,重发机制需要我们自己做,所以我们需要等待到来的ack才能继续发送我们下一个文件,这就需要我们开设一个缓冲区去缓冲我们要发送的数据,但是由于tcp有ack延迟机制,1s钟还是只能发送5个包,但是uip是没有ack延迟机制的,因为它没有滑动窗口,所以收到telnet发送过来的数据,那么就会直接发送ack包过去,所以你会看到你的telnet是可以很流畅的输入字符的,然后返回的话是一整串返回的.
备注:但是我对windows的telnet进行测试却得出如下结果.
可以看出,服务器好像针对telnet的协议栈做了特别的修改(因为telnet客户端都是单字节传送的),然后呢,可以发现服务器直接对我进行回应了上一个包,证明包已经收到,请快速发送下一个字符.(20多ms的延迟).
所以协议栈不一定是按照标准写的.
TCP部分就差不多到这了,都是讲一些比较特别的.因为我不应该重复别人做过的事情,这样子也会浪费大家时间.
PART4:uIP的DHCP优化和勘误(应用层挑选)
要看这部分的时候请先阅读dhcp的四次详细过程,因为里面涉及到一些这方面的知识,当然,我会写的简单点,这样子比较容易理解.
dhcp的入口函数
static PT_THREAD(handle_dhcp(void)) { PT_BEGIN(&s.pt); /* try_again:*/ s.state = STATE_SENDING; s.ticks = CLOCK_SECOND; do { send_discover(); timer_set(&s.timer, s.ticks); PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); if(uip_newdata() && parse_msg() == DHCPOFFER) { s.state = STATE_OFFER_RECEIVED; break; } if(s.ticks CLOCK_SECOND * 60) { s.ticks *= 2; } } while(s.state != STATE_OFFER_RECEIVED); s.ticks = CLOCK_SECOND; do { send_request(); timer_set(&s.timer, s.ticks); PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); if(uip_newdata() && parse_msg() == DHCPACK) { s.state = STATE_CONFIG_RECEIVED; break; } if(s.ticks = CLOCK_SECOND * 10) { s.ticks += CLOCK_SECOND; } else { PT_RESTART(&s.pt); } } while(s.state != STATE_CONFIG_RECEIVED); #if 0 printf("Got IP address %d.%d.%d.%d\n", uip_ipaddr1(s.ipaddr), uip_ipaddr2(s.ipaddr), uip_ipaddr3(s.ipaddr), uip_ipaddr4(s.ipaddr)); printf("Got netmask %d.%d.%d.%d\n", uip_ipaddr1(s.netmask), uip_ipaddr2(s.netmask), uip_ipaddr3(s.netmask), uip_ipaddr4(s.netmask)); printf("Got DNS server %d.%d.%d.%d\n", uip_ipaddr1(s.dnsaddr), uip_ipaddr2(s.dnsaddr), uip_ipaddr3(s.dnsaddr), uip_ipaddr4(s.dnsaddr)); printf("Got default router %d.%d.%d.%d\n", uip_ipaddr1(s.default_router), uip_ipaddr2(s.default_router), uip_ipaddr3(s.default_router), uip_ipaddr4(s.default_router)); printf("Lease expires in %ld seconds\n", ntohs(s.lease_time[0])*65536ul + ntohs(s.lease_time[1])); #endif dhcpc_configured(&s); /* timer_stop(&s.timer);*/ /* * PT_END restarts the thread so we do this instead. Eventually we * should reacquire expired leases here. */ while(1) { PT_YIELD(&s.pt); } PT_END(&s.pt); }
它这里使用了一种很好的方法,叫做协程,避开了我们传统的阻塞方法(通过前后台系统,在该函数运行的时候,死循环等待一个消息到来,消息可以通过更高优先级的中断去更新.),它则是每次进来都跳转到该个地方进行查询,即实现是阻塞所要达到的目的,同时,也释放了该cpu资源.
好了,废话说到这,因为下面会详细讲述:
看了代码可以发现如下错误,在两个do-while循环中间没有进行标志位清空,导致程序误判以为是dhcp已经接收到下一个数据了.另外没有dhcp租约机制没有写进去.所以,基于上述错误,进行修改如下,在stm32平台上测试一切正常,可以在租约时间里面到达自动发送request包去重新续约所要的ip地址.
另外,还有一个错误,那就是如果发送了n个request包还没有回应,那么就会自动回到discover状态,这个可以理解,因为有些dhcp服务器对于你所要续约的ip并不感冒(dhcp服务器换了,或者该ip地址已经有人了,但是有的dhcp服务器会给你另外一个ip,这是不同机制决定的,你要是想的话也可以自己写一个,挺简单的).
但是上述的却是没有把结构体里面的数据清空就直接回到discover状态.所以这是错误的,这个可以留给读者自己更改.我就把第一点改了,可以看我代码:
1 static 2 PT_THREAD(handle_dhcp(void)) 3 { 4 PT_BEGIN(&s.pt); 5 6 /* try_again:*/ 7 s.state = STATE_SENDING; 8 s.ticks = CLOCK_SECOND; 9 10 do 11 { 12 send_discover(); 13 timer_set(&s.timer, s.ticks); 14 PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); 15 16 if(uip_newdata() && parse_msg() == DHCPOFFER) 17 { 18 s.state = STATE_OFFER_RECEIVED; 19 break; 20 } 21 22 if(s.ticks < CLOCK_SECOND * 60) 23 { 24 s.ticks *= 2; 25 } 26 } 27 while(s.state != STATE_OFFER_RECEIVED); 28 29 s.ticks = CLOCK_SECOND; 30 uip_flags = 0; 31 32 request_pro: 33 do 34 { 35 send_request(); 36 timer_set(&s.timer, s.ticks); 37 PT_WAIT_UNTIL(&s.pt, uip_newdata() || timer_expired(&s.timer)); 38 39 if(uip_newdata() && parse_msg() == DHCPACK) 40 { 41 s.state = STATE_CONFIG_RECEIVED; 42 break; 43 } 44 45 if(s.ticks <= CLOCK_SECOND * 10) 46 { 47 s.ticks += CLOCK_SECOND; 48 } 49 else 50 { 51 PT_RESTART(&s.pt); 52 } 53 } 54 while(s.state != STATE_CONFIG_RECEIVED); 55 56 #if 1 57 printf("Got IP address %d.%d.%d.%d\r\n", 58 uip_ipaddr1(s.ipaddr), uip_ipaddr2(s.ipaddr), 59 uip_ipaddr3(s.ipaddr), uip_ipaddr4(s.ipaddr)); 60 printf("Got netmask %d.%d.%d.%d\r\n", 61 uip_ipaddr1(s.netmask), uip_ipaddr2(s.netmask), 62 uip_ipaddr3(s.netmask), uip_ipaddr4(s.netmask)); 63 printf("Got DNS server %d.%d.%d.%d\r\n", 64 uip_ipaddr1(s.dnsaddr), uip_ipaddr2(s.dnsaddr), 65 uip_ipaddr3(s.dnsaddr), uip_ipaddr4(s.dnsaddr)); 66 printf("Got default router %d.%d.%d.%d\r\n", 67 uip_ipaddr1(s.default_router), uip_ipaddr2(s.default_router), 68 uip_ipaddr3(s.default_router), uip_ipaddr4(s.default_router)); 69 printf("Lease expires in %ld seconds\r\n", 70 ntohs(s.lease_time[0]) * 65536ul + ntohs(s.lease_time[1])); 71 #endif 72 73 dhcpc_configured(&s); 74 75 /* timer_stop(&s.timer);*/ 76 77 /* 78 * PT_END restarts the thread so we do this instead. Eventually we 79 * should reacquire expired leases here. 80 */ 81 uip_ipaddr(ipaddr, 192, 168, 0, 149); 82 c = uip_udp_new(&ipaddr, HTONS(6677)); 83 if(c != NULL) { 84 uip_udp_bind(c, HTONS(6677)); 85 } 86 telnetd_init(); 87 /* 判断超时 */ 88 timer_set(&s.timer,(ntohs(s.lease_time[0]) * 65536ul + ntohs(s.lease_time[1]))*50); 89 PT_WAIT_UNTIL(&s.pt, timer_expired(&s.timer)); 90 /* 超时了 */ 91 goto request_pro; 92 93 while (1) 94 { 95 PT_YIELD(&s.pt); 96 } 97 98 PT_END(&s.pt); 99 }
dhcp部分就到这里,一些具体函数我不说了,就是对包内容进行处理,没什么好讲的.
PART5:uIP的ProtoThread讲解(协程)
刚才也说了,在dhcp中应用了一种非常新颖的技术,它通过对一些宏定义的包装和C99新定义标准的应用,给我们封装了非常好用的函数,不得不佩服作者把lightweight深入到这种程度,对于PT(以下都这么称呼),主要适用的场合为:一个函数需要不断去轮询的;函数中间需要等待某种事件的发生;没有中断的系统中.
原因,如果在主函数中阻塞,那么你就必须通过中断来解除阻塞,这对于没有中断的来说是很致命的,所以你能想到的方法就是不断的进该函数,但是每次进该函数都要重头运行到该行,这样子也不行,所以你就用了很多if语句,甚至不能用while(1),这是很难受的,所以,作者封装了这个PT库,可以让我们像线程那样去使用我们的函数,我们甚至可以把这当成是一种挂起,而且它很省内存,就一个结构体的占用(用来记录进入该函数要到哪运行),所以没有独立的堆栈使得你使用其他可以得心应手.
1 static 2 PT_THREAD(函数名(void)) 3 { 4 PT_BEGIN(&s.pt);
14 PT_WAIT_UNTIL(&s.pt,条件); 15 16 while (1) 17 { 18 PT_YIELD(&s.pt); 19 } 20 21 PT_END(&s.pt); 22 }
PT_begin和pt_end是搭套使用的,我最后把他们都翻译成没有封装的,就这这样子了:
1 char PT_YIELD_FLAG = 1; 2 switch(s) { 3 case 0: 4 5 6 //PT_WAIT_UNTIL(pt, condition) 7 do { 8 //LC_SET((pt)->lc); 9 s = __LINE__;// line == 20; 10 case __LINE__:// case 20: 11 if(!(condition)) 12 { 13 return PT_WAITING; 14 } 15 } while(0) 16 17 18 //PT_WAIT_UNTIL(pt, condition) 19 do { 20 //LC_SET((pt)->lc); 21 s = __LINE__; \// line == 30; 22 case __LINE__:// case 30: 23 if(!(condition)) 24 { 25 return PT_WAITING; 26 } 27 } while(0) 28 29 while(1) 30 { 31 //#define PT_YIELD(pt) 32 do { 33 PT_YIELD_FLAG = 0; 34 LC_SET((pt)->lc); 35 if(PT_YIELD_FLAG == 0) { 36 return PT_YIELDED; 37 } 38 } while(0) 39 } 40 //#define PT_END(pt) 41 //LC_END((pt)->lc); 42 PT_YIELD_FLAG = 0; 43 //PT_INIT(pt); 44 pt <= 0; 45 return PT_ENDED; 46 }
这个看出,这个就是个巨大的switch-case函数封装库,说白了他就是通过记录下当前函数运行到哪一行然后记录下来放进静态变量里面,下次进来的时候直接根据上次运行的结果case进来,(s = __LINE__,这是C99新增标准,一般是用来调试使用的,没想到作者这么有才,竟然给他放到了用来跳转行数使用.)这样子就可以跳过前面的代码而直接执行你要的判断,当判断完成之后就可以继续执行下去了,当然,你程序也可以那么写,只是程序会比较难看一点,所以,用作者写的库是不二选择.
具体要了解更多可以通读pt-refman文档.
PART6:关于我的个人协议栈hIP,点图片下文件.
PART7:源代码展示和下载
基于stm32f10x的源代码下载,pt文档下载,uip1.0文档下载
main Code:
1 #ifndef __UIP_CONF_H__ 2 #define __UIP_CONF_H__ 3 4 #include <inttypes.h> 5 6 /** 7 * 8 bit datatype 8 * 9 * This typedef defines the 8-bit type used throughout uIP. 10 * 11 * \hideinitializer 12 */ 13 typedef uint8_t u8_t; 14 15 /** 16 * 16 bit datatype 17 * 18 * This typedef defines the 16-bit type used throughout uIP. 19 * 20 * \hideinitializer 21 */ 22 typedef uint16_t u16_t; 23 24 /** 25 * Statistics datatype 26 * 27 * This typedef defines the dataype used for keeping statistics in 28 * uIP. 29 * 30 * \hideinitializer 31 */ 32 typedef unsigned short uip_stats_t; 33 34 /** 35 * Maximum number of TCP connections. 36 * 最大的tcp连接端口数量,每增大一个连接,都会消耗一部分内存,这个可以自己做实验得出结论 37 * \hideinitializer 38 */ 39 #define UIP_CONF_MAX_CONNECTIONS 10 40 41 /** 42 * Maximum number of listening TCP ports. 43 * 最大的tcp监听端口数量 44 * \hideinitializer 45 */ 46 #define UIP_CONF_MAX_LISTENPORTS 10 47 48 /** 49 * uIP buffer size. 50 * 定义网络最大接收包大小,这个看内存容量而定, 51 * \hideinitializer 52 */ 53 #define UIP_CONF_BUFFER_SIZE 1500 54 55 /** 56 * CPU byte order. 57 * cpu大小端定义,这个阅读datasheet就可以得知,比如stm32都是小端顺序的,而网络字节序都是大端顺序的 58 * \hideinitializer 59 */ 60 #define UIP_CONF_BYTE_ORDER LITTLE_ENDIAN 61 62 /** 63 * Logging on or off 64 * 是否开启日志功能,如果开启,需要重定向日志输出函数 65 * \hideinitializer 66 */ 67 #define UIP_CONF_LOGGING 0 68 69 /** 70 * UDP support on or off 71 * UDP协议支持开关 72 * \hideinitializer 73 */ 74 #define UIP_CONF_UDP 1 75 76 /** 77 * UDP checksums on or off 78 * udp数据校验和使能 79 * \hideinitializer 80 */ 81 #define UIP_CONF_UDP_CHECKSUMS 1 82 83 /** 84 * uIP statistics on or off 85 * uip统计功能开启,这个只有在调试的时候有用,如果不需要,请关掉,节约空间 86 * \hideinitializer 87 */ 88 #define UIP_CONF_STATISTICS 1 89 90 /* Here we include the header file for the application(s) we use in 91 our project. */ 92 /*#include "smtp.h"*/ 93 /*#include "hello-world.h"*/ 94 #include "telnetd.h" 95 //#include "webserver.h" 96 #include "dhcpc.h" 97 #include "resolv.h" 98 /*#include "webclient.h"*/ 99 #include "myAPP.h" 100 101 #endif /* __UIP_CONF_H__ */ 102 103 /** @} */ 104 /** @} */
1 #ifndef __CLOCK_ARCH_H__ 2 #define __CLOCK_ARCH_H__ 3 // 关于这个 CLOCK_CONF_SECOND 是定义uIP的每秒的心跳次数,这个在uIP的内部时钟函数里面会是无比重要的 4 // 下面还有个搭配函数可以用来实现 5 typedef int clock_time_t; 6 #define CLOCK_CONF_SECOND 100 7 8 #endif /* __CLOCK_ARCH_H__ */
1 #include "clock-arch.h" 2 #include "stm32f10x.h" 3 4 extern __IO int32_t g_RunTime; 5 /*---------------------------------------------------------------------------*/ 6 clock_time_t 7 clock_time(void) 8 { 9 return g_RunTime;// 这个用来返回系统当前运行时间,根据我们上面的设定,是10ms加1 // 如果是上面定义的是1000 ,那么就是1ms+1 10 } 11 /*---------------------------------------------------------------------------*/
成果展示:
DHCP获取
UDP主动发送数据
UDP数据处理
TCP测试 telnet