天天看點

uIP學習筆記1.前言2.搭建實驗環境3.硬體和軟體說明4.網卡驅動5.一個簡單有效的定時器6.uIP基本結構與配置7.案例——最簡單的TCP echo程式8.wireshark網絡包分析10.總結11.推薦圖書資料

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位址如下圖所示。

uIP學習筆記1.前言2.搭建實驗環境3.硬體和軟體說明4.網卡驅動5.一個簡單有效的定時器6.uIP基本結構與配置7.案例——最簡單的TCP echo程式8.wireshark網絡包分析10.總結11.推薦圖書資料

圖1 PC機IP位址

    路由器的IP位址為192.168.1.1。那麼開發闆的IP位址可以設定為 192.168.1.2到192.168.1.255。為保證你的調試萬無一失,還是建議通路路由器,确認此時有哪些裝置接入路由器,該步驟的主要功能是避免IP位址重複。

uIP學習筆記1.前言2.搭建實驗環境3.硬體和軟體說明4.網卡驅動5.一個簡單有效的定時器6.uIP基本結構與配置7.案例——最簡單的TCP echo程式8.wireshark網絡包分析10.總結11.推薦圖書資料

圖2 和路由器相連的以太網裝置

3.硬體和軟體說明

3.1 硬體環境

    【奮鬥開發闆】

    奮鬥開發闆上有一片ENC28J60,ENC28J60通過SPI接口控制内部寄存器,并有中斷輸出接口。STM32通過SPI1和ENC28J60相連。具體接口如下:

    [email protected]

    [email protected]

    [email protected]

    [email protected]

    [email protected]

    由于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;
    else
        return 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:[email protected]");
   
    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"
#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.兩個檔案。代碼如下:

#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 ECHO
        uip_send(uip_appdata,uip_len);
    }
}
           
uIP學習筆記1.前言2.搭建實驗環境3.硬體和軟體說明4.網卡驅動5.一個簡單有效的定時器6.uIP基本結構與配置7.案例——最簡單的TCP echo程式8.wireshark網絡包分析10.總結11.推薦圖書資料

圖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)。

uIP學習筆記1.前言2.搭建實驗環境3.硬體和軟體說明4.網卡驅動5.一個簡單有效的定時器6.uIP基本結構與配置7.案例——最簡單的TCP echo程式8.wireshark網絡包分析10.總結11.推薦圖書資料

圖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燈,讀取溫度濕度資料。現在回過頭來看看基礎還是非常重要的。

繼續閱讀