嵌入式Linux开发-USB驱动
  vNOZzpgGXHoP 2024年02月20日 84 0

0.前言

哥们马上就要被裁了,总得整理一下技术方面的积累,准备开始下一轮的面试和找工作之旅了。。。。

1.概述

通用串行总线(USB)是主机和外围设备之间的一种连接。
从拓扑上来看,是一颗由几个点对点的连接构建而成的树。这些连接是连接设备和集线器(hub)的四线电缆(底线、电源线和两根信号线)。USB主控制器(host controller)负责询问每一个USB设备是否有数据需要发送。
Linux内核支持两种主要类型的USB驱动程序:宿主(host)系统上的驱动程序和设备(device)上的驱动程序。宿主系统上的USB驱动程序控制插入其中的USB设备,USB设备的驱动程序控制该设备如何作为一个USB设备和主机通信。
USB驱动程序存在于不同的内核子系统和USB硬件控制器之中,USB核心为USB驱动程序提供了一个用于访问和控制USB硬件的接口。
image

2.USB设备基础

USB设备的构成,包括配置、接口和端点,以及USB驱动程序如何绑定到USB接口上,而不是整个USB设备
image

  • 端点
    USB通信最基本的形式是通过一个名为端点(endpoint)的东西。USB端点只能往一个方向传送数据,从主机到设备(称为输出端点)或者从设备到主机(称为输入端点)。端点可以看作是单向的管道。
    有四种不同类型的端点:
  • 控制
    用来控制对USB设备不同部分的访问。通常用于配置设备、获取设备信息、发送命令到设备或者获取设备的状态报告。每个USB设备都有一个名为“端点0”的控制端点,USB核心使用该端点在插入时进行设备的配置。
  • 中断
    当USB宿主要求设备传输数据时,中断端点就以一个固定的速率来传送少量的数据。通常还用于发送数据到USB设备以控制设备,一般不用来传输大量的数据。
  • 批量
    传输大批量的数据。常见于需要确保没有数据丢失的传输的设备。如果总线上的空间不足以发送整个批量包,它将被分割为多个包进行传输。
  • 等时
    同样可以传送大批量的数据,但数据是否到达是没有保证的。实时的数据收集(例如音频和视频设备)几乎毫无例外都使用这类端点。
    控制和批量端点用于异步的数据传输。中断和等时端点是周期性的。
    内核使用struct usb_host_endpoint结构体来描述USB端点。该结构体在另一个struct usb_endpoint_descriptor的结构体中包含真正的端点信息。
    bEndpointAddress,特定端点的USB地址。还包含了端点的方向。可以结合位掩码USB_DIR_OUT和USB_DIR_IN,以确定该端点的数据是传向设备还是主机。
    bmAttributes,端点的类型。结合位掩码USB_ENDPOINT_XFERTYPE_MASK,以确定此端点的类型是USB_ENDPOINT_XFER_ISOC(等时)、USB_ENDPOINT_XFER_BULK(批量)还是USB_ENDPOINT_XFER_INT(中断)。
    wMaxPacketSize,该端点一次可以处理的最大字节数。驱动程序可以发送数量大于此值的数据到端点,在实际传输到设备时,数据将被分割为wMaxPacketSize大小的块。
    bInterval,如果端点是中断类型,该值是端点的间隔设置。
  • 接口
    USB接口只处理一种USB逻辑连接。内核使用struct usb_interface结构体来描述USB接口。USB核心把该结构体传递给USB驱动程序,之后由USB驱动程序来负责控制该结构体。
    struct usb_host_interface *altsetting,一个接口结构体数组,包含所有可能用于该接口的可选设置。
    unsigned num_altsetting,altsetting指针所指的可选设置的数量。
    struct usb_host_interface *cur_altsetting,指向altsetting数组内部的指针,表示该接口的当前活动设置。
    int minor,如果捆绑到该接口的USB驱动程序使用USB主设备号,这个变量包含USB核心分配给该接口的次设备号,仅在一个成功的usb_register_dev调用之后才有效。
  • 配置
    一个USB设备可以有多个配置,可以在配置之间切换以改变设备的状态,而一个时刻只能激活一个配置。
    Linux使用struct usb_host_config结构体来描述USB配置,使用struct usb_device结构体来描述整个USB设备。
    USB设备驱动程序需要把一个给定的struct usb_interface结构体的数据转换为struct usb_device结构体,用于转换功能的函数是interface_to_usbdev。
    USB设备由许多不同的逻辑单元构成,逻辑单元之间的关系:
    设备通常具有一个或者更多的配置
    配置经常具有一个或者更多的接口
    接口通常具有一个或者更多的设置
    接口没有或者具有一个以上的端点

3.USB urb

Linux内核中的USB代码通过一个称为urb(USB请求块)的东西与所有的USB设备通信,使用struct urb结构体来描述这个请求块。
urb被用来以一种异步的方式往/从特定的USB设备上的特定USB端点发送/接收数据。
一个urb的典型生命周期:
由USB设备驱动程序创建。
分配给一个特定的USB设备的特定端点。
由USB设备驱动程序递交到USB核心。
由USB核心递交到特定设备的特定USB中控制器驱动程序。
由USB主控制器驱动程序处理,从设备进行USB传送。
当urb结束之后,USB主控制器驱动程序通知USB设备驱动程序。

  • 创建和销毁urb
    必须使用usb_alloc_urb函数来创建。
    struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
    iso_packets,是该urb应该包含的等时数据包的数量,如果不打算创建等时urb,该值为0。
    mem_flags,和传递给用于从内核分配内存的kmalloc函数的标志有相同的类型。
    如果成功为urb分配了足够的内存空间,指向该urb的指针将被返回给调用函数,如果返回为NULL,表示USB核心发生了错误。
    必须调用usb_free_urb函数告诉USB核心驱动程序已经使用完urb。
    void usb_free_urb(struct urb *urb);
    urb,指向所需释放的struct urb的指针,在该函数被调用后,urb结构体就会消失,驱动程序不能再访问它。
    中断urb
    usb_fill_int_urb用来正确地初始化即将被发送到USB设备的中断端点的urb。
    static inline void usb_fill_int_urb(struct urb *urb,
    struct usb_device *dev,
    unsigned int pipe,
    void *transfer_buffer,
    int buffer_length,
    usb_complete_t complete_fn,
    void *context,
    int interval);
    struct urb *urb,指向需要初始化的urb的指针。
    struct usb_device *dev,该urb所发送的目标USB设备。
    unsigned int pipe,该urb所发送的目标USB设备的特定端点。使用usb_sndintpipe或者usb_rcvintpipe函数创建。
    void *transfer_buffer,用来保存外发数据或者接收数据的缓冲区的指针。必须使用kmalloc调用来创建。
    int buffer_length,transfer_buffer指针所指向的缓冲区的大小。
    usb_complete_t complete_fn,当该urb结束之后调用的结束处理例程的指针。
    void *context,指向一小数据块,该块被添加到urb结构体中以便进行结束处理例程后面的查找。
    int interval,该urb应该被调度的间隔。
    批量urb
    使用的函数usb_fill_bulk_urb。
    static inline void usb_fill_bulk_urb(struct urb *urb,
    struct usb_device *dev,
    unsigned int pipe,
    void *transfer_buffer,
    int buffer_length,
    usb_complete_t complete_fn,
    void *context);
    参数和usb_fill_int_urb函数一样,不过没有时间间隔参数,pipe变量使用usb_sndbulkpipe或usb_rcvbulkpipe函数来初始化。
    控制urb
    调用usb_fill_control_urb函数。
    static inline void usb_fill_control_urb(struct urb *urb,
    struct usb_device *dev,
    unsigned int pipe,
    unsigned char *setup_packet,
    void *transfer_buffer,
    int buffer_length,
    usb_complete_t complete_fn,
    void *context);
    参数和usb_fill_bulk_urb函数一样,setup_packet指向即将被发送到端点的设备数据包的数据,pipe变量使用usb_sndctrlpipe或usb_rcvctrlpipe函数来初始化。
    等时urb
    必须在驱动程序中“手工地”进行初始化。
  • 提交urb
    一旦urb被USB驱动程序正确地创建和初始化之后,就可以提交到USB核心以发送到USB设备了。通过调用usb_submit_urb函数来完成。
    int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
    urb参数指向即将被发送到设备的urb的指针。
    mem_flags参数等同于传递给kmalloc调用的同一个参数,用于告诉USB核心如何在此时及时地分配内存缓冲区。
  • 结束urb:结束回调处理例程
    如果调用usb_submit_urb成功,把对urb的控制转交给USB核心,该函数返回0,否则,返回负的错误号。如果函数调用成功,当 urb结束的时候,usb的结束处理例程(由结束函数指针指定)正好被调用一次。
    只有三种结束urb和调用结束函数的情形:
    urb被成功地发送到了设备,设备返回了正确的确认。urb中的status变量被设置为0。
    发送数据到设备或者从设备接收数据时发生了错误。错误情况由urb结构体中的status变量的错误值来指示。
    urb从USB核心中被“解开链接”。
  • 取消urb
    调用usb_kill_urb或usb_unlink_urb函数来终止一个已经被提交到USB核心的urb。
    void usb_kill_urb(struct urb *urb);
    int usb_unlink_urb(struct urb *urb);
    urb的参数指向即将被取消的urb的指针。
    如果调用usb_kill_urb函数,该urb的生命周期将被终止。通常是当设备从系统中被断开时,在断开回调函数中调用该函数。
    对于某些驱动程序而言,应该使用usb_unlink_urb函数来告诉USB核心终止一个urb。该函数并不等到urb完全被终止后才返回到调用函数。

4.USB驱动程序

驱动程序把驱动程序对象注册到USB子系统中,稍后再使用制造商和设备标识来判断是否已经安装了硬件。
USB核心使用struct usb_device_id结构体来判断对于一个设备该使用哪一个驱动程序,热插拔脚本使用它来确定当一个特定的设备插入到系统时该自动装载哪一个驱动程序

 struct usb_device_id {
	__u16		match_flags;      /*确定设备和结构体中下列字段中的哪一个相匹配*/
	__u16		idVendor;         /*设备的USB制造商ID*/
	__u16		idProduct;        /*设备的USB产品ID*/
	/*定义了制造商指派的产品的版本号范围的最低和最高值。bcdDevice_hi值包括在内,该值是最高编号的设备的编号*/
	__u16		bcdDevice_lo;     
	__u16		bcdDevice_hi;
    /*分别定义设备的类型、子类型和协议。这些值详细说明了整个设备的行为,包括该设备上的所有接口*/
	__u8		bDeviceClass;
	__u8		bDeviceSubClass;
	__u8		bDeviceProtocol;
    /*分别定义了类型、子类型和单个接口的协议*/
	__u8		bInterfaceClass;
	__u8		bInterfaceSubClass;
	__u8		bInterfaceProtocol;

	__u8		bInterfaceNumber;
	/*不是用来比较是否匹配的,包含了驱动程序在USB驱动程序的探测回调函数中可以用来区分不同设备的信息*/
	kernel_ulong_t	driver_info
		__attribute__((aligned(sizeof(kernel_ulong_t))));
};

有许多用来初始化该结构体的宏:
USB_DEVICE(vend, prod),仅和指定的制造商和产品ID值相匹配。用于需要一个特定驱动程序的USB设备。
USB_DEVICE_VER(vend, prod, lo, hi),仅和某版本范围内的指定制造商和产品ID值相匹配。
USB_DEVICE_INFO(cl, sc, pr),仅和USB设备的指定类型相匹配。
USB_INTERFACE_INFO(cl, sc, pr),仅和USB接口的指定类型相匹配。

  • 注册USB驱动程序
    必须创建的主结构体是struct usb_driver,必须由USB驱动程序来填写,包括许多回调函数和变量,向USB核心代码描述了USB驱动程序。
    struct usb_driver {
    	const char *name;    /*指向驱动程序名字的指针。在内核的所有USB驱动程序中必须是唯一的,通常被设置为和驱动程序模块名相同的名字。*/
    
    	int (*probe) (struct usb_interface *intf,
    		      const struct usb_device_id *id);  /*指向USB驱动程序中的探测函数的指针。*/
    
    	void (*disconnect) (struct usb_interface *intf); /*指向USB驱动程序中的断开函数的指针。当struct usb_interface被从系统中移除或者驱动程序正在从USB核心中卸载时,USB核心将调用该函数。*/
    
    	int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
    			void *buf);
    
    	int (*suspend) (struct usb_interface *intf, pm_message_t message);
    	int (*resume) (struct usb_interface *intf);
    	int (*reset_resume)(struct usb_interface *intf);
    
    	int (*pre_reset)(struct usb_interface *intf);
    	int (*post_reset)(struct usb_interface *intf);
    
    	const struct usb_device_id *id_table;   /*指向struct usb_device_id表的指针,包含了一系列驱动程序可以支持的所有不同类型的USB设备。*/
    
    	struct usb_dynids dynids;
    	struct usbdrv_wrap drvwrap;
    	unsigned int no_dynamic_id:1;
    	unsigned int supports_autosuspend:1;
    	unsigned int disable_hub_initiated_lpm:1;
    	unsigned int soft_unbind:1;
    };
    
    以struct usb_driver指针为参数的usb_register_driver函数调用把struct usb_driver注册到USB核心。
    当USB驱动程序将要被卸载时,需要把struct usb_drvier从内核中注销,通过调用usb_deregister_driver来完成该工作。
  • 探测和断开
    探测和断开回调函数都是在USB集线器内核线程的上下文中被调用的,因此在其中睡眠是合法的。
    在探测回调函数中,USB驱动程序应该初始化人任何可能用于控制USB设备的局部结构体,还应该把所需的任何设备相关信息保存到局部结构体中。
    USB驱动程序需要在设备生命周期的稍后时间获取和该结构体struct usb_interface相关联的局部数据结构体,可以调用usb_set_intfdata函数:
    void usb_set_intfdata(struct usb_interface *intf, void *data);
    该函数接受一个指向任意数据类型的指针,把它保存到struct usb_interface结构体中以便后面访问,调用usb_get_intfdata函数来获取数据:
    void *usb_get_intfdata(struct usb_interface *intf);
    usb_get_intfdata通常在USB驱动程序的打开函数和断开函数中调用。
    如果USB驱动程序没有和设备与用户交互的另一种类型的子系统相关联,驱动程序可以使用USB主设备号,以便在用户空间使用传统的字符驱动程序接口。USB驱动程序需要在探测函数中调用usb_register_dev函数来把设备注册到USB核心。
    usb_register_dev函数需要一个指向struct usb_interface的指针和一个指向struct usb_class_driver结构的指针。
    int usb_register_dev(struct usb_interface *intf, struct usb_class_driver *class_driver);
    
    struct usb_class_driver {
    	char *name;     /*sysfs用来描述设备的名字*/
    	char *(*devnode)(struct device *dev, umode_t *mode); /*回调函数,创建设备节点*/
    	const struct file_operations *fops;  /*指向struct file_operations的指针,驱动程序定义该结构体,用它来注册为字符设备*/
    	int minor_base;  /*为驱动程序指派的次设备号范围的开始值*/
    };
    
    当一个USB设备被断开时,和该设备相关联的所有资源都应该被尽可能的清理掉。必须调用usb_deregister_dev函数把次设备号交还USB核心。
    在断开函数中,从接口获取之前调用usb_set_intfdata设置的任何数据也是很重要的,然后设置struct usb_interface结构体中的数据指针为NULL。
    在USB设备已经被断开之后,如果驱动程序通过调用usb_submit_urb来提交一个urb给它,提交将会失败并返回错误值-EPIPE。
  • 提交和控制urb
    当驱动程序有数据要发送到USB设备时,必须分配一个urb来把数据传输给设备(struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags)),在urb被成功分配之后,创建一个DMA缓冲区以最高效的方式发送数据到设备,传递给驱动程序的数据复制到该缓冲区中(void *usb_alloc_coherent(struct usb_device *dev, size_t size, gfp_t mem_flags, dma_addr_t *dma))。一旦数据从用户空间正确的复制到局部缓冲区中,urb必须在可以被提交给USB核心之前被正确地初始化(static inline void usb_fill_bulk_urb(struct urb *urb, struct usb_device *dev, unsigned int pipe, void *transfer_buffer, int buffer_length, usb_complete_t complete_fn, void *context)),然后就可以被提交给USB核心以传输到设备(int usb_submit_urb(struct urb *urb, gfp_t mem_flags))。
    /* 创建一个urb */
    urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!urb) {
    	retval = -ENOMEM;
    	goto error;
    }
    /* 创建DMA缓冲区,并把数据拷贝到缓冲区 */
    buf = usb_alloc_coherent(dev->udev, writesize, GFP_KERNEL,
    &urb->transfer_dma);
    if (!buf) {
    	retval = -ENOMEM;
    	goto error;
    }
    
    if (copy_from_user(buf, user_buffer, writesize)) {
    retval = -EFAULT;
    goto error;
    }
    
    /* this lock makes sure we don't submit URBs to gone devices */
    mutex_lock(&dev->io_mutex);
    if (!dev->interface) {		/* disconnect() was called */
    	mutex_unlock(&dev->io_mutex);
    	retval = -ENODEV;
    	goto error;
    }
    
    /* 初始化urb */
    usb_fill_bulk_urb(urb, dev->udev,
    			usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
    			buf, writesize, skel_write_bulk_callback, dev);
    urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
    usb_anchor_urb(urb, &dev->submitted);
    
    /* 把数据从批量端口发出 */
    retval = usb_submit_urb(urb, GFP_KERNEL);
    mutex_unlock(&dev->io_mutex);
    if (retval) {
    	dev_err(&dev->interface->dev,
    	"%s - failed submitting write urb, error %d\n",
    	__func__, retval);
    	goto error_unanchor;
    }
    
    在urb被成功传输到USB设备之后(或者传输冲发生了某些事情),urb回调函数将被USB核心调用。
    static void skel_write_bulk_callback(struct urb *urb)
    {
    	struct usb_skel *dev;
    
    	dev = urb->context;
    
    	/* sync/async 解链接故障不是错误 */
    	if (urb->status) {
    		if (!(urb->status == -ENOENT ||
    		    urb->status == -ECONNRESET ||
    		    urb->status == -ESHUTDOWN))
    			dev_err(&dev->interface->dev,
    				"%s - nonzero write bulk status received: %d\n",
    				__func__, urb->status);
    
    		spin_lock(&dev->err_lock);
    		dev->errors = urb->status;
    		spin_unlock(&dev->err_lock);
    	}
    
    	/* 释放已分配的缓冲区 */
    	usb_free_coherent(urb->dev, urb->transfer_buffer_length,
    			  urb->transfer_buffer, urb->transfer_dma);
    	up(&dev->limit_sem);
    }
    
    回调函数中做的第一件事是检查urb的状态,以确定该urb是否已经成功地结束。之后回调函数释放传输时分配给该urb的缓冲区。
    urb回调函数是运行在中断上下文中的,因此它不应该进行任何内存分配、持有任何信号量或者做任何其他可能导致进程睡眠的事情。
  • 不使用urb的USB传输
    有时候USB驱动程序只是要发送或者接收一些简单的USB数据,有两个更简单的接口函数可以使用usb_bulk_msg和 usb_control_msg。
    usb_bulk_msg创建一个USB批量urb,把它发送到指定的设备,然后在返回调用者之前等待它的结束。
    int usb_bulk_msg(struct usb_device *usb_dev, unsigned int pipe, void *data, int len, int *actual_length, int timeout);
    struct usb_device *usb_dev,指向批量消息所发送的目标USB设备的指针。
    unsigned int pipe,该批量消息所发送的目标USB设备的特定端点,调用usb_sndbulkpipe或usb_rcvbulkpipe来创建。
    void *data,如果是一个OUT端点,指向即将发送到设备的数据的指针。如果是一个IN端点,指向从设备读取的数据应该存放的位置的指针。
    int len,data参数所指缓冲区的大小。
    int *actual_length,指向保存实际传输字节数的位置的指针。
    int timeout,以jiffies为单位的应该等待的超时时间。如果该值为0,该函数将一直等待消息的结束。
    如果函数调用成功,返回值为0;否则,返回一个负的错误值。如果成功,actual_length参数包含从该消息发送或者接收的字节数。
    不能在一个中断上下文中或者在持有自旋锁的情况下调用usb_bulk_msg函数。
    usb_control_msg,允许驱动程序发送和接收USB控制消息。
    int usb_control_msg(struct usb_device *dev, unsigned int pipe, __u8 request, __u8 requesttype, __u16 value, __u16 index, void *data, __u16 size, int timeout);
    struct usb_device *dev,指向控制消息所发送的目标USB设备的指针。
    unsigned int pipe,该控制消息所发送的目标USB设备的特定端点,调用usb_sndctrlpipe或usb_rcvctrlpipe来创建。
    __u8 request,控制消息的USB请求值。
    __u8 requesttype,控制消息的USB请求类型值。
    __u16 value,控制消息的USB消息值。
    __u16 index,控制消息的USB消息索引值。
    void *data,如果是一个OUT端点,指向即将发送到设备的数据的指针。如果是一个IN端点,指向从设备读取的数据应该存放的位置的指针。
    __u16 size,data参数所指缓冲区的大小。
    int timeout,以jiffies为单位的应该等待的超时时间。如果该值为0,该函数将一直等待消息的结束。
    如果函数调用成功,返回传输到设备或者从设备读取的字节数;如果不成功,返回一个负的错误值。
    不能在一个中断上下文中或者在持有自旋锁的情况下调用usb_control_msg函数。
  • 其他USB数据函数
    USB核心中的许多辅助函数可以用来从所有USB设备中获取标准的信息。这些函数不能在一个中断上下文中或者持有自旋锁的情况下调用。
    usb_get_descriptor函数从指定的设备获取指定的USB描述符。
    int usb_get_descriptor(struct usb_device *dev, unsigned char desctype, unsigned char descindex, void *buf, int size);
    USB驱动程序可以使用该函数来从struct usb_device结构体中获取任何没有存在于已有struct usb_device和struct usb_interface结构体中的设备描述符。
    struct usb_device *dev,指向想要获取设备描述符的目标USB设备的指针。
    unsigned char desctype,描述符的类型。
    unsigned char descindex,应该从设备获取的描述符的编号。
    void *buf,指向复制描述符到其中的缓冲区的指针。
    int size,buf变量所指内存的大小。
    如果函数调用成功,返回从设备读取的字节数。否则,返回一个由该函数调用的底层的usb_control_msg函数返回的一个负的错误值。
    usb_get_descriptor调用更常用于从USB设备获取一个字符串,提供usb_string的辅助函数来完成该工作。
    int usb_string(struct usb_device *dev, int index, char *buf, size_t size);
    返回从USB设备读取的已经转换为ISO 8859-1格式的字符串。是USB设备的字符串的典型格式。

    参考资料:
    《LINUX设备驱动程序第三版》
    linux-4.9.88
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2024年02月20日 0

暂无评论

推荐阅读
  bgxXjYYEVSxV   14小时前   5   0   0 嵌入式
  bgxXjYYEVSxV   14小时前   4   0   0 嵌入式
  swCWDMUCSvaI   14小时前   5   0   0 嵌入式
  jEmNNF9D14iz   14小时前   5   0   0 嵌入式
I2C
  bgxXjYYEVSxV   14小时前   5   0   0 嵌入式
PWM
  bgxXjYYEVSxV   14小时前   5   0   0 嵌入式
vNOZzpgGXHoP
作者其他文章 更多
最新推荐 更多