Linux--USB驱动开发(二)插入USB后的内核执行程序

一、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

以下是设备插入后的完整流程,包括设备和接口的匹配与注册:

硬件--主机控制器:

  1. 设备插入,产生中断: USB主机控制器检测到新设备,触发硬件中断。

内核--USB核心层:

  1. 分配设备地址: 处理中断,内核分配唯一设备地址,并将usb_device注册到总线上

  2. 获取并解析设备描述符: 内核请求并解析设备描述符。

  3. 获取并解析配置描述符: 内核请求并解析配置描述符,包括接口和端点描述符。

  4. 注册接口 : 为每个接口创建 usb_interface 结构,每个接口作为一个独立的设备进行注册,内核通过调用 device_add 将每个接口注册到设备模型中。

  5. 匹配接口驱动 : 内核调用 usb_device_match 查找并匹配接口驱动。如果找到匹配的驱动,调用驱动的 probe 函数。

内核--设备驱动层:

1、具体的设备驱动driver实现(probe,usb_register_driver等)

关键点

  • 设备和接口层次

    • 整个USB设备有一个顶层的 usb_device 结构体,该结构体管理设备的整体信息。
    • 每个接口有一个 usb_interface 结构体,表示设备的一个独立功能单元。
  • 驱动程序匹配

    • 顶层的 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)

一个设备描述符可以有多个配置描述符;

一个配置描述符可以有多个接口描述符(比如声卡驱动就有两个接口:录音接口和播放接口)

一个接口描述符可以有多个端点描述符;

设备描述符结构体如下:(位于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_tablematch 函数和动态匹配)来判断设备和驱动程序是否适配。对于设备驱动程序接口驱动程序的匹配逻辑分别进行了处理。

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设备,而无需重新启动系统。热插拔的实现依赖于硬件和软件的紧密配合,下面具体讲解其工作原理和机制。

硬件层面

  1. 电气信号检测

    • USB接口有专门的引脚用于检测设备的插入和拔出。USB主机控制器能够检测这些信号的变化。
    • 当USB设备插入时,VBUS电压上升,主机控制器检测到电压变化,并开始通信初始化过程。
  2. 数据线信号检测

    • USB接口的D+和D-数据线在设备连接时会产生特定的电压信号。主机控制器通过这些信号确认设备的连接。

软件层面

  1. 设备检测与枚举

    • 当检测到设备连接时,USB主机控制器会发出一个设备复位信号(Reset Signal),这会将设备置于已知状态。
    • 复位完成后,主机开始与设备通信,获取设备的描述符信息。这包括设备的类型、制造商、产品ID等。
    • 主机通过这些描述符信息来确定设备的驱动程序,并在操作系统中为设备创建相应的节点。
  2. 驱动加载

    • 基于设备的描述符信息,操作系统会搜索并加载相应的驱动程序。驱动程序负责与设备进行高层次的通信。
    • 如果是一个存储设备,操作系统会挂载设备并创建一个文件系统节点;如果是一个输入设备(如键盘或鼠标),则系统会准备好接收输入事件。
  3. 事件通知

    • 当设备被插入或移除时,内核会生成一个热插拔事件(hotplug event),并通知用户空间的管理进程(如udev)。这些进程可以执行相应的脚本或命令,以便用户可以看到设备的状态变化。

Linux 内核中的热插拔机制

Linux 内核通过多个子系统和框架来支持USB的热插拔:

  1. USB Core 子系统

    • 处理USB设备的检测、枚举和基础通信。
    • 提供API和机制供上层驱动程序调用。
  2. USB Host Controller Drivers (HCD)

    • 实现与具体硬件的交互,如EHCI、OHCI、UHCI等。
    • 负责处理底层电气信号和数据传输。
  3. udev

    • 用户空间设备管理守护进程,响应内核生成的设备事件。
    • 通过规则和脚本来管理设备节点的创建、权限设置等。

热插拔的具体流程

  1. 设备插入

    • 检测电压信号变化,主机控制器发出设备复位信号。
    • 设备开始回应,主机读取设备描述符信息。
    • 操作系统加载合适的驱动程序。
  2. 设备移除

    • 检测电压信号变化,主机控制器发出设备断开信号。
    • 操作系统卸载驱动程序,释放资源。
    • 通知用户空间进程(如udev),以便执行清理操作。

实际应用中的注意事项

  • 数据完整性:在移除存储设备时,需要确保没有正在进行的数据传输,以避免数据损坏。
  • 电源管理:热插拔操作需要处理好电源的管理,避免电涌或设备损坏。

通过硬件和软件的紧密配合,USB热插拔机制实现了方便、可靠的设备连接和管理,从而极大地提高了用户的操作体验和系统的灵活性。

相关推荐
xuanzdhc1 小时前
Linux 基础IO
linux·运维·服务器
愚润求学1 小时前
【Linux】网络基础
linux·运维·网络
bantinghy1 小时前
Linux进程单例模式运行
linux·服务器·单例模式
小和尚同志2 小时前
29.4k!使用 1Panel 来管理你的服务器吧
linux·运维
帽儿山的枪手2 小时前
为什么Linux需要3种NAT地址转换?一探究竟
linux·网络协议·安全
shadon1789 天前
回答 如何通过inode client的SSLVPN登录之后,访问需要通过域名才能打开的服务
linux
小米里的大麦9 天前
014 Linux 2.6内核进程调度队列(了解)
linux·运维·驱动开发
程序员的世界你不懂9 天前
Appium+python自动化(三十)yaml配置数据隔离
运维·appium·自动化
算法练习生9 天前
Linux文件元信息完全指南:权限、链接与时间属性
linux·运维·服务器
忘了ʷºᵇₐ9 天前
Linux系统能ping通ip但无法ping通域名的解决方法
linux·服务器·tcp/ip