天天看點

一文看懂網卡驅動原理及移植方法

1、網卡裝置驅動原理

1.1 層次結構

Linux系統對網絡裝置驅動定義了4個層次, 這4個層次有到下分為:

  • 1、網絡協定接口層:實作統一的資料包收發的協定。該層主要負責調用​

    ​dev_queue_xmit()​

    ​​函數發送資料,​

    ​netif_rx()​

    ​​函數接收資料。當上層 ARP 或 IP 協定需要發送資料包時,它将調用網絡協定接口層的​

    ​dev_queue_xmit()​

    ​​函數發送該資料包,同時需傳遞給該函數一個指向​

    ​struct sk_buff​

    ​​資料 結構的指針。​

    ​dev_queue_xmit()​

    ​函數的原型為:
dev_queue_xmit (struct sk_buff * skb );      

同樣地,上層對資料包的接收也通過向 ​

​netif_rx()​

​​函數傳遞一個 ​

​struct sk_buff​

​資料 結構的指針來完成。netif_rx()函數的原型為:

int netif_rx(struct sk_buff *skb);      
  • 2、網絡裝置接口層:通過​

    ​net_device​

    ​結構體來描述一個具體的網絡裝置的資訊,實作不同的硬體的統一
  • 3、裝置驅動功能層:用來負責驅動網絡裝置硬體來完成各個功能, 它通過​

    ​hard_start_xmit()​

    ​ 函數啟動發送操作, 并通過網絡裝置上的中斷觸發接收操作。
  • 4、網絡裝置與媒介層:用來負責完成資料包發送和接收的實體實體, 裝置驅動功能層的函數都在這實體上驅動的

層次結構如下圖所示:

一文看懂網卡驅動原理及移植方法

設計具體的網絡裝置驅動程式時,我們需要完成的主要工作是編寫裝置驅動功 能層的相關函數以填充 net_device 資料結構的内容并将 net_device 注冊入核心。

1.2 網卡驅動的初始化

網卡驅動程式,隻需要編寫網絡裝置接口層,填充​

​net_device​

​​資料結構的内容并将​

​net_device​

​注冊入核心,設定硬體相關操作,使能中斷處理等。

1.2.1 初始化網卡步驟

  • 1)使用​

    ​alloc_netdev()​

    ​來配置設定一個net_device結構體
  • 2)設定網卡硬體相關的寄存器
  • 3)設定​

    ​net_device​

    ​結構體的成員
  • 4)使用​

    ​register_netdev()​

    ​來注冊net_device結構體

1.2.2 net_device結構體

網絡裝置接口層的主要功能是為千變萬化的網絡裝置定義了統一、抽象的資料結 構 net_device 結構體,以不變應萬變,實作多種硬體在軟體層次上的統一。 net_device 結構體在核心中指代一個網絡裝置,網絡裝置驅動程式隻需通過填充 net_device 的具體成員并注冊 net_device 即可實作硬體操作函數與核心的挂接。 net_device 本身是一個巨型結構體,包含網絡裝置的屬性描述和操作接口。當我 們編寫網絡裝置驅動程式時,隻需要了解其中的一部分。

struct net_device
{
       char               name[IFNAMSIZ];      //網卡裝置名稱
       unsigned long      mem_end;             //該裝置的記憶體結束位址
       unsigned long      mem_start;            //該裝置的記憶體起始位址
       unsigned long      base_addr;            //該裝置的記憶體I/O基位址
       unsigned int       irq;                  //該裝置的中斷号

       unsigned char      if_port;              //該字段僅針對多端口裝置,用于指定使用的端口類型
    unsigned char      dma;                  //該裝置的DMA通道
       unsigned long      state;                //網絡裝置和網絡擴充卡的狀态資訊

       struct net_device_stats* (*get_stats)(struct net_device *dev); //擷取流量的統計資訊,運作ifconfig便會調用該成員函數,并傳回一個net_device_stats結構體擷取資訊

      struct net_device_stats  stats;      //用來儲存統計資訊的net_device_stats結構體

 
       unsigned long         features;        //接口特征,     
       unsigned int          flags; //flags指網絡接口标志,以IFF_開頭,包括:IFF_UP( 當裝置被激活并可以開始發送資料包時, 核心設定該标志)、 IFF_AUTOMEDIA(設定裝置可在多種媒介間切換)、IFF_BROADCAST( 允許廣播)、IFF_DEBUG( 調試模式, 可用于控制printk調用的詳細程度) 、 IFF_LOOPBACK( 回環)、IFF_MULTICAST( 允許多點傳播) 、 IFF_NOARP( 接口不能執行ARP,點對點接口就不需要運作 ARP) 和IFF_POINTOPOINT( 接口連接配接到點到點鍊路) 等。

 
       unsigned        mtu;        //最大傳輸單元,也叫最大資料包

       unsigned short  type;     //接口的硬體類型

       unsigned short   hard_header_len;     //硬體幀頭長度,在以太網裝置的初始化函數中一般被賦為ETH_HLEN,即14
 
    unsigned char    *dev_addr;   //存放裝置的MAC位址,需由驅動程式從硬體上讀出
      unsigned char    broadcast[MAX_ADDR_LEN];    //存放裝置的廣播位址,對于以太網而言,位址長度為6個0XFF

       unsigned long    last_rx;    //接收資料包的時間戳,調用netif_rx()後賦上jiffies即可

       unsigned long    trans_start;   //發送資料包的時間戳,當要發送的時候賦上jiffies即可

       int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);//資料包發送函數, 以使得驅動程式能擷取從上層傳遞下來的資料包。
                                   
    void  (*tx_timeout) (struct net_device *dev); //發包逾時處理函數,需采取重新啟動資料包發送過程或重新啟動硬體等政策來恢複網絡裝置到正常狀态
    ... ...
}      

1.2.3 net_device_stats結構體

net_device_stats 結構體定義在核心的 include/linux/netdevice.h 檔案中,其中重要成員如下所示:

struct net_device_stats
{
    unsigned long   rx_packets;     /*收到的資料包數*/
    unsigned long   tx_packets;     /*發送的資料包數    */
    unsigned long   rx_bytes;       /*收到的位元組數,可以通過sk_buff結構體的成員len來擷取*/       unsigned long   tx_bytes;       /*發送的位元組數,可以通過sk_buff結構體的成員len來擷取*/
    unsigned long   rx_errors;      /*收到的錯誤資料包數*/
    unsigned long   tx_errors;      /*發送的錯誤資料包數*/
    ... ...
}      

net_device_stats 結構體适宜包含在裝置的私有資訊結構體中,而其中統計資訊的 修改則應該在裝置驅動的與發送和接收相關的具體函數中完成,這些函數包括中斷處 理程式、資料包發送函數、資料包發送逾時函數和資料包接收相關函數等。我們應該 在這些函數中添加相應的代碼:

/* 發送逾時函數 */ 
void xxx_tx_timeout(struct net_device *dev) 
{ 
  struct xxx_priv *priv = netdev_priv(dev); 
  ... 
  priv->stats.tx_errors++; /* 發送錯誤包數加 1 */ 
  ... 
} 

/* 中斷處理函數 */ 
static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
  switch (status &ISQ_EVENT_MASK) 
  { 
    ... 
    case ISQ_TRANSMITTER_EVENT: / 
      priv->stats.tx_packets++; /* 資料包發送成功,tx_packets 資訊加1 */ 
      netif_wake_queue(dev); /* 通知上層協定 */ 
      if ((status &(TX_OK | TX_LOST_CRS | TX_SQE_ERROR | 
        TX_LATE_COL | TX_16_COL)) != TX_OK) /*讀取硬體上的出錯标志*/ 
      { 
        /* 根據錯誤的不同情況,對 net_device_stats 的不同成員加 1 */ 
        if ((status &TX_OK) == 0) 
          priv->stats.tx_errors++; 
        if (status &TX_LOST_CRS) 
          priv->stats.tx_carrier_errors++; 
        if (status &TX_SQE_ERROR)
          priv->stats.tx_heartbeat_errors++; 
        if (status &TX_LATE_COL) 
          priv->stats.tx_window_errors++; 
        if (status &TX_16_COL) 
          priv->stats.tx_aborted_errors++; 
      } 
      break; 
    case ISQ_RX_MISS_EVENT: 
      priv->stats.rx_missed_errors += (status >> 6); 
      break; 
    case ISQ_TX_COL_EVENT: 
      priv->stats.collisions += (status >> 6); 
      break; 
  } 
}      

1.2.4 sk_buff結構體

sk_buff 結構體非常重要,它的含義為“套接字緩沖區”,用于在 Linux 網絡子系 統中的各層之間傳遞資料,是 Linux 網絡子系統資料傳遞的“中樞神經”。當發送資料包時,Linux 核心的網絡處理子產品必須建立一個包含要傳輸的資料包 的 sk_buff,然後将 sk_buff 遞交給下層,各層在 sk_buff 中添加不同的協定頭直至交 給網絡裝置發送。同樣地,當網絡裝置從網絡媒介上接收到資料包後,它必須将接收 到的資料轉換為 sk_buff 資料結構并傳遞給上層,各層剝去相應的協定頭直至交給用 戶。

參看 linux/skbuff.h 中的源代碼,sk_buff 結構體包含的主要成員如下:

/* /include/linux/skbuff.h */
struct sk_buff {
       /* These two members must be first. */
       struct sk_buff        *next;      //指向下一個sk_buff結構體
       struct sk_buff        *prev;     //指向前一個sk_buff結構體
    ktime_t                tstamp;
      struct sock        *sk;
      struct net_device    *dev;
    /*
       * This is the control buffer. It is free to use for every
       * layer. Please put your private variables there. If you
       * want to keep them across layers you have to do a skb_clone()
       * first. This is owned by whoever has the skb queued ATM.
       */
       ......
       unsigned int         len,         //資料包的總長度,包括線性資料和非線性資料
                            data_len;        //非線性的資料長度
       __u16                mac_len,         //mac標頭長度
                            hdr_len;
      .....
    __u32      priority;          //該sk_buff結構體的優先級
      ......
    __be16    protocol;           //存放上層的協定類型,可以通過eth_type_trans()來擷取
       ... ...

      sk_buff_data_t              transport_header;    //傳輸層頭部的偏移值
      sk_buff_data_t              network_header;     //網絡層頭部的偏移值
      sk_buff_data_t              mac_header;          //MAC資料鍊路層頭部的偏移值
      /* These elements must be at the end, see alloc_skb() for details.  */
    sk_buff_data_t              tail;                    //指向緩沖區的資料包末尾
      sk_buff_data_t              end;                     //指向緩沖區的末尾
      unsigned char            *head,                   //指向緩沖區的協定頭開始位置
                                  *data;                   //指向緩沖區的資料包開始位置
      unsigned int                  truesize;
      atomic_t                      users;
}      
  1. 資料緩沖區指針 head、data、tail 和 end。

head 指針指向記憶體中已配置設定的用于承載網絡資料的緩沖區的起始位址;

data 指針則指向對應目前協定層有效資料的起始位址。各層的有效資料資訊包含的内容都不一樣:對于傳輸層而言,使用者資料和傳輸層協定頭屬于有效資料。 l 對于網絡層而言,使用者資料、傳輸層協定頭和網絡層協定頭是其有效資料。 l 對于資料鍊路層而言,使用者資料、傳輸層協定頭、網絡層協定頭和鍊路層頭 部都屬于有效資料。 是以,data 指針的值需随着目前擁有 sk_buff 的協定層的變化進行相應的移動。

tail 指針則指向對應目前協定層有效資料負載的結尾位址,與 data 指針對應。

end 指針指向記憶體中配置設定的資料緩沖區的結尾,與 head 指針對應。

其實,end 指針所指地 址 數 據 緩 沖 區 的 末 尾 還 包 括 一 個 ​

​skb_shared_info​

​結構體的空間,這個結構體 存放分隔存儲的資料片段,意味着可以将數 據包的有效資料分成幾片存儲在不同的記憶體空間中。 每一個分片frags的長度上限是一頁。

  • sk_buff結構體的空間

    *

  • 一文看懂網卡驅動原理及移植方法
  1. 長度資訊 len、data_len、truesize。

len是指資料包有 效資料的長度,包括協定頭和負載;

data_len 這個成員,它記錄分片的 資料長度;

truesize 表示緩存區的整體長度: sizeof(struct sk_buff) + “傳入 alloc_skb()或dev_alloc_skb()的長度“(但不包括結構體 skb_shared_info 的長度)。

  • sk_buff-> data資料包格式
一文看懂網卡驅動原理及移植方法
  • 套接字緩沖區操作
  • (1)配置設定
  • ​struct sk_buff *alloc_skb(unsigned int len,int priority);​

    ​alloc_skb()函數配置設定一個套接字緩沖區和一個資料緩沖區,參數 len 為資料緩沖區 的空間大小,以 16 位元組對齊,參數 priority 為記憶體配置設定的優先級
  • ​struct sk_buff *dev_alloc_skb(unsigned int len);​

    ​dev_alloc_skb()函數隻是以 GFP_ATOMIC 優先級(代表配置設定過程不能被中斷)調 用上面的 alloc_skb()函數,并儲存 skb->head 和 skb->data 之間的 16 個位元組。
  • 配置設定成功之後,因為還沒有存放具體的網絡資料包,是以 sk_buff 的 data、tail 指 針都指向存儲空間的起始位址 head,而 len 的大小則為 0。
  • (2)釋放
void kfree_skb(struct sk_buff *skb);  //在核心内部使用,而網絡裝置 驅動程式中則必須使用下面3個其一
void dev_kfree_skb(struct sk_buff *skb); //用于非中斷上下文
void dev_kfree_skb_irq(struct sk_buff *skb); //用于中斷上下文
void dev_kfree_skb_any(struct sk_buff *skb); //在中斷和非中斷上下文中皆可采用      
  • (3)指針移動

    套接字緩沖區中的資料緩沖區指針移動操作包括 put(放置)、push(推)、 pull(拉)、reserve(保留)等。

  • put 操作(用于在緩沖區尾部添加資料):
unsigned char *skb_put(struct sk_buff *skb, unsigned int len); //會檢測放入緩沖區的資料
unsigned char *__skb_put(struct sk_buff *skb, unsigned int len); //不會檢測放入緩沖區的資料      

上述函數将 tail 指針下移,增加 sk_buff 的 len 值,并傳回 skb->tail 的目前值。

  • push 操作(用于在資料包發送時添加頭部)
unsigned char *skb_push(struct sk_buff *skb, unsigned int len); //會檢測放入緩沖區的資料
unsigned char *_ _skb_push(struct sk_buff *skb, unsigned int len); //不會檢測放入緩沖區的資料      

push 操作在存儲空間的頭部增加一段可以存儲網絡資料包的空間。

  • pull 操作(用于下層協定向上層協定移交資料包,使 data 指針指向上一層協定的協定頭)
unsigned char * skb_pull(struct sk_buff *skb, unsigned int len);      

skb_pull()函數将 data 指針下移,并減小 skb 的 len 值。

  • reserve 操作(用于在存儲空間 的頭部預留 len 長度的空隙)
void skb_reserve(struct sk_buff *skb, unsigned int len);      

skb_reserve()函數将 data 指針和 tail 指針同時下移

1.3 網卡驅動發包過程

在核心中,當上層要發送一個資料包時, 就會調用網絡裝置層裡net_device資料結構的成員hard_start_xmit()将資料包發送出去。

hard_start_xmit()發包函數需要我們自己建構,該函數原型如下所示:

int    (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);      

這個函數中需要涉及到sk_buff結構體,含義為(socket buffer)套接字緩沖區,用來網絡各個層次之間傳遞資料.

發包函數處理步驟:

  • 1、把資料包發出去之前,需要使用**netif_stop_queue()**來停止上層傳下來的資料包;
  • 2.1、設定寄存器,通過網絡裝置硬體來發送資料
  • 2.2、當資料包發出去後, 再調用dev_kfree_skb()函數來釋放sk_buff,該函數原型如下:​

    ​void dev_kfree_skb(struct sk_buff *skb);​

  • 3、當資料包發出成功,就會進入TX接收中斷函數,然後更新統計資訊,調用**netif_wake_queue()**來喚醒,啟動上層繼續發包下來;
  • 4、若資料包發出去逾時,一直進不到TX中斷函數,就會調用net_device結構體的*tx_timeout逾時成員函數,在該函數中更新統計資訊,并調用netif_wake_queue()來喚醒。

1.4 網卡驅動收包過程

接收資料包主要是通過中斷函數處理,來判斷中斷類型,如果等于ISQ_RECEIVER_EVENT表示為接收中斷,然後進入接收資料函數,通過​

​netif_rx()​

​​将資料上交給上層。例如,下圖核心中自帶的網卡驅動:​

​/drivers/net/cs89x0.c​

一文看懂網卡驅動原理及移植方法

通過擷取的status标志來判斷是什麼中斷,如果是接收中斷,就進入net_rx()。

  • 收包函數處理步驟:
  • 1、使用​

    ​dev_alloc_skb()​

    ​來構造一個新的sk_buff;
  • 2、使用​

    ​skb_reserve(rx_skb, 2)​

    ​ 将sk_buff緩沖區裡的資料包先向後位移2位元組,騰出sk_buff緩沖區裡的頭部空間;
  • 3、讀取網絡裝置硬體上接收到的資料;
  • 4、使用​

    ​memcpy()​

    ​将資料複制到新的sk_buff裡的data成員指向的位址處,可以使用skb_put()來動态擴大sk_buff結構體裡中的資料區;
  • 5、使用​

    ​eth_type_trans()​

    ​來擷取上層協定,将傳回值賦給sk_buff的protocol成員裡;
  • 6、然後更新統計資訊,最後使用netif_rx( )來将sk_fuffer傳遞給上層協定中。
  • skb_put()函數
  • 原型:​

    ​static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len);​

  • 作用:将資料區向下擴大len位元組
  • sk_buff緩沖區變化圖:
  • 一文看懂網卡驅動原理及移植方法

1.5 網卡驅動的注冊與登出

網絡裝置驅動的注冊與登出使用成對出現的​

​register_netdev()​

​​和​

​unregister_netdev()​

​函數完成,這兩個函數的原型為:

int register_netdev(struct net_device *dev); 
void unregister_netdev(struct net_device *dev);      

這兩個函數都接收一個 net_device 結構體指針為參數, net_device 的生成和成員的指派并非一定要由工程師逐個親自動手完成,可以利 用下面的函數幫助我們填充:

struct net_device *alloc_netdev(int sizeof_priv, const char *name,  
                                void(*setup)(struct net_device*));  

struct net_device *alloc_etherdev(int sizeof_priv)
{ 
    /* 以 ether_setup 為 alloc_netdev 的 setup 參數 */ 
    return alloc_netdev(sizeof_priv, "eth%d", ether_setup); 
}      

​alloc_netdev()​

​函數生成一個 net_device 結構體,對其成員指派并傳回該結構體的指針。第一個參數為裝置私有成員的大小,第二個參數為裝置名,第三個參數為 net_device 的 setup()函數指針。setup()函數接收的參數也為 net_device 指針,用 于預置 net_device 成員的值。

alloc_etherdev()是 alloc_netdev()針對以太網的“快捷”函數,其中的ether_setup()是由 Linux 核心提供的一個對以太網裝置 net_device 結構體中公有成員 快速指派的函數

釋放 net_device 結構體 的函數為:

void free_netdev(struct net_device *dev);      

2、編寫虛拟網卡驅動

虛拟網卡驅動,也就是說不需要硬體相關操作,是以就沒有中斷函數,我們通過linux的ping指令來實作發包,然後在發包函數中僞造一個收的ping包函數,實作能ping通任何ip位址。

## 2.1 編寫步驟

  • init初始函數
  • 1)使用alloc_netdev()來配置設定一個net_device結構體
  • 2)設定net_device結構體的成員
  • 3)使用register_netdev()來注冊net_device結構體
  • 發包函數
  • 1)使用netif_stop_queue()來阻止上層向網絡裝置驅動層發送資料包
  • 2)調用收包函數,并代入發送的sk_buff緩沖區, 裡面來僞造一個收的ping包函數
  • 3)使用dev_kfree_skb()函數來釋放發送的sk_buff緩存區
  • 4)更新發送的統計資訊
  • 5)使用netif_wake_queue()來喚醒被阻塞的上層
  • 收包函數:修改發送的sk_buff裡資料包的資料,使它變為一個接收的sk_buff。
  • 1)需要對調上圖的ethhdr結構體 ”源/目的”MAC位址
  • 2)需要對調上圖的iphdr結構體”源/目的” IP位址
  • 3)使用ip_fast_csum()來重新擷取iphdr結構體的校驗碼
  • 4)設定上圖資料包的資料類型,之前是發送ping包0x08,需要改為0x00,表示接收ping包
  • 5)使用dev_alloc_skb()來構造一個新的sk_buff
  • 6)使用skb_reserve(rx_skb, 2);将sk_buff緩沖區裡的資料包先後位移2位元組,來騰出sk_buff緩沖區裡的頭部空間
  • 7)使用memcpy()将之前修改好的sk_buff->data複制到新的sk_buff裡的data成員指向的位址處:
  • 8)設定新的sk_buff 其它成員
  • 9)使用eth_type_trans()來擷取上層協定,将傳回值賦給sk_buff的protocol成員裡
  • 10)然後更新接收統計資訊,最後使用netif_rx( )來将sk_fuffer傳遞給上層協定中
  • 具體代碼
/*
 * 參考 drivers\net\cs89x0.c
 */

#include <linux/module.h>
#include <linux/errno.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/in.h>
#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/init.h>
#include <linux/bitops.h>
#include <linux/delay.h>
#include <linux/ip.h>

#include <asm/system.h>
#include <asm/io.h>
#include <asm/irq.h>

static struct net_device *vnet_dev;

static void emulator_rx_packet(struct sk_buff *skb, struct net_device *dev)
{
    /* 參考LDD3 */
    unsigned char *type;
    struct iphdr *ih;
    __be32 *saddr, *daddr, tmp;
    unsigned char    tmp_dev_addr[ETH_ALEN];
    struct ethhdr *ethhdr;
    
    struct sk_buff *rx_skb;
        
    /* 1.對調ethhdr結構體的"源/目的"的mac位址 */
    ethhdr = (struct ethhdr *)skb->data;
    memcpy(tmp_dev_addr, ethhdr->h_dest, ETH_ALEN);
    memcpy(ethhdr->h_dest, ethhdr->h_source, ETH_ALEN);
    memcpy(ethhdr->h_source, tmp_dev_addr, ETH_ALEN);

    /* 2.對調iphdr結構體的"源/目的"的ip位址 */    
    ih = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
    saddr = &ih->saddr;
    daddr = &ih->daddr;

    tmp = *saddr;
    *saddr = *daddr;
    *daddr = tmp;
    
    /* 3.使用ip_fast_csum()來重新擷取iphdr結構體的校驗碼*/
    ih->check = 0;           /* and rebuild the checksum (ip needs it) */
    ih->check = ip_fast_csum((unsigned char *)ih,ih->ihl);
    
    /* 4.設定資料類型*/
    type = skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr);
    *type = 0; /*原來0x8表示發送ping包,現在0表示接收ping包 */
    
    /* 5.構造一個新的sk_buff */
    rx_skb = dev_alloc_skb(skb->len + 2);
    
    /* 6.使用skb_reserve騰出2位元組頭部空間*/
    skb_reserve(rx_skb, 2); /* align IP on 16B boundary */    
    
    /* 7.将之前修改好的sk_buff->data複制到新的sk_buff裡 */
    memcpy(skb_put(rx_skb, skb->len), skb->data, skb->len);    //用skb_put()擴大sk_buff的資料區,避免溢出

    /* 8.設定新sk_buff的其它成員*/
    rx_skb->dev = dev;
    rx_skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    
    /* 9.使用eth_type_trans()來擷取上層協定 */
    rx_skb->protocol = eth_type_trans(rx_skb, dev);
    
    /* 10.更新接收統計資訊,并向上層傳遞sk_fuffer收包 */
    dev->stats.rx_packets++;
    dev->stats.rx_bytes += skb->len;
    dev->last_rx = jiffies;        //收包時間戳

    // 送出sk_buff
    netif_rx(rx_skb);
}

static int virt_net_send_packet(struct sk_buff *skb, struct net_device *dev)
{
    static int cnt = 0;
    printk("virt_net_send_packet cnt = %d\n", ++cnt);

    /* 1.停止該網卡的隊列,阻止上層向驅動層繼續發送資料包 */
    netif_stop_queue(dev); 
    
    /* 2.真實驅動要把skb的資料寫入網卡 ,但在此先通過emulator_rx_packet模拟 */
    emulator_rx_packet(skb, dev);    /* 構造一個假的sk_buff,上報 */

    /* 3.釋放發送的sk_buff緩存區*/
    dev_kfree_skb (skb);
    
    /* 4.更新統計資訊 */
    dev->stats.tx_packets++;
    dev->stats.tx_bytes += skb->len;
    dev->trans_start = jiffies;    //發送時間戳
    
    /* 5.資料全部發送出去後,喚醒網卡的隊列 (真實網卡應在中斷函數裡喚醒)*/
    netif_wake_queue(dev); 
    
    return 0;
}


static const struct net_device_ops vnetdev_ops = {
    .ndo_start_xmit        = virt_net_send_packet,
};

static int virt_net_init(void)
{
    /* 1. 配置設定一個net_device結構體 */
    vnet_dev = alloc_netdev(0, "vnet%d", ether_setup);;  /* alloc_ether_dev */

    /* 2. 設定 */
    //vnet_dev->hard_start_xmit = virt_net_send_packet;
    vnet_dev->netdev_ops    = &vnetdev_ops;

    /* 設定MAC位址 */
    vnet_dev->dev_addr[0] = 0x08;
    vnet_dev->dev_addr[1] = 0x89;
    vnet_dev->dev_addr[2] = 0x66;
    vnet_dev->dev_addr[3] = 0x77;
    vnet_dev->dev_addr[4] = 0x88;
    vnet_dev->dev_addr[5] = 0x99;

    /* 設定下面兩項才能ping通 */
    vnet_dev->flags           |= IFF_NOARP;
    vnet_dev->features        |= NETIF_F_IP_CSUM;

    /* 3. 注冊 */
    //register_netdevice(vnet_dev);     //編譯會出錯!
    register_netdev(vnet_dev);
    
    return 0;
}

static void virt_net_exit(void)
{
    unregister_netdev(vnet_dev);
    free_netdev(vnet_dev);
}

module_init(virt_net_init);
module_exit(virt_net_exit);

MODULE_AUTHOR("[email protected]");
MODULE_LICENSE("GPL");      

2.2 測試

  • 挂載驅動,可以看到net類下就有了這個網卡裝置,并嘗試ping自己
  • 一文看懂網卡驅動原理及移植方法

上圖的ping之是以成功,并是因為我們在發包函數中僞造了一個來收包,而是因為linux中,ping自己的時候并不調用(不需要)發送函數向網絡裝置發送sk_buff資料包。

  • ping同網段其它位址
  • 一文看懂網卡驅動原理及移植方法

ping其它ip位址之是以成功,是因為我們在發包函數中利用emulator_rx_packet函數僞造了一個接收資料包,并通過netif_rx()來将收包上傳給上層。

3、移植核心自帶的網卡驅動程式

在移植之前,首先我們來看一下mini2440(對應的機器ID為:set machid 7CF)中,是如何支援dm9000網卡的。

進入到入口函數,找到結構體:

static struct platform_driver dm9000_driver = {
  .driver  = {
    .name    = "dm9000",
    .owner   = THIS_MODULE,
    .pm   = &dm9000_drv_pm_ops,
  },
  .probe   = dm9000_probe,
  .remove  = __devexit_p(dm9000_drv_remove),
};      

一般是通過.name這個成員進行比對的,搜尋字元串“dm9000”,找到如下結構體(在平台檔案中:arch\arm\mach-s3c24xx\Mach-mini2440.c):

static struct platform_device mini2440_device_eth = {
  .name    = "dm9000",
  .id    = -1,
  .num_resources  = ARRAY_SIZE(mini2440_dm9k_resource),
  .resource  = mini2440_dm9k_resource,
  .dev    = {
    .platform_data  = &mini2440_dm9k_pdata,
  },
};      

然後搜尋結構體mini2440_device_eth,找到:

static struct platform_device *mini2440_devices[] __initdata = {
  &s3c_device_ohci,
  &s3c_device_wdt,
  &s3c_device_i2c0,
  &s3c_device_rtc,
  &s3c_device_usbgadget,
  &mini2440_device_eth,  //在這裡
  &mini2440_led1,
  &mini2440_led2,
  &mini2440_led3,
  &mini2440_led4,
  &mini2440_button_device,
  &s3c_device_nand,
  &s3c_device_sdi,
  &s3c_device_iis,
  &uda1340_codec,
  &mini2440_audio,
  &samsung_asoc_dma,
};      

然後再搜尋:mini2440_devices,找到:

platform_add_devices(mini2440_devices, ARRAY_SIZE(mini2440_devices));      

這就是把結構體mini2440_devices添加到核心,裡面的關于網卡的結構在裡面,最終比對驅動程式,就可以使用驅動程式了。

(這就是所謂的平台裝置平台驅動的東西了,把可變的東西抽象出來放到平台相關的檔案中定義,而我們的驅動程式,基本上是不需要改變的,它是穩定的内容,我們移植的時候,隻需要把平台層可變的相關結構體加上,需要修改的資源,進行修改就可以了)。

而我們用的是smdk2440(對應的機器ID為:set machid 16a),然後我在Mach-smdk2440.c中添加以下函數:

/* 以下為[email protected] 添加
 * The DM9000 has no eeprom, and it's MAC address is set by
 * the bootloader before starting the kernel.
 */


/* DM9000AEP 10/100 ethernet controller */

#define MACH_SMDK2440_DM9K_BASE (S3C2410_CS4 + 0x300)


static struct resource smdk2440_dm9k_resource[] = {
  [0] = {
    .start = MACH_SMDK2440_DM9K_BASE,
    .end   = MACH_SMDK2440_DM9K_BASE + 3,
    .flags = IORESOURCE_MEM
  },
  [1] = {
    .start = MACH_SMDK2440_DM9K_BASE + 4,
    .end   = MACH_SMDK2440_DM9K_BASE + 7,
    .flags = IORESOURCE_MEM
  },
  [2] = {
    .start = IRQ_EINT7,
    .end   = IRQ_EINT7,
    .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE,
  }
};


static struct dm9000_plat_data smdk2440_dm9k_pdata = {
  .flags    = (DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM),
};

static struct platform_device smdk2440_device_eth = {
  .name    = "dm9000",
  .id    = -1,
  .num_resources  = ARRAY_SIZE(smdk2440_dm9k_resource),
  .resource  = smdk2440_dm9k_resource,
  .dev    = {
    .platform_data  = &smdk2440_dm9k_pdata,
  },
};

/* 以下為[email protected] 添加 */      

在結構體smdk2440_devices中添加網卡成員:

static struct platform_device *smdk2440_devices[] __initdata = {
  &s3c_device_ohci,
  &s3c_device_lcd,
  &s3c_device_wdt,
  &s3c_device_i2c0,
  &s3c_device_iis,
  &smdk2440_device_eth, /* lyy:添加 */
};      

添加頭檔案:

#include <linux/dm9000.h>  /* 以下為[email protected] 添加*/      

然後重新編譯核心。成功。燒寫新核心:

S3C2440A # nfs 30000000 192.168.1.101:/home/leon/nfs_root/first_fs/uImage;

S3C2440A # bootm 30000000      

然後挂載網絡檔案系統:

​​

​mount -t nfs -o nolock 192.168.1.101:/home/leon/nfs_root/first_fs /mnt​

成功挂載網絡檔案系統。

4、自己編寫網卡驅動程式

有時候,核心自帶的網卡驅動程式比較老,而我們的硬體有可能比較新,那麼我們就不能使用核心的網卡驅動程式了,就需要去移植最新的網卡驅動程式,那麼這種類型的,又該如何移植呢?

4.1 網絡裝置驅動程式的子產品加載和解除安裝函數

int xxx_init_module(void) 
{ 
  ... 
  /* 配置設定 net_device 結構體并對其成員指派 */ 
  xxx_dev = alloc_netdev(sizeof(struct xxx_priv), "sn%d", xxx_init); 
  if (xxx_dev == NULL) 
  ... /* 配置設定 net_device 失敗 */ 

  /* 注冊 net_device 結構體 */ 
  if ((result = register_netdev(xxx_dev))) 
  ... 
} 

void xxx_cleanup(void) 
{ 
  ... 
  /* 登出 net_device 結構體 */ 
  unregister_netdev(xxx_dev); 
  /* 釋放 net_device 結構體 */ 
  free_netdev(xxx_dev); 
}      

4.2 網絡裝置的初始化

網絡裝置的初始化主要需要完成如下幾個方面的工作:

  1. 進行硬體上的準備工作,檢查網絡裝置是否存在,如果存在,則檢測裝置使用的硬體資源;
  2. 進行軟體接口上的準備工作,配置設定 net_device 結構體并對其資料和函數指針 成員指派;
  3. 獲得裝置的私有資訊指針并初始化其各成員的值。如果私有資訊中包括自旋 鎖或信号量等并發或同步機制,則需對其進行初始化。

個網絡裝置驅動初始化函數的模闆如下所示:

void xxx_init(struct net_device *dev) 
{ 
  /*裝置的私有資訊結構體*/ 
  struct xxx_priv *priv; 
 
  /* 檢查裝置是否存在、具體硬體配置和設定裝置所使用的硬體資源 */ 
  xxx_hw_init(); 
 
  /* 初始化以太網裝置的公用成員 */ 
  ether_setup(dev); 
 
  /*設定裝置的成員函數指針*/ 
  dev->open = xxx_open; 
  dev->stop = xxx_release; 
  dev->set_config = xxx_config; 
  dev->hard_start_xmit = xxx_tx; 
  dev->do_ioctl = xxx_ioctl; 
  dev->get_stats = xxx_stats; 
  dev->change_mtu = xxx_change_mtu; 
  dev->rebuild_header = xxx_rebuild_header; 
  dev->hard_header = xxx_header; 
  dev->tx_timeout = xxx_tx_timeout; 
  dev->watchdog_timeo = timeout; 
 
  /*如果使用 NAPI,設定 pool 函數*/ 
  if (use_napi) 
  { 
    dev->poll = xxx_poll; 
  } 

  /* 取得私有資訊,并初始化它*/ 
  priv = netdev_priv(dev);
    ... /* 初始化裝置私有資料區 */ 
}      

## 4.3 網絡裝置的打開與釋放

網絡裝置的打開函數需要完成如下工作。

1.  使能裝置使用的硬體資源,申請 I/O 區域、中斷和 DMA 通道等。

2.  調用 Linux 核心提供的 netif_start_queue()函數,激活裝置發送隊列。 網絡裝置的關閉函數需要完成如下工作。

3.  調用 Linux 核心提供的 netif_stop_queue()函數,停止裝置傳輸包。

4. 釋放裝置所使用的 I/O 區域、中斷和 DMA 資源。

Linux 核心提供的 netif_start_queue()和 netif_stop_queue()兩個函數的原型為:

void netif_start_queue(struct net_device *dev);  

void netif_stop_queue (struct net_device *dev);      

根據以上分析,可得出網絡裝置打開和釋放函數的模闆:

int xxx_open(struct net_device *dev) 
{ 
  /* 申請端口、IRQ 等,類似于 fops->open */ 
  ret = request_irq(dev->irq, &xxx_interrupt, 0, dev->name, dev); 
  ... 
  netif_start_queue(dev); 
  ... 
} 
 
int xxx_release(struct net_device *dev) 
{ 
  /* 釋放端口、IRQ 等,類似于 fops->close */ 
  free_irq(dev->irq, dev); 
  ... 
  netif_stop_queue(dev); /* can't transmit any more */ 
  ... 
}      

4.3 資料發送流程

(1)網絡裝置驅動程式從上層協定傳遞過來的 sk_buff 參數獲得資料包的有效數 據和長度,将有效資料放入臨時緩沖區。

(2)對于以太網,如果有效資料的長度小于以太網沖突檢測所要求資料幀的最小 長度 ETH_ZLEN,則給臨時緩沖區的末尾填充 0。

(3)設定硬體的寄存器,驅使網絡裝置進行資料發送操作。

int xxx_tx(struct sk_buff *skb, struct net_device *dev) 
{ 
  int len; 
  char *data, shortpkt[ETH_ZLEN]; 
  /* 獲得有效資料指針和長度 */ 
  data = skb->data; 
  len = skb->len; 
  if (len < ETH_ZLEN) 
  { 
    /* 如果幀長小于以太網幀最小長度,補 0 */ 
  memset(shortpkt, 0, ETH_ZLEN); 
  memcpy(shortpkt, skb->data, skb->len); 
  len = ETH_ZLEN; 
  data = shortpkt; 
  } 
 
  dev->trans_start = jiffies; /* 記錄發送時間戳 */ 
 
  /* 設定硬體寄存器讓硬體把資料包發送出去 */ 
  xxx_hw_tx(data, len, dev); 
  ... 
}      

當資料傳輸逾時時,意味着目前的發送操作失敗,此時,資料包發送逾時處理函 數 xxx_tx_ timeout()将被調用。這個函數需要調用 Linux 核心提供的 netif_wake_queue()函數重新啟動裝置發送隊列:

void xxx_tx_timeout(struct net_device *dev) 
{ 
  ... 
  netif_wake_queue(dev); /* 重新啟動裝置發送隊列 */ 
}      

4.4 資料接收流程

網絡裝置接收資料的主要方法是由中斷引發裝置的中斷處理函數,中斷處理函數 判斷中斷類型,如果為接收中斷,則讀取接收到的資料,配置設定 sk_buffer 資料結構和數 據緩沖區,将接收到的資料複制到資料緩沖區,并調用 netif_rx()函數将 sk_buffer 傳 遞給上層協定。完成這一過程的函數模闆:

static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
    ... 
    switch (status &ISQ_EVENT_MASK) 
    { 
        case ISQ_RECEIVER_EVENT: /* 擷取資料包 */
            xxx_rx(dev); 
            break; 
            /* 其他類型的中斷 */ 
    } 
} 

static void xxx_rx(struct xxx_device *dev) 
{ 
    ... 
    length = get_rev_len (...); 
    /* 配置設定新的套接字緩沖區 */ 
    skb = dev_alloc_skb(length + 2); 
    skb_reserve(skb, 2); /* 對齊 */ 
    skb->dev = dev; 

    /* 讀取硬體上接收到的資料 */ 
    insw(ioaddr + RX_FRAME_PORT, skb_put(skb, length), length >> 1); 
    if (length &1) 
        skb->data[length - 1] = inw(ioaddr + RX_FRAME_PORT); 
 
    /* 擷取上層協定類型 */ 
    skb->protocol = eth_type_trans(skb, dev); 
 
    /* 把資料包交給上層 */ 
    netif_rx(skb); 
 
    /* 記錄接收時間戳 */ 
    dev->last_rx = jiffies; 
    ... 
}      

如果是 NAPI 相容的裝置驅動,則可以通過 poll 方式接收資料包。這種情況下, 我們需要為該裝置驅動提供 xxx_poll()函數:

static int xxx_poll(struct net_device *dev, int *budget) 
{ 
    //dev->quota 是目前 CPU 能夠從所有接口中接收資料包的最大數目,budget 是在初始化階段配置設定給接口的 weight 值
    int npackets = 0, quota = min(dev->quota, *budget); 
    struct sk_buff *skb; 
    struct xxx_priv *priv = netdev_priv(dev); 
    struct xxx_packet *pkt; 

    while (npackets < quota && priv->rx_queue) 
    { 
        /*從隊列中取出資料包*/ 
        pkt = xxx_dequeue_buf(dev); 
 
        /*接下來的處理,和中斷觸發的資料包接收一緻*/ 
        skb = dev_alloc_skb(pkt->datalen + 2); 
        if (!skb) 
        { 
            ... 
            continue; 
        } 
        skb_reserve(skb, 2); 
        memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen); 
        skb->dev = dev; 
        skb->protocol = eth_type_trans(skb, dev); 
        /*調用 netif_receive_skb 而不是 net_rx 将資料包交給上層協定
          這裡展現出了中斷處理機制和輪詢機制之間的差别。
         */ 
        netif_receive_skb(skb); 

        /*更改統計資料 */ 
        priv->stats.rx_packets++; 
        priv->stats.rx_bytes += pkt->datalen; 
        xxx_release_buffer(pkt); 
    } 
    /* 網絡裝置接收緩沖區中的資料包都被讀取完了*/ 
    *budget -= npackets; 
    dev->quota -= npackets; 

    if (!priv->rx_queue) 
    { 
        netif_rx_complete(dev); //把目前指定的裝置從 poll 隊列中清除
        xxx_enable_rx_int (…); /* 再次使能網絡裝置的接收中斷 */ 
        return 0; 
    } 

    return 1; 
}      

雖然 NAPI 相容的裝置驅動以 poll 方式接收資料包,但是仍然需要首次資料包接 收中斷來觸發 poll 過程。與資料包的中斷接收方式不同的是,以輪詢方式接收資料包 時,當第一次中斷發生後,中斷處理程式要禁止裝置的資料包接收中斷。poll 中斷處理函數如下:

static void xxx_poll_interrupt(int irq, void *dev_id, struct pt_regs *regs) 
{ 
    switch (status &ISQ_EVENT_MASK) 
    { 
    case ISQ_RECEIVER_EVENT:
        .../* 擷取資料包 */ 
        xxx_disable_rx_int(...); /* 禁止接收中斷 */ 
        netif_rx_schedule(dev); 
        break;
    .../* 其他類型的中斷 */ 
    } 
}      

上述代碼的 ​

​netif_rx_schedule()​

​函數被輪詢方式驅動的中斷程式調用,将設 備的 poll 方法添加到網絡層的 poll 處理隊列中,排隊并且準備接收資料包,最終觸發 一個 NET_RX_SOFTIRQ 軟中斷,通知網絡層接收資料包。下圖為 NAPI 驅動 程式各部分的調用關系:

一文看懂網卡驅動原理及移植方法

4.5 網絡連接配接狀态

網絡裝置驅動程式中往往設定一個定時器來對鍊路狀态進行周期性地檢查。當定 時器到期之後,在定時器處理函數中讀取實體裝置的相關寄存器獲得載波狀态,進而 更新裝置的連接配接狀态。

網絡裝置驅動可以通過 ​

​netif_carrier_on()​

​​和​

​netif_carrier_off()​

​​函數改變或通知核心網絡裝置的連接配接狀态。此外,函數 ​

​netif_carrier_ok()​

​ 可用于向調用者傳回鍊路上的載波信号是否存在。

void netif_carrier_on(struct net_device *dev); 
void netif_carrier_off(struct net_device *dev); 
int netif_carrier_ok(struct net_device *dev);      

以下代碼顯示了網絡裝置驅動用定時器周期檢查鍊路狀态:

static void xxx_timer(unsigned long data) 
{ 
    struct net_device *dev = (struct net_device*)data; 
    u16 link; 
    ...
    if (!(dev->flags &IFF_UP)) 
    { 
        goto set_timer; 
    }
    /* 獲得實體上的連接配接狀态 */ 
    if (link = xxx_chk_link(dev)) //讀取網絡擴充卡硬體的相關寄存器以獲得鍊路連接配接狀态
    { 
        if (!(dev->flags &IFF_RUNNING)) 
        { 
            netif_carrier_on(dev); 
            dev->flags |= IFF_RUNNING; 
            printk(KERN_DEBUG "%s: link up\n", dev->name); 
        } 
    } 
    else    
    { 
        if (dev->flags &IFF_RUNNING) 
        { 
            netif_carrier_off(dev); 
            dev->flags &= ~IFF_RUNNING; 
            printk(KERN_DEBUG "%s: link down\n", dev->name); 
        } 
    } 
 
    set_timer: 
    priv->timer.expires = jiffies + 1 * HZ; 
    priv->timer.data = (unsigned long)dev; 
    priv->timer.function = &xxx_timer; /* timer handler */ 
    add_timer(&priv->timer); 
}      

從上述源代碼還可以看出,定時器處理函數會不停地利用第 31~35 行代 碼啟動新的定時器以實作周期檢測的目的。那麼最初啟動定時器的地方在哪裡呢?很 顯然,它最适合在裝置的打開函數中完成:

static int xxx_open(struct net_device *dev) 
{ 
  struct xxx_priv *priv = (struct xxx_priv*)dev->priv; 
 
  ... 
  priv->timer.expires = jiffies + 3 * HZ; 
  priv->timer.data = (unsigned long)dev; 
  priv->timer.function = &xxx_timer; /* timer handler */ 
  add_timer(&priv->timer); 
  ...
}      

5. CS8900 網卡裝置驅動執行個體分析

當 CS8900 處于 I/O 模式下時(這裡所說的 CS8900 處于 I/O 模式并非意味着它一定位于 CPU 的 I/O 空間,實 際上,CS8900 I/O 模式下的寄存器仍然映射 ARM 處理器的記憶體空間。是以, 我們直接通過讀/寫寄存器位址ioremap()之後的虛拟位址即可),可以通過以 下幾個 PacketPage 空間内的寄存器來控制 CS8900 的行為(括号内給出的是寄存器地 址相對于 PacketPage 基位址的偏移):

寄存器 作用
LINECTL(0112H) 決定 CS8900 的基本配置和實體接口,可選擇使用 10BASE-T 接口、AUI 接口或者自動選擇。
RXCTL(0104H) 控制 CS8900 接收特定資料包,控制是否接收多點傳播、廣播和單點傳播包。
RXCFG(0102H) RXCFG 控制 CS8900 接收到特定資料包後引發接收中斷,并控制是否使用接收 DMA 和 CRC 校驗。
BUSCT(0116H) BUSCT 可控制晶片的工作模式、DMA 方式、是否使能外部中斷引腳。
BUSST(0138H) 标志網絡裝置的發送狀态,如裝置是否準備好發送。
ISQ(0120H) 網卡晶片的中斷狀态寄存器

在 I/O 模式下,CS8900 發送資料包的步驟如下:

(1)向控制寄存器 TXCMD 寄存器寫入發送指令​

​write_reg(TXCMD, send_cmd);​

​。

(2)将發送資料長度寫入 TXLENG 寄存器​

​write_reg(TXLENG, send_len)​

​。

(3)讀取 PacketPage 空間内的 BUSST 寄存器,确定其第 8 位被設定為 Rdy4TxNOW,即裝置處于準備發送狀态​

​reg(BusST)&0x100​

​。

(4)将要發送的資料循環寫入 PORT0 寄存器​

​write_reg(PORT0, data)​

​。

(5)将資料組織為以太網幀并添加填充位和 CRC 校驗資訊,然後将資料轉化為比特流傳送到網絡媒介。

在 I/O 模式下,CS8900 接收資料包的方法如下:

(1)接收到網絡擴充卡産生的中斷,查詢相對于 I/O 基位址偏移 0008H 中斷狀态 隊列端口,判斷中斷類型為接收中斷。

(2)讀 PORT0 寄存器依次獲得接收狀态 rxStatus、接收資料長度 rxLength。

(3)循環繼續對 PORT0 寄存器讀取 rxLength 次,獲得整個資料包。

(4)驅動程式進行資料包處理并傳遞給上層。

對于一種網絡裝置的驅動而言,工程師主要需完成裝置驅動功能層的設計。在 16.2~ 16.8節已經給出了裝置驅動功能層主要資料結構和函數的設計模闆,是以,在編寫CS8900 的這些資料結構和函數時,實際要完成的工作就是用具體的針對 CS8900 的操作來填充模 闆,具體包括以下工作:

  1. 填充 CS8900 的私有資訊結構體,把 CS8900 特定的資料放入這個私有結構體中。在 CS8900 驅動程式中,這個資料結構為 ​

    ​struct net_local​

    ​。

    在 CS8900 的裝置驅動程式中,核心資料結構 net_device 以全局變量的方式定義, 其數個成員的初始值都被置為空,私有資訊結構體為 net_local:

static struct net_device dev_cs89x0 = 
{ 
    "", 
    0, 0, 0, 0, 
    0, 0, 
    0, 0, 0, NULL, NULL 
};

struct net_local 
{ 
    struct net_device_stats stats; /* 網絡裝置狀态結構體 */ 
    int chip_type; /* 區分晶片類型:CS89x0 */ 
    char chip_revision; /* 晶片版本字母,如"A" */ 
    int send_cmd; /* 發送指令: TX_NOW, TX_AFTER_381 或 TX_AFTER_ALL */ 
    ... 
    spinlock_t lock; /* 并發控制自旋鎖 */ 
};      

當晶片的版本字母不同時,net_local 結構體中記錄的 send_cmd 指令将不同。例如, 同樣是 CS8900 網卡,若晶片版本字母為大于等于“F”,則發送指令為 TX_NOW,而 對于 CS8900A,發送指令為 TX_AFTER_ALL。

  1. 填充裝置初始化模闆,初始化 net_device 結構體,将其注冊入核心。net_device 的注冊與登出在子產品加載與登出函數中完成。在 CS8900 驅動程式中,與此相關的函數有:
struct net_device * _ _init cs89x0_probe(int unit);

int cs89x0_probe1(struct net_device *dev, int ioaddr, int modular); 

int init_module(void);  

void cleanup_module(void);      

裝置的初始化由 net_device 結構體中的 init()函數完成,這個函數将在 net_device 被注冊時自動被調用。init()函數在 CS8900 網卡的驅動程式中對應 于 cs89x0_probe()函數:

int __init cs89x0_probe(struct net_device *dev) 
{ 
    int i; 
 
    SET_MODULE_OWNER(dev); 
    DPRINTK(1, "cs89x0:cs89x0_probe(0x%x)\n", base_addr); 

    BWSCON = (BWSCON & ~(BWSCON_ST3 | BWSCON_WS3 | BWSCON_DW3)) | 
                (BWSCON_ST3 | BWSCON_WS3 | BWSCON_DW(3, BWSCON_DW_16)); 
    BANKCON3= BANKCON_Tacs0 | BANKCON_Tcos4 | BANKCON_Tacc14 | 
                BANKCON_Toch1 | BANKCON_Tcah4 | BANKCON_Tacp6 | BANKCON_PMC1; 
 
    set_external_irq(IRQ_CS8900, EXT_RISING_EDGE, GPIO_PULLUP_DIS); 

    for (i = 0; netcard_portlist[i]; i++) 
    { 
        if (cs89x0_probe1(dev, netcard_portlist[i]) == 0) //驗證網卡的存在,并擷取 CS8900所使用的硬體資源
            return 0; 
    } 
    printk(KERN_WARNING "cs89x0: no cs8900 or cs8920 detected." 
       "Be sure to disable PnP with SETUP\n"); 
    return -ENODEV; 
}


static unsigned int netcard_portlist[] __initdata = 
{ 
    vCS8900_BASE + 0x300,    //假設硬體平台中網卡的基位址為 vCS8900_BASE + 0x300
    0
}; 

/*
 *上述 cs89x0_probe1()函數的流程如下。
 *(1)第 8~20 行配置設定裝置的私有資訊結構體記憶體并初始化,若配置設定失敗,則直接跳入第 78 行的代碼傳回。
 *(2)第 24~26 行從寄存器中讀取晶片的具體類型。
 *(3)第 27~32 行判斷晶片類型,若不是 CS8900 則直接跳入第 77 行的代碼,釋放私有資訊結構體并傳回。
 *(4)當晶片類型為 CS8900 時,第 34~69 行完成 net_device 裝置結構體的初始化,指派其屬性和函數指針。
 */

static int __init cs89x0_probe1(struct net_device *dev, int ioaddr)
{ 
    struct net_local *lp; 
    unsigned rev_type = 0; 
    int ret; 
 
    /* 初始化裝置結構體私有資訊 */ 
    if (dev->priv == NULL) 
    { 
        dev->priv = kmalloc(sizeof(struct net_local), GFP_KERNEL); 
        if (dev->priv == 0) 
        { 
            ret = - ENOMEM; 
            goto before_kmalloc; 
        } 
    lp = (struct net_local*)dev->priv; 
    memset(lp, 0, sizeof(*lp)); 
    spin_lock_init(&lp->lock); 
    } 
    lp = (struct net_local*)dev->priv; 

    dev->base_addr = ioaddr; 
    /* 讀取晶片類型 */ 
    rev_type = readreg(dev, PRODUCT_ID_ADD); 
    lp->chip_type = rev_type &~REVISON_BITS;
    lp->chip_revision = ((rev_type &REVISON_BITS) >> 8) + 'A';30 
    if (lp->chip_type != CS8900) 
    { 
        printk(_ _FILE_ _ ": wrong device driver!\n"); 
        ret = - ENODEV; 
        goto after_kmalloc;
    } 
    /* 根據晶片類型和版本确定正确的發送指令 */ 
    lp->send_cmd = TX_AFTER_ALL; 
    if (lp->chip_type == CS8900 && lp->chip_revision >= 'F') 
        lp->send_cmd = TX_NOW; 

    reset_chip(dev); 

    lp->adapter_cnf = A_CNF_10B_T | A_CNF_MEDIA_10B_T; 
    lp->auto_neg_cnf = EE_AUTO_NEG_ENABLE;
    printk(KERN_INFO "cs89x0 media %s%s", (lp->adapter_cnf &A_CNF_10B_T) ? "RJ-45": "", (lp->adapter_cnf &A_CNF_AUI) ? "AUI" : ""); 

    /* 設定 CS8900 的 MAC 位址 */ 
    dev->dev_addr[0] = 0x00; 
    dev->dev_addr[1] = 0x00; 
    dev->dev_addr[2] = 0xc0; 
    dev->dev_addr[3] = 0xff; 
    dev->dev_addr[4] = 0xee; 
    dev->dev_addr[5] = 0x08; 
    set_mac_address(dev, dev->dev_addr); 
 
    /* 設定裝置中斷号 */ 
    dev->irq = IRQ_LAN; 
    printk(", IRQ %d", dev->irq); 
 
    /* 填充裝置結構體的成員函數指針 */ 
    dev->open = net_open; 
    dev->stop = net_close; 
    dev->tx_timeout = net_timeout; 
    dev->watchdog_timeo = 3 * HZ; 
    dev->hard_start_xmit = net_send_packet; 
    dev->get_stats = net_get_stats; 
    dev->set_multicast_list = set_multicast_list; 
    dev->set_mac_address = set_mac_address; 
 
    /* 填充以太網公用資料和函數指針 */ 
    ether_setup(dev); 

    printk("\n"); 
    DPRINTK(1, "cs89x0_probe1() successful\n"); 
    return 0;
    after_kmalloc: kfree(dev->priv); 
    before_kmalloc: return ret; 
} 


static int __init init_cs8900a_s3c2410(void) 
{ 
    struct net_local *lp; 
    int ret = 0; 
 
    dev_cs89x0.irq = irq; 
    dev_cs89x0.base_addr = io; 
    dev_cs89x0.init = cs89x0_probe; //在使用 register_netdev()函數注net_device 裝置結構體時,cs89x0_probe()函數會被自動調用以完成 net_device 結構體的初始化。
    dev_cs89x0.priv = kmalloc(sizeof(struct net_local), GFP_KERNEL); 
    if (dev_cs89x0.priv == 0) 
    { 
        printk(KERN_ERR "cs89x0.c: Out of memory.\n"); 
        return - ENOMEM; 
    } 
    memset(dev_cs89x0.priv, 0, sizeof(struct net_local)); 
    lp = (struct net_local*)dev_cs89x0.priv; 
    
    //為 CS8900 網卡申請了 NETCARD_IO_EXTENT大小的I/O 位址區域
    request_region(dev_cs89x0.base_addr, NETCARD_IO_EXTENT, "cs8900a");
    spin_lock_init(&lp->lock); 
 
    /* 設定實體接口的正确類型*/ 
    if (!strcmp(media, "rj45")) 
    lp->adapter_cnf = A_CNF_MEDIA_10B_T | A_CNF_10B_T; 
    else if (!strcmp(media, "aui")) 
        lp->adapter_cnf = A_CNF_MEDIA_AUI | A_CNF_AUI; 
    else if (!strcmp(media, "bnc")) 
        lp->adapter_cnf = A_CNF_MEDIA_10B_2 | A_CNF_10B_2; 
    else 
        lp->adapter_cnf = A_CNF_MEDIA_10B_T | A_CNF_10B_T; 
 
    if (duplex == - 1) 
        lp->auto_neg_cnf = AUTO_NEG_ENABLE; 
 
    if (io == 0) 
    { 
        printk(KERN_ERR "cs89x0.c: Module autoprobing not allowed.\n"); 
        printk(KERN_ERR "cs89x0.c: Append io=0xNNN\n"); 
        ret = - EPERM; 
        goto out; 
    } 
    //net_device 裝置結構體的注冊
    if (register_netdev(&dev_cs89x0) != 0) 
    { 
        printk(KERN_ERR "cs89x0.c: No card found at 0x%x\n", io); 
        ret = - ENXIO; 
        goto out; 
    } 
out: if (ret) 
        kfree(dev_cs89x0.priv); 
    return ret; 
} 


static void _ _exit cleanup_cs8900a_s3c2410(void) 
{ 
    if (dev_cs89x0.priv != NULL) 
    { 
        /* 釋放私有資訊結構體 */ 
        unregister_netdev(&dev_cs89x0); 
        outw(PP_ChipID, dev_cs89x0.base_addr + ADD_PORT); 
        kfree(dev_cs89x0.priv); 
        dev_cs89x0.priv = NULL; 
        /* 釋放 CS8900 申請的 I/O 位址區域 */ 
        release_region(dev_cs89x0.base_addr, NETCARD_IO_EXTENT); 
    } 
}      

上述函數第 8~11 行設定 S3C2410A ARM 處理器的片選,第 13行設定 ARM 與 CS8900 網卡對應的中斷,第 15~18 行循環檢索 netcard_portlist[ ]數組中定義的基位址處 是否存在 CS8900 網卡

  1. 填充裝置發送資料包函數模闆,把真實的資料包發送硬體操作填充入​

    ​xxx_tx()​

    ​​ 函數,并填充發送逾時函數​

    ​xxx_tx_timeout()​

    ​​。當發送資料包逾時時,CS8900 驅動程式的資料包發送逾時函數将被調用,它重 新啟動裝置發送隊列。在初始化函數中,CS8900 的資料包發送函數指針​

    ​hard_ start_xmit​

    ​​被指派為 CS8900 驅動程式中的​

    ​net_send_packet()​

    ​,這個函數完成硬體發送序列。具體代碼如下:
static int net_send_packet(struct sk_buff *skb, struct net_device  *dev)
{
    struct net_local *lp = (struct net_local*)dev->priv; 
 
    writereg(dev, PP_BusCTL, 0x0); 
    writereg(dev, PP_BusCTL, readreg(dev, PP_BusCTL) | ENABLE_IRQ); 
 
    spin_lock_irq(&lp->lock);/* 使用自旋鎖阻止多個資料包被同時寫入硬體*/ 
    netif_stop_queue(dev); 
 
    /* 初始化硬體發送序列 */ 
    writeword(dev, TX_CMD_PORT, lp->send_cmd); 
    writeword(dev, TX_LEN_PORT, skb->len); 
 
    /* 檢測硬體是否處于發送 READY 狀态 */ 
    if ((readreg(dev, PP_BusST) &READY_FOR_TX_NOW) == 0) 
    { 
        spin_unlock_irq(&lp->lock); 
        DPRINTK(1, "cs89x0: Tx buffer not free!\n"); 
        return 1; 
    } 
 
    writeblock(dev, skb->data, skb->len);    /* 将資料寫入硬體 */ 
 
    spin_unlock_irq(&lp->lock);    /* 解鎖自旋鎖 */ 
    dev->trans_start = jiffies;    /* 記錄發送開始的時間戳 */ 
    dev_kfree_skb(skb);            /* 釋放 sk_buff 和資料緩沖區 */ 
 
    return 0; 
}

static void net_timeout(struct net_device *dev)
{ 
    DPRINTK(1, "%s: transmit timed out, %s?\n", dev->name,
        tx_done(dev) ? "IRQ conflict ?" : "network cable problem"); 
 
    net_close(dev); //停止網卡
    writereg(dev, PP_SelfCTL, readreg(dev, PP_SelfCTL) | POWER_ON_RESET); //網卡硬複位
    net_open(dev); //再次啟動網卡
}      
  1. 填充裝置驅動程式的中斷處理程式 xxx_interrupt()和具體的資料包接收函數 xxx_rx(),填入真實的硬體操作。在 CS8900 驅動程式中,與此相關的函數有:
irqreturn_t net_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{ 
    struct net_device *dev = dev_id; 
    struct net_local *lp; 
    int ioaddr, status; 
 
    ioaddr = dev->base_addr; 
    lp = (struct net_local*)dev->priv; 
 
    /* 讀取中斷事件類型 */ 
    while ((status = readword(dev, ISQ_PORT))) 
    { 
        DPRINTK(4, "%s: event=%04x\n", dev->name, status); 
        switch (status &ISQ_EVENT_MASK) 
        { 
            case ISQ_RECEIVER_EVENT: 
                /* 獲得資料包 */ 
                net_rx(dev); 
                break; 
            ... /* 其他類型中斷 */ 
        } 
    } 
} 


static void net_rx(struct net_device *dev)
{
    struct net_local *lp = (struct net_local*)dev->priv; 
    struct sk_buff *skb; 
    int status, length; 
 
    int ioaddr = dev->base_addr; 
 
    status = inw(ioaddr + RX_FRAME_PORT); 
    if ((status &RX_OK) == 0) 
    { 
        count_rx_errors(status, lp); 
        return ; 
    } 
 
    length = inw(ioaddr + RX_FRAME_PORT);/* 讀取接收資料包的長度 */ 
 
    /* 配置設定新的套接字緩沖區和資料緩沖區 */ 
    skb = dev_alloc_skb(length + 2); 
    if (skb == NULL) 
    { 
        lp->stats.rx_dropped++; /* 配置設定失敗,統計被丢棄的包數 */ 
        return ; 
    } 
    skb_reserve(skb, 2); 
    skb->len = length; 
    skb->dev = dev;
    readblock(dev, skb->data, skb->len); /* 從硬體中讀取資料包放入資料緩沖區 */
    skb->protocol = eth_type_trans(skb, dev);/* 解析收到資料包的網絡層協定類型 */ 
 
    netif_rx(skb); /* 傳遞給上層協定 */ 
 
    dev->last_rx = jiffies; /* 記錄最後收到資料包的時間戳 */ 
    /* 統計接收資料包數和接收位元組數 */ 
    lp->stats.rx_packets++; 
    lp->stats.rx_bytes += length; 
}      
  1. 填充裝置打開 xxx_open()與釋放 xxx_release()函數代碼。在 CS8900 驅動程式 中,與此相關的函數有:
int net_open(struct net_device *dev);  

int net_close(struct net_device *dev);      
  1. 填充裝置配置與資料統計的具體代碼,填充傳回裝置沖突的​

    ​xxx_stats()​

    ​函數。

6. 網卡驅動移植一般步驟

拿到一塊新的網卡,一般廠家會有自帶的驅動程式給你,你所要做的就是以下幾個事情:

  1. 根據網卡與開發闆的連接配接方式确定網卡的記憶體映射位址iobase,也即确定網卡的片選信号所連接配接的CPU記憶體的哪一個bank(nGCS?),然後根據網卡記憶體的大小,在網卡驅動的初始化函數中調用ioremap()進行位址重映射;
  2. 根據網卡與開發闆的硬體連接配接圖确定中斷号,并在初始化函數中利于request_irq()函數,向核心申請中斷(确定中斷觸發方式、中斷處理函數等);
  3. 根據網卡datasheet檢視網卡的讀寫時序和位寬參數,設定開發闆相應的記憶體控制寄存器BWSCON和BANKCON*。
  4. 将它拷貝到核心源代碼的相關目錄并修改該目錄下的Makefile檔案以添加修改後的網卡驅動目标檔案。假設我們已經改好的網卡驅動程式為:dm9dev9000c.c,編譯也沒有錯誤。
cp dm9dev9000c.c /home/leon/linux-3.4.2/drivers/net/ethernet/davicom/      

修改該目錄Makefile檔案:

#
# Makefile for the Davicom device drivers.
#

#obj-$(CONFIG_DM9000) += dm9000.o
obj-$(CONFIG_DM9000) += dm9dev9000c.o      
  1. 重新編譯核心,燒寫新的uImage檔案到開發闆中,看看是否可以挂載網絡根檔案系統或者可以設定IP位址及ping通網絡。如果可以成功挂載網絡根檔案系統,是以網卡移植是成功的。
nfs 30000000 192.168.1.101:/work/nfs_root/uImage_net_new; 

bootm 30000000
    
mount -t nfs -o nolock,vers=2 192.168.1.101:/work/nfs_root/fs_mini_mdev_new /mnt      
  • 我們也可以設定開機直接挂載網絡根檔案系統,這樣就可以直接開機啟動網絡根檔案系統了。
  • uboot中設定:
set bootargs console=ttySAC0,115200 root=/dev/nfs nfsroot=192.168.1.101:/home/leon/nfs_root/first_fs ip=192.168.1.50:192.168.1.101:192.168.1.1:255.255.255.0::eth0:off
    
save

tftp 30000000 uImage

bootm 30000000      

ip=192.168.1.50:為單闆ip,

192.168.1.101:為伺服器ip,

192.168.1.1為網關,

255.255.255.0為子網路遮罩

net_close(struct net_device *dev);

6.   **填充裝置配置與資料統計的具體代碼**,填充傳回裝置沖突的 `xxx_stats()`函數。

# 6. 網卡驅動移植一般步驟

拿到一塊新的網卡,一般廠家會有自帶的驅動程式給你,你所要做的就是以下幾個事情:

1. 根據網卡與開發闆的連接配接方式确定網卡的記憶體映射位址iobase,也即确定網卡的片選信号所連接配接的CPU記憶體的哪一個bank(nGCS?),然後根據網卡記憶體的大小,在網卡驅動的初始化函數中調用ioremap()進行位址重映射;
2. 根據網卡與開發闆的硬體連接配接圖确定中斷号,并在初始化函數中利于request_irq()函數,向核心申請中斷(确定中斷觸發方式、中斷處理函數等);
3. 根據網卡datasheet檢視網卡的讀寫時序和位寬參數,設定開發闆相應的記憶體控制寄存器BWSCON和BANKCON*。
4. 将它拷貝到核心源代碼的相關目錄并修改該目錄下的Makefile檔案以添加修改後的網卡驅動目标檔案。假設我們已經改好的網卡驅動程式為:dm9dev9000c.c,編譯也沒有錯誤。

```sh
cp dm9dev9000c.c /home/leon/linux-3.4.2/drivers/net/ethernet/davicom/      

修改該目錄Makefile檔案:

#
# Makefile for the Davicom device drivers.
#

#obj-$(CONFIG_DM9000) += dm9000.o
obj-$(CONFIG_DM9000) += dm9dev9000c.o      
  1. 重新編譯核心,燒寫新的uImage檔案到開發闆中,看看是否可以挂載網絡根檔案系統或者可以設定IP位址及ping通網絡。如果可以成功挂載網絡根檔案系統,是以網卡移植是成功的。
nfs 30000000 192.168.1.101:/work/nfs_root/uImage_net_new; 

bootm 30000000
    
mount -t nfs -o nolock,vers=2 192.168.1.101:/work/nfs_root/fs_mini_mdev_new /mnt      
  • 我們也可以設定開機直接挂載網絡根檔案系統,這樣就可以直接開機啟動網絡根檔案系統了。
  • uboot中設定:
set bootargs console=ttySAC0,115200 root=/dev/nfs nfsroot=192.168.1.101:/home/leon/nfs_root/first_fs ip=192.168.1.50:192.168.1.101:192.168.1.1:255.255.255.0::eth0:off
    
save

tftp 30000000 uImage

bootm 30000000      

ip=192.168.1.50:為單闆ip,

192.168.1.101:為伺服器ip,

繼續閱讀