low_level_init函數是與我們使用的與硬件密切相關初始化函數,代碼如下:
static void low_level_init(struct netif *netif)
{
netif->hwaddr_len = ETHARP_HWADDR_LEN; //設置變量enc28j60的hwaddr_len字段
netif->hwaddr[0] = 'F'; //初始化變量enc28j60的MAC地址
netif->hwaddr[1] = 'O'; //設什麼地址用戶自由發揮吧,但是不要與其他
netif->hwaddr[2] = 'R'; //網絡設備的MAC地址重複。
netif->hwaddr[3] = 'E';
netif->hwaddr[4] = 'S';
netif->hwaddr[5] = 'T';
netif->mtu = 1500; //最大允許傳輸單元
//允許該網卡廣播和ARP功能,並且該網卡允許有硬件鏈路連接
netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP;
enc28j60_init(netif->hwaddr); //與底層驅動硬件驅動程序密切相關的硬件初始化函數
}
至此,終於變量enc28j60
被初始化好了,而且它描述的網卡芯片enc28j60
也被初始化好了,而且變量enc28j60
也被鏈入鏈表netif_list
。
接着上面的語句(8)調用netif_set_default
函數初始化缺省網絡接口。協議棧除了有個netif_list
,全局變量指向netif
網絡接口結構的鏈表,還有個全局變量netif_default
,全局變量指向缺省的網絡接口結構。當IP層有數據發送時,它首先會以netif_list
爲索引選擇滿足某個條件的網絡接口發送數據包,但是,當找不到這樣的接口時,協議棧就會調用缺省的網絡接口直接發送數據包,所以(8)中的意思是把變量enc28j60
描述的網絡接口設置爲缺省的網絡接口。
(9) 調用函數netif_set_up
使能網絡接口,這通過一個簡單語句來實現:
netif->flags |= NETIF_FLAG_UP;
至此,網卡初始化完成,能正常接收和發送數據包了。下面我們來討論討論關於網卡數據包的接收和發送。
LWIP中實現了接收一個數據包和發送一個數據包函數的框架,這兩個函數分別是low_level_input
和low_level_output
,用戶需要使用實際網卡驅動程序完成這兩個函數。在第一篇中講過,一個典型的LWIP應用系統包括這樣的三個進程:首先是上層應用程序進程,然後是LWIP協議棧進程,最後是底層硬件數據包接收進程。這裏我們就來講講第三個進程,看看數據包是怎樣被接收並往上層傳遞的。但在這之前,有必要說說以太網網卡所收到的數據包的格式。如下圖,
LWIP使用了一個eth_hdr
的數據結構來描述以太網數據包包頭的14個字節。如下,
PACK_STRUCT_BEGIN
struct eth_hdr {
PACK_STRUCT_FIELD(struct eth_addr dest); //目標MAC地址
PACK_STRUCT_FIELD(struct eth_addr src); //源MAC地址
PACK_STRUCT_FIELD(u16_t type); //類型
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END
其中PACK_STRUCT_xxx
都是與編譯器字對齊相關的宏定義,這裏不作詳細介紹了。上面的dest
、src
和type
三個字段分別和上圖中的目的MAC地址、源MAC地址和類型域字段對應。
在上面討論的基礎上,我們來看看這個數據包接收進程,源代碼如下:
void ethernetif_input(void *arg) //創建該進程時,要將某個網絡接口結構的netif結構指
{
//針作爲參數傳入
struct eth_hdr *ethhdr;
struct pbuf *p;
struct netif *netif = (struct netif *)arg;
while (1)
{
p = low_level_input (netif); // 接收一個數據包
if (p == NULL) // 如果數據包爲空,
continue; // 則循環結束,啓動下次接收過程
ethhdr = p->payload; // 取得數據包內數據
switch (htons(ethhdr->type)) // 判斷數據包類型
{
// 只對IP數據包和ARP數據包進行處理
case ETHTYPE_IP: // IP數據包
case ETHTYPE_ARP: // ARP數據包
if (netif->input(p, netif)!=ERR_OK) // 將數據包發送到上層應用函數
{
pbuf_free(p);
p = NULL;
}
break;
default:
pbuf_free(p);
p = NULL;
break;
} //switch
} //while
} //main函數
要創建上面的這個進程,需要把個網絡接口結構的netif
結構指針作爲參數傳入,在UC/OSII
中要用到下面的語句實現,
OSTaskCreate(ethernetif_input,(void *)&enc28j60,
&T_ETHERNETIF_INPUT_STK[T_ETHERNETIF_INPUT_STKSIZE-1]
ETH_IF_TASK_PRIO);
在數據包接收進程中,有三個需要注意的地方。
一是數據包接收的方法是查詢方式,即處理器不斷向網卡芯片中讀取數據,如果讀不到數據,則控制器會重新啓動一個讀取時序;如果能夠成功讀取到數據,則將數據通過網卡註冊的input函數交往上層進行處理。使用查詢方式實現的數據包接收進程其優先級必須低於系統中其他進程的優先級,否則它會阻塞比它優先級低的進程的運行。
上面的程序有個可以改進的地方,即在讀取到的數據包爲空時,接收進程調用系統函數將自己延時一段時間再啓動下一個讀取過程,這樣可以使其不能阻止優先級更低的進程的運行,缺點是數據包的接收得不到及時的響應。
其實數據包的接收可以採用中斷的方式來實現,這種方式是一種比較好的方式。一般的網卡芯片都有中斷功能,即當網卡接收到一個數據包後,它可以產生中斷信號告訴控制器自己接收到一個數據包。控制器此時啓動一個讀取數據包時序,就能有效的讀取到非空數據包。所以可以這樣來實現一個接收數據包進程:在無數據包收到時,數據包接收進程阻塞在一個信號量下,當有數據包到來時,網卡芯片產生一箇中斷信號,處理器進入中斷處理,並釋放一個信號量。中斷退出後,數據包接收進程得到信號量,並從網卡芯片中讀取數據包,並將數據包遞交給上層進行處理。
第二個需要注意的地方是htons(ethhdr->type)函數的使用,htons
函數的功能是將一個半字長的數據從網絡字節順序轉換到我們的處理器支持的字節順序。解釋一下,在計算機體系結構和計算機通信領域中,對於半字、字等的存儲機制有可能不同。目前通常採用的存儲機制主要有兩種:big-endian
和little-endian
,即大端和小端。對於大端模式,某個半字或字數據的高位字節被在內存的低地址端,低位字節排放在內存的高地址端。對於小端模式,則恰好相反。由於我們使用的ARM處理器使用的是小端模式,而接收到的網絡字節數據用的是大端模式,所以這裏調用函數htons實現大端與小端的轉換,實際就是將兩個字節交換順序即可。這樣調用htons(ethhdr->type)
後,ethhdr->type
的值就爲0x0800
或0x0806
等。
最後需要注意的地方,netif->input
在結構enc28j60
初始化時已經被設置爲指向tcpip_input
函數,所以實際上上面是調用tcpip_input
函數往上層遞交數據包。tcpip_input
屬於IP層函數,從這裏我們可以看出LWIP的一個很大的特點,即各層之間沒有明顯的界限劃分。像前面所講的那樣,LWIP協議棧進程完成初始化相關工作後,會阻塞在一個郵箱上等待數據包的輸入,tcpip_input
函數就是向這個郵箱發送一條消息,且該消息中包含了收到的數據包存儲的地址。LWIP協議棧進程從郵箱中取到該地址後就可以對數據包進行處理了。
至此,數據包的接收可算大功告成,關於數據包的發送,這點很簡單,因爲它不必像數據包接收那樣要使用一個專門的進程來實現,而是這樣的:當上層有數據包要發送時,直接調用netif->linkoutput
發送數據包就可以了。netif->linkoutput
在結構enc28j60
初始化時已經被設置爲指向low_level_output
函數,該函數和底層硬件驅動密切相關,用於實現發送一個數據包的功能。用戶應該結合具體網卡驅動實現該函數。