uIP学习笔记
1.前言
? ? 最近半年的時間一直在學習應用嵌入式以太網。雖然學習的動機僅僅是玩玩,但是以太網真的深深吸引了我。這里我和各位分享一下uIP的使用經驗。uIP是一個簡單好用的嵌入式網絡協議棧,易于移植且消耗的內存空間較少,非常適合學習和使用。可以肯定的說uIP是嵌入式以太網學習的好起點,但不一定是終點。uIP的功能遠不如LwIP強大,但兩者并沒有孰優孰劣之分,uIP和LwIP的作者同為Adam Dunkels,LwIP開發較早uIP開發較晚,uIP經過這幾年的發展從IPV4遷移到IPV6,最終可以適用于無線傳感網絡。總的來說,uIP是一個很好的起點,學好uIP可以遷移到LwIP,也可以遷移到uIPV6。
【uIP官方代碼】
?
1.1 工程代碼
【1】CSDN資源?下載該資源需要1個積分,請可憐可憐我讓我也有機會下載CSDN上某些優質資源
【2】CSDN代碼倉庫
1.2 進階博文
【freemodbus modbus TCP 學習筆記】——使用uIP協議棧實現modbus TCP。
【uip的yeelink實現】——作者為我的(前)同事,使用uIP協議棧與yeelink平臺交互數據,很有意思。
2.搭建實驗環境
? ? 先講一下如何搭建實驗環境。建議于把開發板接到路由器上,而調試使用的PC機通過有線或者無線接到路由器上,保證開發板和PC機接入同一個路由器。由于uIP不支持DHCP(不直接支持),所以需要保證開發板和PC位于相同的子網,開發板的IP地址、路由器地址和子網掩碼都需要手動設定。設定之前最好看看調試PC機IP地址和路由器(網關)地址。例如調試PC機的IP地址如下圖所示。
圖1 PC機IP地址
? ? 路由器的IP地址為192.168.1.1。那么開發板的IP地址可以設定為 192.168.1.2到192.168.1.255。為保證你的調試萬無一失,還是建議訪問路由器,確認此時有哪些設備接入路由器,該步驟的主要功能是避免IP地址重復。
圖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也有類似的部分(或者說完全一樣)。
? ? 詳細代碼如下:
#include "timer.h" #include "stm32f10x_it.h"uint16_t current_clock = 0;void timer_config(void) {/* Systick時鐘每秒觸發CLOCK_SECOND次 */if (SysTick_Config(SystemCoreClock / CLOCK_SECOND)){while (1);} }void SysTick_Handler(void) {/* 時間標志累加 */current_clock++; }uint16_t clock_time(void) {return current_clock; }void timer_set(timer_typedef* ptimer,uint16_t interval) {/* 設置時間間隔 */ptimer->interval = interval;/* 設置啟動時間 */ptimer->start = clock_time(); }void timer_reset(timer_typedef * ptimer) {ptimer->start =ptimer->start + ptimer->interval; }int8_t timer_expired(timer_typedef* ptimer) {/* 一定要裝換為有符號數,進行數學比較時,多使用有符號數 */if((int16_t)(clock_time() - ptimer->start) >= (int16_t)ptimer->interval)return 1;elsereturn 0; }6.uIP基本結構與配置
6.1 uIP基本結構
? ? uIP的代碼編寫需要遵守一定的結構,而且這種結構最好保持穩定(保持不變)。這個結構主要做以下幾個部分任務。
? ? 【1】獲得以太網數據包
? ? 【2】處理ARP報文
? ? 【3】處理IP報文
? ? 【4】定期處理TCP和UDP連接
? ? 【5】定期更新ARP緩沖區
// BUF指向uIP緩沖區 uip_eth_hdr為以太網首部結構體 // 6字節目標MAC地址 6字節源MAC地址 2字節類型 #define BUF ((struct uip_eth_hdr *)&uip_buf[0]) void GPIO_Config(void);int main(void) {timer_typedef periodic_timer, arp_timer;uip_ipaddr_t ipaddr;/* 設定查詢定時器 ARP定時器 */timer_set(&periodic_timer, CLOCK_SECOND / 2);timer_set(&arp_timer, CLOCK_SECOND * 10);GPIO_Config(); /* 禁止SPI其他設備,防止竄擾 */timer_config(); /* 配置systic作為1ms中斷 */BSP_ConfigSPI1();/* 網卡初始化,ENC28J60,包括MAC地址初始化 */tapdev_init();/* UIP協議棧初始化 */uip_init();/* 設置本機IP地址 */uip_ipaddr(ipaddr, 192,168,1,15);uip_sethostaddr(ipaddr);/* 設置默認路由器IP地址 */uip_ipaddr(ipaddr, 192,168,1,1);uip_setdraddr(ipaddr);/* 設置網絡掩碼 */uip_ipaddr(ipaddr, 255,255,255,0);uip_setnetmask(ipaddr);/* 用戶任務初始化 為TCP echo任務*/example1_init();/* 初始化串口 重定義putchar */BSP_ConfigUSART1();/* 打印本機IP地址 */printf("\r\nuip start!\r\n");printf("ipaddr:192.168.1.15\r\n");/* 打印個人信息,呵呵*/printf("eID:xukai871105\r\r");printf("Email:xukai19871105@126.com");while (1){/* 讀取以太網數據包,返回數據長度 */uip_len = tapdev_read();if(uip_len > 0){/* 收到IP數據包 */if(BUF->type == htons(UIP_ETHTYPE_IP)){uip_arp_ipin();uip_input();if (uip_len > 0){uip_arp_out();tapdev_send();}}/* 收到ARP數據包 */else if (BUF->type == htons(UIP_ETHTYPE_ARP)){uip_arp_arpin();if (uip_len > 0){tapdev_send();}}}/* 查詢定時器是否超時 */if(timer_expired(&periodic_timer)){timer_reset(&periodic_timer);/* 測試使用,表現為LED燈閃爍 */GPIOB->ODR ^= GPIO_Pin_5;/* 查詢并處理所有TCP連接*/for(uint8_t i = 0; i < UIP_CONNS; i++){uip_periodic(i);if(uip_len > 0){uip_arp_out();tapdev_send();}}#if UIP_UDP/* 查詢并處理所有UDP連接*/for(uint8_t i = 0; i < UIP_UDP_CONNS; i++){uip_udp_periodic(i);if(uip_len > 0){uip_arp_out();tapdev_send();}} #endif /* UIP_UDP *//* ARP定時是否溢出 */if (timer_expired(&arp_timer)){timer_reset(&arp_timer);uip_arp_timer();}}} }? ?【簡單說明】
? ? 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。
? ? 具體代碼如下:
#include "tapdev.h" #include "uip.h" #include "uip_arp.h" #include "enc28j60.h" // MAC地址 struct uip_eth_addr uip_mac; static unsigned char ethernet_mac[6] = {0x00,0x14,0x0B,0x3F,0x04,0xB1};void tapdev_init(void) {enc28j60_init(ethernet_mac); /*初始化enc28j60 賦值MAC地址*/for (uint8_t i = 0; i < 6; i++){uip_mac.addr[i] = ethernet_mac[i];}uip_setethaddr(uip_mac); /* 設定uip mac地址*/ }uint16_t tapdev_read(void) {return enc28j60_packet_receive(uip_buf,1500); }void tapdev_send(void) {enc28j60_packet_send(uip_buf,uip_len); }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兩個關鍵參數。
????? ? 具體代碼如下:
#ifndef __UIP_CONF_H #define __UIP_CONF_H #include <inttypes.h> typedef uint8_t u8_t; typedef uint16_t u16_t; typedef unsigned short uip_stats_t; /* 最大TCP連接數 */ #define UIP_CONF_MAX_CONNECTIONS 10 /* 最大端口監聽數 */ #define UIP_CONF_MAX_LISTENPORTS 10 /* uIP 緩存大小*/ #define UIP_CONF_BUFFER_SIZE 1500 /* CPU字節順序 */ #define UIP_CONF_BYTE_ORDER UIP_LITTLE_ENDIAN /* 日志開關 */ #define UIP_CONF_LOGGING 1 /* UDP支持開關*/ #define UIP_CONF_UDP 0 /* UDP校驗和開關 */ #define UIP_CONF_UDP_CHECKSUMS 1 /* uIP統計開關 */ #define UIP_CONF_STATISTICS 1 // 加入用戶任務頭文件,請修改 #include "example1.h" #endif7.案例——最簡單的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.兩個文件。代碼如下:
#include "example1.h" #include "uip.h" #include <string.h> #include <stdio.h> #include <stdint.h> void example1_init(void) {uip_listen(HTONS(1234)); } void example1_appcall(void) {if( uip_newdata() ){// 輸出遠程IP和端口號printf("remote ip addr:%d.%d.%d.%d\r\n",(uip_conn->ripaddr[0]) & 0X00ff,(uip_conn->ripaddr[0]) >> 8,(uip_conn->ripaddr[1]) & 0X00ff,(uip_conn->ripaddr[1]) >> 8);printf("remote ip port:%d\r\n",HTONS(uip_conn->rport));// TCP ECHOuip_send(uip_appdata,uip_len);} }圖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燈,讀取溫度濕度數據。現在回過頭來看看基礎還是非常重要的。
?
總結
- 上一篇: QQ出现大规模盗号,为什么会这样?就没有
- 下一篇: 【云原生】理解k8s中的Pod和容器设计