一、USB总线驱动程序的作用
a)识别USB设备
1.1 分配地址
1.2 并告诉USB设备(set address)
1.3 发出命令获取描述符
b)查找并安装对应的设备驱动程序
c)提供USB读写函数
二、USB设备工作流程
由于内核自带了USB驱动,所以我们先插入一个USB键盘到开发板上看打印信息发现以下字段:
如下图,找到第一段话是位于drivers/usb/core/hub.c的第2186行:
这个hub其实就是我们的USB主机控制器的集线器,用来管理多个USB接口
以下是USB设备插入后内核中的工作流程
hub_irq
kick_khubd
hub_thread
hub_events
hub_port_connect
udev = usb_alloc_dev(hdev, hdev->bus, port1);
dev->dev.bus = &usb_bus_type;/*注册到USB总线上*/
choose_devnum(udev); // 给新设备分配编号(地址)
hub_port_init() // usb 1-1: new full speed USB device using s3c2410-ohci and address 3
hub_set_address // 把编号(地址)告诉USB设备
usb_get_device_descriptor(udev, 8); // 获取设备描述符
retval = usb_get_device_descriptor(udev, USB_DT_DEVICE_SIZE);
usb_new_device(udev)
err = usb_get_configuration(udev); // 把所有的描述符都读出来,并解析
usb_parse_configuration
device_add // 把device放入usb_bus_type的dev链表,
// 从usb_bus_type的driver链表里取出usb_driver,
// 把usb_interface和usb_driver的id_table比较
// 如果能匹配,调用usb_driver的probe
以下是设备插入后的完整流程,包括设备和接口的匹配与注册:
硬件--主机控制器:
-
设备插入,产生中断: USB主机控制器检测到新设备,触发硬件中断。
内核--USB核心层:
-
分配设备地址: 处理中断,内核分配唯一设备地址,并将usb_device注册到总线上
-
获取并解析设备描述符: 内核请求并解析设备描述符。
-
获取并解析配置描述符: 内核请求并解析配置描述符,包括接口和端点描述符。
-
注册接口: 为每个接口创建
usb_interface
结构,每个接口作为一个独立的设备进行注册,内核通过调用device_add
将每个接口注册到设备模型中。 -
匹配接口驱动: 内核调用
usb_device_match
查找并匹配接口驱动。如果找到匹配的驱动,调用驱动的probe
函数。
内核--设备驱动层:
1、具体的设备驱动driver实现(probe,usb_register_driver等)
关键点
-
设备和接口层次:
- 整个USB设备有一个顶层的
usb_device
结构体,该结构体管理设备的整体信息。 - 每个接口有一个
usb_interface
结构体,表示设备的一个独立功能单元。
- 整个USB设备有一个顶层的
-
驱动程序匹配:
- 顶层的
usb_device
结构体一般不会直接匹配到驱动程序。驱动程序的匹配主要发生在接口层次,通过usb_interface
结构体进行。但有一些驱动程序是针对整个 USB 设备的,而不是单个接口。例如,某些 USB 设备驱动程序(usb_device_driver
)可能需要管理整个设备,而不仅仅是某个特定接口。通过这种设计,USB 核心可以灵活地支持设备级和接口级的驱动程序匹配。 - 内核通过解析设备的配置描述符,发现设备包含的所有接口,并为每个接口匹配对应的驱动程序。
- 顶层的
三、相关概念补充
1、USB描述符的层次及定义
- USB设备描述符(usb_device_descriptor)
- USB配置描述符(usb_config_descriptor)
- USB接口描述符(usb_interface_descriptor)
- USB端点描述符(usb_endpoint_descriptor)
- USB接口描述符(usb_interface_descriptor)
- USB配置描述符(usb_config_descriptor)
一个设备描述符可以有多个配置描述符;
一个配置描述符可以有多个接口描述符(比如声卡驱动就有两个接口:录音接口和播放接口)
一个接口描述符可以有多个端点描述符;
设备描述符结构体如下:(位于include\linux\usb\Ch9.h)
struct usb_device_descriptor {
__u8 bLength; //本描述符的size
__u8 bDescriptorType; //描述符的类型,这里是设备描述符DEVICE
__u16 bcdUSB; //指明usb的版本,比如usb2.0
__u8 bDeviceClass; //类
__u8 bDeviceSubClass; //子类
__u8 bDeviceProtocol; //指定协议
__u8 bMaxPacketSize0; //端点0对应的最大包大小
__u16 idVendor; //厂家ID
__u16 idProduct; //产品ID
__u16 bcdDevice; //设备的发布号
__u8 iManufacturer; //字符串描述符中厂家ID的索引
__u8 iProduct; //字符串描述符中产品ID的索引
__u8 iSerialNumber; //字符串描述符中设备序列号的索引
__u8 bNumConfigurations; //配置描述符的个数,表示有多少个配置描述符
} __attribute__ ((packed));
配置描述符如下:
struct usb_config_descriptor {
__u8 bLength; //描述符的长度
__u8 bDescriptorType; //描述符类型的编号
__le16 wTotalLength; //配置所返回的所有数据的大小
__u8 bNumInterfaces; //配置所支持的接口个数, 表示有多少个接口描述符
__u8 bConfigurationValue; //Set_Configuration命令需要的参数值
__u8 iConfiguration; //描述该配置的字符串的索引值
__u8 bmAttributes; //供电模式的选择
__u8 bMaxPower; //设备从总线提取的最大电流
} __attribute__ ((packed));
接口描述符如下:
struct usb_interface_descriptor {
__u8 bLength; //描述符的长度
__u8 bDescriptorType; //描述符类型的编号
__u8 bInterfaceNumber; //接口的编号
__u8 bAlternateSetting; //备用的接口描述符编号,提供不同质量的服务参数.
__u8 bNumEndpoints; //要使用的端点个数(不包括端点0), 表示有多少个端点描述符,比如鼠标就只有一个端点
__u8 bInterfaceClass; //接口类型,与驱动的id_table对应
__u8 bInterfaceSubClass; //接口子类型
__u8 bInterfaceProtocol; //接口所遵循的协议
__u8 iInterface; //描述该接口的字符串索引值
} __attribute__ ((packed)
关于描述符的解析,由下图可知,以控制接口为例,接口描述符中写明了CT、IT等端口的信息。
对于端口具体细节可以看这篇UVC 1.5 Class Specification 简解_uvc1.5-CSDN博客
2、USB的.match()函数
static int usb_device_match(struct device *dev, struct device_driver *drv)
{
/* devices and interfaces are handled separately */设备和接口分别处理
if (is_usb_device(dev)) { 首先,检查 dev 是否是 USB 设备。如果是,则执行以下代码块
struct usb_device *udev;
struct usb_device_driver *udrv;
/* interface drivers never match devices */
if (!is_usb_device_driver(drv))
return 0;
udev = to_usb_device(dev);
udrv = to_usb_device_driver(drv);
/* If the device driver under consideration does not have a 检查驱动程序的 id_table 和 match 函数
* id_table or a match function, then let the driver's probe
* function decide.
*/
if (!udrv->id_table && !udrv->match)
return 1;
return usb_driver_applicable(udev, udrv);
} else if (is_usb_interface(dev)) { 检查是否为 USB 接口,如果 dev 是 USB 接口,则执行以下代码块
struct usb_interface *intf;
struct usb_driver *usb_drv;
const struct usb_device_id *id;
/* device drivers never match interfaces */
if (is_usb_device_driver(drv)) 如果驱动程序 drv 是 USB 设备驱动程序,则直接返回 0,表示不匹配。
return 0;
intf = to_usb_interface(dev);
usb_drv = to_usb_driver(drv);
id = usb_match_id(intf, usb_drv->id_table); 尝试匹配usb接口和接口驱动程序
if (id)
return 1;
id = usb_match_dynamic_id(intf, usb_drv);
if (id)
return 1;
}
return 0;
}
这段代码逻辑通过检查设备和驱动程序类型,并利用不同的匹配方法(如 id_table
、match
函数和动态匹配)来判断设备和驱动程序是否适配。对于设备驱动程序和接口驱动程序的匹配逻辑分别进行了处理。
3、USB的probe()函数
此处以uvc_driver.c中的probe为例
uvc_probe
kzalloc //分配video_device
uvc_register_chains
uvc_register_terms
uvc_register_video
vdev->v4l2_dev = &dev->vdev; //设置video_device
vdev->fops = &uvc_fops;
vdev->ioctl_ops = &uvc_ioctl_ops;
vdev->release = uvc_release;
video_register_device //注册video_device
具体对probe函数的分析可以看这篇:UVC 设备框架在 Linux 4.15 内核的演变_v4l2 核心在尝试映射 uvc 控件时找不到相应的文件或目录-CSDN博客
四、相关问题梳理
1、关于驱动和设备接口注册
与platform_driver、i2c_driver类似,usb_driver起到了牵线的作用,即在probe()函数里注册相应的字符、tty设备(此处usb中注册的是接口设备),在disconnect()函数里注销相应的设备,而原先对设备的注册和注销一般直接发生在模块加载和卸载函数中。
2、USB驱动用idtable匹配,不用设备树来描写硬件信息吗
USB是热插拔的,不用在dts中描述,如果写了板子上有一个U盘,但实际上没有,其实反而是制造了麻烦,相反,如果没有写,U盘一旦插入,LinuxUSB子系统会自动探测到一个U盘。
-
固定硬件配置:
- 设备树适用于那些硬件配置相对固定的系统,这些系统的硬件在设计和制造时已经确定。设备树提供了一种静态描述硬件的方法,适合用于固件或操作系统在启动时配置硬件。
- 示例:单板计算机、嵌入式系统中的 SoC(系统级芯片)。
-
动态硬件配置:
- 对于那些硬件配置可能会动态改变的系统,例如支持热插拔设备的系统,通常不使用设备树,而是依赖于总线驱动和热插拔机制(如 USB、PCIe)来动态识别和配置硬件。
- 示例:PC 平台中的 USB 设备、PCIe 设备。
3、USB驱动注册时要分设备和接口吗,设备驱动和接口驱动具体有什么不同(存疑)
驱动其实是与设备的逻辑接口进行匹配,有几个接口匹配成功probe函数就调用几次
4、USB的热插拔机制
USB(通用串行总线)的热插拔机制使得用户可以在系统运行时随时连接或断开USB设备,而无需重新启动系统。热插拔的实现依赖于硬件和软件的紧密配合,下面具体讲解其工作原理和机制。
硬件层面
-
电气信号检测:
- USB接口有专门的引脚用于检测设备的插入和拔出。USB主机控制器能够检测这些信号的变化。
- 当USB设备插入时,VBUS电压上升,主机控制器检测到电压变化,并开始通信初始化过程。
-
数据线信号检测:
- USB接口的D+和D-数据线在设备连接时会产生特定的电压信号。主机控制器通过这些信号确认设备的连接。
软件层面
-
设备检测与枚举:
- 当检测到设备连接时,USB主机控制器会发出一个设备复位信号(Reset Signal),这会将设备置于已知状态。
- 复位完成后,主机开始与设备通信,获取设备的描述符信息。这包括设备的类型、制造商、产品ID等。
- 主机通过这些描述符信息来确定设备的驱动程序,并在操作系统中为设备创建相应的节点。
-
驱动加载:
- 基于设备的描述符信息,操作系统会搜索并加载相应的驱动程序。驱动程序负责与设备进行高层次的通信。
- 如果是一个存储设备,操作系统会挂载设备并创建一个文件系统节点;如果是一个输入设备(如键盘或鼠标),则系统会准备好接收输入事件。
-
事件通知:
- 当设备被插入或移除时,内核会生成一个热插拔事件(hotplug event),并通知用户空间的管理进程(如udev)。这些进程可以执行相应的脚本或命令,以便用户可以看到设备的状态变化。
Linux 内核中的热插拔机制
Linux 内核通过多个子系统和框架来支持USB的热插拔:
-
USB Core 子系统:
- 处理USB设备的检测、枚举和基础通信。
- 提供API和机制供上层驱动程序调用。
-
USB Host Controller Drivers (HCD):
- 实现与具体硬件的交互,如EHCI、OHCI、UHCI等。
- 负责处理底层电气信号和数据传输。
-
udev:
- 用户空间设备管理守护进程,响应内核生成的设备事件。
- 通过规则和脚本来管理设备节点的创建、权限设置等。
热插拔的具体流程
-
设备插入:
- 检测电压信号变化,主机控制器发出设备复位信号。
- 设备开始回应,主机读取设备描述符信息。
- 操作系统加载合适的驱动程序。
-
设备移除:
- 检测电压信号变化,主机控制器发出设备断开信号。
- 操作系统卸载驱动程序,释放资源。
- 通知用户空间进程(如udev),以便执行清理操作。
实际应用中的注意事项
- 数据完整性:在移除存储设备时,需要确保没有正在进行的数据传输,以避免数据损坏。
- 电源管理:热插拔操作需要处理好电源的管理,避免电涌或设备损坏。
通过硬件和软件的紧密配合,USB热插拔机制实现了方便、可靠的设备连接和管理,从而极大地提高了用户的操作体验和系统的灵活性。