linux2.6.27 USB检测枚举过程


最近做一个4G通信模块项目,USB驱动始终加载不上,不得不硬着头皮啃USB这块硬骨头。参考了各种资料,再跟读调试内核源码,总算调通了,这期间对USB有了全新的认知,特意总结一下。
注意我使用的linux内核版本是2.6.27。

USB的硬件组成/拓扑结构理解

贴一张经典的USB拓扑结构图
在这里插入图片描述

我自己总结的USB拓扑结构。

USB总线地址
不同层级的HUB
HUB接口1
HUB接口2
HUB接口N
USB主机控制器
HUB
USB设备1
USB设备2
USB设备N

需要明白以下两点:

  1. 每个USB设备都是通过HUB连接到USB总线,hub设备是第一个USB设备,也是必须的USB设备。
  2. 在物理拓扑中不同层级的HUB,在host端看,可以简化理解成仅仅USB地址总线不同的HUB。

USB识别枚举过程

USB设备枚举驱动的整个过程,可以分成三个阶段,HUB阶段、描述符阶段和匹配阶段。

阶段描述行为
1HUB阶段hub检测USB设备的插入/拔出,此阶段只有host和hub通信
2描述符阶段usb host获取USB设备的全部描述符,此阶段主要是host和usb设备通信
3匹配驱动阶段按照描述符为USB设备匹配驱动

一.HUB阶段

注意,此阶段的usb总线上信息交互的双方是usb host控制器和hub接口。插入的USB设备并未参与到信息交互,这些通信信息对usb设备是不可见的。
host通过hub来检测USB设备的插入/拔出,它能做到这点是因为linux在usb初始化时候,为hub创建了内核线程“khubd”
在这里插入图片描述
省略次要信息(本文所有函数都只贴出重点部分),“khubd”线程是这样一个函数。

do {
	hub_events();
} while (!list_empty(&hub_event_list));	//hub_event_list链表不为空,一直循环执行hub_events();

其核心内容是这样的:每个hub接口对应有一个event_list,当接口上有USB设备插入/拔出事件,这些事件就被记录到event_list并插入到hub_event_list中,然后“khubd”线程被唤醒来进行处理。如果一个event_list都没有,“khubd”进入睡眠,直到事件被触发。

这个处理的的具体处理过程:

  1. 每个Hub监测端口数据线上(D+/D-)的电压,根据是D+(全速USB设备)或D-(高/低速)电平变化来判断是否有设备插入/拔出。
    在这里插入图片描述
  2. 检测到有变化的Hub利用中断传输向usb host报告(最终调用kick_khubd,看函数名字,很形象),并向hub_event_list插入event_list。
static void kick_khubd(struct usb_hub *hub)
{
...
	if (!hub->disconnected && list_empty(&hub->event_list)) {
		list_add_tail(&hub->event_list, &hub_event_list);
		wake_up(&khubd_wait);/*唤醒hub_events函数*/
	}
...
}
  1. khubd 线程被唤醒,开始执行hub_events函数。首先,usb host找到是哪个hub的哪个port有状态变化。通过usb_hub_port_status()发送请求,hub收到该请求后,返回该接口的具体状态status和变化change。
static int hub_port_status(struct usb_hub *hub, int port1,
		u16 *status, u16 *change)
{
	int ret;
	ret = get_port_status(hub->hdev, port1, &hub->status->port);
	/*标准请求,获取port的状态,小于4是有问题的*/
	if (ret < 4) {
		if (ret >= 0)
			ret = -EIO;
	} else {
		*status = le16_to_cpu(hub->status->port.wPortStatus);
		*change = le16_to_cpu(hub->status->port.wPortChange);
		ret = 0;
	}
	return ret;
}
  1. host处理获取到的status和change,判断出该hub的端口是否真有设备插入/移除,亦或仅仅是电磁干扰。特别的,hub如果报告中change的连接变化标志位被置位,host给hub发送清除该标志的请求,使得USB设备一下次重新连接也可以被识别。
if (portchange & USB_PORT_STAT_C_CONNECTION) {
	/*清掉链路状态变化,让下一次连接可以正确识别*/
	clear_port_feature(hdev, i,
		USB_PORT_FEAT_C_CONNECTION);
	connect_change = 1;
}
...
if (connect_change){//确认有设备插入/移除,才处理
	hub_port_connect_change(hub, i, portstatus, portchange);
}			
  1. 在host判断出有端口有新设备插入的情况,usb host调用hub_port_connect_change()开始处理,先为这个端口上的USB设备创建新的udev,并为其分配USB设备地址。
  2. host向hub发送复位请求让USB设备复位,然后向hub轮询 Get_Port_Status请求,以查询usb设备是否复位成功。
static void hub_port_connect_change(struct usb_hub *hub, int port1,
					u16 portstatus, u16 portchange)
{
	/*因为有“不按标准来”的厂家,不得不多重试几次*/
	for (i = 0; i < SET_CONFIG_TRIES; i++) {
		...
		udev = usb_alloc_dev(hdev, hdev->bus, port1);
		...
		choose_address(udev);
		...
		//非常重要的函数,复位hub,重新获取USB设备的状态,并获取usb设备endpoint0支持的最大数据长度。
		status = hub_port_init(hub, udev, port1, i);
		if (status < 0){
			goto loop;
		}
		...
...		
loop:
	//未正确识别USB设备,复位endpoit0,释放申请的设备内存
	usb_ep0_reinit(udev);//提一嘴,USB端口0很特殊,USB设备未分配地址前都通过端口0来控制传输收发消息
	release_address(udev);		
...
}				

其中hub_port_init,会调用hub_port_reset复位hub,然后在hub_port_wait_reset()中多次读取USB的连接状态。

static int hub_port_wait_reset(struct usb_hub *hub, int port1,
				struct usb_device *udev, unsigned int delay)
{
...
		ret = hub_port_status(hub, port1, &portstatus, &portchange);
		if (ret < 0)
			return ret;
		if (!(portstatus & USB_PORT_STAT_CONNECTION)){
			/*复位后HUB反应设备状态有断开连接标志,停止尝试*/
			return -ENOTCONN;
		}
...
		if (!(portstatus & USB_PORT_STAT_RESET) &&
		    (portstatus & USB_PORT_STAT_ENABLE)) {
			if (hub_is_wusb(hub))
				udev->speed = USB_SPEED_VARIABLE;
			else if (portstatus & USB_PORT_STAT_HIGH_SPEED)
				udev->speed = USB_SPEED_HIGH;
			else if (portstatus & USB_PORT_STAT_LOW_SPEED)
				udev->speed = USB_SPEED_LOW;
			else
				udev->speed = USB_SPEED_FULL;
			return 0;//USB设备正确连接,此时也得到了设备支持的速度标准
		}
}
...

如果usb设备正确复位并重新连接,USB设备才算被host所识别,此后host才真正意义上和usb设备有总线通信。
另外补充一点:支持usb2.0的host在第6阶段会尝试先用full speed的主机控制器ehci驱动尝试Get_Port_Status,如果usb设备未正确连接,说明USB设备可能不支持full speed,此时会使用低速的ohci/uhci主机控制器驱动来尝试。

二.描述符阶段

  1. host通过的endpoint0发送Get_Descriptor请求获取usb设备描述符,这第一次请求,仅仅为了获取到USB设备支持的端点最大包长度,为下一步获取完整的设备描述符做准备。
int usb_new_device(struct usb_device *udev)
	---> usb_configure_device(udev)
			--->usb_get_descriptor(dev, USB_DT_CONFIG, cfgno, buffer, USB_DT_CONFIG_SIZE)
			
int usb_get_descriptor(struct usb_device *dev, unsigned char type,
		       unsigned char index, void *buf, int size)
{
	/*多尝试几次也是因为总有厂家不按“”规矩来“*/
	for (i = 0; i < 3; ++i) {
	/*这里的dev是HUB阶段为USB设备注册好dev对象,用的通道是dev的enponit0*/
		result = usb_control_msg(dev, usb_rcvctrlpipe(dev, 0),
				USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,
				(type << 8) + index, 0, buf, size,
				USB_CTRL_GET_TIMEOUT);
		...
		break;
	}
}					
  1. 获取到最大包长度后,再次通过hub复位usb设备,确保usb设备处在初始化状态。
  2. host为usb设备分配endpoint号,并通过发送Set_Address(写地址)请求,告知USB设备所分配的地址。此后的通信都是通过新的endpoint号,从而将endpoint0释放出来。
  3. host发送 Get_Descriptor请求读取完整的设备描述符。
  4. 解析设备描述符信息,并按照描述符信息,获取完usb设备全部的描述符,并逐层解析保存在内存中。这些描述符包含配置描述符,接口描述符,端点描符等等,这里不分析。

三.匹配驱动阶段

其实“USB枚举”发生应该分两种情况,一种是有新USB设备被注册时,“枚举驱动”,另外一种是有新USB驱动注册时,“枚举设备”。原理一样,这里接这上面的内容,只分析"枚举驱动"的情况。

  1. 拿到解析好的描述符信息后,host逐一枚举已在usb总线注册的设备驱动,按照每个驱动提供的match_flag信息(标明usb设备要与哪些成员需要匹配)逐项比较驱动中下文中各成员的值,如果全部匹配,则认为驱动和设备匹配。
__u8  bDeviceClass;      /* USB分配的设备类code */
__u8  bDeviceSubClass;   /* USB分配的子类code */
__u8  bDeviceProtocol;   /* USB分配的协议code */
__le16 idVendor;         /* 厂商编号 */
__le16 idProduct;        /* 产品编号 */
__le16 bcdDevice;      /* 设备出厂编号 */

例如U盘驱动的match_flag ,它的值为:

match_flag =USB_DEVICE_ID_MATCH_INT_CLASS | 
USB_DEVICE_ID_MATCH_INT_SUBCLASS | USB_DEVICE_ID_MATCH_INT_PROTOCOL。

usb协议又规定U盘的Subclass是属于US_SC_SCSI的.而其通信协议使用的是Bulk-Only(0x50),因此,任意厂家的U盘设备只要bDeviceClass,bDeviceSubClass, bDeviceProtocol符合以上规定,都能被正确枚举。

  1. usb host调用该驱动的probe函数,来初始化USB设备并选择好配置。最后host把usb设备挂上设备总线,此后USB设备驱动就可以控制USB设备了。
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值