Linux 驱动开发之USB设备分析3(基于Linux6.6)---USB驱动程序介绍
前言
USB 驱动程序在 Linux 中依然遵循 设备模型,即 总线 (Bus)、设备 (Device)、驱动 (Driver) 的模型,这一模型帮助组织和管理各种硬件设备。这个模型本质上是 Linux 内核中硬件抽象和驱动绑定的框架。USB 子系统也继承了这一模型,使得 USB 设备的管理、驱动程序的加载和卸载变得更加系统化和统一。
设备模型(Bus-Device-Driver 模型)
-
总线 (Bus)
- 总线是一个硬件平台或协议的抽象,它连接所有设备并管理它们之间的通信。
- 在 USB 子系统中,
usb_bus
表示 USB 总线,每个总线上可以连接多个 USB 设备。USB 总线是一个重要的概念,它允许通过 USB 控制器与多个 USB 设备进行交互。 - 总线通常有一个根集线器(root hub),负责管理连接的设备。
-
设备 (Device)
- 设备是硬件的抽象,表示与主机系统连接的实际硬件设备。在 USB 子系统中,每个 USB 设备都会通过
usb_device
结构表示。 - 每个 USB 设备都有一个唯一的设备标识符,并且可以包含多个接口 (
usb_interface
) 和端点 (usb_endpoint
)。 - 设备模型负责表示 USB 设备的状态、属性和配置,它确保系统能够识别和管理所有连接的设备。
- 设备是硬件的抽象,表示与主机系统连接的实际硬件设备。在 USB 子系统中,每个 USB 设备都会通过
-
驱动 (Driver)
- 驱动程序是负责管理特定硬件设备的软件模块。它负责与设备通信,并执行对设备的操作。
- 在 USB 子系统中,
usb_driver
结构体代表一个 USB 驱动程序,它包含了设备的探测(probe)和移除(disconnect)功能等。 - 驱动程序通过匹配设备的设备描述符(例如,
usb_device_id
)来决定它是否可以控制某个设备。如果匹配,驱动程序就会“绑定”到设备上,开始控制设备。
总线、设备和驱动之间的关系
1. 总线和设备的关系
- 每个 USB 设备都连接到一个 USB 总线。一个 USB 总线上的设备可以通过
usb_bus
结构管理,usb_device
结构代表实际的设备。 - 每个 USB 设备都可以通过接口 (
usb_interface
) 和端点 (usb_endpoint
) 来进行数据传输。 - 总线是设备的父节点,每个设备都隶属于一个总线。
2. 设备和驱动的关系
- 设备驱动模型:每当一个 USB 设备被插入时,Linux 内核会根据设备的描述符来查找合适的驱动程序。设备描述符通常包含了设备的 ID(如 VID/PID)等信息,驱动程序会通过
usb_device_id
来匹配这些信息。 - 设备与驱动的绑定:如果驱动匹配成功,驱动会通过
usb_driver
的probe()
函数与设备进行绑定,之后驱动程序负责控制设备的行为。 - 驱动程序还可以在设备被移除时通过
disconnect()
函数来清理设备。
3. 总线、设备和驱动的生命周期管理
- 设备的注册:当 USB 设备被插入时,内核会自动创建一个
usb_device
结构并将其注册到系统中。设备也会被添加到其对应的 USB 总线(usb_bus
)中。 - 驱动的绑定:当内核检测到一个新的 USB 设备时,它会遍历注册的 USB 驱动程序,查看是否有驱动程序能够匹配该设备。匹配成功后,内核将驱动绑定到设备上,启动驱动程序的相关功能。
- 设备的移除:当设备被拔出时,内核会注销该设备并触发与之相关的驱动程序的断开操作(通过
disconnect()
函数)。
示例:USB 驱动程序的基本工作流程
-
USB 设备连接
- 当用户插入一个 USB 设备时,Linux 内核会通过设备模型的机制发现该设备,并为其创建一个
usb_device
结构,注册到 USB 总线。
- 当用户插入一个 USB 设备时,Linux 内核会通过设备模型的机制发现该设备,并为其创建一个
-
设备与驱动程序匹配
- 内核会根据 USB 设备的描述符(如 VID、PID)来匹配已加载的驱动程序。如果找到匹配的驱动,内核会调用驱动的
probe()
函数,驱动程序开始控制设备。
- 内核会根据 USB 设备的描述符(如 VID、PID)来匹配已加载的驱动程序。如果找到匹配的驱动,内核会调用驱动的
-
驱动程序管理设备
- 驱动程序通过提供的 API(如
usb_control_msg()
、usb_submit_urb()
)与设备进行通信,并执行设备的相关操作(如读取、写入数据)。
- 驱动程序通过提供的 API(如
-
USB 设备断开
- 当设备被拔出时,内核会将该设备从设备模型中移除,并调用驱动的
disconnect()
函数以清理资源,最终解除驱动与设备的绑定。
- 当设备被拔出时,内核会将该设备从设备模型中移除,并调用驱动的
一、注册USB驱动程序
Linux的设备驱动,特别是这种hotplug的USB设备驱动,会被编译成模块,然后在需要时挂在到内核。所以USB驱动和注册与正常的模块注册、卸载是一样的,下面是USB驱动的注册与卸载:
static int __init usb_skel_init(void)
{
int result;
/* register this driver with the USB subsystem */
result = usb_register(&skel_driver);
if (result)
err("usb_register failed. Error number %d", result);
return result;
}
static void __exit usb_skel_exit(void)
{
/* deregister this driver with the USB subsystem */
usb_deregister(&skel_driver);
}
module_init (usb_skel_init);
module_exit (usb_skel_exit);
MODULE_LICENSE("GPL");
USB设备驱动的模块加载函数通用的方法是在I2C设备驱动的模块加载函数中使用usb_register(struct *usb_driver)函数添加usb_driver的工作,而在模块卸载函数中利用usb_deregister(struct *usb_driver)做相反的工作。 对比I2C设备驱动中的 i2c_add_driver(&i2c_driver)与i2c_del_driver(&i2c_driver)。
struct usb_driver是USB设备驱动,需要实现其成员函数:
drivers/usb/usb-skeleton.c
static struct usb_driver skel_driver = {
.name = "skeleton",
.probe = skel_probe,
.disconnect = skel_disconnect,
.suspend = skel_suspend,
.resume = skel_resume,
.pre_reset = skel_pre_reset,
.post_reset = skel_post_reset,
.id_table = skel_table,
.supports_autosuspend = 1,
};
module_usb_driver(skel_driver);
重要的是probe函数与disconnect函数。先谈一下id_table:
id_table 是struct usb_device_id 类型,包含了一列该驱动程序可以支持的所有不同类型的USB设备。如果没有设置该变量,USB驱动程序中的探测回调该函数将不会被调用。对比I2C中struct i2c_device_id *id_table,一个驱动程序可以对应多个设备,i2c 示例:
static const struct i2c_device_id mpu6050_id[] = {
{ "mpu6050", 0},
{}
};
usb子系统通过设备的production ID和vendor ID的组合或者设备的class、subclass跟protocol的组合来识别设备,并调用相关的驱动程序作处理。可以看看这个id_table到底是什么东西:
drivers/usb/usb-skeleton.c
/* Define these values to match your devices */
#define USB_SKEL_VENDOR_ID 0xfff0
#define USB_SKEL_PRODUCT_ID 0xfff0
/* table of devices that work with this driver */
static const struct usb_device_id skel_table[] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
{ } /* Terminating entry */
};
MODULE_DEVICE_TABLE(usb, skel_table);
MODULE_DEVICE_TABLE的第一个参数是 设备的类型,如果是USB设备,那自然是usb。后面一个参数是 设备表, 这个设备表的最后一个元素是空的,用于标识结束。代码定义了USB_SKEL_VENDOR_ID是0xfff0,USB_SKEL_PRODUCT_ID是0xfff0,也就是说,当有一个设备接到集线器时,usb子系统就会检查这个设备的vendor ID和product ID,如果它们的值是0xfff0时,那么子系统就会调用这个skeleton模块作为设备的驱动。
当USB设备接到USB控制器接口时,usb_core就检测该设备的一些信息,例如生产厂商ID和产品的ID,或者是设备所属的class、subclass跟protocol,以便确定应该调用哪一个驱动处理该设备。
下面所要做的就是对probe函数与disconnect函数的填充了,但是在对probe函数与disconnect函数填充之前,有必要先学习三个重要的数据结构,这在后面probe函数与disconnect函数中有很大的作用:
二、USB驱动程序中重要数据结构
2.1、usb-skeleton
usb-skeleton 是一个局部结构体,用于与端点进行通信。下面先看一下Linux内核源码中的一个usb-skeleton(就是usb驱动的骨架咯),其定义的设备结构体就叫做usb-skel:
drivers/usb/usb-skeleton.c
/* Structure to hold all of our device specific stuff */
struct usb_skel {
struct usb_device *udev; /* the usb device for this device */
struct usb_interface *interface; /* the interface for this device */
struct semaphore limit_sem; /* limiting the number of writes in progress */
struct usb_anchor submitted; /* in case we need to retract our submissions */
struct urb *bulk_in_urb; /* the urb to read data with */
unsigned char *bulk_in_buffer; /* the buffer to receive data */
size_t bulk_in_size; /* the size of the receive buffer */
size_t bulk_in_filled; /* number of bytes in the buffer */
size_t bulk_in_copied; /* already copied to user space */
__u8 bulk_in_endpointAddr; /* the address of the bulk in endpoint */
__u8 bulk_out_endpointAddr; /* the address of the bulk out endpoint */
int errors; /* the last request tanked */
bool ongoing_read; /* a read is going on */
spinlock_t err_lock; /* lock for errors */
struct kref kref;
struct mutex io_mutex; /* synchronize I/O with disconnect */
unsigned long disconnected:1;
wait_queue_head_t bulk_in_wait; /* to wait for an ongoing read */
};
#define to_skel_dev(d) container_of(d, struct usb_skel, kref)
从开发人员的角度看,每一个usb设备有若干个配置(configuration)组成,每个配置又可以有多个接口(interface),每个接口又有多个设置,而接口本身可能没有端点或者多个端点(end point)
2.2、USB 接口数据结构 struct usb_interface
include/linux/usb.h
struct usb_interface {
/* array of alternate settings for this interface,
* stored in no particular order */
struct usb_host_interface *altsetting;
struct usb_host_interface *cur_altsetting; /* the currently
* active alternate setting */
unsigned num_altsetting; /* number of alternate settings */
/* If there is an interface association descriptor then it will list
* the associated interfaces */
struct usb_interface_assoc_descriptor *intf_assoc;
int minor; /* minor number this interface is
* bound to */
enum usb_interface_condition condition; /* state of binding */
unsigned sysfs_files_created:1; /* the sysfs attributes exist */
unsigned ep_devs_created:1; /* endpoint "devices" exist */
unsigned unregistering:1; /* unregistration is in progress */
unsigned needs_remote_wakeup:1; /* driver requires remote wakeup */
unsigned needs_altsetting0:1; /* switch to altsetting 0 is pending */
unsigned needs_binding:1; /* needs delayed unbind/rebind */
unsigned resetting_device:1; /* true: bandwidth alloc after reset */
unsigned authorized:1; /* used for interface authorization */
struct device dev; /* interface specific device info */
struct device *usb_dev;
struct work_struct reset_ws; /* for resets in atomic context */
};
#define to_usb_interface(d) container_of(d, struct usb_interface, dev)
在逻辑上,一个USB设备的功能划分是通过接口来完成的。比如说一个USB扬声器,可能会包括有两个接口:一个用于键盘控制,另外一个用于音频流传输。而事实上,这种设备需要用到不同的两个驱动程序来操作,一个控制键盘,一个控制音频流。但也有例外,比如蓝牙设备,要求有两个接口,第一用于ACL跟EVENT的传输,另外一个用于SCO链路,但两者通过一个驱动控制。在Linux上,接口使用struct usb_interface来描述,以下是该结构体中比较重要的字段:
a -- struct usb_host_interface *altsetting(注意不是usb_interface)
b -- struct usb_host_interface *cur_altsetting
当前活动的设置,指向altsetting数组中的一个
struct usb_host_interface数据结构:
include/linux/usb.h
/* host-side wrapper for one interface setting's parsed descriptors */
struct usb_host_interface {
struct usb_interface_descriptor desc;
int extralen;
unsigned char *extra; /* Extra descriptors */
/* array of desc.bNumEndpoints endpoints associated with this
* interface setting. these will be in no particular order.
*/
struct usb_host_endpoint *endpoint;
char *string; /* iInterface string, if present */
};
c -- unsigned num_altstting
可选设置的数量,即altsetting所指数组的元素个数
d -- int minor
当捆绑到该接口的USB驱动程序使用USB主设备号时,USB core分配的次设备号。仅在成功调用usb_register_dev之后才有效。
2.3、USB 端点 struct usb_host_endpoint
Linux中用struct usb_host_endpoint 来描述USB端点。
include/linux/usb.h
struct usb_host_endpoint {
struct usb_endpoint_descriptor desc;
struct usb_ss_ep_comp_descriptor ss_ep_comp;
struct usb_ssp_isoc_ep_comp_descriptor ssp_isoc_ep_comp;
struct list_head urb_list;
void *hcpriv;
struct ep_device *ep_dev; /* For sysfs info */
unsigned char *extra; /* Extra descriptors */
int extralen;
int enabled;
int streams;
};
每个usb_host_endpoint中包含一个struct usb_endpoint_descriptor结构体,当中包含该端点的信息以及设备自定义的各种信息,这些信息包括:
a -- bEndpointAddress(b for byte)
8位端点地址,其地址还隐藏了端点方向的信息(之前说过,端点是单向的),可以用掩码USB_DIR_OUT和USB_DIR_IN来确定。
b -- bmAttributes
端点的类型,结合USB_ENDPOINT_XFERTYPE_MASK可以确定端点是USB_ENDPOINT_XFER_ISOC(等时)、USB_ENDPOINT_XFER_BULK(批量)还是USB_ENDPOINT_XFER_INT(中断)。
c -- wMaxPacketSize
端点一次处理的最大字节数。发送的BULK包可以大于这个数值,但会被分割传送。
d -- bInterval
如果端点是中断类型,该值是端点的间隔设置,以毫秒为单位。
include/uapi/linux/usb/ch9.h
/* USB_DT_ENDPOINT: Endpoint descriptor */
struct usb_endpoint_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u8 bEndpointAddress;
__u8 bmAttributes;
__le16 wMaxPacketSize;
__u8 bInterval;
/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));
三、探测和断开函数分析
USB驱动程序指定了两个USB核心在适当时间调用的函数。
3.1、探测函数
当一个设备被安装而USB核心认为该驱动程序应该处理时,探测函数被调用;
探测函数应该检查传递给他的设备信息,确定驱动程序是否真的适合该设备。当驱动程序因为某种原因不应控制设备时,断开函数被调用,它可以做一些清洁的工作。
系统会传递给探测函数的信息是什么呢?一个usb_interface * 跟一个struct usb_device_id *作为参数。他们分别是该USB设备的接口描述(一般会是该设备的第0号接口,该接口的默认设置也是第0号设置)跟它的设备ID描述(包括Vendor ID、Production ID等)。
USB驱动程序应该初始化任何可能用于控制USB设备的局部结构体,它还应该把所需的任何设备相关信息保存到局部结构体中。例如,USB驱动程序通常需要探测设备对的端点地址和缓冲区大小,因为需要他们才能和端点通信。
下面具体分析探测函数做了哪些事情:
a -- 探测设备的端点地址、缓冲区大小,初始化任何可能用于控制USB设备的数据结构
下面是一个实例代码,他们探测批量类型的IN和OUT端点,把相关信息保存到一个局部设备结构体中:
/* set up the endpoint information */
/* use only the first bulk-in and bulk-out endpoints */
iface_desc = interface->cur_altsetting;
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
if ( !dev->bulk_in_endpointAddr &&
((endpoint->bEndpointAddress & USB_ENDPOINT_DIR_MASK) = = USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) = = USB_ENDPOINT_XFER_BULK)) {
/* we found a bulk in endpoint */
buffer_size = le16_to_cpu(endpoint->wMaxPacketSize);
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!dev->bulk_in_buffer) {
err("Could not allocate bulk_in_buffer");
goto error;
}
}
if (!dev->bulk_out_endpointAddr &&
((endpoint->bEndpointAddress & USB_ENDPOINT_DIR_MASK)= =USB_DIR_OUT) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)= = USB_ENDPOINT_XFER_BULK)) {
/* we found a bulk out endpoint */
dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;
}
}
if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {
err("Could not find both bulk-in and bulk-out endpoints");
goto error;
}
具体流程如下:
该代码块首先循环访问该接口中存在的每一个端点,赋予该端点结构体的局部指针以使稍后的访问更加容易
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
endpoint = &iface_desc->endpoint[i].desc;
然后,有了一个端点,而还没有发现批量IN类型的端点时,查看该端点的方向是否为IN。这可以通过检查位掩码 USB_DIR_IN 是否包含在bEndpointAddress 端点变量中来确定。如果是的话,测定该端点类型是否批量,这首先通过USB_ENDPOINT_XFERTYPE_MASK 位掩码来取bmAttributes变量的值,然后检查它是否和USB_ENDPOINT_XFER_BULK 的值匹配来完成。
if ( !dev->bulk_in_endpointAddr &&
((endpoint->bEndpointAddress & USB_ENDPOINT_DIR_MASK) = = USB_DIR_IN) &&
((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK) = = USB_ENDPOINT_XFER_BULK)) {
如果这些都通过了,驱动程序就知道它已经发现了正确的端点类型,可以把该端点相关的信息保存到一个局部结构体中,就是前面的usb_skel ,以便稍后使用它和端点进行通信:
/* we found a bulk in endpoint */
buffer_size = le16_to_cpu(endpoint->wMaxPacketSize);
dev->bulk_in_size = buffer_size;
dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;
dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);
if (!dev->bulk_in_buffer) {
err("Could not allocate bulk_in_buffer");
goto error;
}
b -- 把已经初始化数据结构的指针保存到接口设备中
接下来的工作是向系统注册一些以后会用的的信息。首先来说明一下usb_set_intfdata(),他向内核注册一个data,这个data的结构可以是任意的,这段程序向内核注册了一个usb_skel结构,就是刚刚看到的被初始化的那个,这个data可以在以后用usb_get_intfdata来得到
usb_set_intfdata(interface, dev);
c -- 注册USB设备
如果USB驱动程序没有和处理设备与用户交互(例如输入、tty、视频等)的另一种类型的子系统相关联,驱动程序可以使用USB主设备号,以便在用户空间使用传统的字符驱动程序接口。如果要这样做,USB驱动程序必须在探测函数中调用 usb_resgister_dev 函数来把设备注册到USB核心。只要该函数被调用,就要确保设备和驱动陈旭都处于可以处理用户访问设备的要求的恰当状态
retval = usb_register_dev(interface, &skel_class);
skel_class结构。这个结构又是什么?就来看看这到底是个什么东西:
drivers/usb/usb-skeleton.c
/*
* usb class driver info in order to get a minor number from the usb core,
* and to have the device registered with the driver core
*/
static struct usb_class_driver skel_class = {
.name = "skel%d",
.fops = &skel_fops,
.minor_base = USB_SKEL_MINOR_BASE,
};
它其实是一个系统定义的结构,里面包含了一名字、一个文件操作结构体还有一个次设备号的基准值。事实上它才是定义真正完成对设备IO操作的函数。所以它的核心内容应该是skel_fops。
因为usb设备可以有多个interface,每个interface所定义的IO操作可能不一样,所以向系统注册的usb_class_driver要求注册到某一个interface,而不是device,因此,usb_register_dev的第一个参数才是interface,而第二个参数就是某一个usb_class_driver。
通常情况下,linux系统用主设备号来识别某类设备的驱动程序,用次设备号管理识别具体的设备,驱动程序可以依照次设备号来区分不同的设备,所以,这里的次设备好其实是用来管理不同的interface的,但由于这个范例只有一个interface,在代码上无法求证这个猜想。
drivers/usb/usb-skeleton.c
static const struct file_operations skel_fops = {
.owner = THIS_MODULE,
.read = skel_read,
.write = skel_write,
.open = skel_open,
.release = skel_release,
.flush = skel_flush,
.llseek = noop_llseek,
};
3.2、断开函数
当设备被拔出集线器时,usb子系统会自动地调用disconnect,他做的事情不多,最重要的是注销class_driver(交还次设备号)和interface的data:
dev = usb_get_intfdata(interface);
usb_set_intfdata(interface, NULL);
/* give back our minor */
usb_deregister_dev(interface, &skel_class);
四、USB请求块
USB 设备驱动代码通过urb和所有的 USB 设备通讯。urb用 struct urb 结构描述(include/linux/usb.h )。
struct urb {
...
};
urb 以一种异步的方式同一个特定USB设备的特定端点发送或接受数据。一个 USB 设备驱动可根据驱动的需要,分配多个 urb 给一个端点或重用单个 urb 给多个不同的端点。设备中的每个端点都处理一个 urb 队列, 所以多个 urb 可在队列清空之前被发送到相同的端点。
一个 urb 的典型生命循环如下:
(1)被创建;
(2)被分配给一个特定 USB 设备的特定端点;
(3)被提交给 USB 核心;
(4)被 USB 核心提交给特定设备的特定 USB 主机控制器驱动;
(5)被 USB 主机控制器驱动处理, 并传送到设备;
(6)以上操作完成后,USB主机控制器驱动通知 USB 设备驱动。
urb 也可被提交它的驱动在任何时间取消;如果设备被移除,urb 可以被USB核心取消。urb 被动态创建并包含一个内部引用计数,使它们可以在最后一个用户释放它们时被自动释放。
struct urb {
/* private: usb core and host controller only fields in the urb */
struct kref kref; /* reference count of the URB */
int unlinked; /* unlink error code */
void *hcpriv; /* private data for host controller */
atomic_t use_count; /* concurrent submissions counter */
atomic_t reject; /* submissions will fail */
/* public: documented fields in the urb that can be used by drivers */
struct list_head urb_list; /* list head for use by the urb's
* current owner */
struct list_head anchor_list; /* the URB may be anchored */
struct usb_anchor *anchor;
struct usb_device *dev; /* (in) pointer to associated device */
struct usb_host_endpoint *ep; /* (internal) pointer to endpoint */
unsigned int pipe; /* (in) pipe information */
unsigned int stream_id; /* (in) stream ID */
int status; /* (return) non-ISO status */
unsigned int transfer_flags; /* (in) URB_SHORT_NOT_OK | ...*/
void *transfer_buffer; /* (in) associated data buffer */
dma_addr_t transfer_dma; /* (in) dma addr for transfer_buffer */
struct scatterlist *sg; /* (in) scatter gather buffer list */
int num_mapped_sgs; /* (internal) mapped sg entries */
int num_sgs; /* (in) number of entries in the sg list */
u32 transfer_buffer_length; /* (in) data buffer length */
u32 actual_length; /* (return) actual transfer length */
unsigned char *setup_packet; /* (in) setup packet (control only) */
dma_addr_t setup_dma; /* (in) dma addr for setup_packet */
int start_frame; /* (modify) start frame (ISO) */
int number_of_packets; /* (in) number of ISO packets */
int interval; /* (modify) transfer interval
* (INT/ISO) */
int error_count; /* (return) number of ISO errors */
void *context; /* (in) context for completion */
usb_complete_t complete; /* (in) completion routine */
struct usb_iso_packet_descriptor iso_frame_desc[];
/* (in) ISO ONLY */
};
4.1、创建和注销 urb
struct urb 结构不能静态创建,必须使用 usb_alloc_urb 函数创建. 函数原型:
extern struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags);
如果驱动已经对 urb 使用完毕, 必须调用 usb_free_urb 函数,释放urb。函数原型
extern void usb_free_urb(struct urb *urb);
4.2、初始化 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);
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);
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);
//struct urb *urb :指向要被初始化的 urb 的指针
//struct usb_device *dev :指向 urb 要发送到的 USB 设备.
//unsigned int pipe : urb 要被发送到的 USB 设备的特定端点. 必须使用前面提过的 usb_******pipe 函数创建
//void *transfer_buffer :指向外发数据或接收数据的缓冲区的指针.注意:不能是静态缓冲,必须使用 kmalloc 来创建.
//int buffer_length :transfer_buffer 指针指向的缓冲区的大小
//usb_complete_t complete :指向 urb 结束处理例程函数指针
//void *context :指向一个小数据块的指针, 被添加到 urb 结构中,以便被结束处理例程函数获取使用.
//int interval :中断 urb 被调度的间隔.
//函数不设置 urb 中的 transfer_flags 变量, 因此对这个成员的修改必须由驱动手动完成
/*等时 urb 没有初始化函数,必须手动初始化,以下为一个例子*/
urb->dev = dev;
urb->context = uvd;
urb->pipe = usb_rcvisocpipe(dev, uvd->video_endp-1);
urb->interval = 1;
urb->transfer_flags = URB_ISO_ASAP;
urb->transfer_buffer = cam->sts_buf[i];
urb->complete = konicawc_isoc_irq;
urb->number_of_packets = FRAMES_PER_DESC;
urb->transfer_buffer_length = FRAMES_PER_DESC;
for (j=0; j < FRAMES_PER_DESC; j++) {
urb->iso_frame_desc[j].offset = j;
urb->iso_frame_desc[j].length = 1;
}
4.3、提交 urb
一旦 urb 被正确地创建并初始化, 它就可以提交给 USB 核心以发送出到 USB 设备. 这通过调用函数 usb_submit_urb 实现:
extern int usb_submit_urb(struct urb *urb, gfp_t mem_flags);
//struct urb *urb :指向被提交的 urb 的指针
//gfp_t mem_flags :使用传递给 kmalloc 调用同样的参数, 用来告诉 USB 核心如何及时分配内存缓冲
/*因为函数 usb_submit_urb 可被在任何时候被调用(包括从一个中断上下文), mem_flags 变量必须正确设置. 根据 usb_submit_urb 被调用的时间,只有 3 个有效值可用:
GFP_ATOMIC
只要满足以下条件,就应当使用此值:
1.调用者处于一个 urb 结束处理例程,中断处理例程,底半部,tasklet或者一个定时器回调函数.
2.调用者持有自旋锁或者读写锁. 注意如果正持有一个信号量, 这个值不必要.
3.current->state 不是 TASK_RUNNING. 除非驱动已自己改变 current 状态,否则状态应该一直是 TASK_RUNNING .
GFP_NOIO
驱动处于块 I/O 处理过程中. 它还应当用在所有的存储类型的错误处理过程中.
GFP_KERNEL
所有不属于之前提到的其他情况
*/
在 urb 被成功提交给 USB 核心之后, 直到结束处理例程函数被调用前,都不能访问 urb 结构的任何成员.
4.4、urb结束处理例程
如果 usb_submit_urb 被成功调用, 并把对 urb 的控制权传递给 USB 核心, 函数返回 0; 否则返回一个负的错误代码. 如果函数调用成功, 当 urb 被结束的时候结束处理例程会被调用一次.当这个函数被调用时, USB 核心就完成了这个urb, 并将它的控制权返回给设备驱动。
只有 3 种结束urb并调用结束处理例程的情况:
(1)urb 被成功发送给设备, 且设备返回正确的确认.如果这样, urb 中的status变量被设置为 0。
(2)发生错误, 错误值记录在 urb 结构中的 status 变量。
(3)urb 从 USB 核心unlink. 这发生在要么当驱动通过调用 usb_unlink_urb 或者 usb_kill_urb告知 USB 核心取消一个已提交的 urb,或者在一个 urb 已经被提交给它时设备从系统中去除。
1.流程图描述:
+-------------------------------------------+
| urb 结束处理 |
+-------------------------------------------+
|
v
+---------------------+
| urb 是否成功发送? |
+---------------------+
| Yes
v
+-----------------------------+
| 设备返回正确的确认吗? |
+-----------------------------+
| Yes
v
+---------------------------+
| 设置 urb->status = 0 |
+---------------------------+
|
v
+---------------------------+
| 调用结束处理例程 |
+---------------------------+
| No
v
+---------------------------+
| 设置 urb->status 错误值 |
+---------------------------+
|
v
+---------------------------+
| 调用结束处理例程 |
+---------------------------+
| No
v
+---------------------------+
| urb 是否被取消或设备移除? |
+---------------------------+
| Yes
v
+---------------------------+
| 设置 urb->status = -ECONNRESET |
+---------------------------+
|
v
+---------------------------+
| 调用结束处理例程 |
+---------------------------+
2.流程图步骤解析:
-
urb 是否成功发送?
- 判断
urb
是否成功发送给设备并等待确认。
- 判断
-
设备返回确认?
- 如果设备返回了正确的确认,设置
urb->status = 0
,表示成功。 - 否则,设置
urb->status
为发生的错误代码,并调用结束处理例程。
- 如果设备返回了正确的确认,设置
-
urb 是否被取消或设备移除?
- 如果
urb
被取消或设备从系统中移除(例如通过调用usb_unlink_urb
或usb_kill_urb
,或者设备被移除),则设置urb->status = -ECONNRESET
,并调用结束处理例程。
- 如果
4.5、取消 urb
使用以下函数停止一个已经提交给 USB 核心的 urb:
extern int usb_unlink_urb(struct urb *urb);
extern void usb_kill_urb(struct urb *urb);
如果调用usb_kill_urb函数,则 urb 的生命周期将被终止. 这通常在设备从系统移除时,在断开回调函数(disconnect callback)中调用.
对一些驱动, 应当调用 usb_unlink_urb 函数来使 USB 核心停止 urb. 这个函数不会等待 urb 完全停止才返回. 这对于在中断处理例程中或者持有一个自旋锁时去停止 urb 是很有用的, 因为等待一个 urb 完全停止需要 USB 核心有使调用进程休眠的能力(wait_event()函数).
五、举例应用
1. 简单 USB 驱动程序示例:USB 设备的插拔管理
这个例子展示了一个简单的 USB 驱动程序,它会在设备插入和拔出时输出日志信息。该程序不会执行复杂的数据传输,而是集中在设备的插入、拔出和管理。
步骤 1:创建一个简单的模块
首先,我们创建一个基础的 USB 驱动模块,负责检测 USB 设备的插拔事件。
代码结构:
- usb_device_driver.c:这是 USB 驱动的主代码文件,负责处理设备插拔事件。
- Makefile:构建驱动的 Makefile。
2. USB 驱动代码:usb_device_driver.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/usb.h>
#include <linux/init.h>
#include <linux/errno.h>
// 设备供应商和产品ID,用于识别 USB 设备
#define MY_USB_VENDOR_ID 0x1234
#define MY_USB_PRODUCT_ID 0x5678
// 定义驱动的名称
#define DRIVER_NAME "MyUSBDriver"
// USB 设备的识别 ID 表
static struct usb_device_id usb_ids[] = {
{ USB_DEVICE(MY_USB_VENDOR_ID, MY_USB_PRODUCT_ID) },
{} // 以空项结束
};
// 设备连接处理函数
static int my_usb_probe(struct usb_interface *interface, const struct usb_device_id *id)
{
printk(KERN_INFO DRIVER_NAME ": USB device connected (Vendor ID: 0x%04x, Product ID: 0x%04x)\n",
id->idVendor, id->idProduct);
return 0;
}
// 设备断开连接处理函数
static void my_usb_disconnect(struct usb_interface *interface)
{
printk(KERN_INFO DRIVER_NAME ": USB device disconnected\n");
}
// 定义 USB 驱动的结构
static struct usb_driver my_usb_driver = {
.name = DRIVER_NAME,
.id_table = usb_ids,
.probe = my_usb_probe,
.disconnect = my_usb_disconnect,
};
// 模块初始化函数
static int __init my_usb_init(void)
{
int result;
result = usb_register(&my_usb_driver);
if (result < 0) {
printk(KERN_ERR DRIVER_NAME ": USB driver registration failed\n");
return result;
}
printk(KERN_INFO DRIVER_NAME ": USB driver registered successfully\n");
return 0;
}
// 模块退出函数
static void __exit my_usb_exit(void)
{
usb_deregister(&my_usb_driver);
printk(KERN_INFO DRIVER_NAME ": USB driver unregistered\n");
}
// 注册模块的初始化和退出函数
module_init(my_usb_init);
module_exit(my_usb_exit);
// 模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple USB driver example");
MODULE_DEVICE_TABLE(usb, usb_ids);
3. 代码说明
-
USB 设备的识别 ID:
usb_device_id
结构体用来定义设备的 Vendor ID 和 Product ID。只有匹配这些 ID 的 USB 设备会触发驱动的加载。- 这个示例中的 ID 是假设的
0x1234
和0x5678
,您可以根据实际情况更改为您的设备的 ID。
-
设备连接 (
probe
函数):my_usb_probe
是设备被插入时调用的函数,它会输出设备的 Vendor ID 和 Product ID。
-
设备断开连接 (
disconnect
函数):my_usb_disconnect
在设备拔出时调用,它会输出断开连接的信息。
-
驱动注册:
- 使用
usb_register
注册驱动,并在模块退出时使用usb_deregister
注销驱动。
- 使用
-
模块初始化与退出:
my_usb_init
用于加载模块时初始化驱动。my_usb_exit
用于卸载模块时清理资源。
4. Makefile:Makefile
makefile
obj-m += usb_device_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
5. 编译与加载模块
-
编译模块: 在终端中,运行以下命令来编译模块:
-
make
-
加载模块: 使用
insmod
加载模块: -
sudo insmod usb_device_driver.ko
-
查看日志: 查看内核日志,确认设备插入或拔出的信息:
dmesg | tail -n 20
您将看到类似下面的输出:
MyUSBDriver: USB device connected (Vendor ID: 0x1234, Product ID: 0x5678)
-
卸载模块: 使用
rmmod
卸载模块:
sudo rmmod usb_device_driver
卸载后,您将看到类似以下的输出:
MyUSBDriver: USB device disconnected