全部博文(13)
分类: LINUX
2022-04-23 21:10:45
当USB插入到USB集线器后,一切的工作都是由USB集线器开始。从USB集线器开始,可以更好地了解USB设备插入到系统后的情景分析。
当USB设备插入到USB集线器的一个端口后,该端口就马上检测到有设备插入。检测是依据USB设备在D+或D-的上拉电阻。当设备的上拉电阻在D+时,为全速设备;当设备的上拉电阻在D-时,为低速设备。也是说,USB集线器在检测到有设备插入的同时还要检测到设备是全速还是低速。(至于如何检测高速设备还不知道)当端口完成检测后,需要相应的寄存器置位。
这时USB集线器的驱动程序就需要对端口检测到的状态报告给USB子系统了。
对于每个USB集线器都有一个中断处理函数为之服务,该函数名为:hub_irq。其实在USB驱动子系统中,这是USB集线器唯一能作出反应的函数。
该函数的工作实其很简单:描述USB集线器的各个端口状态,如果端口状态发生改变(不管是设备插入、设备拔出、端口过流……)就会置位相应的hub->event_bits[0]位置位;调用kick_khubd函数把该USB集线器对象hub添加到hub_event_list中,唤醒khubd_wait内核线程。
注册在khubd_wait内核线程的函数是hub_thread。该函数会对hub_event_list链表中的所有USB集线器对象做出处理。
在hub_thread函数中是调用hub_events处理hub_event_list链表中的hub。当hub_events函数返回后,hub_thread进入休眠,等待下一次的唤醒。
看看hub_events函数做了什么。
在hub_events函数中的主体是一个while大循环。每一次循环中,都会从hub_event_list中取出一个hub对象,然后对hub作出处理。当hub_event_list中的所有hub对象全部取出后,while退出。hub_events函数也返回。
再看看在一次循环中,如何处理hub:
l 根据hub同时获得它的usb_device对象hdev和USB接口对象intf。
l 查看这个hub是否已经断开了连接,如果是则退出这一次循环。
l 判断这个hub是否已经插入到(其它USB集线器,当然root除外)端口上,如果没有退出这一次循环。
l 调用usb_autopm_get_interface()函数给这个USB接口的电源引用计数加一。这样这个设备就不允许autosuspend(自动挂起)。
l 这时,程序就开始扫描hub的各个端口的状态了。hub->descriptor->bNbrPort记录了该USB集线器的端口个数。我们看看扫描每个hub的端口时,都做了什么:
n 检查该端口是否处于忙状态,如果是则跳过。一般来说当这个端口正在执行reset或者resume操作时,才会处于忙状态。
n 调用hub_port_status()函数获取portstatus和portchange两个值。这两个是通过向USB集线器发出GET_STATUS的USB_REQ_GET_STATUS请求而获得的。下面任务就是根据这两个值处理对应的端口以及是否把connect_change变量置1。
n 检查端口的Current Connect Status位是否变化。如果有,则表明端口的连接状态发生了变化。这时发出USB_CLEAR_FEATURE的USB_RT_PORT请求清除这个标志,同时把connect_change置1。
n 检查端口的开关状态。这是叫做enable和disable一个端口。如果端口开关发生变化,发出请求清除这个位。当端口开关发生变化时,都是端口从enable状态变成了disable状态。原因是hub端口是不可以直接设置成enable的。把hub端口设置成eable的方法只有“reset hub port”。
如果端口有设备连接,端口连接没有发生变化,而端口变成了disable,那么唯一的原因是发生了电磁干扰了(EMI)。这时,把connect_change设置为1。
n 检查这个端口的设备的suspend状态是否有变化。如果有,那一定是从suspend状态出来的。那么把该端口下设备唤醒。
n 检查这个端口是否发生过过流的状况。如果有,那么其它端口一定发生过断电流的情况。那么要做的就是清除这个位,给hub重新上电。
n 检查这个端口是否复位过。如果是,那么端口是从disable状态进了enable状态。这清除这个位。
n 如果connect_change被设置成了1,则调用hub_port_connect_change()函数。在该函数中会传该端口号的number,portstatus和portchange。在该函数中,设备到底刚插上去,还是刚拔下来,还是刚唤醒都会得到相应的处理。
l 然后检查下一个端口。可能端口检查完了,函数返回。
前面说过,当hub的端口发生变化时,hub_port_connect_change函数就会针对该端口作出处理。由于在前面的connect_change被设置为1,而导致hub_port_connect_change调用的情况有二:1、端口连接发生变化;2、发生电磁干扰(上面有红色标注)。而端口变化也有两种情况:设备插入到端口和设备从端口中拔出。
在hub的该端口中(hdev->children[port - 1])获取struct usb_device对象udev。如果该对象是存在,那么该设备一定已经从端口拔出。调用usb_disconnect函数该设备。在该函数中会关闭该端口,销毁该udev,回收USB地址。
为了确定设备是否真的插入到了端口,还需要做消抖处理。做法是在一段时间内不断地发出GET_STATUS请求给该端口,获得连接改变状态和连接状态(portstatus和portchange)。如果在这段时间内返回的状态那是设备插入状态,那么可以判断设备是插稳了。
下面的工作就是给新插入USB设备生成struct usb_device对象;分配USB地址;配置USB设备;那设备找到驱动程序。在这时设备进入了Attached状态。
调用usb_alloc_dev()函数给一个struct usb_device类型的udev指针分配内存空间,同时作一些初始化工作。在该函数udev的dev的type成员被设置成了usb_device_type,这是非常重要的,在下面有描述。
这里是调用了usb_set_device_state()函数把udev设置为USB_STATE_POWERED,进入Power状态。这里仅是对变量设置。而没有设置到USB设备中。同时:
l 把udev的speed成员设置为USB_SPEED_UNKNOW;
l 把udev的bus_mA设置为hub->mA_per_port。这是把USB集线器一个端口能供给的电流给这个USB设备。如果这个USB集线器是自供电,那就能分配500mA的电流;如果这个USB集线器是总线供电,那最多能分配500mA除以端口数的电流。
l 把udev的level成员设置为hub->level + 1。在USB总线中,所有设备是通过USB集线器级连的。第一个USB集线器,其下的端口就是增加了一级。
l 同时设置udev所属的USB集线器就是这个hub。
在这时仅是把这个地址分配给这个udev,并没有设置到设备中。这时设备还是用默认的0地址。
下面就进入hub_port_init()函数对udev作进一步的初始化了。
l 首先调用hub_port_reset()函数复位hub的端口,然后获取设备的速率类型(高、全、低速)。在该函数中,先向端口发送USB_PORT_FEAT_RESET命令到hub复位该端口;然后发送USB_REQ_GET_STATUS命令到hub获取该端口的状态和改变状态(其实前面也做了一次的)。在返回状态中判断出设备的速率类型。
l 根据设备的速率类型设置设备端点0单个包的最大传输大小。对于无线USB设备(WUSB)是512字节;对高速USB设备是64字节;对低速是8字节。但对于全速USB设备就不好说了,可能是8字节、16字节、32字节或者64字节。在这个阶段对于全速设备先设置为64字节。
l 现在需要正式确定端口0的单个传输包大小的值了。这里的确定是需要在USB设备的端口中读取设备描述符中获取。这个设备描述符有18个字节,在第8个字节就标记了这个值。但问题就是:我们知道这个值在设备的描述符中,但我们又依靠这个值来获取这个描述符。正规的解决是向设备发出请求设备描述符命令,要求设备返回8个字节,那么在返回的第8个字节中,就是我们需要的值。windows的方法是要求设备返回64个字节,如果设备一次最多传输32字节或64字节,那么直接返回18个字节(设备描述符的大小);如果设备一次最多传输8个或16个字节,那个18个字节得分多次返回了。但我们只要前面的第8个就可以了,所以后面的我们不要求设备传输了,做法就是把设备直接复位。windows的方法是有bug的,但很多的厂商都是这样测试的。在这里也只能兼容这个方法。
在这里先用windows的方法试,然后调用hub_set_address()函数把地址设置到设备中。这里设备进入地址状态。如果windows的方法不通(使用USB_NEW_SCHEME来测试),就使用Linux的方法。
l 完成后,就向设备请求设备描述符。
l hub_port_init()也返回了。
如果是,那么是查看分配给它的电流是否大于100mA(这是上一级hub分配的)。如果小于100mA,那么就需要向设备请求状态。在返回状态中,查看是否自供电设备,如果不是,出错返回。
配置设备的过程实际就在设备中读取的设备描述符、配置描述符、接口描述符等,进一步初始化这个设备对象udev。在但这之前,需要先确定hub是还处于连接状态。如果是把udev添加到hub->children中。
配置设备和寻找驱动通过在通过usb_new_device()中完成的。在该函数中通过usb_configure_device()函数完成配置设备,这其实设备枚举过程。配置完成设备后,设备进入配置状态,就可以把这个udev的dev成员注册后usb_bus_type中了。在这里会为它找到合适的驱动程序的。具体情况在下一章详细讲。
在上一章说到,当USB设备插入到USB集线器后,USB集线器会检测到这个插入设备,然后为它加电、分配struct usb_device对象、分配地址、配置,最后为它找到驱动程序。这里我们重点讲USB驱动子系统是如何配置USB设备的,因为USB设备完成配置后,它的struct usb_device对象(udev)已经完全丰满了。
一般来说单个USB设备驱动程序是不会直接提供所谓的USB设备文件节点给用户直接使用的。USB驱动程序都是以其它形式的驱动使用,如键盘驱动、摄像头驱动、串口驱动等。那么USB驱动程序都有通用的数据结构,按通用的方法找到USB设备。如果我们知道USB驱动程序是如何找到USB设备的,那么我们就更清楚USB设备需要初始化什么内容,也是说需要配置什么内容。
USB驱动程序的主体数据结构是struct usb_driver,如程序清单 2.1所示。
程序清单 2.1 usb_driver结构
struct usb_driver {
const char *name;
int (*probe) (struct usb_interface *intf,
const struct usb_device_id *id);
void (*disconnect) (struct usb_interface *intf);
int (*ioctl) (struct usb_interface *intf, unsigned int code,
void *buf);
int (*suspend) (struct usb_interface *intf, pm_message_t message);
int (*resume) (struct usb_interface *intf);
int (*reset_resume)(struct usb_interface *intf);
int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf);
const struct usb_device_id *id_table;
struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned int no_dynamic_id:1;
unsigned int supports_autosuspend:1;
unsigned int soft_unbind:1;
};
在该结构中,我们只关心加粗的部分就够了。probe函数指针是用于找到USB设备的功能(接口)后的初始化工作;disconnect函数指针是拔USB设备后的清理工作。那么我们还需要关注id_table成员,这是一个数组列表成员。该列表列出了哪设备(什么厂商、什么产品、什么类型等)可以被本驱动程序所驱动。
当USB驱动程序struct usb_driver结构准备好后就会注册到usb_bus_type总线结构中。当设备对象注册到usb_bus_type时,就会把已经注册到usb_bus_type的但还有没有找到驱动程序的所有设备对象和它进行配对。配对标准就是把struct usb_driver对象的id_table成员的列表中是否有一个和设备对象可以吻合。如果吻合就可以驱动了。在一个设备对象注册到usb_bus_type时,也会历经同样的过程。
那么再说说id_table列表中元素是什么样的数据结构,如程序清单 2.2所示。
程序清单 2.2 usb_device_id结构
struct usb_device_id {
/* which fields to match against? */
__u16 match_flags;
/* Used for product specific matches; range is inclusive */
__u16 idVendor;
__u16 idProduct;
__u16 bcdDevice_lo;
__u16 bcdDevice_hi;
/* Used for device class matches */
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
/* Used for interface class matches */
__u8 bInterfaceClass;
__u8 bInterfaceSubClass;
__u8 bInterfaceProtocol;
/* not matched against */
kernel_ulong_t driver_info;
};
抄录LDD3对该结构的描述:
idVendor 设备的USB制造商ID。该编号是USB论坛指派给其成员的,不会由其他人指定。
idProduct 设备的USB产品ID。所有指派了制造商ID的制造商都可以随意地赋予其产品ID。
bcdDevice_lo
bcdDevice_hi
定义了制作商指派的产品的版本号范围的最低和最商值。bcdDevice_hi值包括在内;该值是最高编号的设备的编号。这两个值以二进制编码的十进制(BCD)格式来表示。这些变量,加上idVendor和idProduct,用来指定设备的特定版本号。
bDeviceClass
bDeviceSubClass
bDeviceProtocol
分别定义设备的类型、子类型和协议。这些编号由USB论坛指派,定义在USB规范中。这些值详细说明了整个设备的行为,包括该设备的所有接口。
bInterfaceClass
bInterfaceSubClass
bInterfaceProtocol
和上述设备特定值很类似,这些值分别定义类型、子类型和单个接口的协议。这些编号由USB论坛指派,定义在USB规范中。
当然上述这么多的ID也不是所有的都是匹配,具体需要什么样的ID是由match_flags成员来指定。哪些ID是必须匹配的,那么就在match_flags中对应的位置位。相应的位可以下面的宏来表示:
#define USB_DEVICE_ID_MATCH_VENDOR 0x0001
#define USB_DEVICE_ID_MATCH_PRODUCT 0x0002
#define USB_DEVICE_ID_MATCH_DEV_LO 0x0004
#define USB_DEVICE_ID_MATCH_DEV_HI 0x0008
#define USB_DEVICE_ID_MATCH_DEV_CLASS 0x0010
#define USB_DEVICE_ID_MATCH_DEV_SUBCLASS 0x0020
#define USB_DEVICE_ID_MATCH_DEV_PROTOCOL 0x0040
#define USB_DEVICE_ID_MATCH_INT_CLASS 0x0080
#define USB_DEVICE_ID_MATCH_INT_SUBCLASS 0x0100
#define USB_DEVICE_ID_MATCH_INT_PROTOCOL 0x0200
那么到这里就很清楚:当一个USB设备插入到USB集线器后,在对该设备完成配置后,其struct usb_device对象必须能表明自己的身份(收集各种ID信息),才能在注册到usb_bus_type后能找到正确的驱动程序。
设备只有通过枚举才能被驱动。枚举的过程就是USB子系统在USB设备中读取其描述符。且看看这四种描述符以及相关的数据结构。
每一个USB设备都有一个设备描述符,而且只有一个。其它数据结构定义如程序清单 2.3所示。
程序清单 2.3 设备描述符
struct usb_device_descriptor {
__u8 bLength; //描述符长度,对于设备描述符应该18
__u8 bDescriptorType; //描述类型,对于设备描述符是USB_DT_DEVICE
__le16 bcdUSB; //USB协议的版本号
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0; //这是第8字节,端口0的一次可以处理最大的字节数
__le16 idVendor; //厂商ID
__le16 idProduct; //产品ID
__le16 bcdDevice; //设备版本号
__u8 iManufacturer; //厂商的字符串的索引值
__u8 iProduct; //产品的字符串的索引值
__u8 iSerialNumber; //序列号的字符串的索引值
__u8 bNumConfigurations; //设备当前速度模式下的支持的配置数量。
} __attribute__ ((packed));
一个USB设备可能有多个配置描述符,每一个配置描述符代表一个配置。例如一台手机在作为USB设备插入主机后,可能作为U盘使用,或作为摄影头使用,或作为网卡使用,或作为键盘使用(当然是很愚蠢假设)。具体是哪一种方式是看使用哪一个配置。这个配置的选择可能由手机用户在手机上作了设置,让主机只能读到这个唯一配置描述符。或者主机读取了所有配置描述符,然后按主机用户或驱动的规则自行选择。总之,配置描述符让USB设备可以表现不同的功能。
配置描述符的数据结构定义如程序清单 2.4。
程序清单 2.4 配置描述符
struct usb_config_descriptor {
__u8 bLength; //描述符的长度,这里应该是USB_DTCONFIG_SIZE
__u8 bDescriptorType; //描述符的类型,这里应该是USB_DT_CONFIG
__le16 wTotalLength; //
__u8 bNumInterfaces; //这个配置包含的接口个数
__u8 bConfigurationValue;
__u8 iConfiguration; //描述配置信息的字符串描述符的索引值
__u8 bmAttributes; //表示了配置的一些特点,可以设置下面的宏
#define USB_CONFIG_ATT_ONE (1 << 7) /* must be set */
#define USB_CONFIG_ATT_SELFPOWER (1 << 6) /* 自供电 */
#define USB_CONFIG_ATT_WAKEUP (1 << 5) /* 支持远程*/
#define USB_CONFIG_ATT_BATTERY (1 << 4) /* battery powered */
__u8 bMaxPower; //设备正常运转时,从总线那里分得的最大电流值,以2mA为单位。
设备可以使用这个成员向hub表明自己需要的电流,但如果设备需求电流求多,请求超出了hub所能给予的,hub就会直接拒绝。
} __attribute__ ((packed));
当驱动使用GET-DESCRIPTOR请求从设备中获得配置描述符信息时,返回的数据长度可能会包括配置描述符、接口描述符、端点描述符,总长度由wTotalLength描述。
当驱动获取了USB设备的所有配置描述符后,就必须决定USB设备使用哪一个配置。当决定使用哪一个配置后,就使用SET_CONFIGURATION请求向USB设备设置这个配置值。具体配置值由其描述符的iConfiguration成员描述。
Linux还有使用另一个数据结构对配置描述符进一步包裹。该数据结构如程序清单 2.5所示。
程序清单 2.5 usb_host_config的定义
struct usb_host_config {
struct usb_config_descriptor desc; // 配置描述符
char *string; /* iConfiguration string, if present */
struct usb_interface_assoc_descriptor *intf_assoc[USB_MAXIADS];
struct usb_interface *interface[USB_MAXINTERFACES];
struct usb_interface_cache *intf_cache[USB_MAXINTERFACES];
unsigned char *extra; /* Extra descriptors */
int extralen;
};
interface[USB_MAXINTERFACES],配置所含的接口。这个数组的顺序未必是按照配置晨接口号的顺序。
intf_cache[USB_MAXINTERFACES]是接口缓存。
如上例所示,假如把手机的配置成了键盘模式,而该模式同时有键盘和模拟鼠标功能,那么我们就说在该配置下有两个接口。自然而然,为了让这两功能也能同时使用,这两个功能对应接口也需要各自的驱动,分别是键盘驱动和鼠标驱动。关于接口的数据结构如程序清单 2.6所示。
程序清单 2.6 usb_interface结构
struct usb_interface {
/* 可选设置列表 */
struct usb_host_interface *altsetting;
struct usb_host_interface *cur_altsetting; /* 当前的设置 */
unsigned num_altsetting; /* 可选设置数量 */
/* If there is an interface association descriptor then it will list
* the associated interfaces */
struct usb_interface_assoc_descriptor *intf_assoc;
int minor; /* minor number this interface is bound to */
enum usb_interface_condition condition; /* state of binding */
unsigned is_active:1; /* the interface is not suspended */
unsigned sysfs_files_created:1; /* the sysfs attributes exist */
unsigned unregistering:1; /* unregistration is in progress */
unsigned needs_remote_wakeup:1; /* driver requires remote wakeup */
unsigned needs_altsetting0:1; /* switch to altsetting 0 is pending */
unsigned needs_binding:1; /* needs delayed unbind/rebind */
struct device dev; /* interface specific device info */
struct device *usb_dev;
int pm_usage_cnt; /* usage counter for autosuspend */
};
设置的数据结构如程序清单 2.7所示。
程序清单 2.7 usb_host_interface结构
struct usb_host_interface {
struct usb_interface_descriptor desc; //该设置下的接口描述符
/* array of desc.bNumEndpoint endpoints associated with this
* interface setting. these will be in no particular order.
*/
struct usb_host_endpoint *endpoint;
char *string; /* iInterface string, if present */
unsigned char *extra; /* Extra descriptors */
int extralen;
};
一个接口可以有多个设置,使用不同的设置,接口描述符的信息会有不同,所以接口描述符并没有放在struct usb_interface结构中,而是放在表示接口设置的struct usb_host_interface结构中。这里需要让我们记住的是并不是一个接口描述符就是代表一个接口,而一个设置。接口描述符的bInterfaceNumber域指其所属的接口,bAlternateSetting域指出其设置号。该结构如程序清单 2.8所示。
程序清单 2.8 接口描述符
struct usb_interface_descriptor {
__u8 bLength; // 描述符长度
__u8 bDescriptorType; //描述符类型,这里应该是USB_DT_INTERFACE
__u8 bInterfaceNumber; //接口号。每个配置可以包含多个接口,这个就是索引值
__u8 bAlternateSetting; //接口使用的是哪个可选设置。默认是0
__u8 bNumEndpoints; //该接口的端点数,不包括0,端点0是所有设备都必须的
__u8 bInterfaceClass; //接口类型
__u8 bInterfaceSubClass; //接口子类
__u8 bInterfaceProtocol;
__u8 iInterface; //字符串索引值
} __attribute__ ((packed));
端点是USB数据传输的终点。看一看它在内核的定义,如程序清单 2.9所示。
程序清单 2.9 usb_host_endpoint结构
struct usb_host_endpoint {
struct usb_endpoint_descriptor desc; // 端点描述符
struct list_head urb_list; //该端点下的urb列队
void *hcpriv; //是给HCD(Host Controller Driver)用的
struct ep_device *ep_dev; /* For sysfs info */
unsigned char *extra; /* Extra descriptors */
int extralen;
int enabled;
};
端点描述符如程序清单 2.10所示。
程序清单 2.10 端点描述符的定义
struct usb_endpoint_descriptor {
__u8 bLength; //描述符长度
__u8 bDescriptorType; //描述符类型,这里应该是USB_DT_ENDPOINT
__u8 bEndpointAddress; //端点地址,第0~3位表示端点号,第8位表示方向
__u8 bmAttributes; //其中前2位:00表示控制传输,01表示传输,10表示批量传输
//11表示中断传输
__le16 wMaxPacketSize; //该端点一次可以处理的最大字节数
__u8 bInterval; //USB是轮询式总线,该值是希望USB轮询自己的时间间隔
/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));
接住前面的话题,我们讲在USB设备被复位、分配地址后,是如何被配置的。
在配置USB设备之前,我们需要先读取USB设备的所有描述符,然后对描述符进行分析。该操作是在usb_get_configuration函数中进行。注意在进入该函数之前USB设备的设备描述符已经在地址配置阶段读出(见第一章,1.4.2小节)。在设备描述符中已经标记了有多少个配置描述符,这里假设有ncfg个。那个我们需要从0 ~ ncfg作为索引值读出所有的配置描述符。
假设我们要读取这个配置描述符的索引号为n,我们看看是如何读取配置描述符。
读取配置描述符的操作很简单,只向USB设备发送USB_REQ_GET_DESCRIPTOR请求以获取描述符,描述符的类型是USB_DT_CONF,索引号为n,要求返回的数据大小是USB_DT_CONFIG_SIZE(一个配置描述符的大小)。但是这里仅是返回一个配置描述符,我们还需要该配置下的所有接口描述符、端点描述符。其实,向USB设备发送USB_REQ_GET_DESCRIPTOR请求USB_DT_CONF类型的描述符方法也获得该配置下所有接口描述符和端点描述符,关键是指出这些所有的配置描述符、接口描述符和端点描述符总共大小。而这个值在就在刚才获得的配置描述符中的wTotalLength域中。使用这个值,我们就可以再一次发送请求获得我们需要的数据。返回的配置描述符,以及该配置下的接口描述符、端点描述符保存在bigbuffer缓冲区中。
在bigbuffer缓冲中,各个描述符并不是杂乱无序的。一般来说,配置描述符后面跟的是第一个接口描述符,接着是这个接口里第一个端点的端点描述符,如果有class-和vendor-specific描述符的话,会紧跟在对应的标准描述符后面,不管接口有多少端点都是按照这个规律顺序排列,如图 2.1所示。
图 2.1 描述符排序
当然有一些设备生产厂商可能会有点不一样,喜欢选择返回第二个接口然后再返回第一个接口。
分析bigbuffer缓冲区的第一任务就是把配置描述符提取出来。做法很简单,只需要把bigbuffer缓冲区的头USB_DT_CONFIG_SIZE(配置描述符的大小),复制到udev的->config[n]->desc即可。该任务是在usb_get_configuration()函数中进行。
在分析bigbuffer中的接口时,还是再一次强调的是:bigbuffer缓冲区中有多个接口描述符,但并不是说一个接口描述符就是代表一个接口;所有接口号相同的接口描述符集合是代表一个接口;接口号相同而设置号相异的接口描述符是代表其各自的设置。因此我们需要先统计bigbuffer缓冲区中有多少个接口,每个接口下面有多个设置。
l 统计接口和设置
由于前面已经分析出了配置描述符,所以分析其余的描述符是从如图 2.2所示的位置开始。
图 2.2 扫描接口
要找出上面的缓冲区中有多少个接口,每个接口下面有多少个设置,那么首先就要扫描出bigbuffer缓冲区中的所有接口描述符。但是上面的各种描述符都挤在一起如何判断哪些数据是什么描述符呢?其实虽然各种描述符的数据长度各异,但都是有相同的数据头,如图 2.3所示。
图 2.3 描述符的数据头
所有描述符的第一个字节都是表示描述符长度的域,第二个字节都是表示描述符类型的域。那么如果知道第一个描述符的开始,就在其第一个字节中得知该描述符的长度,同时也知道该描述符结束位置。在该描述符的结束位置的下一个字节,就是下一个描述符的开始。如此类推,我们就可以介定bigbuffer缓冲区中的所有描述符。
在扫描每一个描述符时,通过其第二个字节得知该描述符类型。如果该描述符的类型是USB_DT_INTERFACE,也是就是接口描述符类型,那么我们就作以下统计:
n 根据该接口描述符的接口号inum,记录在inums[i](inums[i] = inum);如果之前已经探测到有同样的接口号,就不必记录了。这个i值的含义是这样:第1个扫描出来的接口号就放在inums[0];第i个扫描出来的接口号就放在inums[i]中。如果想知道接口n在inums的什么位置,用下面的代码就可以了:
for (i = 0 ; i < num_of_intf; i++) { // num_of_intf是接口的个数
if (n == inums[i])
break;
}
n nalts[i]++。表示该接口下又检测了一个设置,所以加一。
上面我们已经统计出该配置下有多少个接口,以及每个接口下有多少设置,但还没有作实质的处理。哪作什么样的处理呢?如果udev->config[i]表示该配置,那么udev->config[i]->intf_cache[]数组中,每一个元素是struct usb_interface_cache结构,代表该配置下一个接口。struct usb_interface_cache结构中的num_altsetting成员表示该接口下有多少个设置;其altsetting[0]成员是一个struct usb_host_interface结构的可变长度数组,每个数组元素下的desc成员是指向一个接口描述符,如图 2.4所示。
图 2.4 配置、接口、设置的组织结构
既然我们都已经知道了结果,那么其中的过程也就很简单了,这里不再描述了。
在图 2.1知道,在bigbuffer缓冲区中每个接口描述符后面都紧跟着它的所有端点描述符。那么在上面的整理到每一个设置时,也需要把该设置后面的端点充实到其endpoint中,如图 2.5所示。
图 2.5 端点的组织
完成了上述的操作后,内核执行路径就返回到usb_new_device()函数中。该函数会把udev->dev注册到usb_bus_type总线结构中。我们应该还记得udev->dev的的type成员被设置成了usb_device_type。完成注册后usb_device_type的match成员指向usb_device_match()函数就会被调用。
在usb_device_match()函数中如果传入参数dev的类型是usb_device_type。那么内核就会为它指定驱动:usb_generic_driver。同时调用usb_generic_driver的probe指向探测函数。该驱动是一个USB通用驱动。这个驱动不是用于驱动具体的USB设备的,是用于完成USB设备的配置工作的。
在driver/usb/core/usb.c的初始化函数中就注册了一个struct usb_device_driver类型驱动对象usb_generic_driver。usb_generic_driver的probe成员指向generic_probe()函数。当内核注册了一个usb_device_type类型USB设备后,generic_probe()函数就会被执行,同时传入这个USB设备udev作为参数。
我们好好看看这个generic_probe函数是如何完成配置USB设备工作的。
l 选择配置
在前面的描述中得知,我们从USB设备中读取出来的配置描述符可能有多个,这表示USB设备可以有多个配置使用。但是一个时刻只可以有一个配置可以使用。哪么到底是选择一个配置呢?按什么样的标准来选择配置呢?usb_choose_configuration() 函数中完成这一切。
在usb_choose_configuration()函数中会逐个扫描udev中的配置(其config成员指向数组)。对于每一个配置usb_choose_configuration()函数都按自己的标准判断它是否合适使用:该配置是否有接口;配置需要的电流是否过大;是否更符合USB_CLASS_VIDEO、USB_CLASS_AUDIO类型。当然如果实在找不到“最好的”,那就找随意一个的。
当usb_choose_configuration()函数找到合适的配置后,返回这个配置在udev->config指向的数组中的索引值c。
l 配置设备
选择好配置后,就可以配置设备了,这是由usb_set_configuration()函数完成的。配置设备的操作很简单,只需要向USB设备发出USB_REQ_SET_CONFIGURATION请求即可,参数是刚找到的配置的配置号。但前提是先把这个USB设备唤醒,同时这个USB设备已经处于地址状态。这时USB设备进入了配置状态。完成后udev->actconfig指向这个配置,表示当前USB设备正在使用的配置。
配置是处理好了,但该配置下的各个接口还等着处理。根据该配置描述符的bNumInterface域就可以知道该配置下有多少个接口,然后为每个接口分配内存空间。对于第i个接口对象intf作如下处理:
1. 使udev->actconfig->interface[i]指向intf。建立当前配置与这个接口的联系。
2. 指出这个接口intf有哪些可选设置。就是把intf->altsetting指向udev->actconfig->intf_cache[i]。
3. 启用该接口的第1个设置。就是在udev->actconfig->intf_cache[i]中找出设置号为0的设置,然后使用intf->cur_altsetting指向这个设置。
4. 启用刚才启用设置的所有端点。
5. 初始化这个intf的dev成员:
n 使dev->parent指向udev->dev
n 使dev->bus指向&usb_bus_type。这样当这个dev注册后,就会注册到usb_bus_type上,并在usb_bus_type寻找驱动。
n 把dev类型设置为usb_if_device_type。
n 注册这个dev。
n 为这个dev在/sys下建立文件系统。
整个过程的结果如图 2.6所示。
图 2.6 生成的接口
到此注册udev设备的操作已经完成。
到这里,我们清楚地看到在当USB设备插入到集线器后,集线器驱动为这个USB设备生成的struct usb_device类型的对象udev并没有直接驱动USB设备的任何具体功能。真正驱动USB设备的具体功能的是udev下的struct usb_interface类型的各接口对象。所谓的USB摄像头驱动、USB键盘驱动等,它们驱动的是接口对象。虽然它们的dev都是注册到usb_bus_type总线上,但是它们的设备类型不一样:udev的dev类型是usb_device_type;接口对象的dev类型是usb_if_device_type。当一个dev注册到usb_bus_type总线时,就会检测这个dev的类型,如果这个dev类型是usb_device_type,说明这个设备是udev,就会为它找到通用驱动usb_generic_driver,为它配置设备,生成接口,并注册接口;如果检测这个dev类型是usb_if_device_type时,说明这个设备是接口,就会为它找到专用的驱动。
当一个设备注册到usb_bus_type总线时,内核就会扫描注册到usb_bus_type总线上所有驱动。每扫描一个驱动,这以这个驱动和这个设备作为参数调用usb_bus_type的match成员指向usb_device_match()函数。如果这个设备是usb_device_type类型的,那么只有usb_generic_driver驱动和它相匹配;如果这个设备是usb_if_device_type类型的,说明这个是接口设备,那么就对比这个驱动的id_table列表是否和这个接口匹配,若匹配就可以驱动了。找到驱动后,驱动的probe指向的探测函数就被调用,并以这个接口作为参数,完成初始化工作,详情参见USB驱动程序这一小节。
检测接口设备与驱动是否匹配是在usb_match_id()函数中进行。该函数把id_table列表的每一项都和接口进行检测是否匹配。id_table列表每一项都包括多个匹配的信息,但不是每一个都是必须检测匹配的,至于是哪些信息是必须检测的,由其检测掩码决定。假如id_table的其中一项为id,那么在检测id与接口是否匹配时,过程分两部份:
l 检测接口所属的udev的设备描述符信息和id的设备部分信息是否匹配,包括厂商ID、产品ID、产品版本ID等。如果匹配进入下一步。
l 检测接口当前设置的接口描述符信息和id的接口部分信息是否匹配,包括检测接口类型、接口子类、接口协议。
只要id_table列表下的项目有一项和接口匹配,那么这个驱动就可以驱动这个接口了。具体检测过程如图 2.7所示。
图 2.7 检测接口和id_table是否匹配
到此为止,如果注册的接口都能找适合的驱动。那么这个USB设备的功能就可以使用了。
在上一章,我们说了USB集线器的工作过程。USB集线器本质是为其下面的端口服务的,主要是用于处理其端口的USB设备的插入、拔出、挂起。但是我们还没有谈到USB集线器的驱动程序。了解USB集线器的驱动对了解整个USB驱动子程序的结构很有帮助。
要知道USB集线器在本质也是USB设备。当一个USB集线器插入到上一级的USB集线器的端口时,就会经历:
1. 上一级的USB集线器也会立即知道有端口的状态发生变化;
2. 然后上一级USB集线器把自己的hub对象添加到hub_event_list,唤醒hub_thread线程;
3. 在hub_thread线程被唤醒后,hub_events函数就会被调用,在该函数中会检测到上一级USB集线器的哪一个端口状态发生变化,并知道是有新的USB设备插入;
4. 然后就会这个新插入的USB设备分配udev、分配USB地址、读取设备描述符、注册udev;
5. 在完成注册udev后,就会在usb_generic_driver驱动中配置设备,然后找到接口;
6. 找到接口后,就会为接口分配struct usb_interface类型的接口对象;
7. 最后注册接口对象。因为这是集线器设备,所有会为接口对象找到USB集线器驱动。
它经历了所有一般USB设备在进入生命历程的一切,因此它和所有的USB设备没有本质的区别。但和一般USB设备不同的是,它支撑了USB总线的架构。
在drivers/usb/core/usb.c的usb_init()初始化函数中,就调用了usb_hub_init()函数用于初始化USB集线器驱动。usb_hub_init()函数定义在drivers/usb/core/hub.c文件中,如程序清单 2.11所示。
程序清单 2.11 hub驱动的初始化函数
int usb_hub_init(void)
{
if (usb_register(&hub_driver) < 0) {
printk(KERN_ERR "%s: can't register hub driver\n",
usbcore_name);
return -1;
}
khubd_task = kthread_run(hub_thread, NULL, "khubd");
if (!IS_ERR(khubd_task))
return 0;
/* Fall through if kernel_thread failed */
usb_deregister(&hub_driver);
printk(KERN_ERR "%s: can't start khubd\n", usbcore_name);
return -1;
}
该函数很简单:注册了一个hub_driver;生成了hub_thread内核线程。hub_thread内核线程的作用在前面了详细的描述。且看看hub_driver的定义,如程序清单 2.12所示。
程序清单 2.12 hub驱动对象
static struct usb_driver hub_driver = {
.name = "hub",
.probe = hub_probe,
.disconnect = hub_disconnect,
.suspend = hub_suspend,
.resume = hub_resume,
.reset_resume = hub_reset_resume,
.pre_reset = hub_pre_reset,
.post_reset = hub_post_reset,
.ioctl = hub_ioctl,
.id_table = hub_id_table,
.supports_autosuspend = 1,
};
这里我们重点看上面加粗的部分就可以了。id_table是记录了hub设备的基本信息,如程序清单 2.13所示。
程序清单 2.13 hub_id_table的定义
#define USB_CLASS_HUB 9
static struct usb_device_id hub_id_table [] = {
{ .match_flags = USB_DEVICE_ID_MATCH_DEV_CLASS, // 表示只需匹配设备类型ID
.bDeviceClass = USB_CLASS_HUB},
{ .match_flags = USB_DEVICE_ID_MATCH_INT_CLASS, // 表示只需匹配接口类型ID
.bInterfaceClass = USB_CLASS_HUB},
{ } /* Terminating entry */
};
hub_id_table表明,如果一个接口注册后,若它的设备类型ID或接口类型ID为9,那么它就可以被hub_driver驱动。那么hub_driver的probe成员指向hub_probe函数就会被调用,执行这个hub设备的初始化工作。
当hub_probe被调用时,传入的参数是这个接口的struct usb_interface 类型的intf对象以及这个接口所匹配的id指针。看看hub_probe需要做点什么:
l 所获得这个接口的当前设置desc以及这个接口的所属的USB设备(struct usb_device类型的)hdev。
l 检查这个接口的子类型id是否0或1。如果不是,则说明这不是一个合法的hub设备。
l 要知道hub设备是只有一个端点,而且是中断传输。那么这里就需要检测这个设备是不是只有一个端点,如果是,则获取这个端点描述符endpoint;如果不是,这不是合法的hub设备。
l 检查endpoint是不是中断传输端点。
l 走到这一步,我们基本可以确定系统的确是有hub设备接入。这时我们可以生成一个struct usb_hub类型的对象hub。下面的工作的重点就是初始化这个hub。
l 初始hub->event_list链表头。要知道当需要把这个hub添加到hub_event_list链表时(见第1章,第1节的内容 ),就是依靠这个成员。
l 然后调用hub_configure()函数初始化hub,传入的参数有hub和endpoint。再看看hub_configure()做了什么:
n 向新接入的集线器发出USB_REQ_GET_DESCRIPTOR请求,请求内容是USB_DT_HUB。这是USB协议的标准请求,是为了在集线器中获取集线器描述符。看看这个集线器描述符的内容,如表 2.1所示。
表 2.1 集线器描述符
偏移 |
域 |
大小 |
描述 |
0 |
bDescLength |
1 |
这个描述符的字节数,包括这个字节 |
1 |
bDescriptorType |
1 |
描述符类型,集线器描述符是29H |
2 |
bNbrPorts |
1 |
这个集线器支持的下行端口数量 |
3 |
wHubCharacteristisc |
2 |
D1~D0:逻辑电源开关模式 00 成组的电源开关(所有端口一起上电) 01 单个的端口电源开关 1x 保留 D2:识别一个复合设备 0 集线器不是复合设备的一部分 1 集线器是复合设备的一部分 D4~D3:过电流保护模式 00 整个的过电流保护。集线器报告所有端口电流总和的过电流情况,不细分为每个端口的过电流状态 01 个别端口的过电流保护。集线器在每个端口的基础上报告过电流的情况。每个端口有一个过电流状态 1x 没有过电流保护。这个选项只允许需要执行过电流保护的总线供电集线器使用 D6~D5:TT考虑时间 00 TT在全速/低速下行总线上要求处理间间隙最多是8个FS位时间 01 TT要求最多16个FS位时间 02 TT要求最多24个FS位时间 03 TT要求最多 32个FS位时间 D7:支持的端口指示灯 0 该下行端口不支持指示灯,而且PORT_INDICATORY请求无效。 1 该下行端口支持端口指示灯,而且PORT_INDICATOR请求有效。 D15~D8:保留 |
5 |
bPwrOn2PwrGood |
1 |
从端口启动上电序列到端口电源有效的时间(在2ms的间隔内)。USB系统软件使用这个值确定在访问上电的端口前要等待多少时间 |
6 |
bHubContrCurrent |
1 |
集线器控制器电子要求的最大电流(以mA为单位计算) |
7 |
DeviceRemoveable |
变量,取决hub端口数 |
指出端口连接的是否为可拆卸设备。这个场用一字节的宽度来报告。在一字节中,如果给定的位置没有端口存在,则这个场返回0来表示该端口特性 位值的定义: 0B——可拆卸设备 1B——不拆卸设备
这是对应于集线器各个端口的位映像: 位0 保留到将来使用 位1 端口1 位2 端口2 …… 位n 端口n(由应用确定,最大可达255个端口) |
变量 |
PortPwrCtrMask |
变量,取决于集线器的端口数 |
这个场是为了与为USB1.0设备所写的软件兼容而产生。 |
n 检查读入集线器描述符中的bNbrPorts域是否大于31。USB集线器的下行端口是不能大于31个。
n 检查读入集线器描述符中的wHubCharacteristisc域的D2位,看集线器设备是否复合设备,打印信息(什么也没有做)。
n 检查读入集线器描述符中的wHubCharacteristisc域的D1~D0位,看集线器的逻辑电源开关模式,打印信息(什么也没有做)。
n 检查读入集线器描述符中的wHubCharacteristisc域的D4~D3位,看集线器的过电流保护模式,打印信息(什么也没有做)。
n 处理TT电路。
n 检查读入集线器描述符中的wHubCharacteristisc域的D7位,如果该为1则把hub->has_indicators设置为1。表示这个集线器是有指示灯的。
n 向集线器发出GET_STATUS请求,请求内容是USB_RECIP_DEVICE,表示返回集线器的状态。集线器在接收到这个请求后就会返回一个16位的集线器状态值,如表 2.2所示。
表 2.2 集线器状态域
位 |
描述 |
0 |
本地电源:这是一个本地供电的电源 这个域指出集线器的电源是外部电源提供还是由USB提供。这个场允许USB系统软件确定从集线器向下行端口提供的电源量 0 本地电源良好 1 本地电源丢失(不激活) |
1 |
过电流: 如果集线器支持以集线器为基础报告过电流的情况,那么这个域指出所有端口的电流总和超过指定的最大值,而且所有端口已经进入掉电状态。如果集线器在每个端口的基础上报告过电流的情况或者没有过电流检测的能力,那么这个场一般是0. 0 当前不存在过电流的情况 1 存在集线器过电流的情况 |
2 ~ 15 |
保留 |
n 接下来就要处理集线器的电源了。对于所有不能自供电(总线供电)的USB设备,当它插入到USB集线器后,它就靠集线器来给它提供电源了。那么集线器就必须考虑它能为每一个下行端口最大提供多少的电流。同样当一个不能自供电的USB集线器插入到上一级集线器的端口,它也必须像一般的不能自供电的USB设备一样依靠上一级集线器的端口提供电源,所以它必须计算它能从上一级的集线器端口中获得多大的电流,除去自身的电流消耗,它又能为所有下行端口最大提供电流是多少。
在第1章的1.4.22.小节,我们知道当集线器的struct usb_device的对象(udev)hdev在设置为USB_STATUS_POWERED阶段时,其bus_mA已经被设置了。现在我们还要用到这个值来决定下行端口的供电大小。我们考虑下面情况:
1. 如果这个集线器是root(根)集线器,那么这是一个特殊的集线器。它只有一个下行端口的。它的hdev的bus_mA是由HCD驱动设置的,这里先不讨论。在这里只作这样处理:如果hdev->bus_mA大于500mA或等于0mA,那么hub->mA_per_port就等于500mA;其它情况hub->mA_per_port等于hdev->bus_mA,设置hub->limited_power为1,表示这是有限供电集线器。
2. 如果这个集线器不是自供电(总线供电)设备,那么它能使用的电流是不可能大于hdev->bus_mA。但它还是为每个端口分配100mA的电流,同时把设置hub->limited_power为1,表示有限供电。如果hdev->bus_mA减去自身消耗的电流(集线器描述符的bHubContrCurrent有记录)剩下电流不足100mA,它只是打印警告信息,但这个集线器的下行端口除去能插入自供电USB设备,总线供电USB设备将不能使用了。
3. 在其它情况下(一般是这个集线器是自供电设备),它能为每个端口分配500mA:把hub->mA_per_port设置500mA。
n 生成并填充一个urb,并使用输入参数endpoint(这是中断传输端点)生成了一个中断传输管道。并为这个集线器安装了一个“中断处理函数”hub_irq()。
n 然后调用hub_activate()函数激活这个集线器。这个函数中,最重要的是调用了usb_submit_urb()提交这个urb,启动这个中断传输管道;调用kick_khubd(),把这个集线器自身的hub加入到hub_event_list链表,并唤醒hub_thread内核线程。这是因为当这个集线器在插入到上一级集线器端口时,这个集线器的下行端口可能已经插入了其它的USB设备,所以需要马上处理。
l 到此hub_probe()函数的调用完成。
当集线器完成了初始化后,如果它的任何一个端口状态发生变化时(有USB设备插入、拔出、休眠、唤醒、过流)时,它就能过中断传输管道触发注册在这个集线器的“中断处理函数”hub_irq()函数的执行。
程序清单 2.14 hub_irq函数的实现
static void hub_irq(struct urb *urb)
{
struct usb_hub *hub = urb->context;
int status = urb->status;
int i;
unsigned long bits;
switch (status) {
case -ENOENT: /* synchronous unlink */
case -ECONNRESET: /* async unlink */
case -ESHUTDOWN: /* hardware going away */
return;
default: /* presumably an error */
/* Cause a hub reset after 10 consecutive errors */
dev_dbg (hub->intfdev, "transfer --> %d\n", status);
if ((++hub->nerrors < 10) || hub->error)
goto resubmit;
hub->error = status;
/* FALL THROUGH */
/* let khubd handle things */
case 0: /* we got data: port status changed */
bits = 0;
for (i = 0; i < urb->actual_length; ++i)
bits |= ((unsigned long) ((*hub->buffer)[i]))
<< (i*8);
hub->event_bits[0] = bits;
break;
}
hub->nerrors = 0;
/* Something happened, let khubd figure it out */
kick_khubd(hub);
resubmit:
if (hub->quiescing)
return;
if ((status = usb_submit_urb (hub->urb, GFP_ATOMIC)) != 0
&& status != -ENODEV && status != -EPERM)
dev_err (hub->intfdev, "resubmit --> %d\n", status);
}
当hub_irq函数被执行时,传入参数是urb。在urb的context成员指出了事件来源是来自哪一个hub。事件状态由urb的status成员指出。接着描述USB集线器的各个端口状态,如果端口状态发生改变(不管是设备插入、设备拔出、端口过流……)就会置位相应的hub->event_bits[0]位置位。调用kick_khubd函数把该USB集线器对象hub添加到hub_event_list中,唤醒khubd_wait内核线程。
最后提交这个urb,继续监视集线器。