最近做一个4G通信模块项目,USB驱动始终加载不上,不得不硬着头皮啃USB这块硬骨头。参考了各种资料,再跟读调试内核源码,总算调通了,这期间对USB有了全新的认知,特意总结一下。
注意我使用的linux内核版本是2.6.27。
USB的硬件组成/拓扑结构理解
贴一张经典的USB拓扑结构图
我自己总结的USB拓扑结构。
需要明白以下两点:
- 每个USB设备都是通过HUB连接到USB总线,hub设备是第一个USB设备,也是必须的USB设备。
- 在物理拓扑中不同层级的HUB,在host端看,可以简化理解成仅仅USB地址总线不同的HUB。
USB识别枚举过程
USB设备枚举驱动的整个过程,可以分成三个阶段,HUB阶段、描述符阶段和匹配阶段。
阶段 | 描述 | 行为 |
---|---|---|
1 | HUB阶段 | 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”进入睡眠,直到事件被触发。
这个处理的的具体处理过程:
- 每个Hub监测端口数据线上(D+/D-)的电压,根据是D+(全速USB设备)或D-(高/低速)电平变化来判断是否有设备插入/拔出。
- 检测到有变化的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函数*/
}
...
}
- 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;
}
- 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);
}
- 在host判断出有端口有新设备插入的情况,usb host调用hub_port_connect_change()开始处理,先为这个端口上的USB设备创建新的udev,并为其分配USB设备地址。
- 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主机控制器驱动来尝试。
二.描述符阶段
- 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;
}
}
- 获取到最大包长度后,再次通过hub复位usb设备,确保usb设备处在初始化状态。
- host为usb设备分配endpoint号,并通过发送Set_Address(写地址)请求,告知USB设备所分配的地址。此后的通信都是通过新的endpoint号,从而将endpoint0释放出来。
- host发送 Get_Descriptor请求读取完整的设备描述符。
- 解析设备描述符信息,并按照描述符信息,获取完usb设备全部的描述符,并逐层解析保存在内存中。这些描述符包含配置描述符,接口描述符,端点描符等等,这里不分析。
三.匹配驱动阶段
其实“USB枚举”发生应该分两种情况,一种是有新USB设备被注册时,“枚举驱动”,另外一种是有新USB驱动注册时,“枚举设备”。原理一样,这里接这上面的内容,只分析"枚举驱动"的情况。
- 拿到解析好的描述符信息后,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符合以上规定,都能被正确枚举。
- usb host调用该驱动的probe函数,来初始化USB设备并选择好配置。最后host把usb设备挂上设备总线,此后USB设备驱动就可以控制USB设备了。