Linux 驱动开发之WIFI设备分析4(基于Linux6.6)---USB接口WiFi介绍

从硬件层面上看,WIFI设备与CPU通信是通过USB接口的,与其他WIFI设备之间的通信是通过无线射频(RF)。

从软件层面上看,Linux操作系统要管理WIFI设备,那么就要将WIFI设备挂载到USB总线上,通过USB子系统实现管理。而同时为了对接网络,又将WIFI设备封装成一个网络设备。

以USB接口的WIFI模块进行分析:

a -- 从USB总线的角度去看,它是USB设备;

b -- 从Linux设备的分类上看,它又是网络设备;

c -- 从WIFI本身的角度去看,它又有自己独特的功能及属性,因此它又是一个私有的设备;

一、框架整理

1.1、USB设备驱动

现在我们先从USB设备开始,要写一个USB设备驱动,那么大致步骤如下:

a -- 需要针对该设备定义一个USB驱动,对应到代码中即定义一个usb_driver结构体变量

代码如下:

struct usb_driver xxx_usb_wifi_driver;

b -- 填充该设备的usb_driver结构体成员变量

代码如下:

static struct usb_driver xxx_usb_wifi_driver = {
	.name = "XXX_USB_WIFI",
	.probe = xxx_probe,
	.disconnect = xxx_disconnect,
	.suspend = xxx_suspend,
	.resume = xxx_resume,
	.id_table = xxx_table,
};

c -- 将该驱动注册到USB子系统

代码如下:

usb_register(&xxx_usb_wifi_driver);

以上步骤只是一个大致的USB驱动框架流程,而最大和最复杂的工作是填充usb_driver结构体成员变量。以上步骤的主要工作是将USB接口的WIFI设备挂载到USB总线上,以便Linux系统在USB总线上就能够找到该设备。

1.2、网络设备驱动

接下来是网络设备的线索,网络设备驱动大致步骤如下:

a -- 定义一个net_device结构体变量ndev

代码如下:

struct net_device *ndev;

b -- 初始化ndev变量并分配内存

代码如下:

ndev=alloc_etherdev();

c -- 填充ndev -> netdev_ops结构体成员变量

代码如下:

static const struct net_device_ops xxx_netdev_ops= {
	.ndo_init = xxx_ndev_init,
	.ndo_uninit = xxx _ndev_uninit,
	.ndo_open = netdev_open,
	.ndo_stop = netdev_close,
	.ndo_start_xmit = xxx_xmit_entry,
	.ndo_set_mac_address = xxx_net_set_mac_address,
	.ndo_get_stats = xxx_net_get_stats,
	.ndo_do_ioctl = xxx_ioctl,
};

d -- 填充ndev->wireless_handlers结构体成员变量,该变量是无线扩展功能

代码如下:

ndev->wireless_handlers = (struct iw_handler_def *)&xxx_handlers_def;

e -- 将ndev设备注册到网络子系统

代码如下:

register_netdev(ndev);

1.3、 WIFI设备本身私有的功能及属性

如自身的配置及初始化、建立与用户空间的交互接口、自身功能的实现等。

a -- 自身的配置及初始化

代码如下:                

xxx_read_chip_info();
 
xxx_chip_configure();
 
xxx_hal_init();

b -- 主要是在proc和sys文件系统上建立与用户空间的交互接口

代码如下:

xxx_drv_proc_init();
 
xxx_ndev_notifier_register();

c -- 自身功能的实现

WIFI的网络及接入原理,如扫描等。同时由于WIFI在移动设备中,相对功耗比较大,因此,对于功耗、电源管理也会在驱动中体现。

二、USB 设备驱动分析

wifi模块上的数据传输有两端,一端是wifi芯片与wifi芯片之间,通过无线射频(RF)进行数据传输;另一端则是wifi芯片与CPU之间,通过USB进行数据传输。

USB驱动分为两种:一种是USB主机驱动;另一种是USB设备驱动。而我们的USB接口的wifi模块对于CPU(主机)来说,属于USB设备,因此采用USB设备驱动。

有了以上信息之后,先让Linux系统识别该USB接口的wifi模块,首先在驱动源码中大致添加以下几步工作:

a -- 定义一个usb_driver结构体变量

b -- 填充该设备的usb_driver结构体成员变量

c -- 将该驱动注册到USB子系统

简单完成以上几步工作,再加上板级文件(arch/mach-xxx.c)对USB设备的支持,Linux的USB子系统几乎可以挂载该wifi模块为USB设备了。但是这并不是最终想要的结果。还要让Linux系统知道它挂载的USB设备属于无线网络设备,同时能够访问它,利用它实施无线网络的工作。

若要让USB设备真正工作起来,需要对USB设备的4个层次(设备、配置、接口、端点)进行初始化。当然这四个层次并不是一定都要进行初始化,而是根据USB设备的功能进行选择的,大致初始化流程如下伪代码:

static struct dvobj_priv *usb_dvobj_init(struct usb_interface *usb_intf)
{
	int    i;
	u8     val8;
	int    status= _FAIL;
	struct dvobj_priv *pdvobjpriv;
 
	//设备
	struct usb_device *pusbd;
	struct usb_device_descriptor *pdev_desc;
 
	//配置
	struct usb_host_config *phost_conf;
	struct usb_config_descriptor *pconf_desc;
 
	//接口
	struct usb_host_interface *phost_iface;
	struct usb_interface_descriptor *piface_desc;
	
	//端点
	struct usb_host_endpoint *phost_endp;
	struct usb_endpoint_descriptor *pendp_desc;
 
 
	//设备的初始化
	pdvobjpriv->pusbintf = usb_intf ;
	pusbd =pdvobjpriv->pusbdev = interface_to_usbdev(usb_intf);
	usb_set_intfdata(usb_intf, pdvobjpriv);
	pdev_desc =&pusbd->descriptor;
 
	
	//配置的初始化
	phost_conf =pusbd->actconfig;
	pconf_desc =&phost_conf->desc;
 
 
	//接口的初始化
	phost_iface =&usb_intf->altsetting[0];
	piface_desc =&phost_iface->desc;
 
	
	//端点的初始化,由于wifi模块属于网络设备,传输批量数据,因此需要初始化为批量端点,端点方向(输入、输出)等。同时,由于wifi驱动功能比较多,需要初始化几个输入输出端点。
	for (i = 0; i <pdvobjpriv->nr_endpoint; i++)
	{
		phost_endp = phost_iface->endpoint +i;
		if (phost_endp)
		{
			pendp_desc =&phost_endp->desc;
 
			//检查是否为输入端点
			usb_endpoint_is_bulk_in(pendp_desc);
 
			//检查是否为输出端点
			usb_endpoint_is_bulk_out(pendp_desc);
		}
 
	}
 
	usb_get_dev(pusbd);
 
}

 完成以上的初始化工作之后,接下来我们需要理清一下USB接口的作用,它是wifi芯片内部的固件程序与主机上的Linux系统进行数据通信。USB设备通信不像普通字符设备那样采用I/O内存和I/O端口的访问,而是采用一种称为URB(USB Request Block)的USB请求块,URB在整个USB子系统中,相当于通电设备中的“电波”,USB主机与设备的通信,通过“电波”来传递。

void xxx_wifi_usb_intf_ops(struct _io_ops     *pops)
{
	//当需要进行简单数据的读取时,采用以下操作
	pops->_read8 = &usb_read8;
	pops->_read16 = &usb_read16;
	pops->_read32 = &usb_read32;
 
	//当需要进行批量数据的读取时,采用以下操作
	pops->_read_port = &usb_read_port;   
 
	//当需要进行简单数据的写时,采用以下操作
	pops->_write8 = &usb_write8;
	pops->_write16 = &usb_write16;
	pops->_write32 = &usb_write32;
	pops->_writeN = &usb_writeN;
 
	//当需要进行批量数据的写时,采用以下操作
	pops->_write_port = &usb_write_port;
 
	//取消读写urb
	pops->_read_port_cancel = &usb_read_port_cancel;
	pops->_write_port_cancel = &usb_write_port_cancel;
 
}

在进行批量数据的读写时,如usb_read_port()和usb_write_port()函数,需要完成urb创建、初始化、提交、完成处理这个完整的流程。伪代码如下:

1)批量读操作

static u32 usb_read_port(struct intf_hdl *pintfhdl, u32 addr, u32 cnt, u8 *rmem)
{       
	int err;
	unsigned intpipe;
	PURB purb =NULL;
	structrecv_buf         *precvbuf = (structrecv_buf *)rmem;
	structusb_device    *pusbd = pdvobj->pusbdev;
 
	//创建urb,这里是在其它地方创建完成之后,传递过来
	purb =precvbuf->purb;
 
	//初始化批量urb
	usb_fill_bulk_urb(purb, pusbd, pipe,
		precvbuf->pbuf,
		MAX_RECVBUF_SZ,
		usb_read_port_complete,
		precvbuf);//contextis precvbuf
	
	//提交urb
	err =usb_submit_urb(purb, GFP_ATOMIC);
 
}

2)批量写操作

u32 usb_write_port(struct intf_hdl *pintfhdl, u32 addr, u32 cnt, u8 *wmem)
{   
	unsigned int pipe;
	intstatus;
	PURB        purb = NULL;
 
	structxmit_priv       *pxmitpriv =&padapter->xmitpriv;
	structxmit_buf *pxmitbuf = (struct xmit_buf *)wmem;
	structxmit_frame *pxmitframe = (struct xmit_frame *)pxmitbuf->priv_data;
	structusb_device *pusbd = pdvobj->pusbdev;
	structpkt_attrib *pattrib = &pxmitframe->attrib;
 
	//创建urb,这里是在其它地方创建完成之后,传递过来
	 purb = pxmitbuf->pxmit_urb[0];
 
	//初始化批量urb
	usb_fill_bulk_urb(purb, pusbd, pipe,
		pxmitframe->buf_addr,//= pxmitbuf->pbuf
		cnt,
		usb_write_port_complete,
		pxmitbuf);//contextis pxmitbuf
 
	//提交urb
	status = usb_submit_urb(purb,GFP_ATOMIC);
 
	return ret;
 
}

 完成以上批量数据的读写操作之后,大家可能会疑问:这不是一般USB设备驱动的操作流程吗?貌似和wifi没有半毛钱的关系啊!从上面看,确实和wifi没有任何联系,但是以上只是一个铺垫。我们一直强调USB接口在wifi模块中充当什么角色,既然是接口,那么它就是为数据传输而生。所以,和wifi扯上关系的就在于usb_read_port()和usb_write_port()这两个函数。

三、读写函数分析

USB接口在wifi模块中的最重要两个函数是usb_read_port()和usb_write_port()。那它们是怎么和wifi扯上关系的呢?从以下三个方面去分析:

a -- 首先需要明确wifi模块是USB设备,主控(CPU)端是USB主机;

b -- USB主机若需要对wifi模块进行数据的读写时,就必须经过USB接口;

c -- 既然涉及到数据的读写操作,必然要用相应的读写函数,那么usb_read_port()和usb_write_port()即是它们的读写函数。

先从读数据开始进行分析,在分析之前,必须了解USB设备驱动的读数据过程。USB读取数据操作流程如下:

a -- 通过usb_alloc_urb()函数创建并分配一个URB,作为传输USB数据的载体;

b -- 创建并分配DMA缓冲区,以DMA方式快速传输数据;

c -- 初始化URB,根据wifi的传输数据量,我们需要初始化为批量URB,相应操作函数为usb_fill_bulk_urb();

d -- 将URB提交到USB核心;

e -- 提交成功后,URB的完成函数将被USB核心调用。

只有当wifi模块有数据可读时,主控端才能成功地读取数据。那么wifi模块什么时候有数据可读呢?——下面重点来了!wifi模块通过RF端接收到无线网络数据,然后缓存到wifi芯片的RAM中,此时,wifi模块就有数据可读了。

经过上面的分析,找到了一条USB接口与wifi模块扯上关系的线索,就是wifi模块的接收数据,会引发USB接口的读数据;

现在,转到wifi模块的接收函数中,看看是不是真的这样?

在wifi接收函数初始化中,可以看到usb_alloc_urb()创建一个中断URB。

int xxxwifi_init_recv(_adapter *padapter)  
{  
    struct recv_priv *precvpriv = &padapter->recvpriv;  
    int i, res = _SUCCESS;  
    struct recv_buf *precvbuf;  
  
    tasklet_init(&precvpriv->recv_tasklet, (void(*)(unsigned long))rtl8188eu_recv_tasklet, (unsigned long)padapter);  
  
    precvpriv->int_in_urb = usb_alloc_urb(0, GFP_KERNEL); //创建一个中断URB  
  
    precvpriv->int_in_buf = rtw_zmalloc(INTERRUPT_MSG_FORMAT_LEN);  
    //init recv_buf  
    _rtw_init_queue(&precvpriv->free_recv_buf_queue);  
    _rtw_init_queue(&precvpriv->recv_buf_pending_queue);  
  
    precvpriv -> pallocated_recv_buf = rtw_zmalloc(NR_RECVBUFF *sizeof(struct recv_buf) + 4);  
    precvbuf = (struct recv_buf*)precvpriv->precv_buf;  
  
    for(i=0; i < NR_RECVBUFF ; i++)  
    {  
        _rtw_init_listhead(&precvbuf->list);  
        _rtw_spinlock_init(&precvbuf->recvbuf_lock);  
        precvbuf->alloc_sz = MAX_RECVBUF_SZ;  
  
        res = rtw_os_recvbuf_resource_alloc(padapter, precvbuf);  
  
        precvbuf->ref_cnt = 0;  
        precvbuf->adapter =padapter;  
        precvbuf++;  
    }  
    precvpriv->free_recv_buf_queue_cnt = NR_RECVBUFF;  
  
    skb_queue_head_init(&precvpriv->rx_skb_queue);  
  
#ifdef CONFIG_PREALLOC_RECV_SKB  
    {  
        int i;  
        SIZE_PTR tmpaddr=0;  
        SIZE_PTR alignment=0;  
        struct sk_buff *pskb=NULL;  
        skb_queue_head_init(&precvpriv->free_recv_skb_queue);  
        for(i=0; i<NR_PREALLOC_RECV_SKB; i++)  
        {  
            pskb = rtw_skb_alloc(MAX_RECVBUF_SZ + RECVBUFF_ALIGN_SZ);  
            if(pskb)  
            {  
                pskb->dev = padapter->pnetdev;  
                tmpaddr = (SIZE_PTR)pskb->data;  
                alignment = tmpaddr & (RECVBUFF_ALIGN_SZ-1);  
                skb_reserve(pskb, (RECVBUFF_ALIGN_SZ - alignment));  
                skb_queue_tail(&precvpriv->free_recv_skb_queue, pskb);  
            }  
            pskb=NULL;  
        }  
    }  
#endif  
    return res;  
}  

 在rtw_os_recvbuf_resource_alloc函数中,创建一个批量URB和一个DMA缓冲区。

int rtw_os_recvbuf_resource_alloc(_adapter *padapter, struct recv_buf *precvbuf)  
{  
    int res=_SUCCESS;  
    struct dvobj_priv   *pdvobjpriv = adapter_to_dvobj(padapter);  
    struct usb_device   *pusbd = pdvobjpriv->pusbdev;  
  
    precvbuf->irp_pending = _FALSE;  
    precvbuf->purb = usb_alloc_urb(0, GFP_KERNEL); //创建一个批量URB  
  
    precvbuf->pskb = NULL;  
    precvbuf->reuse = _FALSE;  
    precvbuf->pallocated_buf  = precvbuf->pbuf = NULL;  
    precvbuf->pdata = precvbuf->phead = precvbuf->ptail = precvbuf->pend = NULL;  
    precvbuf->transfer_len = 0;  
    precvbuf->len = 0;  
  
    #ifdef CONFIG_USE_USB_BUFFER_ALLOC_RX  
    precvbuf->pallocated_buf = rtw_usb_buffer_alloc(pusbd, (size_t)precvbuf->alloc_sz, &precvbuf->dma_transfer_addr);  //创建一个DMA缓冲区  
    precvbuf->pbuf = precvbuf->pallocated_buf;  
    if(precvbuf->pallocated_buf == NULL)  
        return _FAIL;  
    #endif //CONFIG_USE_USB_BUFFER_ALLOC_RX  
      
    return res;  
}  

在usb_read_port()函数中,通过usb_fill_bulk_urb()初始化批量URB,并且提交给USB核心,也即USB读取数据操作流程的第3、4步。在usb_fill_bulk_urb()函数中,初始化URB的完成函数usb_read_port_complete(),只有当URB提交完成后,函数usb_read_port_complete()将被调用。

static u32 usb_read_port(struct intf_hdl *pintfhdl, u32 addr, u32 cnt, u8 *rmem)  
{     
    struct recv_buf *precvbuf = (struct recv_buf *)rmem;  
    _adapter        *adapter = pintfhdl->padapter;  
    struct dvobj_priv   *pdvobj = adapter_to_dvobj(adapter);  
    struct pwrctrl_priv *pwrctl = dvobj_to_pwrctl(pdvobj);  
    struct recv_priv    *precvpriv = &adapter->recvpriv;  
    struct usb_device   *pusbd = pdvobj->pusbdev;  
  
    rtl8188eu_init_recvbuf(adapter, precvbuf);        
  
    precvpriv->rx_pending_cnt++;  
  
    purb = precvbuf->purb;  
  
    //translate DMA FIFO addr to pipehandle  
    pipe = ffaddr2pipehdl(pdvobj, addr);  
  
    usb_fill_bulk_urb(purb, pusbd, pipe,   
                    precvbuf->pbuf,  
                            MAX_RECVBUF_SZ,  
                            usb_read_port_complete,  
                            precvbuf);//context is precvbuf  
  
    err = usb_submit_urb(purb, GFP_ATOMIC);  
  
    return ret;  
}  

通过上面的代码,可以得知在wifi模块为接收数据做初始化准备时,分配了URB和DMA缓冲区。而在usb_read_port()函数中初始化URB和提交URB。

四、举例应用

4.1、常见的嵌入式 USB WiFi 适配器及其驱动应用

以下是几个常见的 USB WiFi 适配器类型及其在嵌入式 Linux 系统中的驱动应用举例。


1. Realtek RTL8188/RTL8192 USB WiFi 驱动

常见适配器:

  • Realtek RTL8188EUS
  • Realtek RTL8192CU

驱动程序:

  • rtl8188eu:用于 RTL8188EUS。
  • rtl8192cu:用于 RTL8192CU。

安装和配置步骤:

  1. 确保设备被识别: 插入 USB WiFi 适配器到嵌入式设备的 USB 接口,并运行以下命令查看适配器是否被识别:

lsusb

输出示例:

  • Bus 001 Device 004: ID 0bda:8176 Realtek Semiconductor Corp. RTL8188EUS 802.11n Wireless Network Adapter
    
  • 检查内核模块: Realtek 的 WiFi 适配器通常需要加载相应的内核模块。查看当前内核是否已加载 rtl8188eu 或 rtl8192cu 模块:

  • lsmod | grep rtl
    
  • 安装驱动程序: 如果内核未包含相应驱动,可以手动安装驱动:

    对于 rtl8188eu,可以使用以下命令:

  • sudo apt-get install rtl8188eu-dkms
    
  • 启动网络接口: 安装并加载驱动后,可以通过以下命令启用无线接口:

sudo ifconfig wlan0 up

或者使用 ip 命令:

  • sudo ip link set wlan0 up
    
  • 连接 WiFi 网络: 使用 nmcli 或 iw 工具连接到网络:

  1. sudo nmcli device wifi connect "SSID" password "your_password"
    

注意事项:

  • 对于某些嵌入式系统,您可能需要从源码编译驱动程序,特别是在使用较旧的内核或自定义内核的情况下。
  • 确保您的嵌入式系统支持 USB 2.0 或 3.0 接口,并且具有足够的处理能力来驱动 WiFi 适配器。

2. Atheros AR9271 USB WiFi 驱动

常见适配器:

  • TP-Link TL-WN722N(使用 Atheros AR9271 芯片组)

驱动程序:

  • ath9k_htc:适用于 Atheros AR9271 系列 USB WiFi 适配器。

安装和配置步骤:

  1. 确保设备被识别: 插入 Atheros AR9271 适配器,使用 lsusb 查看设备:

lsusb

输出示例:

  • Bus 002 Device 003: ID 0cf3:9271 Atheros Communications, Inc. AR9271 802.11n
    
  • 加载驱动程序: 通过 ath9k_htc 驱动来支持 Atheros AR9271。检查系统是否已加载该驱动:

lsmod | grep ath9k_htc

如果没有加载,可以手动加载:

  • sudo modprobe ath9k_htc
    
  • 安装固件: 某些 Atheros WiFi 适配器需要固件支持,安装固件包:

  • sudo apt-get install firmware-atheros
    
  • 启动网络接口: 使用 ifconfig 或 ip 命令启动无线接口:

  • sudo ifconfig wlan0 up
    
  • 连接到 WiFi 网络: 使用 nmcli 或 iw 工具连接到无线网络:

  1. sudo nmcli device wifi connect "SSID" password "your_password"
    

3. Broadcom BCM43xx USB WiFi 驱动

常见适配器:

  • Broadcom BCM4313
  • Broadcom BCM4321

驱动程序:

  • b43、b43legacy、brcmsmac

安装和配置步骤:

  1. 检查设备是否被识别: 插入 Broadcom USB WiFi 适配器,使用 lsusb 查看设备:

lsusb

示例输出:

  • Bus 001 Device 004: ID 14e4:4313 Broadcom Corp. BCM4313 802.11bgn Wireless Network Adapter
    
  • 加载驱动程序: 对于 Broadcom WiFi 适配器,通常需要加载 b43 或 brcmsmac 驱动。检查是否已加载驱动:

lsmod | grep b43

如果未加载,可以手动加载:

  • sudo modprobe b43
    
  • 安装固件: Broadcom 驱动程序通常依赖固件。安装固件包:

  • sudo apt-get install bcmwl-kernel-source
    
  • 启动无线接口: 使用 ifconfig 或 ip 命令启动无线网络接口:

  • sudo ifconfig wlan0 up
    
  • 连接到 WiFi 网络: 使用 nmcli 或 iw 工具连接到网络:

  1. sudo nmcli device wifi connect "SSID" password "your_password"
    

4. USB WiFi 适配器的通用驱动加载和网络配置

无论使用哪种 USB WiFi 适配器,嵌入式 Linux 系统的通用驱动加载和网络配置步骤如下:

  1. 加载驱动: 根据适配器的芯片组(如 Realtek、Atheros、Broadcom),加载对应的内核模块。例如:

  • sudo modprobe rtl8188eu   # Realtek 8188EU
    sudo modprobe ath9k_htc   # Atheros AR9271
    sudo modprobe b43         # Broadcom b43系列
    
  • 检查无线网络接口: 使用 ifconfig 或 ip a 命令检查无线接口是否已启用:

  • ifconfig wlan0
    
  • 连接 WiFi 网络: 使用 nmcli 工具或 iw 命令连接 Wi-Fi 网络。例如:

  • nmcli device wifi connect "SSID" password "your_password"
    
  • 配置静态 IP(如果需要): 如果您希望设置静态 IP,可以通过编辑 /etc/network/interfaces 文件或使用 nmcli 配置网络:

  • sudo nmcli con mod "SSID" ipv4.addresses 192.168.1.100/24
    sudo nmcli con up "SSID"
    

更多推荐