Linux驱动框架——USB驱动简单分析

Linux驱动框架——USB驱动

​ 通用串行总线USB(Universal serial bus)通常用于外围设备与主机之间的连接,USB使用树型结构,主机作为根,集线器作为节点,外围设备作为树叶。

​ Linux当前支持几乎所有USB类设备(标准类型的设备,如键盘,鼠标,调制解调器,打印机和扬声器),以及越来越多的特定于供应商的设备(如USB到串行转换器,数码相机,以太网设备和MP3播放器等)。

​ 也有一部分USB设备是Linux不支持的,这些设备一般都是由供应商单独开发协议,大部分会开放协议给驱动人员开发使用。

​ 由于不同的通信协议都需要自定义创建新的驱动程序,因此Linux开发者开发了一套USB驱动通用框架,它是源码中的drivers/usb/usb-skeleton.c。

​ USB设备很复杂(http://www.usb.org)。幸运的是Linux内核提供了一个叫做USB core的驱动子系统处理大多数的复杂情况。如下图,USB Core向上提供给驱动接口,让驱动可以简单的对USB主机控制器进行控制操作,从而对USB硬件设备进行控制操作。

Linux驱动框架——USB驱动简单分析_第1张图片

USB设备基础知识

下面介绍Linux中USB设备的组织结构。

Linux驱动框架——USB驱动简单分析_第2张图片

端点Endpoint

USB的通过端点来进行通信,USB的端点类似于单向管道,它的通信是单工的。USB的端点一共有以下四种类型:

控制传输

控制传输主要用于向设备发送配置信息、获取设备信息、发送命令道设备,或者获取设备的状态报告。控制传输一般发送的数据量较小,当USB设备插入时,USB核心使用端点0对设备进行配置,另外,端口0与其他端点不一样,端点0可以双向传输。

中断传输

中断传输就是中断端点以一个固定的速度来传输较少的数据,USB键盘和鼠标就是使用这个传输方式。这里说的中断和硬件上下文中的中断不一样,它不是设备主动发送一个中断请求,而是主机控制器在保证不大于某个时间间隔内安排一次传输。中断传输对时间要求比较严格,所以可以用中断传输来不断地检测某个设备,当条件满足后再使用批量传输传输大量的数据。

批量传输

批量传输通常用在数据量大、对数据实时性要求不高的场合,例如USB打印机、扫描仪、大容量存储设备、U盘等。

等时传输

等时传输同样可以传输大批量数据,但是对数据是否到达没有保证,它对实时性的要求很高,例如音频、视频等设备。


端点的数据结构

内核中,使用usb_host_endpoint来描述USB端点,这个结构体中包含了一个保存端点信息的结构体usb_endpoint_descriptor,驱动一般会关心其中保存的端点地址bEndpointAddress、端点类型bmAttributes、端点处理的最大字节数wMaxPacketSize、以及如果是中断类型端点的中断时间bInterval。

Linux驱动框架——USB驱动简单分析_第3张图片

接口Interface

由上面的USB设备结构图我们可以看到,USB设备的端点包含在接口Interface中。USB接口用于每个USB接口仅控制着一种类型的USB连接,例如鼠标、键盘或者音频流。大多数的USB设备存在多个接口,因为每一个接口代表一个基础功能,因此每一个接口都需要一个驱动程序驱动。

USB接口在内核中使用usb_interface结构体描述,这个结构体一般由USB core传递给USB驱动,由USB驱动进行控制。比较重要的成员变量有:struct usb_host_interface *altsetting控制字段、unsigned num_altsetting控制数量、struct usb_host_interface *cur_altsetting指向控制选项的指针、int minor次设备号。

配置Configurations

由上面的USB设备结构图我们可以看到,USB设备的接口包含在配置Configurations中。 一个USB设备可以有多种配置,但是每一个时间点只能启用生效一个配置。

Linux内核中使用usb_host_config结构体描述USB设备的配置。一般来说驱动无需对配置进行读写操作。

USB驱动框架源码分析

​ 其实稍微阅读一下源码就会发现,USB驱动框架和之前研讨过的帧缓冲驱动框架非常相似。都是一个文件用来组装加载驱动框架的内核模块usb-skeleton.c(帧缓冲是fbmem.c),一个文件用来描述驱动框架相关的数据结构以及驱动注册方法等usb.h(帧缓冲是fb.h)。

​ 因此,先初步断定,Linux中的驱动框架都是一个套路,所以按照以往的经验,先从usb-skeleton.c这个驱动框架的模块入手分析。既然是Linux模块,当然要从入口处先分析了!下面审计模块初始函数usb_skel_init。

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;
}

和猜想没错, 驱动模块加载必然是先注册驱动结构体,这个注册的驱动结构体usb_driver在模块中被初始化写好了name、probe探测函数、disconnect断开函数、id_table表(用来告诉内核该驱动支持的设备)。

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,
};

但该usb_driver结构体所赋值的一些函数方法其实并不是最核心的device_driver方法,这只是在device_driver的基础上高级封装的新结构体,usb_driver中的drvwrap变量内包含了Linux内核的基础驱动对象device_driver,在调用USB驱动注册函数的过程中,该对象同样被初始化了一系列的函数、驱动总线、名字等,并调用了核心的driver_register真正的在内核中创建了驱动对象。

Linux驱动框架——USB驱动简单分析_第4张图片

驱动的总线默认设置如下


struct bus_type usb_bus_type = {
	.name =		"usb",
	.match =	usb_device_match,
	.uevent =	usb_uevent,
	.suspend =	usb_suspend,
	.resume =	usb_resume,
};

追踪其match方法,可以看到,实际上是通过使用两个函数来判断设备和驱动是否是USB类型的,如果是则匹配成功。

Linux驱动框架——USB驱动简单分析_第5张图片

追踪其最底层的probe方法,可以看出它最主要工作是通过传入的dev对象,首先使用to_usb_driver拿到包括该device_driver对象的usb_driver结构体对象,这里用的是前面经常见到的经典宏:container_of;

#define	to_usb_driver(d) container_of(d, struct usb_driver, drvwrap.driver)

其次使用to_usb_interface的函数转换成usb_interface接口类型,然后对usb设备的接口进行一系列的配置。

前面说过,usb_driver高级封装了probe等函数,因此,拿到该usb_driver后,会调用这些高级封装的probe,调用这些参数时会使用配置的usb_interface进行传参。

/* called from driver core with dev locked */
static int usb_probe_interface(struct device *dev)
{
	struct usb_driver *driver = to_usb_driver(dev->driver);
	struct usb_interface *intf;
	...
	intf = to_usb_interface(dev);
	...
		mark_active(intf);
		intf->condition = USB_INTERFACE_BINDING;
		intf->pm_usage_cnt = !(driver->supports_autosuspend);
		error = driver->probe(intf, id);//调用高级封装的probe函数
		if (error) {
			mark_quiesced(intf);
			intf->needs_remote_wakeup = 0;
			intf->condition = USB_INTERFACE_UNBOUND;
		} else
			intf->condition = USB_INTERFACE_BOUND;
		usb_autosuspend_device(udev);
	}

而调用了usb_driver的probe方法,我们就应该拉回视角到usb-skeleton.c中看看高级封装的skel_probe函数默认做了什么事情。

static int skel_probe(struct usb_interface *interface,
const struct usb_device_id *id)
{
struct usb_skel *dev; //特定设备结构体
struct usb_host_interface *iface_desc; //设置结构体
struct usb_endpoint_descriptor *endpoint; //端点描述符
size_t buffer_size;
int i;
int retval = -ENOMEM;
dev = kzalloc(sizeof(*dev), GFP_KERNEL); //分配内存
kref_init(&dev->kref);
sema_init(&dev->limit_sem, WRITES_IN_FLIGHT); //初始化信号量
mutex_init(&dev->io_mutex); //初始化互斥锁
spin_lock_init(&dev->err_lock); //初始化信号量
init_usb_anchor(&dev->submitted);
init_completion(&dev->bulk_in_completion); //初始化完成量
dev->udev = usb_get_dev(interface_to_usbdev(interface)); //获取usb_device结构体
dev->interface = interface; //获取usb_interface结构体
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 &&
usb_endpoint_is_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); //缓冲区
...
dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL); //分配urb空间
...
}
if (!dev->bulk_out_endpointAddr &&
usb_endpoint_is_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;
}
usb_set_intfdata(interface, dev); //将特定设备结构体设置为接口的私有数据
retval = usb_register_dev(interface, &skel_class); //注册USB设备
....
}

通过上面分析,我们知道,usb_driver的probe函数中根据usb_interface的成员寻找第一个批量输入和输出的端点,将端点地址、缓冲区等信息存入USB骨架程序定义的usb_skel结构体中,并将usb_skel通过usb_set_intfdata传为USB接口的私有数据,最后注册USB设备,最后注册USB设备时,我们可以看到传入了skel_class变量。这个变量中指定了文件操作集,这些默认的操作集函数同样写在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,
};
static struct usb_class_driver skel_class = {
	.name =		"skel%d",
	.fops =		&skel_fops,
	.minor_base =	USB_SKEL_MINOR_BASE,
};

同理,usb_driver->usbdrv_wrap>devicer_driver中的其它操作函数,例如remove,默认remove的实现中必然也和上述逻辑相同,最后会执行usb_driver中高级封装的一些方法。

因此,简单来看usb-skeleton.c这一驱动框架的组成和简单特性我们已经分析完毕了,这个框架的套路和帧缓冲设备的驱动框架类似,这个模块所做的工作就是加载驱动、匹配总线后初始化设备资源,并且默认了一些对设备的操作方法等。

参考资料

《LINUX DEVICE DRIVERS edition 3》 Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman

内核文档:https://www.kernel.org/doc/html/latest/driver-api/usb/writing_usb_driver.html

Linux下的USB总线驱动(一):https://blog.csdn.net/weiqing1981127/article/details/8215708#commentBox

Linux下USB驱动框架分析:https://www.cnblogs.com/general001/articles/2319552.html

你可能感兴趣的:(课程笔记——操作系统定制技术)