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

时间:2024-07-15 14:32:17

一、USB总线驱动程序的作用

a)识别USB设备

1.1 分配地址
1.2 并告诉USB设备(set address)
1.3 发出命令获取描述符

b)查找并安装对应的设备驱动程序

c)提供USB读写函数

二、USB设备工作流程

由于内核自带了USB驱动,所以我们先插入一个USB键盘到开发板上看打印信息发现以下字段:

img

如下图,找到第一段话是位于drivers/usb/core/hub.c的第2186行:

img

这个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-****博客

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 函数和动态匹配)来判断设备和驱动程序是否适配。对于设备驱动程序接口驱动程序的匹配逻辑分别进行了处理。

四、相关问题梳理

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热插拔机制实现了方便、可靠的设备连接和管理,从而极大地提高了用户的操作体验和系统的灵活性。