Linux USB 驅動開發實例 (三)—— 基於USB總線的無線網卡淺析


回顧一下USB的相關知識

       USB(Universal Serial Bus)總線又叫通用串行外部總線,它是20世紀90年代發展起來的。USB接口現在得到了廣泛的應用和普及,現在的PC機中都帶有大量的USB接口。它最大的特點就是方便通用、支持熱插拔並且可以在一個接口上插上多個設備。當設備用電量小的時候,它還可以充當電源。它的眾多優點使得它得到了廣泛的應用。

       在PC機器內部有個USB中央控制器,這個中央控制器負責管理插到USB接口上的設備。當主機要向設備發送或接受數據時,都是向USB中央控制器發出命令USB設備不具備主動與主機通信的能力。編寫USB設備驅動不用考慮申請設備地址空間,因為USB中央控制器會給設備分配一個設備號,這個設備號就代表這個設備。  

       USB設備和USB中央控制器之間的通信是通過端點來完成的。端點的職能有點類似一棟大樓的傳達室。例如每個樓層都有一個傳達室,當要訪問5樓的10號房間時,那就是向5號端點發起對話,並提供偏移量,也就10號房間。USB接口的端點按傳輸信息的類型分為以下4種:  

a -- 控制端點

     主要用來傳輸控制信息的,例如配置設備時發出的控制信息。控制端點一般都是雙向,既可以輸入又可以輸出。其他端點的輸出方向一般是單向的,要么是輸入,要么是輸出的。這里是站在主機的角度來談論輸入輸出的。  

b -- 中斷端點

     主要用來傳輸中斷信息的,由於USB設備是受USB中央控制器管理的,因此USB設備沒有向主機發出中斷的能力,並且USB設備不能主動向主機發出請求,只有主機可以向USB設備發出命令請求,因此所謂的中斷是指主機周期性的查詢USB設備。  

c -- 批量端點

     主要用來傳輸批量信息的,批量信息就意味着大量的信息。U盤一般主要使用的就是批量端點。本文研究的USB無線網卡也是使用批量斷點來傳輸數據的。發送和接收函數都是使用批量端點和USB設備傳輸數據的。  

b -- 等時端點

    主要用來傳輸等時信息的,主要用於傳輸實時性要求較高的信息,例如實時的音頻、視頻等信息。有代表性的USB設備是USB攝像頭等。  

    在一個具體的USB設備中不要求一定都存在這4種類型的端點,例如U盤一般就只有批量端點和控制端點。在Linux內核中用來描述USB設備端點信息的數據結構如下:

struct usb_endpoint_descriptor {          
__u8 bLength;
__u8 bDescriptorType;
__u8 bEndpointAddress;
__u8 bmAttributes;
__le16 wMaxPacketSize;
__u8 bInterval;
__u8 bRefresh;
_u8 bSynchAddress;
} __attribute__ ((packed));

      成員bLength描述本數據結構共有多少字節,因為后兩個成員是針對音頻設備的,如果不是音頻設備則可以沒有后兩個成員。成員bDescriptorType是描述本數據結構要描述的類型,這里是描述端點的,在內核中0x05就代表端點

      成員bEndpointAddress包含端點號和輸出方向,bits0-bits3表示的是端點號,從這里可以看出一個USB設備最多只能有不超過16個端點,bits8是代表傳輸方向的,如果該位是1就代表輸入,也就是讀設備;如果該位為0就代表輸出,也就是寫設備。

     成員bmAttributes 表示該端點的類型,如上述的4種類型。

     成員wMaxPacketSize表示該端點一次可以傳輸的最多字節數。如果要傳輸的數據大於這個數字,那就要分多次傳輸。成員bInterval代表的是該端點希望主機輪詢自己的時間間隔,這只是一種希望,具體還要看主機怎么做。

     該數據結構最后的__attribute__((packed))代表在分配該數據結構時數據成員之間不要為了內存對齊而留下空隙,例如有這樣一個數據結構的相鄰兩個成員類型是u8 和u16,在一般情況下u8后面要空一個字節,然后才是u16成員,如果有上面attribute的要求后,在u8后面就不要留空間,緊接着就是u16成員。在內核中有很多需要訪問設備的數據結構都有這樣的要求,因為在一個設備中一般沒有內存對齊的要求。


 

一、USB設備驅動程序的構成

1、設備的探測

       用於檢查傳遞給探測函數的設備信息,確認驅動程序是否適合該設備。

2、數據的發送和接收

       負責主機到設備的發送和設備到主機的數據接收。

3、設備斷開

      當設備斷開時候,模塊負責清除和該設備關聯的所有資源。

4、模塊的加載和卸載

      用於加載和卸載usb接口的無線網卡驅動程序。


二、USB無線網卡的構成

        USB無線網卡主要由USB接口、MAC控制器、基帶處理、調制解調器、功率放大器收發器及天線等組成。

        MAC控制器是核心部件,它負責從主機讀取數據並發送出去,或者接收數據並發送給主機等。它負責通道選擇、速率選擇、加密解密等等的控制。

        固件存儲區是用來存儲MAC控制器要運行的微碼。固件是一種經過編譯的可執行代碼,一般是由設備的芯片來執行的。

       幀緩存就是用來存儲數據的暫時場所。

       EEPROM是否有沒有要看具體的設備,有的設備是沒有的,EEPROM一般都存放一些本設備的一些參數,例如本設備的MAC地址,本設備在家族產品中的型號等等。

       基帶處理和ADC、DAC是數模擬轉換的功能部分。要發送的數據或者接收的模擬信號在這個地方進行轉換。

       收發器的功能類似調制解調器,收發器內部有個功率放大器,把弱信號增強到一定的強信號,收發器還負責濾波等工作。

      天線系統就是負責把數據通過天線發送或接收。天線的作用是使傳輸距離更遠。

USB接口無線網卡的硬件邏輯:



USB無線網卡的通道和速率是多個的,在發送和接收時通道和速率是可以變換的。在Linux中通道用如下數據結構表示:

struct ieee80211_channel {
enum ieee80211_band band;
u16 center_freq;
u16 hw_value;
u32 flags;
int max_antenna_gain;
int max_power;
bool beacon_found;
u32 orig_flags;
int orig_mag, orig_mpwr;
};


三、模塊的加載

       在編寫USB無線網卡驅動函數之前,首先先了解一下設備在插入到USB接口到設備成功找到它自己的驅動這一過程。

1、獲取設備一些信息,發生在USB核心

      當把USB設備插到USB接口上后,USB主機控制器會檢測到有設備插入USB接口了,Linux內核會給設備分配一個數據結構來代表這個設備。本文中涉及的硬件是USB設備,因此Linux會分配一個struct usb_device數據結構來代表該設備,該數據結構記錄設備的一些屬性及數據。並把該數據結構掛載到一個全局的USB設備鏈上。在這一期間主機通過0號端點(控制端點)得知了設備的一些信息,並知道了設備的廠家號和產品號。

2、找到匹配的驅動,發生在USB核心

      然后到一個全局的USB驅動鏈上查找,看看哪個驅動程序支持的設備列表中有該設備的廠家號和產品號。當找到后設備就和驅動匹配上了。 

    了解了上面的過程后,首先需要注冊一個代表USB驅動的數據結構,並要明確表示本驅動要支持的設備。在模塊初始化函數module_init中,通過usb_register_driver注冊一個usb驅動程序。USB核心將調用通過usb_register_driver注冊的探測回調函數,在Linux中代表USB驅動的數據結構部分成員如下:

struct usb_driver{
.name="alld";
.probe=ad_probe;
.disconnect=ad_disconnect;
.id_table=ad_usb_ids;
};

      該數據結構中name成員是代表該驅動的名稱,該名稱在USB驅動中必須要獨一無二的,不能和別的驅動的名字重復,在起名字的時候最好和模塊名字相同。      

     成員 probe()函數指針就是本章要實現的探索函數,該函數在本驅動和設備的廠家號和產品號相匹配后調用,作用是探索該驅動是否支持該設備,如果支持該設備的接口,那么在probe函數中調用usb_set_intfdata(struct usb_interface *intf, void *data)函數,該函數中的第一個參數就是的驅動要支持的那個設備接口數據結構的指針,第二個參數是該驅動為了實現接口正常運行而分配的自己的數據結構。

      usb_set_intfdata()的作用就是把接口和它的驅動要用到的數據結構關聯起來。成功后返回0;如果不支持該設備那么返回-ENODEV。

      函數probe()的參數usb_interface驗證了前文所說的一個接口對應一個驅動,本文所涉及的設備都是單一接口的,因此沒有太區分接口和設備的差別,probe()的第二個參數usb_device_id數據結構就包含了上文提及的廠家號和產品號。它是設備的廠家號和產品號,而usb_driver的id_table是本驅動支持的所有設備的廠家號和產品號的列表。

      成員disconnect函數指針指向的函數的作用是當設備已經移走或者模塊被卸載時調用,主要就是處善后工作,例如已經注冊的取消注冊,已經分配的內存釋放掉。


四、私有數據結構的設計

       上文中提到 probe()函數中要調用usb_set_intfdata()函數,該函數的第二個參數就是本文驅動程序要用到的私有數據結構。由於驅動程序是工作在ieee802.11協議層,ieee802.11為驅動程序提供了一個分配內存函數ieee80211_hw*ieee80211_alloc_hw(size_t priv_data_len,const struct ieee80211_ops *ops),該函數第一個參數是自己驅動程序中的私有數據結構的長度,第二個參數是上文提及的指向驅動程序各個函數的數據結構的指針,正是在這里把驅動程序的所有函數提供給ieee802.11協議層的。ieee80211_alloc_hw()函數是即分配了802.11協議層需要的內存結構,又順便分配了驅動的私有數據結構,該函數分配的內存結構如下圖所示。圖中除了驅動程序自己的私有數據結構,其他幾個數據結構都是802.11協議層使用的數據結構。需要設計自己的私有數據結構,把這個私有數據結構抽象成為設備,把和設備有關的參數都設計成為數據結構放到這個私有數據結構中,在編寫驅動程序的各個函數時,只要傳遞了私有數據結構的指針,就能找到所有關於設備的參數,並且它是全局的。


        函數ieee80211_alloc_hw()成功后返回的是struct ieee80211_hw結構的指針,而該結構的priv指向了的私有數據結構。本文設計的私有數據結構如下:

struct priv_dev{  
unsigned long flags;
struct usb_device *udev;
struct usb_interface *intf;
struct ieee80211_hw *hw;
loff_t savep;
char fw_name[64];
char path[64];
u8 *eeprom; struct ieee80211_supported_band bands[IEEE80211_NUM_BANDS];
enum ieee80211_band curr_band;
spinlock_t list_lock;
struct mutex list_op,rw_lock;
int timeout;
struct list_head cfmg_list[30];
u8 bulk[BULKSIZE];
struct config_msg *msg_fun[10];//record config_msg() position
unsigned char *skb_data,*skb_tail,*rx_skb_data,*rx_skb_tail;
struct data_queue *rx,*tx,*beacon;
struct prob_desc probdesc;
struct priv_rate *privrate;
struct priv_channel *privchannel;
struct privdev_rx_status rxstatus;
struct priv_intf privintf;
u32 parameter[PRIV_PARAMETER_SIZE];
int sparameter[PRIV_PARAMETER_SIZE];
struct pstack ps;
};
       其中成員udev、intf和hw成員都是指向上層的數據結構,有了這些成員后可以很 方便的尋找上層數據結構。成員savep是用於在讀參數文件時記錄參數文件的偏移量,path成員是參數文件所在路徑及參數文件的名字。成員fw_name是用來存放設備固件程序的名字。成員eeprom只有在設備存在EEPROM的時候才有意義,如果設備有EEPROM,那么本文的做法是分配一個和設備EEPROM一樣大小的內存來存放EEPROM中所有的數據,這樣的好處是當要從EEPROM中讀數據時,就從內存讀取,這樣提升了讀取的速度。這樣也防止錯誤代碼把EEPROM中的數據沖掉了。成員bands和curr_band記錄本設備所在的頻帶及通道和速率列表,bands數據結構中存在指向通道和速率的指針成員。成員list_lock、list_op和rw_lock都是鎖[29],list_lock是自旋鎖,它用於短時間的鎖,它的特點是在獲取鎖失敗后不睡眠,而是一直循環查詢鎖的狀態。List_op和rw_lock是互斥鎖,它可以用於長時間鎖,它的特點是獲取鎖不成功就阻塞在鎖的鏈表上。成員timeout是和設備通信的定時器時間,由於本驅動框架想要支持多個設備,那么它的值就從參數文件中讀取。成員cfmg_list就是上文提及的參數鏈鏈頭指針數組,struct list_head數據結構是Linux中的常用的雙向鏈表結構,它的結構非常簡單: 

struct list_head {          

           struct list_head *next, *prev;   

};  

成員next指向下一個list_head數據結構,prev指向上一個list_head數據結構。那么如何使用list_head呢?在使用時把list_head嵌入到宿主數據結構中,只要知道list_head的地址,就可以算出宿主數據結構的地址。內核中給提供了list_entry(ptr,type,member)這個宏來計算宿主數據結構的地址,ptr就是宿主數據結構中list_head成員的地址,type是宿主數據結構的類型,member是list_head數據結構在宿主數據結構中的成員名字,在本文中如果知道list_head的指針例如head,那么config_msg的地址就是list_entry(head,struct config_msg,list)。 



五、操作函數集

        當探索完成后,就要編寫驅動程序的打開、發送等函數。這些函數都要填充到下面 struct ieee80211_ops數據結構中去:

struct ieee80211_ops{
int (*tx)(struct ieee80211_hw *hw, struct sk_buff *skb);
int (*start)(struct ieee80211_hw *hw);
void (*stop)(struct ieee80211_hw *hw);
int (*add_interface)(struct ieee80211_hw *hw,
struct ieee80211_if_init_conf *conf);
void (*remove_interface)(struct ieee80211_hw *hw,
struct ieee80211_if_init_conf *conf);
int (*config)(struct ieee80211_hw *hw, u32 changed);
void (*bss_info_changed)(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_bss_conf *info,
u32 changed);
};
   這里只列舉了部分主要的函數,一個驅動程序不一定要把這個數據結構中的所有函數指針所指向的函數都實現了,這要根據具體設備的情況而定。其中tx函數指針是指向發送函數,start函數指針指向的是開始函數,config函數指針指向的是配置函數,stop函數是停止函數等等。當把這里必須要實現的函數指針實現后,驅動程序就算寫完了。


六、USB接口無線網卡數據的接收

       與pci、pcmia等無線網卡不同,usb總線沒有中斷資源。因此usb無線網卡的數據接收不通過中斷實現,而是在open函數通過主機主動查詢是否有數據需要讀取

       因此,在open函數中向usb core發送一個讀請求的urb,使得網絡數據到來時候,主機能夠接收到。

open回調函數主要代碼:

......
usb_fill_bulk_urb(dev->rx_urb,//構造讀請求的urb
dev->udev,
usb_rcvbulkpipe(dev->udev,6),//指定讀得端點
dev->rx_skb->data,
512,//count
rx_complete,//讀請求的回調函數
dev
);

if(result=usb_submit_urb(dev->urb,GFP_KERNEL))
{
將發送給kernel的usb core
}

     讀請求完成時候,read_bulk_callback函數將被內核調用,它構造一個skb_bufff數據結構來描述數據包,並調用netif_rx把數據包傳給網絡子系統,從而完成一次數據的接收過程。


七、USB接口無線網卡數據的發送

       當網絡子系統要發送一個數據時候,上層協議會構造一個sk_buff來描述一個數據包,並調用驅動程序注冊和實現的hard_start_xmit來發送數據包,由於該函數被調用時候,網絡子系統持有xmit_lock自旋鎖,因此驅動程序不必考慮設備寫操作的同步問題。hard_start_xmit根據數據包的長度,拆分成usb設備可以傳輸的長度,然后構造相應地寫請求urb,發送至usb core即可。

hard_start_xmit回調函數的主要代碼:

......
usb_fill_bulk_urb(dev->tx_urb,//構造寫請求的urb
dev->udev,
usb_sndbulkpipe(dev->udev,2),//指定寫端點
skb->data,
512,//count
write_bulk_callback,//寫請求的回調函數
dev
);

if(result=usb_submit_urb(dev->tx_urb,GFP_ATOMIC))
{
將發送給usb core
}

寫請求完成時候,write_bulk_callback回調函數將被調用,根據發送情況更新統計數據


八、設備的斷開

       我們已經分析了usb_driver結構的探測函數,與設備探測對應的是設備的斷開。設備斷開可以看做是設備探測的逆過程,主要工作是釋放驅動程序已經分配的系統資源。

       設備斷開調用了usb_driver結構的disconnect(struct usb_interface *)函數,函數首先通過調用usb_get_intfdata()獲取相關資源,然后通過usb_set_intfdata(intf,NULL)將資源清零,並釋放資源。


九、模塊的卸載

      與模塊加載對應的是模塊的卸載,module_exit函數首先調用usb_rtusb_exit()卸載網卡驅動程序,接着調用usb_deregister(&rtusb_driver)實現設備的注銷。


十、IOCTL函數

       Linux中要讓網卡正常工作需要配置IP地址、SSID、工作頻段、工作模式等,這些控制操作都是通過ifconfig和iwconfig調用驅動實現的IOCTL函數實現的。驅動程序通過IOCTL為應用程序提供了一些諸如IO內存地址讀寫訪問、配置空間寄存器讀寫訪問、數據成員讀寫訪問等函數,通過這些函數,應用程序就可以對設備進行相應地操作,其各種函數都是通過IOCTL命令實現的。應用程序將IOCTL命令將有關信息傳遞到驅動程序的內核空間,驅動程序再處理相應地操作。

      例如該函數的原型:

      rtxxx_ioctl(struct net_device * net_dev,struct ifreq * ,int cmd)




注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com