我对USB的认识

时间:2024-08-29 09:04:38

一、USB协议规范

(1)      基本概念

 

每一个设备(device)会有一个或者多个的逻辑连接点在里面,每个连接点叫endpoint.每个endpoint有四种数据传送方式:控制(Control)方式传送;同步(isochronous)方式传送;中断(interrupt)方式传送;大量(bulk)传送.但是所有的endpoint0都被用来传送配置和控制信息。

在host和设备的endpoint之间的连接叫作管道“pipe",endpoint0叫做缺省(default pipe)。 

对于同样性质的一组的endpoint的组合叫做接口(interface),如果一个设备包含不止一个的接口就可以称之为复合设备(composite device)。 (应该为组合设备,此处有问题)

同样的道理,对于同样的类型的接口的组合可以称之为“配置"(configuration)。但是每次只能有一个配置是可用的,而一旦该配置激活,里面的接口和endpoint就都同时可以使用。 

host从设备发过来的描述字(descriptors)中来判断用的是哪个配置,哪个接口等等,而这些的描述字通常是在endpoint0中传送。

  1、USB协议本身很复杂,但方便在提供了统一的接口方式,使得驱动程序在使用设备的时候,工作简化到了类似操作串行接口。

  2、USB设备可以看作提供了多个串口的设备,依据USB的规范,我们将每个串口称作端点(Endpoint),要和这个端点通信,我们就要打开到这个端点的连接,这个连接就是管道(Pipe)。

3、打开端点之后,就可以像串口一样进行数据传输了。USB有4种不同类型的传输方式:控制传输(Control Transfer),批量传输(Bulk Transfer),中断传输(Interrupt Transfer)和实时传输(IsochTransfer)。

不管你千变万化, 怎么玩概念, 数据传输也就是同步和异步2种了. Usb定义了4种传输, bulk, interrupt, control, isochronous.前面3种都是异步, 最后是同步传输. 前面3种就没什么太大的差别了. 只是在时间限制, 数据大小格式, 用途不同. 另外,同步传输是不可靠传输,不支持重传. 特别对interrupt这种传输要理解的是, 这只是一种’伪中断’的实现. 因为, 根据硬件链路特点分析, 只有一条data线, 那么设备不可能发送IRQ给主机. 所以这里所谓的interrupt, 是一种周期polling方式. 端点描述符里面指定了查询频率, 在1ms~255ms之间.

  4、由于一个设备可能要适应多种情况,端点的设置会有多套,以备使用。端点设置称为接口(Interface)。USB设备展现给我们能够找到的东西就是这些Interface,我们选择要用的Interface,就可以找到Endpoint,再打开Endpoint,就可以传输数据了。所以,在驱动程序开始的时候,需要记录下这些Interface。

  5、例如:OV511+的端点0是控制端点,用来设置参数以及起停设备;端点1是实时传输端点,用来传输视频。端点1有8套不同的设置,主要区别就在于一次传输的数据帧的大小,所以在USBDeviceAttach的时候,要记录这些设置到驱动程序中,后面才能够选用。

(2)数据包

USB  最主要的的是要理解   USB主机发送命令给设备,设备要对主机的命令进行响应, USB通讯的基本单位为 “包”   理解好“包”这个概念是学习USB的关键所在。 

包有如下分类: 

分别是令牌包、数据包、握手包和特殊包(其实是由PID决定的) 

令牌包:可分为输入包、输出包、设置包和帧起始包(注意这里的输入包是用于设置输入命令的,输出包是用来设置输出命令的,而不是放据数的)其中输入包、输出包和设置包的格式都是一样的: 

SYNC+PID+ADDR+ENDP+CRC5(五位的校验码) 

帧起始包: 

SYNC+PID+11位FRAM+CRC5(五位的校验码) 

数据包:分为DATA0包和DATA1包,当USB发送数据的时候,当一次发送的数据长度大于相应端点的容量时,就需要把数据包分为好几个包,分批发送,DATA0包和DATA1包交替发送,即如果第一个数据包是DATA0,那第二个数据包就是DATA1。但也有例外情况,在同步传输中(四类传输类型中之一),所有的数据包都是为DATA0,格式如下: 

SYNC+PID+0~1023字节+CRC16 

握手包:结构最为简单的包,格式如下 

SYNC+PID 



下面举几个例子来说明USB的通讯过程: 

1:主机想要向设备传送一串数据。 过程如下: 

(1) 主机向从机发送 “令牌包”,令牌包的类型为输出包,表示主机要向从机发送数据了。 

(2) 主机向从机发送完令牌以后,USB处理器件根据发送的令牌,会将中断状态寄存器标志置位,从机CPU通过查询USB处理器件的中断状态寄存器,对主机的令牌包进行响应 

(3) 从机判别出中断类型,于是,准备从主机接收数据。 

(4) 从机准备好了,于是主机开始发送“数据包” 这时,USB处理器件会自动将从主发送过来的数据放如它的内部缓冲区内,接收完这个数据包后,从机向主机发送“应答包”  

这就是一个完整的通讯过程。 

由以上可以看出,USB若是想要传送数据,那么主机必须先发一个 IN 或OUT的令牌包,然后发送DATA0,或DATA1数据包。 

简单的用现实生活中的事件进行描述:  老板想让员工去做一件事情,老板 先会发出命令,告诉要做什么事情,员工准备好以后呢,老板再把做这件事情的经费发放给员工,当员工把发放的经费清点以后,发现数目正确,他会给老板一个回应信息,告诉老板,钱已经收到了,而且数目正确。 

老板想让员工做的事:  对应USB通讯里的令牌包。 

老板想要发放的经费:  对应USB通讯里的数据包。 

员工给老板的回应:    对应USB通讯里的握手包。 

这里尤其需要注意一个问题就是: 

USB主机向设备发送令牌包的时候,接收令牌是有USB器件来完成的,而不是有从机CPU来完成的,如主机发送一个如下的令牌: 

SYNC+PID+ADDR+ENDP+CRC5 

USB器件回根据PID的类型来判断是哪种类型的令牌 根据ADDR的值来判断是否是和自己通讯,根据ENDP的值来判断是和哪个端点进行通讯,根据校验来判断,数据传送是否无误。根据以上的令牌包信息,USB器件会将其内部的中断状态寄存器相应的位置位,从机CPU可以查询这个中断状态寄存器来进行相应的操作。

(3)USB描述符

标准的USB设备有5种USB描述符:设备描述符,配置描述符,字符串描述符,接口描述符,端点描述符。下面详解:

1、设备描述符:一个设备只有一个设备描述符

typedef struct _USB_DEVICE_DESCRIPTOR_

{

BYTE        bLength,

BYTE        bDescriptorType,

WORD      bcdUSB,

BYTE        bDeviceClass,

BTYE        bDeviceSubClass,

BYTE        bDeviceProtol,

BYTE        bMaxPacketSize0,

WORD      idVenderI,

WORD      idProduct,

WORD      bcdDevice,

BYTE        iManufacturer,

BYTE        iProduct,

BYTE        iSerialNumber,

BYTE        iNumConfiguations

}USB_DEVICE_DESCRIPTOR;

bLength : 描述符大小.固定为0x12.

bDescriptorType : 设备描述符类型.固定为0x01.

bcdUSB : USB 规范发布号.表示了本设备能适用于那种协议,如2.0=0200,1.1=0110等.

bDeviceClass : 类型代码(由USB指定)。当它的值是0时,表示所有接口在配置描述符里,并且所有接口是独立的。当它的值是1到FEH时,表示不同的接口关联的。当它的值是FFH时,它是厂商自己定义的.

bDeviceSubClass : 子类型代码(由USB分配).如果bDeviceClass值是0,一定要设置为0.其它情况就跟据USB-IF组织定义的编码.

bDeviceProtocol : 协议代码(由USB分配).如果使用USB-IF组织定义的协议,就需要设置这里的值,否则直接设置为0。如果厂商自己定义的可以设置为FFH.

bMaxPacketSize0 : 端点0最大分组大小(只有8,16,32,64有效).

idVendor : 供应商ID(由USB分配).

idProduct : 产品ID(由厂商分配).由供应商ID和产品ID,就可以让操作系统加载不同的驱动程序.

bcdDevice : 设备出产编码.由厂家自行设置.

iManufacturer : 厂商描述符字符串索引.索引到对应的字符串描述符. 为0则表示没有.

iProduct : :产品描述符字符串索引.同上.

iSerialNumber : 设备序列号字符串索引.同上.

bNumConfigurations : 可能的配置数.指配置字符串的个数

2、配置描述符:配置描述符定义了设备的配置信息,一个设备可以有多个配置描述符

typedef struct _USB_CONFIGURATION_DESCRIPTOR_

{

BYTE      bLength,

BYTE      bDescriptorType,

WORD    wTotalLength,

BYTE      bNumInterfaces,

BYTE      bConfigurationValue,

BYTE      iConfiguration,

BYTE      bmAttributes,

BYTE      MaxPower

}USB_CONFIGURATION_DESCRIPTOR;

bLength : 描述符大小.固定为0x09.

bDescriptorType : 配置描述符类型.固定为0x02.

wTotalLength : 返回整个数据的长度.指此配置返回的配置描述符,接口描述符以及端点描述符的全部大小.

bNumInterfaces : 配置所支持的接口数.指该配置配备的接口数量,也表示该配置下接口描述符数量.

bConfigurationValue : 作为Set Configuration的一个参数选择配置值.

iConfiguration : 用于描述该配置字符串描述符的索引.

bmAttributes : 供电模式选择.Bit4-0保留,D7:总线供电,D6:自供电,D5:远程唤醒.

MaxPower : 总线供电的USB设备的最大消耗电流.以2mA为单位.

3、接口描述符:接口描述符说明了接口所提供的配置,一个配置所拥有的接口数量通过配置描述符的bNumInterfaces决定

typedef struct _USB_INTERFACE_DESCRIPTOR_

{

BYTE      bLength,

BYTE      bDescriptorType,

BYTE      bInterfaceNumber,

BYTE      bAlternateSetting,

BYTE      bNumEndpoint,

BYTE      bInterfaceClass,

BYTE      bInterfaceSubClass,

BYTE      bInterfaceProtocol,

BYTE      iInterface

}USB_INTERFACE_DESCRIPTOR;

bLength : 描述符大小.固定为0x09.

bDescriptorType : 接口描述符类型.固定为0x04.

bInterfaceNumber: 该接口的编号.

bAlternateSetting : 用于为上一个字段选择可供替换的位置.即备用的接口描述符标号.

bNumEndpoint : 使用的端点数目.端点0除外.

bInterfaceClass : 类型代码(由USB分配).

bInterfaceSunClass : 子类型代码(由USB分配).

bInterfaceProtocol : 协议代码(由USB分配).

iInterface : 字符串描述符的索引

4、端点描述符:USB设备中的每个端点都有自己的端点描述符,由接口描述符中的bNumEndpoint决定其数量

typedef struct _USB_ENDPOINT_DESCRIPTOR_

{

BYTE        bLength,

BYTE        bDescriptorType,

BYTE        bEndpointAddress,

BYTE        bmAttributes,

WORD      wMaxPacketSize,

BYTE        bInterval

}USB_ENDPOINT_DESCRIPTOR;

bLength : 描述符大小.固定为0x07.

bDescriptorType : 接口描述符类型.固定为0x05.

bEndpointType : USB设备的端点地址.Bit7,方向,对于控制端点可以忽略,1/0:IN/OUT.Bit6-4,保留.BIt3-0:端点号.

bmAttributes : 端点属性.Bit7-2,保留.BIt1-0:00控制,01同步,02批量,03中断.

wMaxPacketSize : 本端点接收或发送的最大信息包大小.

bInterval : 轮训数据传送端点的时间间隔.对于批量传送和控制传送的端点忽略.对于同步传送的端点,必须为1,对于中断传送的端点,范围为1-255.

5、字符串描述符:其中字符串描述符是可选的.如果不支持字符串描述符,其设备,配置,接口描述符内的所有字符串描述符索引都必须为0

typedef struct _USB_STRING_DESCRIPTION_

{

BYTE      bLength,

BYTE      bDescriptionType,

BYTE      bString[1];

}USB_STRING_DESCRIPTION;

bLength : 描述符大小.由整个字符串的长度加上bLength和bDescriptorType的长度决定.

bDescriptorType : 接口描述符类型.固定为0x03.

bString[1] : Unicode编码字符串.

(4)USB枚举

USB协议定义了设备的6种状态,仅在枚举过程种,设备就经历了4个状态的迁移:上电状态(Powered),默认状态(Default),地址状态(Address)和配置状态(Configured)(其他两种是连接状态和挂起状态(Suspend))。

下面步骤是Windows系统下典型的枚举过程,但是固件不能依此就认为所有的枚举操作都是按照这样一个流程行进。设备必须在任何时候都能正确处理所有的主机请求。

1.用户把USB设备插入USB端口或给系统启动时设备上电

这里指的USB端口指的是主机下的根hub或主机下行端口上的hub端口。Hub给端口供电,连接着的设备处于上电状态。

2.Hub监测它各个端口数据线上(D+/D-)的电压

在hub端,数据线D+和D-都有一个阻值在14.25k到24.8k的下拉电阻Rpd,而在设备端,D+(全速,高速)和D-(低速)上有一个1.5k的上拉电阻Rpu。当设备插入到hub端口时,有上拉电阻的一根数据线被拉高到幅值的90%的电压(大致是3V)。hub检测到它的一根数据线是高电平,就认为是有设备插入,并能根据是D+还是D-被拉高来判断到底是什么设备(全速/低速)插入端口(全速、高速设备的区分在我将来的文章中描述)。如下图。

USB全速/高速设备上电连接

(Full-speed Device Cable and Resistor Connections)

检测到设备后,hub继续给设备供电,但并不急于与设备进行USB传输。

3. Host了解连接的设备

每个hub利用它自己的中断端点向主机报告它的各个端口的状态(对于这个过程,设备是看不到的,也不必关心),报告的内容只是hub端口的设备连接/断开的事件。如果有连接/断开事件发生,那么host会发送一个 Get_Port_Status请求(request)以了解更多hub上的信息。Get_Port_Status等请求属于所有hub都要求支持的hub类标准请求(standard
hub-class requests)。

4.Hub检测所插入的设备是高速还是低速设备

hub通过检测USB总线空闲(Idle)时差分线的高低电压来判断所连接设备的速度类型,当host发来Get_Port_Status请求时,hub就可以将此设备的速度类型信息回复给host。(USB 2.0规范要求速度检测要先于复位(Reset)操作)。

5.hub复位设备

当主机获悉一个新的设备后,主机控制器就向hub发出一个 Set_Port_Feature请求让hub复位其管理的端口。hub通过驱动数据线到复位状态(D+和D-全为低电平 ),并持续至少10ms。当然,hub不会把这样的复位信号发送给其他已有设备连接的端口,所以其他连在该hub上的设备自然看不到复位信号,不受影响。

6.Host检测所连接的全速设备是否是支持高速模式

因为根据USB 2.0协议,高速(High Speed)设备在初始时是默认全速(Full Speed )状态运行,所以对于一个支持USB 2.0的高速hub,当它发现它的端口连接的是一个全速设备时,会进行高速检测,看看目前这个设备是否还支持高速传输,如果是,那就切到高速信号模式,否则就一直在全速状态下工作。

同样的,从设备的角度来看,如果是一个高速设备,在刚连接bub或上电时只能用全速信号模式运行(根据USB 2.0协议,高速设备必须向下兼容USB 1.1的全速模式)。随后hub会进行高速检测,之后这个设备才会切换到告诉模式下工作。假如所连接的hub不支持USB 2.0,即不是高速hub,不能进行高速检测,设备将一直以全速工作。

高速设备检测的过程在我另外一篇文章(USB2.0速度识别)中有详细描述,这里不具体深入。

7. Hub建立设备和主机之间的信息通道

主机不停得向hub发送 Get_Port_Status请求,以查询设备是否复位成功。Hub返回的报告信息中有专门的一位用来标志设备的复位状态。

当hub撤销了复位信号,设备就处于默认/空闲状态(Default state),准备着主机发来的请求。设备和主机之间的通信通过控制传输,默认地址0,端点号0进行。在此时,设备能从总线上得到的最大电流是100mA。

8.主机发送Get_Descriptor请求获取默认管道的最大包长度

默认管道(Default Pipe)在设备一端来看就是端点0。主机此时发送的请求是默认地址0,端点0,虽然所有位分配地址的设备都是通过地址0来获取主机发来的信息,但由于枚举过程不是多个设备并行处理,而是一次枚举一个设备的方式进行,所以不会发生多个设备同时响应主机发来的请求。

设备描述符的第8字节代表设备端点0的最大包大小。对于Windows系统来说,Get_Descriptor请求中的wLength一项都会设为64,虽然说设备所返回的设备描述符(Device Descriptor)长度只有18字节,但系统也不在乎,此时,描述符的长度信息对它来说是最重要的,其他的瞄一眼就过了。Windows系统还有个怪癖,当完成第一次的控制传输后,也就是完成控制传输的状态阶段,系统会要求hub对设备进行再一次的复位操作(USB规范里面可没这要求)。再次复位的目的是使设备进入一个确定的状态。

9.主机给设备分配一个地址

主机控制器通过Set_Address请求向设备分配一个唯一的地址。在完成这次传输之后,设备进入地址状态(Address state),之后就启用新地址继续与主机通信。这个地址对于设备来说是终生制的,设备在,地址在;设备消失(被拔出,复位,系统重启),地址被收回。同一个设备当再次被枚举后得到的地址不一定是上次那个了。

10.主机获取设备的信息

主机发送 Get_Descriptor请求到新地址读取设备描述符,这次主机发送Get_Descriptor请求可算是诚心,它会认真解析设备描述符的内容。设备描述符内信息包括端点0的最大包长度,设备所支持的配置(Configuration)个数,设备类型,VID(Vendor ID,由USB-IF分配), PID(Product ID,由厂商自己定制)等信息。Get_Descriptor请求(Device
type)和设备描述符(已抹去VID,PID等信息)见下图:

标准Get_Descriptor请求

(Get_Descriptor Request)

设备描述符(Device Descriptor)

之后主机发送Get_Descriptor请求,读取配置描述符(Configuration Descriptor),字符串等,逐一了解设备更详细的信息。事实上,对于配置描述符的标准请求中,有时wLength一项会大于实际配置描述符的长度(9字节),比如255。这样的效果便是:主机发送了一个Get_Descriptor_Configuration 的请求,设备会把接口描述符,端点描述符等后续描述符一并回给主机,主机则根据描述符头部的标志判断送上来的具体是何种描述符。

11.主机给设备挂载驱动(复合设备除外)

主机通过解析描述符后对设备有了足够的了解,会选择一个最合适的驱动给设备。在驱动的选择过程中,Windows系统会和系统inf文件里的厂商ID,产品ID,有时甚至用到设备返回来的产品版本号进行匹配。如果没有匹配的选项,Windows会根据设备返回来的类,子类,协议值信息选择。如果该设备以前在系统上成功枚举过,操作系统会根据以前记录的登记信息而非inf文件挂载驱动。当操作系统给设备指定了驱动之后,就由驱动来负责对设备的访问。

对于复合设备,通常应该是不同的接口(Interface)配置给不同的驱动,因此,需要等到当设备被配置并把接口使能后才可以把驱动挂载上去。

设备-配置-接口-端点关系见下图:

USB 设备-配置-接口-端点关系

(Device Configuration)

实际情况没有上述关系复杂。一般来说,一个设备就一个配置,一个接口,如果设备是多功能符合设备,则有多个接口。端点一般都有好几个,比如Mass Storage设备一般就有两个端点(控制端点0除外)。

12. 设备驱动选择一个配置

驱动(注意,这里是驱动,之后的事情都是有驱动来接管负责与设备的通信)根据前面设备回复的信息,发送Set_Configuration请求来正式确定选择设备的哪个配置(Configuration)作为工作配置(对于大多数设备来说,一般只有一个配置被定义)。至此,设备处于配置状态,当然,设备也应该使能它的各个接口(Interface)。

对于复合设备,主机会在这个时候根据设备接口信息,给它们挂载驱动。

13. 设备可使用

二、Windows CE USB驱动架构

为了支持不同类型的外围设备,WinCE平台提供了具有定制接口的流接口驱动程序模型。因为大部分USB外围设备由于功能性更适合流接口驱动的结构,所以一般都采用加载式流接口驱动程序模型来开发USB设备驱动程序。

(1)USB系统结构分析

  WinCE下USB系统软件由两层组成:较高USB设备驱动程序层和较低的USB函数层。较低的USB函数层本身又由两部分组成:较高的通用串行总线驱动程序(USBD)模块和较低的主控制器驱动程序(HCD)模块。通过HCD模块功能和USBD模块实现高层的USBD接口函数,USB设备驱动程序就能与外围设备进行通讯。

在数据传输的过程中,操作流程通常按下列的次序进行:①USB设备驱动程序进行数据传输的初始化,即通过USBD接口函数给USBD模块发送数据传输的请求。②USBD模块将该请求分成一些单独的事务。③HCD模块排出事务次序。④主控制器硬件执行事务。这里需要提醒的是,所有的事务都是从主机发出的,外围设备完全是被动接受型的。

(2)USB设备驱动程序入口点函数

  从结构分析我们可知,所有的USB设备驱动程序必须在它们的DLL库设置一定的入口点与USBD模块进行适当的交互。设置入口点函数有两个作用:一是使得 USBD 模块能与外部设备

交互;二是使得驱动程序能创建和管理任何可能需要的注册键。

下面简要介绍相关函数的作用:USBDeviceAttach是当 USB 设备连接到主计算机时运行,USBD模块会调用这个函数初始化USB设备,取得USB设备信息和配置USB设备,并且申请必需的资源。 USBInstallDrive是在第一次加载USB设备驱动程序时首先被调用,它使得驱动程序能创建需要的注册键,用于将一个驱动程序所需的注册表信息写入到HKEY_LOCAL_MACHINE/Drivers/USB/ClientDrivers目录下,例如设备名称等。需要注意的是,USB设备驱动程序不使用标准的注册表函数,而是使用RegisterClientDriverID()、RegisterClientSettings()函数来注册相应的设备信息。

USBUninstallDriver是在用户删除USB设备驱动程序时调用,负责删除注册键并释放其它相关资源。它通过调用 UnRegisterClientSettings()和UnRegisterClientDriverID()函数来删除由驱动程序的 USBInstallDriver()函数创建的所有注册键。因此,我们在驱动程序中就需要严格按照这三个函数的原型来实现,否则就不能为设备管理器所识别。