onps棧移植說明(3)——添加網卡

4. 添加網卡

       移植的最後一步就是編寫網卡驅動然後將網卡添加到協議棧。網卡驅動其本質上完成的是數據鏈路層的工作,在整個通訊鏈路上處於通訊樞紐位置,通訊報文的發送和接收均由其實際完成。針對網卡部分的移植工作共三步:

1)編寫網卡驅動;

2)註冊網卡到協議棧;

3)對接網卡數據收發接口;

協議棧目前支持兩種網卡類型:ethernet和ppp。兩種網卡的移植工作雖然步驟一樣,但具體移植細節還是有很大區別的,需要分開單獨進行。

4.1 ethernet網卡

       從移植的角度看,ethernet網卡驅動要提供三個接口函數並完成與協議棧的對接:

1)網卡初始化函數,完成網卡初始及啟動工作,並將其添加到協議棧;

2)網卡發送函數,發送上層協議傳遞的通訊報文到對端;

3)網卡接收函數,接收到達的通訊報文並傳遞給上層協議;

對於網卡初始化函數,其要做的工作用一句話總結就是:參照網卡數據手冊對其進行配置,然後將其註冊到協議棧:

#define DHCP_REQ_ADDR_EN 1 //* dhcp請求ip地址使能宏
static PST_NETIF l_pstNetifEth = NULL; //* 協議棧返回的netif結構
int ethernet_init(void)
{
    /* 進行初始配置,比如引腳配置、使能時鐘、相關工作參數配置等工作 */ 
    //* 在這裡添加能夠完成上述工作的相關代碼,請參照目標網卡的技術手冊編寫
    ……
    ……
    /* 到這裡網卡配置工作完成,但還未啟動 */
    
    //* 添加網卡到協議棧,一定要注意啟動以太網卡之前一定要先將其添加到協議棧
    EN_ONPSERR enErr; 
    ST_IPV4 stIPv4; 
#if !DHCP_REQ_ADDR_EN
    //* 分配一個靜態地址,請根據自己的具體網絡情形設置地址
    stIPv4.unAddr = inet_addr_small("192.168.0.4"); 
    stIPv4.unSubnetMask = inet_addr_small("255.255.255.0"); 
    stIPv4.unGateway = inet_addr_small("192.168.0.1"); 
    stIPv4.unPrimaryDNS = inet_addr_small("1.2.4.8"); 
    stIPv4.unSecondaryDNS = inet_addr_small("8.8.8.8"); 
    stIPv4.unBroadcast = inet_addr_small("192.168.0.255"); 
#else
    //* 地址清零,為dhcp客戶端申請動態地址做好準備
    memset(&stIPv4, 0, sizeof(stIPv4)); 
#endif  
    
    //* 註冊網卡,也就是將網卡添加到協議棧
    l_pstNetifEth = ethernet_add(……); 
    if(!l_pstNetifEth)
    {
#if SUPPORT_PRINTF    
        printf("ethernet_add() failed, %s\r\n", onps_error(enErr)); 
#endif    
        return -1; 
    }
    
    //* 啟動網卡,開始工作,在這裡添加與目標網卡啟動相關的代碼
    ……
    
#if DHCP_REQ_ADDR_EN  
    //* 啟動一個dhcp客戶端,從dhcp服務器申請一個動態地址
    if(dhcp_req_addr(l_pstNetifEth, &enErr))  
    {
    #if SUPPORT_PRINTF     
        printf("dhcp request ip address successfully.\r\n"); 
    #endif    
    }
    else
    {
    #if SUPPORT_PRINTF     
        printf("dhcp request ip address failed, %s\r\n", onps_error(enErr)); 
    #endif    
    }
#endif
    
    return 0; 
}

上面給出的樣例代碼中,省略的部分是與目標系統相關的網卡初始配置代碼,其餘則是與協議棧有關的網卡註冊代碼。這部分代碼主要是完成了兩塊工作:一,註冊網卡到協議棧;二,指定或申請一個靜態/動態地址。註冊網卡的工作是由協議棧提供的ethernet_add()函數完成的,其詳細說明如下:

//* 註冊ethernet網卡到協議棧,只有如此協議棧才能正常使用該網卡進行數據通訊。
//*           pszIfName:網卡名稱
//*          ubaMacAddr:網卡mac地址
//*             pstIPv4:指向ST_IPV4結構體的指針(include/netif/netif.h),這個結構體保存用戶指定的ip地址、網關、dns、子網掩碼等配置信息
//*
//*        pfunEmacSend:函數指針,指向發送函數,函數原型為INT(* PFUN_EMAC_SEND)(SHORT sBufListHead, UCHAR *pubErr),這個指針指向的其實
//*                      就是網卡發送函數
//*
//* pfunStartTHEmacRecv:函數指針,協議棧使用該函數啟動網卡接收線程,該線程為協議棧內部工作線程,用戶移植時只需提供啟動該線程的接口函數即可
//* 
//*           ppstNetif:二維指針,協議棧成功註冊網卡後ethernet_add()函數會返回一個PST_NETIF指針給調用者,這個參數指向這個指針,其最終會被
//*                      協議棧通過pvParam參數傳遞給pfunStartTHEmacRecv指向的函數
//* 
//*              penErr:如果註冊失敗,ethernet_add()函數會返回一個錯誤碼,這個參數用於接收這個錯誤碼
//*
//* 註冊成功,返回一個PST_NETIF類型的指針,後續的報文收發均用到這個指針;註冊失敗則返回NULL。具體錯誤信息參見penErr參數攜帶的錯誤碼。
PST_NETIF ethernet_add(const CHAR *pszIfName, const UCHAR ubaMacAddr[ETH_MAC_ADDR_LEN], PST_IPV4 pstIPv4, PFUN_EMAC_SEND pfunEmacSend, 
                       void (*pfunStartTHEmacRecv)(void *pvParam), PST_NETIF *ppstNetif, EN_ONPSERR *penErr);

ethernet_add()函數提供的參數看起來較為複雜,但其實就完成了一件事情:告訴協議棧這個新增加的網卡的相關身份信息及功能接口,包括名稱、地址、數據讀寫接口等。這個函數有兩個地方需要特別說明:一個是樣例代碼中該函數的返回值保存在了一個靜態存儲時期的變量l_pstNetifEth中;另一個是入口參數pfunStartTHEmacRecv。前一個用於接收註冊成功後返回的PST_NETIF指針;後一個則是需要提供一個線程啟動函數,啟動協議棧內部的以太網接收線程thread_ethernet_ii_recv(),該線程在協議棧源碼ethernet.c文件中實現。PST_NETIF指針非常重要,它是網卡能夠正常工作的關鍵。報文收發均用到這個指針。它的生命周期應該與協議棧的生命周期相同,因此這個指針變量在上面的樣例代碼中被定義成一個靜態存儲時期的變量,並確保網卡的接收、發送函數均能訪問。pfunStartTHEmacRecv參數指向的函數要實現的功能與前面我們編寫的os適配層函數os_thread_onpstack_start()相同,其就是調用os提供的線程啟動函數啟動thread_ethernet_ii_recv()線程。比如rt-thread下:

#define THETHIIRECV_PRIO      21      //* ethernet網卡接收線程(任務)優先級
#define THETHIIRECV_STK_SIZE  384 * 4 //* 接收線程棧大小,這個棧要相對大一些,太小會報錯
#define THETHIIRECV_TIMESLICE 10      //* 單次任務調度線程能夠工作的最大時間片
static void start_thread_ethernet_ii_recv(void *pvParam)
{
  rt_thread_t tid = rt_thread_create("EthRcv", thread_ethernet_ii_recv, pvParam, THETHIIRECV_STK_SIZE, THETHIIRECV_PRIO, THETHIIRECV_TIMESLICE);  
  if(tid != RT_NULL)
    rt_thread_startup(tid);
}

其餘os與之類似。我們啟動的這個以太網接收線程完成實際的以太網層的報文接收及處理工作。其輪詢等待網卡接收中斷函數發送的報文到達信號,收到信號則立即讀取並處理到達的報文。我們在後面講述網卡接收函數的移植細節時還會提到這個接收線程。

       對於網卡發送函數,有一點需要注意的是——其原型必須符合協議棧的要求,因為我們在進行網卡註冊時還要向協議棧註冊發送函數的入口地址。前面在介紹ethernet_add()註冊函數時我們已經給出了發送函數的原型定義,也就是pfunEmacSend參數指向的函數原型。協議棧的目標系統是資源受限的單片機系統,為了最大限度節省內存,協議棧採用了寫時零複製(zero copy)技術,網卡發送函數需要結合協議棧的buf list機制編寫實現代碼,其偽代碼實現如下:

int ethernet_send(SHORT sBufListHead, UCHAR *pubErr)
{
    SHORT sNextNode = sBufListHead;
    UCHAR *pubData; 
    USHORT usDataLen;
    
    //* 調用buf_list_get_len()函數計算當前要發送的ethernet報文長度,其由協議棧提供
    UINT unEthPacketLen = buf_list_get_len(sBufListHead);
    
    //* 逐個取出buf list節點發送出去
__lblGetNextNode:
    pubData = (UCHAR *)buf_list_get_next_node(&sNextNode, &usDataLen); //* 獲取下一個節點,buf_list_get_next_node()函數由協議棧提供
    if (NULL == pubData) //* 返回空意味着已經到達鏈表尾部,沒有要發送的數據了,直接返回就可以了
        return (int)unEthPacketLen; 
    
    //* 啟動發送,將取出的數據發送出去,其中pubData指向要發送的數據,usDataLen為要發送的數據長度,這兩個值已經通過buf_list_get_next_node()函數得到
    //* 在這裡添加與具體目標網卡相關的數據發送代碼
    …… 
    
    //* 取下一個數據節點
    goto __lblGetNextNode; 
}

關於buf list,其實現機制其實很簡單。以udp通訊為例,用戶要發送數據到對端,會直接調用udp發送函數,將數據傳遞給udp層。udp層收到用戶數據後,為了節省內存,避免複製,協議棧直接將用戶數據掛接到buf list鏈表上成為鏈表的數據節點。接着,udp層會再申請一個節點把udp報文頭掛接到數據節點的前面,組成一個擁有兩個節點的完整udp報文鏈表——鏈表第一個節點掛載udp報文頭,第二個節點掛載用戶要發送的數據。至此,udp層的報文封裝工作完成,數據繼續向ip層傳遞。ip層會繼續申請一個節點把ip報文頭掛接到udp報文頭節點的前面,組成一個擁有三個節點的完整ip 報文鏈表。ip報文在ip層經過路由選擇後被送達數據鏈路層,也就是ethernet層。在這一層,協議棧再將ethernet ii報文頭掛接到ip報文頭節點的前面。至此,整個報文的封裝完成。協議棧此時會根據網卡註冊信息調用對應網卡的ethernet_send()函數將報文發送出去。ethernet_send()函數的核心處理邏輯就是按照上述機制再依序取出鏈表節點攜帶的各層報文數據,然後順序發送出去。

       網卡移植拼圖的最後一塊就是完成網卡接收函數,把網卡收到的數據推送給協議棧。其偽代碼實現如下:

//* 網卡接收函數,可以是接收中斷服務子函數,也可以是普通函數,普通函數必須確保能夠在數據到達的第一時間就能讀取並推送給協議棧
void ethernet_recv(void)
{
    EN_ONPSERR enErr;
    unsigned int unPacketLen;
    unsigned char *pubRcvedPacket; 
    
    //* 在這裡添加與具體網卡相關的代碼,等待接收報文到達,如果數據到達將報文長度賦值unPacketLen變量,將報文首地址賦值給pubRcvedPacket
    ……
    ……
    //* 讀取到達報文並將其推送給協議棧進行處理,首先利用協議棧mmu模塊動態申請一塊內存用於保存到達的報文
    unsigned char *pubPacket = (UCHAR *)buddy_alloc(sizeof(ST_SLINKEDLIST_NODE) + unPacketLen, &enErr);    
    //* 申請成功,根據協議棧要求,剛才申請的內存按照PST_SLINKEDLIST_NODE鏈表節點方式組織並保存剛剛收到的報文
    PST_SLINKEDLIST_NODE pstNode = (PST_SLINKEDLIST_NODE)pubPacket;
    pstNode->uniData.unVal = unPacketLen; 
    memcpy(pubPacket + sizeof(ST_SLINKEDLIST_NODE), (UCHAR *)pubRcvedPacket, unPacketLen);
    
    //* 將上面組織好的報文節點放入接收鏈表,這個接收鏈表由協議棧管理,ethernet_put_packet()函數由協議棧提供
    //* thread_ethernet_ii_recv()接收線程負責等待ethernet_put_packet()函數投遞的信號並讀取這個鏈表
    //* 參數l_pstNetifEth為前面註冊網卡時由協議棧返回的PST_NETIF指針值
    ethernet_put_packet(l_pstNetifEth, pstNode); 
}

其中buddy_alloc()函數在功能上與c語言的標準庫函數malloc()完全相同,都是動態分配一塊指定大小的內存給調用者使用,使用完畢後再由用戶通過buddy_free()函數釋放。這兩個函數由協議棧的內存管理(mmu)模塊提供。ethernet_put_packet()函數需要重點解釋一下。這個函數由協議棧提供。它完成的工作非常重要,它在網卡接收函數與協議棧以太網接收線程thread_ethernet_ii_recv()之間搭建了一個數據流通的「橋」。接收函數收到報文後按照協議棧要求,將報文封裝成ST_SLINKEDLIST_NODE類型的鏈表節點,然後傳遞給ethernet_put_packet()函數。該函數將立即把傳遞過來的節點掛載到由協議棧管理的以太網接收鏈表的尾部,然後投遞一個「有新報文到達」的信號量。前文提到的以太網接收線程thread_ethernet_ii_recv()會輪詢等待這個信號量。一旦信號到達,接收線程將立即讀取鏈表並取出報文交給協議棧處理。

       至此,ethernet網卡相關的移植工作完成。

4.2 ppp撥號網卡

       在Linux系統,2g/4g/5g模塊作為一個通訊終端,驅動層會把它當作一個tty設備來看待。Linux下ppp棧也是圍繞着操作一個標準的tty設備來實現底層通訊邏輯的。至於如何操作這個ppp撥號終端進行實際的數據收發tty層並不關心。所以,協議棧完全借鑒了這個成功的設計思想,在底層驅動與撥號終端之間增加了一個tty層,將具體的設備操作與上層的業務邏輯進行了剝離。ppp撥號網卡的的移植工作其實就是完成tty層到底層驅動的封裝工作。協議棧利用句柄來唯一的標識一個tty設備。在os_datatype.h文件中定義了這個句柄類型:

#if SUPPORT_PPP
   typedef INT HTTY;       //* tty終端句柄
   #define INVALID_HTTY -1 //* 無效的tty終端句柄
#endif

這個句柄類型非常重要,所有與tty操作相關的函數都要用到這個句柄類型。tty層要完成的驅動封裝工作涉及的函數原型定義依然是在os_adapter.h文件中:

#if SUPPORT_PPP
//* 打開 tty 設備,返回 tty 設備句柄,參數 pszTTYName 指定要打開的 tty 設備的名稱
OS_ADAPTER_EXT HTTY os_open_tty(const CHAR *pszTTYName);

//* 關閉 tty 設備,參數 hTTY 為要關閉的 tty 設備的句柄
OS_ADAPTER_EXT void os_close_tty(HTTY hTTY);

//* 向 hTTY 指定的 tty 設備發送數據,返回實際發送的數據長度
//*     hTTY:設備句柄
//*  pubData:指針,指向要發送的數據的指針
//* nDataLen:要發送的數據長度
//* 返回值為實際發送的位元組數
OS_ADAPTER_EXT INT os_tty_send(HTTY hTTY, UCHAR *pubData, INT nDataLen);

//* 從參數 hTTY 指定的 tty 設備等待接收數據,阻塞型
//*       hTTY:設備句柄
//*  pubRcvBuf:指針,指向數據接收緩衝區的指針,用於保存收到的數據
//* nRcvBufLen:接收緩衝區的長度
//*  nWaitSecs:等待的時長,單位:秒。0 一直等待;直至收到數據或報錯,大於 0,等待指定秒數;小於 0,不支持
//* 返回值為實際收到的數據長度,單位:位元組
OS_ADAPTER_EXT INT os_tty_recv(HTTY hTTY, UCHAR *pubRcvBuf, INT nRcvBufLen, INT nWaitSecs); 

//* 複位 tty 設備,這個函數名稱體現了2g/4g/5g模塊作為tty設備的特殊性,其功能從本質上看就是一個 modem,modem 設備出現通訊
//* 故障時,最好的修復故障的方式就是直接複位,複位可以修復絕大部分的因軟件問題產生的故障
OS_ADAPTER_EXT void os_modem_reset(HTTY hTTY);
#endif

依據上述函數的原型定義及功能說明,我們在os_adapter.c文件編碼實現它們,相關偽代碼實現如下:

#if SUPPORT_PPP
HTTY os_open_tty(const CHAR *pszTTYName)
{
    //* 如果你的系統存在多個ppp撥號終端,那麼pszTTYName參數用於區分打開哪個串口
    //* 在這裡添加串口打開代碼將連接撥號終端的串口打開
    ……
    ……

    //* 如果目標系統只有一個撥號終端,那麼這裡返回的tty句柄x值為0,如果目標系統存在多個模塊,這裡需要你根據參數
    //* pszTTYName指定的名稱來區分是哪個設備,並據此返回不同的tty句柄,句柄值x應從0開始自增,步長為1
    return x;
}

void os_close_tty(HTTY hTTY)
{
    //* 在這裡添加串口關閉代碼,關閉哪個串口的依據是tty設備句柄hTTY
    ……
}

INT os_tty_send(HTTY hTTY, UCHAR *pubData, INT nDataLen)
{
    //* 在這裡添加數據發送代碼,其實就是調用對應的串口驅動函數發送數據到撥號終端,如果存在多個tty設備,請依據參數hTTY來
    //* 確定需要調用哪個串口驅動函數發送數據,返回值為實際發送的位元組數
    ……
}

INT os_tty_recv(HTTY hTTY, UCHAR *pubRcvBuf, INT nRcvBufLen, INT nWaitSecs)
{
    //* 同上,在這裡添加數據讀取代碼,其實就是調用對應的串口驅動函數從撥號終端讀取數據,如果存在多個tty設備,請依據參數hTTY來
    //* 確定需要調用哪個串口驅動函數讀取數據,返回值為實際讀取到的位元組數
    ……
}

void os_modem_reset(HTTY hTTY)
{
    //* 在這裡添加撥號終端的複位代碼,如果你的目標板不支持軟件複位模塊,可以省略這一步
    //* 複位模塊的目的是解決絕大部分的因軟件問題產生的故障
    ……
}
#endif

參照上述偽代碼,依據目標系統具體情況編寫相應功能代碼即可。注意,上述代碼能夠正常工作的關鍵是目標系統的串口驅動必須能夠正常工作且健壯、可靠。因為tty層封裝的其實就是操作ppp撥號終端的串口驅動代碼,tty只是做了一層簡單封裝罷了。os_adapter.c文件中關於ppp部分還有如下幾項定義需要根據你的實際目標環境進行配置:

#if SUPPORT_PPP
//* 連接ppp撥號終端的串口名稱,有幾個模塊,就指定幾個,其存儲的單元索引應等於os_open_tty()返回的對應串口的tty句柄值
const CHAR *or_pszaTTY[PPP_NETLINK_NUM] = { …… /* 如"串口1", "串口2"等 */ }; 

//* 指定ppp撥號的apn、用戶和密碼,系統支持幾路ppp,就需要指定幾組撥號信息
//* ST_DIAL_AUTH_INFO結構體保存這幾個信息,該結構體的詳細內容參見協議棧源碼ppp/ppp.h文件
//* 這裡設置的apn等撥號認證信息會替代前面說過的APN_DEFAULT、AUTH_USER_DEFAULT、AUTH_PASSWORD_DEFAULT等缺省設置
const ST_DIAL_AUTH_INFO or_staDialAuth[PPP_NETLINK_NUM] = {
  { "4gnet", "card", "any_char" },  //* 注意ppp賬戶和密碼盡量控制在20個位元組以內,太長需要需要修改chap.c
                                    //* 中send_response()函數的szData數組容量及pap.c中pap_send_auth_request()函數的
                                    //* ubaPacket數組的容量,確保其能夠封裝一個完整的響應報文
  /* 系統存在幾路ppp鏈路,就在這裡添加幾路撥號認證信息 */
}; 

//* ppp鏈路協商的初始協商配置信息,協商成功後這裡保存最終的協商結果,ST_PPPNEGORESULT結構體的詳細說明參見下文
ST_PPPNEGORESULT o_staNegoResult[PPP_NETLINK_NUM] = {
  {
    { 0, PPP_MRU, ACCM_INIT,{ PPP_CHAP, 0x05 /* CHAP協議,0-4未使用,0x05代表採用MD5算法 */ }, TRUE, TRUE, FALSE, FALSE },
    { IP_ADDR_INIT, DNS_ADDR_INIT, DNS_ADDR_INIT, IP_ADDR_INIT, MASK_INIT }, 0
  }, 

  /* 系統存在幾路ppp鏈路,就在這裡添加幾路的協商初始值,如果不確定,可以將上面預定義的初始值直接複製過來即可 */
}; 
#endif

上面給出的代碼做了幾件事情:
1)指定tty設備連接的串口名稱;
2)指定撥號認證信息:apn、用戶和密碼;
3)指定ppp鏈路協商初始值;

總之,你的目標系統連接了幾個撥號終端,這幾件事情就要針對特定的終端分別做一遍,單獨指定。這裡需要重點說明的是ppp鏈路協商配置信息。這些信息由ST_PPPNEGORESULT結構體保存(參見negotiation_storage.h文件):

typedef struct _ST_PPPNEGORESULT_ {
    struct {
        UINT unMagicNum; //* 幻數(魔術字)
        USHORT usMRU;    //* 最大接收單元,缺省值由PPP_MRU宏指定,一般為1500位元組
        UINT unACCM;     //* ACCM,異步控制字符映射,指定哪些字符需要轉義,如果不確定,建議採用ACCM_INIT宏指定的缺省值
        struct { //* 保存認證信息的結構體
            USHORT usType;     //* 指定認證類型:chap或pap,缺省chap認證
            UCHAR ubaData[16]; //* 認證報文攜帶的數據,不同的協議攜帶的數據類型不同,一般情況下採用協議棧的缺省值即可
        } stAuth;
        BOOL blIsProtoComp;            //* 是否採用協議域壓縮(本地設置項,代表協議棧一側,協商結果不影響該字段)
        BOOL blIsAddrCtlComp;          //* 是否採用地址及控制域壓縮(本地設置項,代表協議棧一側,協商結果不影響該字段)
        BOOL blIsNegoValOfProtoComp;   //* 協議域是否壓縮的協商結果值(遠端設置項,代表對端是否支持該配置,協商結果影響該字段)
        BOOL blIsNegoValOfAddrCtlComp; //* 地址及控制域是否壓縮的協商結果值(遠端設置項,同上)
    } stLCP;
    
    //* 存儲ppp鏈路的初始及協商成功後的地址信息
    struct {
        UINT unAddr;             //* ip地址,初始值由協議棧提供的IP_ADDR_INIT宏指定,不要擅自修改
        UINT unPrimaryDNS;       //* 主dns服務器地址,初始值由協議棧提供的DNS_ADDR_INIT宏指定,不要擅自修改
        UINT unSecondaryDNS;     //* 次dns服務器地址,初始值由協議棧提供的DNS_ADDR_INIT宏指定,不要擅自修改
        UINT unPointToPointAddr; //* 點對點地址,初始值由協議棧提供的IP_ADDR_INIT宏指定,不要擅自修改
        UINT unSubnetMask;       //* 子網掩碼
    } stIPCP;
    UCHAR ubIdentifier;   //* 標識域,從0開始自增,唯一的標識一個ppp報文,用於確定應答報文
    UINT unLastRcvedSecs; //* 最近一次收到對端報文時的秒數,其用於ppp鏈路故障探測,無需關心,協議棧底層使用
} ST_PPPNEGORESULT, *PST_PPPNEGORESULT;

基本上,要調整的地方几乎沒有,我們直接採用缺省值即可。

       移植工作的最後一步就是把ppp網卡的主處理線程thread_ppp_handler()添加到os適配層的工作線程列表中。也就是前面講解os適配層移植工作時提到的lr_stcbaPStackThread數組。這個數組保存了協議棧內部工作線程列表,我們先前已經添加了one-shot定時器工作線程thread_one_shot_timer_count()。我們再把ppp主處理線程添加到這個數組中即可。偽代碼實現如下:

//* 協議棧內部工作線程列表
const static STCB_PSTACKTHREAD lr_stcbaPStackThread[] = {
	{ thread_one_shot_timer_count, NULL}, 	
#if SUPPORT_PPP
	//* 在此按照順序建立ppp工作線程,入口函數為thread_ppp_handler(),線程入口參數為os_open_tty()返回的tty句柄值
	//* 其直接強行進行數據類型轉換即可,即作為線程入口參數時直接以如下形式傳遞:
	//* (void *)句柄值
	//* 不要傳遞參數地址,即(void *)&句柄,這種方式是錯誤的
#endif
};

ppp主處理線程將在協議棧加載時由os適配層函數os_thread_onpstack_start()啟動。在這裡只需把其添加到工作線程列表中即可,剩下的交由協議棧自動處理。在這裡需要特別說明的是主處理線程的入口參數為tty句柄。其值應直接傳遞給線程,不能傳遞句柄地址(參見上面的偽代碼注釋)。比如實際移植到目標系統時如果系統只存在一路ppp,os_open_tty()返回的tty句柄值為0,那麼添加到工作線程列表中的ppp主處理線程入口參數的值應為「(void *)0」。不用關心前面的「(void *)」,這段數據類型強制轉換代碼只是為了避免編譯器報錯。ppp鏈路建立成功後,協議棧會以「ppp+tty句柄」的方式命名該鏈路,命名時的tty句柄值就是通過這個啟動參數獲得的,所以這個值一定要配置正確。對於單路ppp,由於tty句柄值為0,所以ppp鏈路的名稱為「ppp0」。

       至此,ppp網卡相關的移植工作完成。