Linux内核usb子系统
了解USB子系统框架是编写和调试usb设备驱动的必要程序,了解USB子系统框架的前提基础知识是linux内核设备驱动模型。
USB设备结构
在usb规范中,一个usb设备通过一些层级结构的描述符来描述。在linux内核中,通过usb_device结构来表示一个usb设备,通常一个usb设备有一个设备描述符,一个或多个配置描述符,以及一个或多个接口描述符。设备描述符用来描述设备的信息,包含厂家ID,产品ID等:
1 | /* USB_DT_DEVICE: Device descriptor */ |
在设备描述符中同时通过bNumConfigurations成员来描述该设备有多少个配置描述符。
在linux内核中,配置描述符和接口描述符用usb_host_config结构来表示,在设备描述符中bNumConfigurations的值为多少就有多少个usb_host_config,usb_host_config中包含了usb设备的配置描述符,以及接口描述符。
usb设备的配置描述符用usb_config_descriptor结构来表示:
1 | struct usb_config_descriptor { |
在配置描述符中通过bNumInterfaces成员来描述该配置描述符包含几个接口描述符。在Linux内核中用usb_interface 结构来描述一个接口设备,配置描述符中的bNumInterfaces的值为多少就有多少个usb_interface。linux内核中用usb_host_interface结构表示接口描述,在该结构中包含了该接口的接口描述符,usb_host_interface结构位于usb_interface结构中。接口描述符定义如下:
1 | /* USB_DT_INTERFACE: Interface descriptor */ |
USB设备类型
usb设备
usb设备用usb_device结构来表示,它表示一个物理的usb设备。
通过下面的接口判断是否是usb设备:
1 | static inline int is_usb_device(const struct device *dev) |
usb接口
usb接口通过usb_interface结构来表示,表示一个usb设备的某个接口。
通过下面的接口判断设备是否为usb接口设备:
1 | static inline int is_usb_interface(const struct device *dev) |
USB驱动类型
驱动结构
和usb设备对应,在linux内核usb设备驱动模型中,usb驱动分为设备驱动和接口驱动,usb设备驱动用来解析和配置usb接口设备,以及注册usb的接口设备。
usb的设备驱动在内核中用usb_device_driver结构表示:
1 | struct usb_device_driver { |
usb的接口驱动在内核中用usb_driver结构表示:
1 | struct usb_driver { |
驱动类型的区分
在usb_device_driver和usb_driver结构中都包含一个usbdrv_wrap类型的drvwrap成员,该成员结构定义如下:
1 | struct usbdrv_wrap { |
其中driver成员是usb驱动结构中内嵌的device_driver对象,是设备驱动模型的driver部件。for_device成员用来表示该驱动是usb设备驱动还是usb接口驱动。for_device的值为1表示usb设备驱动,为0表示usb接口驱动。
内核中通过下面的接口判断usb驱动是设备驱动还是接口驱动:
1 | static inline int is_usb_device_driver(struct device_driver *drv) |
驱动注册
usb设备驱动的注册和注销接口如下:
1 | int usb_register_device_driver(struct usb_device_driver *new_udriver, |
设备驱动注册函数中对drvwrap.for_devices成员赋值为1,表示注册的usb驱动是usb设备驱动,并且调用driver_register接口将drvwrap.driver注册到内核设备驱动模型中。代码如下:
1 | new_udriver->drvwrap.for_devices = 1; |
usb接口驱动的注册和注销接口如下:
1 | /* use a define to avoid include chaining to get THIS_MODULE & friends */ |
接口驱动注册函数中对drvwrap.for_devices成员赋值为0,表示注册的是一个usb接口驱动,并且调用driver_register接口将drvwrap.driver注册到内核设备驱动模型中。代码如下:
1 | new_driver->drvwrap.for_devices = 0; |
USB设备驱动模型
概述
usb设备驱动模型是内核设备驱动模型的应用,它包含usb总线,usb设备和usb驱动,以及用户空间视图的sysfs等。
usb总线的作用是维护所有的usb设备和usb驱动,以及为usb设备和usb驱动创建关联,以达到用户通过usb驱动操作关联的usb设备的目的。
USB总线
usb总线在内核中的定义如下:
1 | struct bus_type usb_bus_type = { |
总线名称为usb,即在/sys/bus/目录下存在usb目录,表示usb总线。
match函数指针用来进行设备和驱动的匹配,当向总线上添加一个usb设备(或usb驱动)时,linux内核设备驱动模型核心就会遍历usb总线上的驱动(或设备),并为其调用usb总线的match方法,以判断新插入的设备能否被某个驱动处理。match方法成功返回1,否则返回0。当内核设备驱动核心得到匹配成功的结果后就会为usb设备和usb驱动创建关联,并调用驱动的probe函数。
在usb总线的match方法中需要判断驱动和设备的类型,设备和驱动都分为usb设备和接口两个类型。对于设备类型和设备驱动类型的匹配,直接返回成功,而对于接口设备和接口驱动的匹配,则需要通过接口驱动的id_table成员中的信息和接口设备的信息进行匹配。id_table由usb接口驱动设置,用来描述该驱动能处理那些设备。
usb设备
usb设备分为usb设备和usb接口设备两种,通常一个usb物理设备关联一个usb设备对象,usb设备对象关联一个或多个usb接口对象。
当一个新的usb设备对象或者usb设备接口对象被注册到usb总线上的时候,内核设备驱动核心为其在usb总线上查找能处理其的驱动,并执行驱动的probe函数,以初始化设备和提供相关操作函数。
usb驱动
usb驱动也分为usb设备驱动和usb接口设备,usb设备驱动用来处理usb设备,usb接口驱动用来处理usb接口。通常大部分usb驱动都是usb接口驱动,比如usb存储,usb串口等。
在usb子系统初始化的时候会注册一个与设备相关的usb通用设备驱动,由usb总线match函数的代码可知该驱动能匹配所有usb设备。
当usb hub的事件线程监测到设备接入后,会向usb总线上注册新接入的设备,注册的设备首先将和usb通用驱动进行绑定,在usb通用驱动的probe函数中再申请注册usb接口设备。当usb接口设备注册到usb总线上时,再进行接口设备和接口驱动的绑定。
sysfs
sysfs是内核设备驱动模型的用户空间视图,usb子系统在sysfs中的路径是/sys/bus/usb目录。该目录下存在devices和drivers目录,用来呈现usb总线上的设备和驱动。
generic驱动
generic驱动在usb子系统初始化的时候由usb核心注册。该驱动负责处理所有的usb物理设备,当usb hub驱动监测到设备接入后,会向usb总线上注册新接入的设备,注册的设备首先将会和usb通用设备驱动关联,并调用usb_generic_driver的probe函数。
在usb_generic_driver的probe函数中,将对设备进行配置和解析,并向usb总线注册该usb设备的设备接口设备。
当接口设备注册到usb总线上时,设备驱动核心将会为其在usb总线上查找和关联驱动,并调用关联驱动的probe函数。
比如插入一个4G模块,将由usb_generic_driver配置4G模块的所有接口,并将接口注册到usb总线上,然后由设备驱动模型核心再为4G模块的所有接口在usb总线上查找和关联4G相关的驱动。
hub驱动
usb的hub驱动也是一个usb总线上的接口驱动,root hub设备由usb的host控制器驱动来创建和注册,当host驱动向usb总线注册了root hub设备的时候就会匹配到usb的hub驱动。
hub驱动会不停的监测新设备的接入。当hub驱动监测到新usb设备接入时会向内核设备驱动模型核心申请和注册usb设备,新设备所属总线为usb总线,所以当设备注册到usb总线上时,内核设备驱动模型核心会在usb总线上为其查找和绑定合适的usb驱动。
USB控制器驱动
usb的主控制器驱动结构为hc_driver,对应的usb主控制器设备的结构为usb_hcd,hc_driver用来处理相应的usb_hcd。usb的控制器属于上一级的总线设备驱动模型的子节点,比如硬件上挂在pci总线上的usb控制器本身由pci核心来枚举,pci总线上的所有设备抽象为pci_dev结构,驱动抽象为pci_driver结构。当pci总线上枚举到pci_dev设备并注册设备后,将会调用pci_driver的probe函数并绑定pci_driver,在pci_driver的probe函数中,将创建和添加usb_hcd,同时从pci的pci_device_id中获取到hc_driver,并将hc_driver和usb_hcd绑定。
usb主控制器(hcd)
linux内核中用usb_hcd类型来描述一个usb主控制器,一个usb主控制器通常对应一个usb_bus类型的总线(此总线非设备驱动模型之总线)来维护该控制器上的设备树。
一个usb主控制器通常对应一个root hub设备,root hub设备是usb_device结构类型的。root hub作为usb总线(usb_bus)设备树上的其它usb设备的父设备,它被关联在usb_bus结构中。
root hub作为usb_device类型的对象,其关联的设备驱动模型的总线是usb总线,关联的设备驱动是usb hub驱动。在添加usb_hcd的时候,将会注册usb_bus以及root hub。root hub被初始化和注册后和usb hub驱动创建关联,之后的事就交给hub驱动和hub线程了。
控制器驱动(hc)
控制器驱动主要提供控制器的硬件操作。比如中断设置,提供控制器的初始化,开始停止,urb入队出队,usb端点屏蔽使能,hub状态获取,数据通信控制等功能。
源代码分析
USB子系统初始化
1.初始化debugfs,注册usb acpi总线。
1 | usb_init_pool_max(); |
2.注册usb总线。
1 | retval = bus_register(&usb_bus_type); |
usb_bus_type定义和初始化代码如下:
1 | struct bus_type usb_bus_type = { |
由usb总线的定义可知,总线名称为“usb”,匹配方法为usb_device_match函数,uevent方法为usb_uevent。
3.注册usb总线通知链。
1 | retval = bus_register_notifier(&usb_bus_type, &usb_bus_nb); |
通知链是linux内核用来实现异步通知的一种机制,通知链由一个通知链头结构和一些通知链上的block组成。usb核心会把一些事件发送到通知链,通知链核心将遍历通知链上的所有block,并调用block的回调函数以实现异步的效果。
总线上的通知链头结构包含在总线结构的p成员结构中。
usb总线通知链初始化的时候注册了一个通知块usb_bus_nb,该通知块定义如下:
1 | static struct notifier_block usb_bus_nb = { |
通知块回调函数为usb_bus_notify,该函数接收处理总线上添加或删除设备/接口设备的事件,当usb核心向内核设备驱动模型中添加或删除usb设备或接口设备后,向usb总线上发送通知,在usb_bus_notify通知处理函数中,向sysfs添加或删除设备或接口设备的属性文件。
4.注册usb主设备号
1 | retval = usb_major_init(); |
在usb_major_init函数中调用register_chrdev接口向内核注册了usb主设备号,usb的主设备号用USB_MAJOR宏定义,该宏定义的值为180。
5.注册usbfs接口驱动
1 | retval = usb_register(&usbfs_driver); |
6.初始化usb字符设备io空间
1 | retval = usb_devio_init(); |
6.1在usb_devio_init函数中,首先按最大值为usb设备申请字符设备:
1 | retval = register_chrdev_region(USB_DEVICE_DEV, USB_DEVICE_MAX, |
6.2接着向usb内部通知链usb_notifier_list上注册通知块usbdev_nb:
1 | usb_register_notify(&usbdev_nb); |
usbdev_nb定义如下:
1 | static struct notifier_block usbdev_nb = { |
在usbdev_notify回调函数中判读如果有usb设备移除了,就会做一些相应的文件io回收释放等操作。
7.注册usb hub驱动,申请hub工作队列。
1 | retval = usb_hub_init(); |
7.1在usb_hub_init函数中,首先注册了hub_driver。
1 | if (usb_register(&hub_driver) < 0) { |
hub_driver定义如下:
1 | static struct usb_driver hub_driver = { |
由该定义可知,usb hub驱动是一个接口设备驱动,驱动名称为“hub”,id_table成员为hub_id_table,probe函数为hub_probe,当一个hub设备添加到内核设备驱动模型中的时候,如果hub设备测参数和hub_id_table中的参数匹配,就会调用到hub_probe函数。
7.2注册了hub_driver之后,申请hub工作队列。
1 | hub_wq = alloc_workqueue("usb_hub_wq", WQ_FREEZABLE, 0); |
在hub的中断轮询的时候,在回调函数hub_irq中就会向hub_wq中添加任务。在该任务中再去做hub端口检查,新设备枚举等工作。
8.注册usb通用驱动。
1 | retval = usb_register_device_driver(&usb_generic_driver, THIS_MODULE); |
至此,完成usb子系统的初始化。
USB总线匹配方法
usb子系统初始化的时候向内核设备驱动模型注册了usb_bus_type总线类型,该总线初始化match方法为usb_device_match函数。usb的匹配条件由usb_device_id结构描述,只有usb_driver驱动结构具有该类型的成员id_table,usb_device_driver结构没有此成员。usb_device_id在内核中的定义如下:
1 | struct usb_device_id { |
usb总线match方法通过usb接口驱动的该结构成员与接口设备的属性进行匹配来判断驱动和设备是否可以关联。
1.判断设备类型和驱动类型,如果设备类型是usb设备,驱动类型是usb设备驱动,则直接返回成功,即所有的usb设备都会和usb genery driver匹配成功。
1 | if (is_usb_device(dev)) { |
2.如果是usb接口设备,并且是接口驱动,则需要调用usb_match_id函数,通过usb驱动的id_table成员去匹配。
1 | id = usb_match_id(intf, usb_drv->id_table); |
2.1.在usb_match_id函数中调用usb_match_one_id函数进行id匹配。
1 | for (; id->idVendor || id->idProduct || id->bDeviceClass || |
2.1.1在usb_match_one_id函数中首先判断设备条件是否匹配,然后继续判断接口条件是否匹配。这两个条件满足则匹配成功。
1 | if (!usb_match_device(dev, id)) |
3.如果usb_match_id匹配不成功,则继续调用usb_match_dynamic_id和动态ID进行匹配。在这里动态id实际匹配过程和上面静态id的实际匹配过程相同,不同之处在于,静态id由驱动的id_table成员提供并随内核或模块编译而静态生成,而动态id则是由用户空间通过sysfs中驱动的new_id属性文件操作而生成,动态id是由usb_driver结构的dynids成员维护的一个链表。dynamic_id功能需要内核配置开启。
1 | id = usb_match_dynamic_id(intf, usb_drv); |
USB 通用驱动
当usb的hub任务监测到新设备接入时就会枚举新USB设备,新USB设备将会被添加到内核设备驱动模型中,根据usb的匹配函数可知,对于usb_device_type类型的设备,直接和usb设备驱动匹配成功,这里匹配到的设备驱动就是在usb子系统初始化的时候创建的usb_generic_driver驱动,所有的usb设备接入后先和该驱动绑定,然后再由该驱动枚举设备的接口设备。该驱动类型是usb_device_driver,一般其他的usb驱动都是usb_driver类型的,即usb接口设备驱动。usb_generic_driver在内核中定义如下:
1 | struct usb_device_driver usb_generic_driver = { |
由该定义可知,usb_generic_driver驱动的名称为“usb”,probe函数为generic_probe。下面分析probe流程。
1.在generic_probe函数中首先调用usb_choose_configuration函数根据设备的配置信息,选择一个最佳的配置。
1 | c = usb_choose_configuration(udev); |
2.调用usb_set_configuration接口枚举和申请注册usb接口设备。
1 | err = usb_set_configuration(udev, c); |
当接口添加到设备驱动模型中后,设备驱动模型核心再去为接口设备在usb总线上匹配和绑定接口驱动。
USB HUB驱动
usb的hub驱动在内核中的定义如下:
1 | static struct usb_driver hub_driver = { |
由定义可知,hub驱动的probe函数是hub_probe,下面是hub_probe函数的代码核心部分的流程:
1.首先申请和初始化hub。
1 | hub = kzalloc(sizeof(*hub), GFP_KERNEL); |
其中,初始化了hub的events成员,该成员是work_struct类型的,属于工作队列的上的一个实例,其回调函数设置为hub_event函数。
2.调用hub_config为hub申请资源,进一步初始化,以及开启hub中断轮询。
1 | if (hub_configure(hub, &desc->endpoint[0].desc) >= 0) |
2.1在hub_configure中首先申请各种hub的资源,获取hub描述符,初始化资源等。其中也申请和初始化usb数据传输相关的资源,有管道,端点,urb等。
1 | pipe = usb_rcvintpipe(hdev, endpoint->bEndpointAddress); |
可以看到在设置urb的时候设置的中断轮询的回调函数是hub_irq。
2.2调用hub_activate函数开始hub的中断轮询和事件处理。
1 | hub_activate(hub, HUB_INIT); |
在hub_activate函数中继续做一些hub端口以及其他资源的初始化,最后会向usb核心提交上一步设置的urb。当urb轮询得到设备的回复后就会调用到设置urb的时候设置的中断轮询的回调函数hub_irq。
3.hub_irq函数
hub_irq函数是hub中断轮询的回调函数,该函数首先调用kick_hub_wq函数,以唤醒hub工作队列处理轮询结果,然后调用hub_resubmit_irq_urb函数重新提交中断轮询urb以继续监测hub端口。
1 | /* Something happened, let hub_wq figure it out */ |
kick_hub_wq中会调用queue_work调度hub的events工作,前面的流程中events的回调函数设置为hub_event函数,所以工作队列调度events就会执行到hub_event回调函数。
1 | if (queue_work(hub_wq, &hub->events)) |
4.hub_event函数中检查如果有事件产生则对所有端口调用port_event函数。
1 | /* deal with port status changes */ |
4.1在port_event中检查如果是否有connect_change产生,有则调用hub_port_connect_change函数进行处理。
1 | if (connect_change) |
4.1.1在hub_port_connect_change函数中调用hub_port_connect函数,处理端口上的连接事件。
1 | hub_port_connect(hub, port1, portstatus, portchange); |
4.1.1.1在hub_port_connect函数中,调用usb_alloc_dev接口申请usb设备。
1 | udev = usb_alloc_dev(hdev, hdev->bus, port1); |
4.1.1.2为新接入的设备选择一个地址编号以提供枚举时使用。
1 | choose_devnum(udev); |
选择的设备编号最终将在hub_port_init函数中通过hub_set_address接口设置给usb设备。
4.1.1.3调用hub_port_init函数复位设备和获取设备描述符。
1 | status = hub_port_init(hub, udev, port1, i); |
在usb设备枚举之前,使用端点0进行通信。
4.1.1.4调用usb_new_device接口枚举和注册usb设备。
1 | status = usb_new_device(udev); |
在该接口中会获取设备的配置描述符,并注册usb设备到内核设备驱动模型中,它将会和usb generic驱动绑定并由generic驱动去解析和注册其接口设备。
USB_HCD注册
以ehci_hcd_pci为例说明,下面的源代码分析省略了硬件上的初始化部分,比如唤醒使能,one-time初始化等等。
1.执行ehci_pci_probe
当pci_driver的probe被执行的时候,会调用到usb核心层的usb_hcd_pci_probe函数。
1 | static int ehci_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) |
2.创建usb_hcd
在ehci_pci_probe中首先会创建usb_hcd。
1 | hcd = usb_create_hcd(driver, &dev->dev, pci_name(dev)); |
3.添加usb_hcd
1 | retval = usb_add_hcd(hcd, hcd_irq, IRQF_SHARED); |
3.1.注册usb_bus
在usb_add_hcd函数中调用usb_register_bus注册usb_bus,该注册函数实现上就是在usb_bus_idr空间中为要注册的usb_bus申请一席之地,即usb_bus结构中的busnum成员,然后向usb通知中心发送添加usb_bus的事件。
1 | retval = usb_register_bus(&hcd->self); |
3.2.为usb_bus申请root hub设备
1 | rhdev = usb_alloc_dev(NULL, &hcd->self, 0); |
这里usb_alloc_dev是一个创建新usb设备的接口,该接口中申请usb_device设备,并且指定usb_device设备所属的设备驱动模型总线是usb_bus_type,设备类型是usb_device_type。同时初始化usb_device的其它成员。
3.3.调用register_root_hub注册root hub
1 | /* starting here, usbcore will pay attention to this root hub */ |
3.3.1在register_root_hub函数中调用usb_new_device将root hub设备注册到内核设备驱动模型中。
1 | retval = usb_new_device (usb_dev); |
这里的usb_new_device是一个枚举和注册usb设备的接口,该接口中调用usb_enumerate_device函数读取设备的描述符以枚举设备,之后会调用device_add接口将新的usb设备注册到设备驱动模型中。
至此,完成usb hcd的注册。
usb驱动编写流程
一般usb驱动指的是usb接口设备驱动。
定义驱动
1.定义驱动
1 | static struct usb_driver xxx = { |
2.定义id_table,描述该驱动能处理哪些设备。
1 | static struct usb_device_id xxx_id_table = { |
在id_table中可以描述多个匹配信息,上例是匹配厂家ID为0x1234, 产品ID为0x5678的所有设备。可以写入接口信息,或者接口设备组合信息。
3.实现probe和disconnect函数,probe函数用来在设备添加到内核中后对设备进行初始化,资源维护以及提供相关功能。disconnect函数用来在设备拨出后释放资源等操作。
注册驱动
使用module_usb_driver接口将usb驱动注册到usb核心,module_usb_driver是一个宏,展开后使用提供模块init和exit函数,以及使用usb_register接口注册usb驱动。
1 | module_usb_driver(xxx); |
驱动功能
驱动功能在驱动probe函数中实现。probe函数原型为:
1 | int (*probe) (struct usb_interface *intf, |
在probe函数中根据接口信息获取接口的传输端点,一般鼠标和hub设备等实时性要求高,传输数据少的接口使用中断传输端点,而要求效率高,传输数据大的则使用批量传输端点。
端点具有方向性,中断端点一般是输入的。批量传输一般需要输入和输出两个端点。
数据传输的通道叫做管道。端点是传输地址,管道是传输路径。
数据传输由一个urb结构对象来管理,使用者只需要申请设置好urb,然后提交给usb核心,由usb核心调度数据传输,当数据传输完成时调用usr对象中设置的回调函数。
以usbmouse驱动为例说明驱动功能的主要步骤。
1.获取端点
1 | endpoint = &interface->endpoint[0].desc; |
2.创建管道
1 | pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); |
3.申请传输数据空间,这里申请了dma空间
1 | mouse->data = usb_alloc_coherent(dev, 8, GFP_ATOMIC, &mouse->data_dma); |
4.申请urb
1 | mouse->irq = usb_alloc_urb(0, GFP_KERNEL); |
5.设置urb
1 | usb_fill_int_urb(mouse->irq, dev, pipe, mouse->data, |
这里usb使用的是中断输入端点,所以在urb设置的回调函数usb_mouse_irq中需要重新提交urb以进行循环轮询。
6.提交urb
1 | usb_submit_urb(mouse->irq, GFP_KERNEL) |
在usb鼠标驱动中,是在usbmouse设备文件的open函数中提交urb的。
以上就是一个usb中断传输的驱动程序usb功能实现部分,其他传输端点类似。