關於uIP的移植以及部分特性解析和勘誤
原文:http://www.cnblogs.com/CodeHXH/archive/2012/01/19/2327426.html
關於嵌入式網絡的領域,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