基于USB总线的无线网卡驱动程序

时间:2022-09-24 16:06:25

一、USB设备驱动程序的构成

1、设备的探测,用于检查传递给探测函数的设备信息,确认驱动程序是否适合该设备。

2、数据的发送和接收,负责主机到设备的发送和设备到主机的数据接收。

3、设备断开,当设备断开时候,模块负责清除和该设备关联的所有资源。

4、模块的加载和卸载,用于加载和卸载usb接口的无线网卡驱动程序。


二、USB无线网卡的构成

USB无线网卡主要由USB接口、MAC控制器、基带处理、调制解调器、功率放大器和收发器及天线等组成

MAC控制器是核心部件,它负责从主机读取数据并发送出去,或者接收数据并发送给主机等。它负责通道选择、速率选择、加密解密等等的控制。固件存储区是用来存储MAC控制器要运行的微码。固件是一种经过编译的可执行代码,一般是由设备的芯片来执行的。帧缓存就是用来存储数据的暂时场所。EEPROM是否有没有要看具体的设备,有的设备是没有的,EEPROM一般都存放一些本设备的一些参数,例如本设备的MAC地址,本设备在家族产品中的型号等等。基带处理ADCDAC是数模拟转换的功能部分。要发送的数据或者接收的模拟信号在这个地方进行转换。收发器的功能类似调制解调器,收发器内部有个功率放大器,把弱信号增强到一定的强信号,收发器还负责滤波等工作。天线系统就是负责把数据通过天线发送或接收。天线的作用是使传输距离更远。

USB接口无线网卡的硬件逻辑:

基于USB总线的无线网卡驱动程序



三、模块的加载

在编写USB无线网卡驱动函数之前,首先先了解一下设备在插入到USB接口到设备成功找到它自己的驱动这一过程。

过程一(获取设备一些信息,发生在USB核心):当把USB设备插到USB接口上后,USB主机控制器会检测到有设备插入USB接口了,Linux内核会给设备分配一个数据结构来代表这个设备。本文中涉及的硬件是USB设备,因此Linux会分配一个struct usb_device数据结构来代表该设备,该数据结构记录设备的一些属性及数据。并把该数据结构挂载到一个全局的USB设备链上。在这一期间主机通过0号端点(控制端点)得知了设备的一些信息,并知道了设备的厂家号和产品号

过程二(找到匹配的驱动,发生在USB核心):然后到一个全局的USB驱动链上查找,看看哪个驱动程序支持的设备列表中有该设备的厂家号和产品号。当找到后设备就和驱动匹配上了。

了解了上面的过程后,首先需要注册一个代表USB驱动的数据结构,并要明确表示本驱动要支持的设备。在模块初始化函数module_init中,通过usb_register_driver注册一个usb驱动程序。USB核心将调用通过usb_register_driver注册的探测回调函数,Linux中代表USB驱动的数据结构部分成员如下:

struct usb_driver{

.name="bcm";

.probe=bcm_probe;

.disconnect=bcm_disconnect;

.id_table=bcm_usb_ids;

};

成员 probe()函数指针就是本章要实现的探索函数,该函数在本驱动和设备的厂家号和产品号相匹配后调用,作用是探索该驱动是否支持该设备,如果支持该设备的接口,那么在probe函数中调用usb_set_intfdata(struct usb_interface *intf, void *data)函数,该函数中的第一个参数就是的驱动要支持的那个设备接口数据结构的指针,第二个参数是该驱动为了实现接口正常运行而分配的自己的数据结构。usb_set_intfdata()的作用就是把接口和它的驱动要用到的数据结构关联起来。成功后返回0;如果不支持该设备那么返回-ENODEV。函数probe()的参数usb_interface验证了前文所说的一个接口对应一个驱动,本文所涉及的设备都是单一接口的,因此没有太区分接口和设备的差别,probe()的第二个参数usb_device_id数据结构就包含了上文提及的厂家号和产品号。它是设备的厂家号和产品号,而usb_driverid_table是本驱动支持的所有设备的厂家号和产品号的列表。成员disconnect函数指针指向的函数的作用是当设备已经移走或者模块被卸载时调用,主要就是处善后工作,例如已经注册的取消注册,已经分配的内存释放掉。

探测回调函数的主要工作流程如图:

基于USB总线的无线网卡驱动程序


四、私有数据结构的设计

上文中提到 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协议层使用的数据结构。需要设计自己的私有数据结构,把这个私有数据结构抽象成为设备,把和设备有关的参数都设计成为数据结构放到这个私有数据结构中,在编写驱动程序的各个函数时,只要传递了私有数据结构的指针,就能找到所有关于设备的参数,并且它是全局的。

802.11私有数据结构的内存布局:

基于USB总线的无线网卡驱动程序



五、操作函数集

当探索完成后,就要编写驱动程序的打开、发送等函数。

这些函数都要填充到下面 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)。

如图是它的执行流程

基于USB总线的无线网卡驱动程序