最近在研究U盘的驱动,遇到很多难以理解的问题,虽然之前也参考过一些很不错的书籍如:《USB那些事》,但最终还是觉得下载一份最新的源码,慢慢啃源码会有更具象的了解,在这里做一些笔记。本文基于linux-5.16.9源码描述。
我就USB-Storage的调用栈花了个思维导图,放在这了USB-Storage驱动调用思维导图(免费下载)
一、USB子系统要想了解USB-Storage驱动,我们就要从USB子系统开始说起。
1.USB CoreUSB core 是USB子系统的核心部分,在这里完成了USB子系统的初始化、注册USB总线、注册USB根集线器、初始化调试文件系统(usbfs)、注册通用驱动(generic_driver)以及定义了许多关键的数据结构。
1.1 从init开始写过驱动的朋友们一定很熟悉,就像C++ 中的main函数一样,驱动是从init开始的。
static int __init usb_init(void)
{
int retval;
if (usb_disabled()) {
pr_info("%s: USB support disabledn", usbcore_name);
return 0;
}
usb_init_pool_max();
//用于调试USB的虚拟文件系统初始化
usb_debugfs_init();
//
usb_acpi_register();
//注册USB总线
retval = bus_register(&usb_bus_type);
if (retval)
goto bus_register_failed;
//各个子系统往往是相互独立的,因此当总线出现变化之后,需要通知其他总线
retval = bus_register_notifier(&usb_bus_type, &usb_bus_nb);
if (retval)
goto bus_notifier_failed;
//注册字符设备,主设备号180
retval = usb_major_init();
if (retval)
goto major_init_failed;
//注册usbfs驱动
retval = usb_register(&usbfs_driver);
if (retval)
goto driver_register_failed;
retval = usb_devio_init();
if (retval)
goto usb_devio_init_failed;
//初始化根集线器,这里边完成了驱动的注册
retval = usb_hub_init();
if (retval)
goto hub_init_failed;
//USb通用设备驱动的注册
retval = usb_register_device_driver(&usb_generic_driver, THIS_MODULE);
if (!retval)
goto out;
usb_hub_cleanup();
//下面是一些异常处理 略
...
}
是不是很简洁,一个init下逐项排列着另外一些init,有的看名字就能大概知道它的作用是什么,好的,接下来一步一步看。接下来我们只介绍一些关键的内容,有的会和电源管理有关,而有的是为特殊设备做的适配,有的是为调试建立的文件系统,我就暂且舍“末” 逐 “本”了。
1.2 注册USB总线一切总线都需要被注册,总线就是设备和驱动的桥梁,桥梁的两端被连接起来,设备才能正常地工作实现各种功能。
struct bus_type usb_bus_type = {
.name = "usb",
.match = usb_device_match,
.uevent = usb_uevent,
.need_parent_lock = true,
};
.name,就是这条总线的身份证——usb,用于和其他总线区分。
.match, 只要总线上出现新的设备,match函数就会尝试为其匹配合适的驱动,每一个设备都要感谢match函数
.uevent,用于发送总线相关的总线事件
关于UEvent,在Android中,负责U盘挂载的native进程 vold 就会利用netlinkmanager监听Subsystem为“block”的Uevent,进而挂载U盘,因此UEvent可以用来Debug。(这里立个Flag,要写一篇从UEvent角度看USB子系统的笔记)
(1)match函数static int usb_device_match(struct device *dev, struct device_driver *drv)
{
if (is_usb_device(dev)) {
struct usb_device *udev;
struct usb_device_driver *udrv;
if (!is_usb_device_driver(drv))
return 0;
udev = to_usb_device(dev);
udrv = to_usb_device_driver(drv);
if (!udrv->id_table && !udrv->match)
return 1;
return usb_driver_applicable(udev, udrv);
} else if (is_usb_interface(dev)) {
struct usb_interface *intf;
struct usb_driver *usb_drv;
const struct usb_device_id *id;
if (is_usb_device_driver(drv))
return 0;
intf = to_usb_interface(dev);
usb_drv = to_usb_driver(drv);
id = usb_match_id(intf, usb_drv->id_table);
if (id)
return 1;
id = usb_match_dynamic_id(intf, usb_drv);
if (id)
return 1;
}
return 0;
}
可以看到这里有两条路可以走,一条路是给设备走的,另一条路是给接口走的。
在USB设备的逻辑组织中,包含设备、配置、接口和端点4个层次。
每个USB设备都提供了不同级别的配置信息,可以包含一个或多个配置,不同的配置使设备表现出不同的功能组合(在探测/连接期间需从其中选定一个),配置由多个接口组成。
在USB协议中,接口由多个端点组成,代表一个基本的功能,是USB设备驱动程序控制的对象,一个功能复杂的USB设备可以具有多个接口。每个配置中可以有多个接口,而设备接口是端点的汇集(collection)。例如USB扬声器可以包含一个音频接口以及对旋钮和按钮的接口。一个配置中的所有接口可以同时有效,并可被不同的驱动程序连接。每个接口可以有备用接口,以提供不同质量的服务参数。
端点是USB通信的最基本形式,每一个USB设备接口在主机看来就是一个端点的集合。主机只能通过端点与设备进行通信,以使用设备的功能。在USB系统中每一个端点都有惟一的地址,这是由设备地址和端点号给出的。每个端点都有一定的属性,其中包括传输方式、总线访问频率、带宽、端点号和数据包的最大容量等。一个USB端点只能在一个方向承载数据,或者从主机到设备(称为输出端点),或者从设备到主机(称为输入端点),因此端点可看作一个单向的管道。端点0通常为控制端点,用于设备初始化参数等。只要设备连接到USB上并且上电端点0就可以被访问。端点1、2等一般用作数据端点,存放主机与设备间往来的数据。
这里match函数先按下不讲,待到有设备或者接口出现的时候我们再展开描述。
关于根集线器,其本身是一个物理硬件集成在USB主机控制器上一般位于我们的主板上。所有的USB设备都要连接在根集线器上。(但并不意味着,只有一个根集线器)因此,USB设备的拓扑结构是一个树状结构。
以我是用的电脑为例,下面列出了我的USB设备:
lil_crystal@lil-crystal-pc:~$ lsusb
Bus 002 Device 002: ID 05e3:0626 Genesys Logic, Inc. USB3.1 Hub
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 413c:301a Dell Computer Corp. Dell MS116 USB Optical Mouse
Bus 001 Device 002: ID 413c:2113 Dell Computer Corp. Dell KB216 Wired Keyboard
Bus 001 Device 008: ID 0781:5567 SanDisk Corp. Cruzer Blade
Bus 001 Device 004: ID 05e3:0610 Genesys Logic, Inc. 4-port hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
可以看到我就有两个根集线器,一个是USB2.0协议,另一个是3.0协议。一共有这么几个USB设备:一个鼠标、一个键盘、一个U盘以及两个Hub(一个是我自己的Hub 4-port hub那个,另一个是电脑前面板的几个接口,其本质也还是一个hub)
(1)注册集线器驱动 usb_register(&hub_driver)hub_driver:
static struct usb_driver hub_driver = {
.name = "hub",
.probe = hub_probe,
.disconnect = hub_disconnect,
.suspend = hub_suspend,
.resume = hub_resume,
.reset_resume = hub_reset_resume,
.pre_reset = hub_pre_reset,
.post_reset = hub_post_reset,
.unlocked_ioctl = hub_ioctl,
.id_table = hub_id_table,
.supports_autosuspend = 1,
};
注册完驱动后,probe函数就会执行。但是这里就会有一个疑问:probe函数的调用是设备和驱动缺一不可。驱动已经注册完成了,但是设备呢?
按照我的理解,所有的USB设备都连接在USB主机控制器上的根集线器上,而USB主机控制器是连接在SCSI总线上,他应该是一个SCSI设备,是在SCSI子系统中完成注册的。
hub_probe中设置了电源管理的相关信息,对根集线器和普通设备有区别处理,但这些并不是我主要关心的。
主要的内容就是为这个hub分配内存空间,初始化相关信息,在工作队列中添加hub事件的监听任务并配置hub。
在较早版本的kernel中,是利用内核线程去监控hub_event,而在5.16.9中是利用工作队列去实现的。
核心代码为:INIT_WORK(&hub->events, hub_event);
每当有设备连接到USB接口时,USB总线在查询hub状态信息的时候会触发hub的中断服务程序hub_irq, 在该函数中置位event_bits,运行工作队列。进入hub_event函数,该函数用来处理端口变化的事件。然后通过一个for循环来检测每个端口的状态信息。利用usb_port_status获取端口信息,如果发生变化就调用hub_port_connect_change函数来配置端口
hub_event在之后有USB设备插入在展开描述。
hub_configure是在probe里边被调用的,包括不同的hub的判断和配置,接口数量。
首先就是获取这个hub的描述符
ret = get_hub_descriptor(hdev, hub->descriptor);
struct usb_hub_descriptor {
__u8 bDescLength;
__u8 bDescriptorType;
__u8 bNbrPorts;
__le16 wHubCharacteristics;
__u8 bPwrOn2PwrGood;
__u8 bHubContrCurrent;
union {for (i = 0; i < maxchild; i++) {
ret = usb_hub_create_port_device(hub, i + 1);
if (ret < 0) {
dev_err(hub->intfdev,
"couldn't create port%d device.n", i + 1);
break;
}
}
struct {
__u8 DeviceRemovable[(USB_MAXCHILDREN + 1 + 7) / 8];
__u8 PortPwrCtrlMask[(USB_MAXCHILDREN + 1 + 7) / 8];
} __attribute__ ((packed)) hs;
struct {
__u8 bHubHdrDecLat;
__le16 wHubDelay;
__le16 DeviceRemovable;
} __attribute__ ((packed)) ss;
} u;
} __attribute__ ((packed));
接着读取hub的端口数量,linux对于一个hub,能够支持的最大端口数量是31个。读取供电数据,roothub和普通hub的供电是不同的。由于USB是由主板进行供电的,因此无法支撑功耗大的设备,故对每个端口有最大电流限制。按照USB协议,每个端口是5V1A供电。但是代码里看到的hub的是900mA,这里有些疑问,可以评论区讨论。
if (hdev == hdev->bus->root_hub) {
if (hcd->power_budget > 0)
hdev->bus_mA = hcd->power_budget;
else
hdev->bus_mA = full_load * maxchild;
if (hdev->bus_mA >= full_load)
hub->mA_per_port = full_load;
else {
hub->mA_per_port = hdev->bus_mA;
hub->limited_power = 1;
}
if (hub_is_superspeed(hdev)) {
unit_load = 150;
full_load = 900;
} else {
unit_load = 100;
full_load = 500;
}
之后填充urb并调用hub_irq发送控制信息,遍历hub上的各个端口,并为其创建设备(如果有设备),这里边的设备就是说在连接上hub的时候,hub上本来就有设备,比如开机前U盘就插在电脑上。而不是我们插入设备触发hub_event进而添加设备的。流程和插入设备一致。
for (i = 0; i < maxchild; i++) {
ret = usb_hub_create_port_device(hub, i + 1);//Here add device
if (ret < 0) {
dev_err(hub->intfdev,
"couldn't create port%d device.n", i + 1);
break;
}
}
接下来是hub_activate(hub, HUB_INIT)
这个函数没有深入看,大概就是对hub上电,消除抖动,使其状态稳定。之后如果有需要会再进行补充
//USb通用设备驱动的注册 retval = usb_register_device_driver(&usb_generic_driver, THIS_MODULE);
一句话就完成了这个通用驱动的注册,这主要是因为,为了减轻USB接口驱动开发者的开发难度,USB Core首先获取一些信息并做一些设置,根据这些信息判断USB设备该匹配哪一种具体的接口设备。
下面是这个通用设备驱动的信息。
struct usb_device_driver usb_generic_driver = {
.name = "usb",
.match = usb_generic_driver_match,
.probe = usb_generic_driver_probe,
.disconnect = usb_generic_driver_disconnect,
#ifdef CONFIG_PM
.suspend = usb_generic_driver_suspend,
.resume = usb_generic_driver_resume,
#endif
.supports_autosuspend = 1,
};
usb_generic_driver_match:做一个判断,看看这个设备是不是一个USB通用设备。
usb_generic_driver_probe所做的工作:从设备可能的众多配置中选择一个合适的,然后去配置设备,从而让设备进入期待已久的Configured状态。(USB设备有多种状态,这些状态在USB协议中有明确规定)probe将在插入USB设备后展开描述。
至此,USB子系统完成了初始化,但其实呢,我还什么都没说,因为总线只是注册,没有设备进入,但他已经准备好迎接设备的到来。也留了很多坑,match函数没有讲,hub_event加入了工作队列,但如何触发?USB设备的probe函数干了什么,状态怎么就变成configured了。这些都会在下一节描述。
下一节,就从插入一个U盘讲起,未完待续,,,,,,,


![[Linux] USB-Storage驱动 源码阅读笔记(一) [Linux] USB-Storage驱动 源码阅读笔记(一)](http://www.mshxw.com/aiimages/31/752443.png)
