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相連。具體接口如下:
[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);
}
}
圖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燈,讀取溫度濕度資料。現在回過頭來看看基礎還是非常重要的。