USB原理学习:1

USB原理学习:1本文详细介绍了 USB 从设备接入 通信过程 描述符集合 枚举流程到数据传输的原理 包括 USB 接口的物理连接 主机如何识别设备 数据传输的机制和描述符的作用

大家好,欢迎来到IT知识分享网。

理论学习
本章将由浅入深介绍USB原理,逐步解释以下问题:
    第一节:USB从接入到使用,讲述USB设备接入主机后经历了哪些过程;
    第二节:USB通信过程,解释USB设备和主机之间如何通信;
    第三节:从机的属性,也称从机的描述符集合,介绍如何区分不同类型的USB设备;
    第四节:枚举的详细过程,概括主机认识USB设备的具体过程;
另外,下文将用主机/从机统一描述USB主机和USB设备:
    主机:USB主机(Win/Android/Mac等)
    从机:USB设备(鼠标/键盘/U盘等)







USB从接入到使用
    主机发现从机接入后,开始识别从机,成功识别后就可以使用从机的功能了。其中,发现从机接入/拔出的过程称为USB拔插,识别从机的过程称为枚举。

USB拔插:主机发现从机的接入/拔出
【摘要】主机通过检测USB D+/D-的电平变化感知从机接入/拔出。
一般USB接口包含4根线(OTG为5根),分别是:Vcc, D+, D-, GND。如图所示:

USB原理学习:1
    主机端D+/D-下拉15KΩ电阻到GND(0V),从机端D+/D-上拉1.5KΩ电阻到3.3V。当从机接入主机时,D+/D-上的电压变为3V,双方通过电平变化就可以发现USB的拔插事件。 USB拔插事件会触发主机的中断(或回调),执行从机的加载、释放过程。

USB枚举:主机认识从机的方式
【摘要】主机通过获取设备的描述符集合来识别USB设备,这个过程称为“枚举”。
    USB设备(从机)的类型非常多,常见的有鼠标、键盘、游戏手柄等USB HID(Human Interface Device)设备,串口调试的CDC(Communication Device Class)设备,User自定义传输内容的WINUSB设备等。
    那么对于新接入的从机,主机如何区分它属于哪种类型呢?
    当然是让从机“介绍”自己。但主机是很忙的(软件、其他从机、其他接口的设备等),不会时时刻刻等着新从机的加入。从机不合时宜地发消息,只会对主机造成困扰。因此,从机就像门口排队的面试者一样,手里拿着自己的“简历”,等待着主机问询递交。
    从机的“简历”,称为描述符集合(Descriptor Collection)。它包含从机的名字、籍贯、性别等最基本的信息(设备描述符)、从事的职业(配置描述符)、掌握的技能(接口描述符、端点描述符)和补充信息(字符串描述符、其他特殊描述符)。他们都必须遵循相应的格式,以便主机可以快速了解从机的所有信息。只要从机正确地遵循主机的流程(枚举),按固定格式提供主机索要的信息,就可以通过“面试”,成为主机的USB部门的一员。
    每个USB设备都必须有描述符集合来详细介绍自己的所有功能和用途。USB连接后,主机通过访问描述符集合来识别从机并配置从机(枚举过程),就可以根据从机提供的信息使用从机的功能。





USB使用:主机使用从机的功能
【摘要】从机以等待主机轮询的方式发数据,以中断的方式收数据,从而实现相应的功能。
    枚举成功后,从机开始履行自己的职责。
    前文提到,从机不能擅自介绍自己,因为主机是很忙的。同样,在主机认识并接受从机后,从机依然不能擅自报告自己的行为、状态等信息。那么从机和主机要如何通信呢?
    主机会定期到USB部门来视察工作,依次询问USB部门的所有成员是否需要汇报工作。当然,有些从机希望主机询问自己的频率高一些,就必须在“简历”中附上声明:请每隔XXX的时间问一次我的情况。对于没有附上声明的从机,主机会以自己的设定定期询问,或者由应用软件“催促”主机询问(自定义的USB设备)。
    以鼠标为例,它一般会声明:请每隔10毫秒来问一下我的情况。主机会尽量遵守这个声明,及时询问鼠标。在某一次询问中,鼠标报告自己:我刚刚移动了10个像素点。那么主机就会让屏幕上的光标移动。
    因此,从机准备好发送的数据后必须进入等待(一般不会等太久),直到主机轮询到此功能时,才开始发送。假设从机可以任意触发数据的发送过程,且主机连接多个从机,那么当多个从机同时发送数据到主机的USB总线上时就会引发冲突。
    反之,当主机需要发送数据时,从机必须尽快接收,所以从机一般会用中断处理主机发送数据的请求。这是因为主机需要轮询很多从机,每次轮询都有固定的时间,超时后就通信失败了。
【Q】从机发送/接收数据,主机发送/接收数据是否容易概念混淆?
【A】 是的。因此USB的数据传输过程描述以主机端为主。“从机–>主机”(Device-to-host) 方向的数据传输称为输入(Data In),“从机<–主机”(Host-to-device) 方向的数据传输为输出(Data Out)。
【Q】主机轮询到从机的输入功能时,没有数据要发送怎么办?
【A】
当然是PASS,从机直接回复NAK(即没有数据)或STALL(设备挂起)。









USB通信过程
主机如何访问指定USB设备?
【摘要】主机为所有从机分配唯一的设备地址,通过该地址来访问从机。
    以PC为例,一般PC的USB设备可能包括鼠标、键盘、HUB扩展坞、蓝牙/WiFi适配器等。那么假如PC想访问鼠标设备时,该如何实现呢?


USB原理学习:1

    答案是设备地址。主机给所有已连接的从机分配设备地址,并确保不会重复。对刚接入还没来得及分配地址的从机,主机使用默认地址<Addr0>与之通信。交换少量的信息后,主机分配新地址,然后双方用新地址(Addr1~AddrN)通信。
【Q】枚举成功后,从机再次拔插还可以用之前分配的地址通信吗?
【A】
主机会重新分配设备地址,但可能分配的碰巧就是之前的地址。
【Q】主机为分配地址前,如何与从机通信?
【A】
USB规定,对于刚接入的从机,主机用默认地址(Addr0)通信。

主机如何访问指定USB设备的指定功能?
【摘要】主机通过<设备地址(Address),设备端点(Endpoint)>访问指定从机的指定接口(功能)。
    假设设备A是USB复合设备,同时支持鼠标、键盘、CDC功能,那么主机给设备A分配设备地址后,如何访问从机A的其中一个功能(比如键盘功能)?且当这个键盘功能同时支持发送和接收数据时,如何避免收发冲突呢?

主机、从机如何读/写数据
【摘要】主机用默认端点0(EP0)创建通道枚举从机,根据描述符集中的其他端点创建对应通道访问其他功能。
    首先,从机必须支持默认端点EP0。对刚接入的从机,主机使用<Addr0, EP0>访问从机,创建EP0的端点通道,开始枚举并分配地址,然后使用<new Addr, EP0>重新枚举。枚举成功后,主机根据从机提供的信息创建相应的资源和通道,访问从机的功能。
    当然,从机的功能多种多样,可能要持续传输大量数据,也可能要求实时性高,或是偶尔传输数据等。那么访问的需求不一样,主机怎么区分呢?
    当然是给端点加上属性(Attribute)。在端点描述符中声明属性,可以告诉主机构建什么样的数据通道,以何种方式读/写数据。



一次完整的通信过程
【摘要】一次完整的通信分为三个过程:请求过程(令牌包)、数据过程(数据包)和状态过程(握手包),没有数据要传输时,跳过数据过程。

    通信过程包含以下三种情况:

 USB原理学习:1

    主机发送令牌包(Token)开始请求过程,如果请求中声明有数据要传输则有数据过程,最后由数据接收方(有数据过程)或从机(无数据过程)发起状态过程,结束本次通信。
    与USB全速设备通信时,主机将每秒等分为1000个帧(Frame)。主机在每帧开始时,向所有从机广播一个帧起始令牌包(Start Of Frame,SOF包)。它的作用有两个:一是通知所有从机,主机的USB总线正常工作;二是从机以此同步主机的时序。
    与USB高速设备通信时,主机将帧进一步等分为8个微帧(Microframe),每个微帧占125μ \muμs。在同一帧内,8个微帧的帧号都等于当前SOF包的帧号。
注意: 下文所有USB包结构均不包括前导码(同步码)。


#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Token_SOF_t{ uint8_t bPID; // 0xA5, SOF(0101B) uint16_t b11FrameID:11; // 帧号 uint16_t b5CRC:5; // wFrameID字段(11bit)的CRC校验码 }USB_Token_SOF_t; 

【Q】为什么PID是4bit的,字段长度却有8bit?
【A】
因为PID字段高4bit是低4bit的校验位:pid(i+4) = ~pid(i)。
【Q】为什么CRC不校验PID字段?
【A】
因为PID字段本身带有校验位。

请求过程(请求包)
    主机广播SOF包之后,会发送带有地址和端点信息的令牌包(Token) 来指定要访问的从机,分别有:建立令牌包(SETUP)、输出令牌包(OUT)、输入令牌包(IN)。
    这三种令牌包统称为请求包,结构如下:

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Token_t{ uint8_t bPID; // 0xE1, OUT (0001B); 0x69, IN (1001B); 0x2D, SETUP (1101B); uint16_t b7Addr:7; // 要访问的设备地址 uint16_t b4Endpoint:4; // 要访问的端点号 uint16_t b5CRC:5; // wFrameID字段(11bit)的CRC校验码 }USB_Token_t; 
#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Request_t{ uint8_t bmRequestType; // 请求类型 uint8_t bRequest; // 具体请求,参考USB 2.0 Spec Chapter 9.4 uint16_t wValue; // 内容和Request有关 uint16_t wIndex; // 内容和Request有关 uint16_t wLength; // 数据过程可传输的最大字节数 }USB_Request_t; typedef struct _bmRequestType_t{ uint8_t b5Recipient:5; // 0 = Device, 1 = Interface 2 = Endpoint, 3 = Other 4..31 = Reserved uint8_t b2Type:2; // 0 = Standard, 1 = Class 2 = Vendor, 3 = Reserved uint8_t b1Direction:1; // 0 = Host-to-device 1 = Device-to-host }bmRequestType_t; 

数据过程(数据包)
    请求的bmRequestType字段中,Direction标志位声明了数据要传输的方向。
    当请求为输出(Data OUT,Direction = 1)时,从机接收不超过wLength字段中声明长度的数据,并根据请求的内容解析接收到的数据;当请求为输入时(Data IN,Direction = 0)时,从机根据请求的内容发送对应的数据(不超过wLength中声明的长度)。
    数据包(Data Packets)的结构如下:


#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Data_Packet_t{ uint8_t bPID; // 0xC3, DATA0 (0011B); even 0x4B, DATA1 (1011B); odd 0x87, DATA2 (0111B); for usb high speed 0x0F, MDATA (1111B); for usb high speed uint8_t bData[]; // 0 ~ 8192B uint16_t wCRC16; // bData字段的CRC校验码 }USB_Data_Packet_t; 

【Q】为什么要分DATA0和DATA1?
【A】
在USB全速设备中,数据包以DATA0、DATA1的PID交替发送。当接收方连续收到两个PID相同的DATA包时,就知道丢包了。而DATA2与MDATA则是USB高速设备所使用的PID,参考《USB 2.0 Spec》Chapter 5.9.2。

状态过程(握手包)
    进入状态过程后,发送的包是握手包(Handshake Packets),结构如下:

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Handshake_t{ uint8_t bPID; // 0xD2, ACK (0010B); 确认接收 0x5A, NAK (1010B); 没有数据要返回 0x1E, STALL (1110B); 无法执行的请求 0x96, NYET (0110B); 接收成功但无法 接收下一次数据,仅在usb高速设备中使用。下 次主机发送数据需要先发送PING包试探设备。 }USB_Handshake_t; 

通信异常
    当从机还没准备好时主机请求数据;从机收到未知请求;端点通信数据量溢出;主机不应发送的请求;或没有数据要发送等情况时,本轮通信会直接进入状态过程,从机发送NYET/ERR/STALL/NAK包。
    当数据传输出错时,数据的发送方停止发送数据,直到本次通信超时。

从机的属性
【摘要】描述符集描述了从机的所有功能细节,它包含唯一的设备描述符,至少一个配置描述符和接口描述符,每个接口描述符至少包含一个端点描述符,此外还有其他可选的特殊描述符进行补充。
    前文提到,主机通过请求从机的描述符集来认识从机,那么描述符集包含了哪些信息呢?
    描述符集主要包含设备描述符(Device Descriptor)、配置描述符(Configuration Descriptor)、接口描述符(Interface Descriptor)、端点描述符(Endpoint Descriptor)、字符串描述符(String Descriptor)及其他描述符。


描述符集的层次结构

USB原理学习:1

    一个USB设备有且仅有一个设备描述符
    一个设备描述符指向一个(或多个)配置描述符
    一个配置描述符指向一个(或多个)接口描述符
    一个接口描述符指向一个(或多个)端点描述符,还可能带有接口补充描述符


    上述描述符如果带有字符串索引号(String Index),主机会根据索引号向从机请求对应的字符串描述符,进一步提供可供用户阅读的信息。
    对于一些接口(HID/CDC等),配置集合就包含一种接口补充描述符——特殊类描述符。不同的接口补充描述符作用不同,结构也可能不一样。如HID描述符会声明报告描述符的存在,由报告描述符进一步补充接口信息。如果补充描述符中又声明了其他描述符,主机会按接口索引号单独向从机请求其他描述符。
    需要注意的是,同一时间从机只能有一个生效的配置集合,生效的配置通过主机选择(Set Configuration)来指定,因为配置集合“复用”了从机的硬件资源。
    上述描述符中,除其他特殊描述符外主机能够单独获取的只有设备描述符、字符串和配置描述符,因为这些描述符是全局有效的。但接口描述符、端点描述符和特殊类描述符是某个配置集合内(局部)生效的,需要补充配置描述符一起发送。事实上,枚举过程中主机会一次性获取整个配置集合


设备描述符(Device Descriptor)

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Desc_Device_t { uint8_t bLength; // 固定值18B uint8_t bDescriptorType; // 固定值Device(0x01) uint16_t wBcdUSB; // USB Spec版本 uint8_t bDeviceClass; // 设备类型 uint8_t bDeviceSubClass; // 设备子类型 uint8_t bDeviceProtocol; // 协议类型 uint8_t bMaxPacketSize0; // EP0的最大包长度 uint16_t wIdVendor; // 厂商ID uint16_t wIdProduct; // 产品ID uint16_t wBcdDevice; // 设备软件版本 uint8_t bStringIndexManufacturer; // 厂商名称字符串索引号 uint8_t bStringIndexProduct; // 产品名称字符串索引号 uint8_t bStringIndexSerialNumber; // 序列号索字符串引号 uint8_t bNumConfigurations // 配置数量>=1 }USB_Desc_Device_t; 

      其中,设备类型、设备子类型、协议类型参考USB IF的定义。EP0最大包长度则为从机默认端点EP0一次可传输的最大包的大小。其典型值为64B,早期的USB设备为8B。字符串索引号分别对应一个字符串,主机用它向从机请求对应的文本信息。
【Q】Vendor ID和Product ID有什么作用?
【A】
Vendor ID(VID)的商用需要向USB组织申请,开发者可直接使用开发平台的厂商ID。Product ID(PID)由厂商自行管理。VID和PID的作用是让主机快速识别某些著名的设备(Windows可以在完成枚举之前依此直接派发驱动),它们也常常作为搜索从机的条件(如libusb)。

配置描述符(Configuration Descriptor)

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Desc_Configuration_t { uint8_t bLength; // 固定值9B uint8_t bDescriptorType; // 固定值Configuration(0x02) uint16_t wTotalConfigurationSize; // 配置集合的总大小 uint8_t bTotalInterfaces; // 配置集合的接口数量 uint8_t bConfigurationNumber; // 当前配置的序号(从1开始) uint8_t bConfigurationStrIndex; // 配置名称的字符串索引号 uint8_t bConfigAttributes; // 配置集合的属性 uint8_t bMaxPowerConsumption; // 最大供电电流,单位是2mA }USB_Desc_Configuration_t; // 配置集合的属性 typedef struct _bConfigAttributes_t{ uint8_t b5reserved:5; // 保留置0 uint8_t b1RemoteWakeup:1; // 置1表示支持远程唤醒 uint8_t b1Selfpowerd:1; // 置1表示支持自己供电 uint8_t b1reserved:1; // 保留置1 }bConfigAttributes_t; 

     配置集合的总大小是当前配置集合内配置描述符、接口描述符、端点描述符和特殊类描述符的总长度。需注意,如果供电电流为100mA,“bMaxPowerConsumption”字段的值应当为50。

接口描述符(Interface Descriptor)

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Desc_Interface_t { uint8_t bLength; // 固定值9B uint8_t bDescriptorType; // 固定值Interface(0x04) uint8_t bInterfaceNum; // 接口索引号 uint8_t bAlternateSetting; // 备用接口号 uint8_t bNumberEndpoints; // 端点数量 uint8_t bInterfaceClass; // 接口类型 uint8_t bInterfaceSubclass; // 接口子类型 uint8_t bInterfaceProtocol; // 接口协议 uint8_t bInterfaceStringIndex; // 接口名称的字符串索引号 }USB_Desc_Interface_t; 

      其中,接口类型、子类型、接口协议参考USB IF的定义。备用接口号用于声明另一个可以替代当前接口的备用接口。

端点描述符(Endpoint Descriptor)

#pragma data_alignment=1 //对齐方式为Byte //参考USB Spec 2.0 Table 9-13 typedef struct _USB_Desc_Endpoint_t{ uint8_t bLength; // 固定值7B uint8_t bDescriptorType; // 固定值Endpoint(0x05) uint8_t bEndpointAddress; // 端点地址 uint8_t bmAttributes; // 端点属性 uint16_t wMaxPacketSize; // 端点支持的最大包大小 uint8_t bInterval; // 轮询间搁(仅中断端点有效) }USB_Desc_Endpoint_t; // 端点地址 typedef struct _bEndpointAddress_t{ uint8_t b4EndpointNumber:4; // 端点号 uint8_t b3Reserved:3; // 保留置0 uint8_t b1Direction:1; // 传输方向(IN/OUT) }bEndpointAddress_t; // 端点属性 typedef struct _bmAttributes_t{ uint8_t b2TransferType:2; // 传输类型 00 = Control 01 = Isochronous 10 = Bulk 11 = Interrupt uint8_t b2SynchronizationType:2; // 仅iso传输有效 uint8_t b2UsageType:2; // 仅iso传输有效 uint8_t b2Reserved:2; // 保留置0 }bmAttributes_t; 

      端点支持的最大包大小是端点通道一次可以传输的最大数据量。在批量传输(bulk transfer)中,超过该值的数据会被分包传输,一般来说,如果接收方接收到恰好为最大包长度的数据,则会认为还有数据要传输。当然,bulk传输的方式本身是可以自定义的,具体行为可以由开发者控制。而在中断传输(interrupt transfer)中,不允许超过最大包长度的数据量传输。

批量传输(Bulk Transfer)
    批量传输是最好理解的,它几乎没有什么限制,全看怎么实现,语法、语义都是私有的。它适合需要传输大量数据且对数据实时性要求不高的场景。一般来说,传输过程中会以传输包是否小于最大包长度作为本轮传输结束的标志。下文的例程Winusb就是使用这种传输方式。具体参考USB Spec 2.0 Chapter 5.8。

控制传输(Control Transfer)
    控制传输适用于数据量少且对时序有严格要求的场景。顾名思义,它就是用来传输设备信息和主机信息的。所有的从机都必须支持控制传输,以便和主机交换信息,也就是说,从机的默认端点0的类型都是控制传输。具体参考USB Spec 2.0 Chapter 5.5。

中断传输(Interrupt Transfer)
    中断传输适用于传输数据量少但需要定时询问的场景,如键鼠设备。端点描述符的轮询间搁字段声明了主机两次访问之间的最长间搁。具体参考USB Spec 2.0 Chapter 5.7。

同步传输(Synchronous Transfer)*
    参考USB Spec 2.0 Chapter 5.6。同步传输适合数据量大且实时性要求高的场景,比如音频传输。

【Q】端点EP in 1(0x01)和端点EP out 1(0x81)是同一个端点吗?
【A】
端点号≠端点地址。EP in 1和EP out 1的端点号虽然相同,但传输方向不同,构建的端点通道(Pipe)也不同。因此不能认为它们是同一个端点。

字符串描述符(String Descriptor)

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Desc_String_t{ uint8_t bLength; // 字符串描述符的长度 uint8_t bDescriptorType; // 固定值String(0x03) wchar_t wUnicodeString[]; }USB_Desc_String_t; 

       UnicodeString是wchar_t型字符串。如果希望定义设备为”DevName”,则需定义L”DevName”(长度16B,包含了停止位L”\0″),bLength字段的值则为14。

其他描述符
    除了上述基本的描述符,USB设备还会带有其他特殊的描述符,对设备功能、信息作进一步补充。以下列举一些常见的特殊描述符:

设备限定符描述符(Qualifier Descriptor)

#pragma data_alignment=1 //对齐方式为Byte typedef struct _USB_Desc_Device_Qualifier_t{ uint8_t bLength; // 固定值18B uint8_t bDescriptorType; // 固定值Device(0x01) uint16_t wBcdUSB; // USB Spec版本 uint8_t bDeviceClass; // 设备类型 uint8_t bDeviceSubClass; // 设备子类型 uint8_t bDeviceProtocol; // 协议类型 uint8_t bMaxPacketSize0; // EP0的最大包长度 uint8_t bNumConfigurations // 配置数量>=1 uint8_t bReserved; // 保留置0 }USB_Desc_Device_Qualifier_t; 

       可以看到,设备限定描述符的结构就是设备描述符的一部分。假如主机和从机正在全速USB的速率通信,结果主机发现从机带有设备限定描述符,且在描述符中声明从机支持高速USB通信,那么主机就会复位从机,重新以高速USB的通信速率进行通信。

特殊类描述符(Class-specific Descriptor)
    特殊类描述符的结构取决于接口的实际类型。比如HID描述符:

#pragma data_alignment=1 //对齐方式为Byte //Human Interface Device Descriptor,参考 Device Class Definition for HID 1.11 Chapter 6.2.1 typedef struct _USB_Desc_HID_t{ uint8_t bLength; uint8_t bDescriptorType; uint16_t wBcdHID; // 遵循的Hid协议版本 uint8_t bCountryCode; // 国区代码 uint8_t bNumDescriptors; // 其他特殊描述符的个数 uint8_t bDescriptorType; // 其他特殊描述符的类型,一般为Report(0x22) uint16_t wDescriptorLength; // 其他特殊描述符的长度 (optional)uint8_t bDescriptorType; (optional)uint16_t wDescriptorLength; ... }USB_Desc_HID_t; //Hub Descriptor,参考 USB Spec 2.0 Chapter 11.23.2 typedef struct _USB_Desc_Hub_t{ uint8_t bDescLength; uint8_t bDescriptorType; uint8_t bNbrPorts; uint16_t wHubCharacteristics; uint8_t bPwrOn2PwrGood; uint8_t bHubContrCurrent; uint8_t abDeivceRemovable[]; uint8_t abPortPwrCtrlMask[]; }USB_Desc_Hub_t; 
#pragma data_alignment=1 //对齐方式为Byte //参考 CDC120--track Chapter 5.2.3 typedef struct _USB_Desc_Functional_t{ uint8_t bFunctionLength; uint8_t bDescriptorType; uint8_t bDescriptorSubType; uint8_t abFunctionSpecificData[]; // data[0] ~ data[N - 1] }USB_Desc_Functional_t; 

物理描述符(Physical Descriptor)
   
 参考Device Class Definition for HID 1.11 Chapter 6.2.3。

微软系统描述符(Microsoft OS Descriptor)
   
 微软系统描述符是由微软定义的,参考Microsoft docs。

枚举的详细过程
    ①USB设备接入后,主机复位从机,使用<addr0, EP0>构建端点通道(Pipe)请求设备描述符,从机发送完整的设备描述符或只发送前8B内容(当EP0最大包长度只有8B);
    ②主机分配唯一的设备地址并发送Set Address请求,收到应答后再次复位从机;
    ③主机再次请求完整的设备描述符,当一次请求不足以获取完整的描述符,主机会请求多次;
    ④主机请求完整的配置描述符;
    ⑤根据设备描述符和配置描述符中声明的字符串描述符索引号,请求所有字符串描述符;
    ⑥(可选)主机请求限定符描述符,当描述符中声明了支持更高速的USB协议时,主机复位从机,用新的USB协议重新枚举从机,当获取描述符失败时,认为从机不支持此功能,按原协议重新枚举并跳过此步骤;
    ⑦根据配置描述符中声明的集合长度,请求配置集合。其中配置集合包括配置描述符、接口描述符、端点描述符以及特殊类描述符。当从机包含多个配置描述符集合时,会多次请求。
    ⑧主机请求选择配置(Set Configuration);
    ⑨主机选择接口,请求接口空闲状态(Set Idle),此时接口生效。根据接口描述符,可能会请求其他的特殊描述符(一般这些描述符是对接口描述符的补充描述)。如果从机包含多个接口,此步骤会重复多次;
    ⑩主机知道USB设备的类型、通信方式和工作方式后,采用恰当的对策轮询USB设备。在Windows平台,主机完成枚举后会给从机派发相应的驱动(符合官方支持的设备标准)或者不派发驱动(找不到对应驱动,需要手动安装)。









#pragma data_alignment=1 //对齐方式为Byte const USB_Desc_Device_t stDevWinusb = { 0x12, // sizeof(USB_Desc_Device_t) 0x01, // descriptor type: device 0x0200, // USB Spec 2.0 0x00, // no device class 0x00, // no device subclass 0x00, // no device protocol 0x40, // max ep0 packet size: 64B 0x1234, // vendor id 0x5678, // product id 0x0001, // product release number 0x01, // manufacturer string index 0x02, // product string index 0x03, // serial number string index 1 // configuration numbers }; typedef struct _USB_Winusb_Configuration_t{ USB_Desc_Configuration_t stDescConfiguration; USB_Desc_Interface_t stDescInterface; USB_Desc_Endpoint_t stDescEndpointIn; USB_Desc_Endpoint_t stDescEndpointOut; }USB_Winusb_Configuration_t; const USB_Winusb_Configuration_t stConfWinusb = { // configuration descriptor { 0x09, // sizeof(USB_Desc_Configuration_t) 0x02, // descriptor type: configuration 0x0020, // sizeof(USB_Winusb_Configuration_t) 0x01, // interface numbers 0x01, // configuration index 0x00, // no configuation string 0x80, // no attributes 0x32 // max power: 50*2 = 100 mA }, // interface descriptor { 0x09, // sizeof(USB_Desc_Interface_t) 0x04, // descriptor type: interface 0x00, // index of interface 0x00, // no alternate setting 0x02, // endpoint numbers: 2 0xFF, //Interface Class: Vendor defined 0x00, //Interface Subclass: none 0x00, //Interface Protocol: none }, // endpoint descriptor { 0x07, // sizeof(USB_Desc_Endpoint_t) 0x05, // descriptor type: endpoint 0x81, // endpoint in 1 0x02, // transfer type: bulk 0x40, // max packet size: 64B 0x00, // useless in bulk }, // endpoint descriptor { 0x07, // sizeof(USB_Desc_Endpoint_t) 0x05, // descriptor type: endpoint 0x01, // endpoint out 1 0x02, // transfer type: bulk 0x40, // max packet size: 64B 0x00, // useless in bulk } } //当主机请求index为 manufacturer string index(0x01)的字符串时 USB_Desc_String_t stVendorStr = { 0x1A, // 1 + 1 + sizeof(L"SampleVendor") - 2 0x03, // descriptor type: string L"SampleVendor" } //当主机请求index为 product string index(0x02)的字符串时 USB_Desc_String_t stProductStr = { 0x1C, // 1 + 1 + sizeof(L"SampleProduct") - 2 0x03, // descriptor type: string L"SampleProduct" } //当主机请求index为 serial number string index(0x03)的字符串时 USB_Desc_String_t stSerialStr = { 0x14, // 1 + 1 + sizeof(L"W") - 2 0x03, // descriptor type: string L"W" } 

例程2:HID键盘设备
    本例程的目标是实现一个键盘设备,它属于HID(Human Interface Device)类别,即可以与人交互的设备。常见的键盘设备主要包含三个功能:
    - 输入按键信息(ESC/Win/Ctrl/A/B等);
    - (可选)主机输出按键状态(Numlock/Capslock/Scroll等);
    - (可选)输入多媒体控制(快进/快退/暂停等);
    那么,我们就分别需要3个端点来对应上述功能:输入端点1对应输入按键、输出端点1对应按键状态、输入端点2对应多媒体控制。然而,在USB HID设备中,多媒体控制和输入按键是可以通过唯一的报告标识号(Report ID)来区分的,所以输入端点只要一个就可以了(只要数据前面使用Report ID)。当然,第二、三个功能即使不支持也是可以的,那么这样一个键盘设备就只需要一个端点。




以下是键盘设备的描述符集合:

#pragma data_alignment=1 //对齐方式为Byte const USB_Desc_Device_t stDevKeyboard = { 0x12, // sizeof(USB_Desc_Device_t) 0x01, // descriptor type: device 0x0200, // USB Spec 2.0 0x00, // no device class 0x00, // no device subclass 0x00, // no device protocol 0x40, // max ep0 packet size: 64B 0x1234, // vendor id 0x5679, // product id 0xABCD, // product release number 0x01, // manufacturer string index 0x02, // product string index 0x03, // serial number string index 1 // configuration numbers }; typedef struct _USB_Keyboard_Configuration_t{ USB_Desc_Configuration_t stDescConfiguration; USB_Desc_Interface_t stDescInterface; USB_Desc_HID_t stDescHid; USB_Desc_Endpoint_t stDescEndpointIn; USB_Desc_Endpoint_t stDescEndpointOut; }USB_Keyboard_Configuration_t; USB_Keyboard_Configuration_t stConfKeyboard = { // configuration descriptor { 0x09, // sizeof(USB_Desc_Configuration_t) 0x02, // descriptor type: configuration 0x003B, // sizeof(USB_Winusb_Configuration_t) 0x01, // interface numbers 0x01, // configuration index 0x00, // no configuation string 0x80, // no attributes 0x32 // max power: 50*2 = 100 mA }, // interface descriptor { 0x09, // sizeof(USB_Desc_Interface_t) 0x04, // descriptor type: interface 0x00, // index of interface 0x00, // no alternate setting 0x02, // endpoint numbers: 2 0x03, // Interface Class: HID 0x01, // Interface Subclass: Boot Supported 0x00, // Interface Protocol: none }, // hid descriptor { 0x09, // sizeof(USB_Desc_Interface_t) 0x21, // descriptor type: HID 0x111, // Hid Spec Version 1.1.1 0x21, // Country Code: US 0x01, // Descriptor Numbers 0x22, // Descriptor Type: Report 0xXXXX, // Descriptor Length: sizeof(bReportKeyboard) }, // endpoint descriptor { 0x07, // sizeof(USB_Desc_Endpoint_t) 0x05, // descriptor type: endpoint 0x81, // endpoint in 1 0x03, // transfer type: interrupt 0x10, // max packet size: 16B 0x0A, // polling interval: 10ms }, // endpoint descriptor { 0x07, // sizeof(USB_Desc_Endpoint_t) 0x05, // descriptor type: endpoint 0x01, // endpoint out 1 0x03, // transfer type: interrupt 0x08, // max packet size: 8B 0x0A, // polling interval: 10ms } }; //当主机请求index为 manufacturer string index(0x01)的字符串时 USB_Desc_String_t stVendorStr = { 0x14, // 1 + 1 + sizeof(L"SampleHid") - 2 0x03, // descriptor type: string L"SampleHid" } //当主机请求index为 product string index(0x02)的字符串时 USB_Desc_String_t stProductStr = { 0x1E, // 1 + 1 + sizeof(L"SampleKeyboard") - 2 0x03, // descriptor type: string L"SampleKeyboard" } //当主机请求index为 serial number string index(0x03)的字符串时 USB_Desc_String_t stSerialStr = { 0x14, // 1 + 1 + sizeof(L"K") - 2 0x03, // descriptor type: string L"K" } 
#define SUPPORT_KEYBOARD_SWITCH //支持获取主机按键状态 #define SUPPORT_MEDIA_CONTROL //支持多媒体控制 const uint8_t bReportKeyboard[] = { 0x05, 0x01, // USAGE_PAGE(Generic Desktop) 0x09, 0x06, // USAGE(Keyboard) 0xA1, 0x01, // COLLECTION(Application) 0x05, 0x07, // USAGE(Keypad) #ifndef SUPPORT_KEYBOARD_SWITCH //如果只有一个输入报告可以忽略Report ID字段 0x85, 0x01, // REPORT_ID(0x01) #endif 0x19, 0xE0, // USAGE_MINIMUM(Left Control) 0x29, 0xE7, // USAGE_MAXIMUM(Right GUI) 0x15, 0x00, // LOGICAL_MINIMUM(0) 0x25, 0x01, // LOGICAL_MAXIMUM(1) 0x95, 0x08, // REPORT_COUNT(8) 0x75, 0x01, // REPORT_SIZE(1) 0x81, 0x02, // INPUT(Data, Var, Abs) 0x95, 0x01, // REPORT_COUNT(1) 0x75, 0x08, // REPORT_SIZE(8) 0x81, 0x03, // INPUT(Const, Var, Abs) 0x05, 0x07, // USAGE(Keypad) 0x19, 0x00, // USAGE_MINIMUM(0) 0x29, 0x68, // USAGE_MAXIMUM(104) 0x15, 0x00, // LOGICAL_MINIMUM(0) 0x25, 0x68, // LOGICAL_MAXIMUM(104) 0x95, 0x06, // REPORT_COUNT(6) 0x75, 0x08, // REPORT_SIZE(8) 0x81, 0x00, // INPUT(Data, Array, Abs) #ifdef SUPPORT_KEYBOARD_SWITCH 0x05, 0x08, // USAGE(LEDs) 0x19, 0x01, // USAGEMinimum (NumLock) 0x29, 0x05, // USAGEMaximum (Kana) 0x95, 0x05, // Report Count (5) 0x75, 0x01, // Report Size Bit(s) (1) 0x91, 0x02, // Output (Data, Var, Abs) 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size Bit(s) (3) 0x91, 0x01 // Output(Const, Array, Abs) #endif 0xC0 // End Collection #ifdef SUPPORT_MEDIA_CONTROL , 0x05, 0x0C, // USAGE_PAGE(Consumer) 0x09, 0x01, // USAGE(Consumer Control) 0xA1, 0x01, // COLLECTION(Application) 0x85, 0x02, // REPORT_ID(Media Control) 0x09, 0xB5, // USAGE(Scan Next Track) 0x09, 0xB6, // USAGE(Scan Previous Track) 0x09, 0xB7, // USAGE(Stop) 0x09, 0xCD, // USAGE(Play/Pause) 0x09, 0xE2, // USAGE(Mute) 0x09, 0xE9, // USAGE(Volume Up) 0x09, 0xEA, // USAGE(Volume Down) 0x15, 0x00, // LOGICAL_MINIMUM(0) 0x25, 0x01, // LOGICAL_MAXIMUM(1) 0x75, 0x01, // REPORT_SIZE(1) 0x95, 0x07, // REPORT_COUNT(7) 0x81, 0x02, // INPUT(Data, Var, Abs) 0x95, 0x01, // REPORT_COUNT(1) 0x81, 0x03 // INPUT(Cnst, Var, Abs) 0xC0 // END_COLLECTION #endif }; 

USB原理学习:1
    对于上文的键盘报告描述符,每行都是一个短条目,它们的意义需要在《HID Usage Tables 1.2》中查表。
    因此报告描述符就像是一本翻译指南:查字典、造句。
    首先,“0x05, 0x01”查询短条目(《HID Spec 1.1.1》Chapter 5.2)的Tag定义可知需要查询用途页0x01(《HID Usage Tables 1.2》),也就是说,让我们把“字典”翻到页面:Generic Desktop(通用桌面);
    接下来,“0x09, 0x06”可知需要在Generic Desktop用途页下查询用途0x06:Keyboard(键盘);
    同理,“0xA1, 0x01”可知开始构建一个App Collection(应用集合),这个集合的用途就是键盘;
    … …(大家可以试着自己解析一下,再往下看)





 

USB原理学习:1
    第二个报告是没有Report ID的输出报告(因为输出类型的报告只有一个,可以省略ID字段),长度为1B,作用:主机告诉从机当前按键状态:numlock、capslock等。其结构为:低5bit分别对应一个按键的状态,高3bit为常量,为了对5bit数据进行对齐。
    第三个报告是Report ID为0x02的输入报告,长度为2B,作用:从机告诉主机暂停/继续播放、快进/快退、音量+/-、静音。其结构为:第一个字节为Report ID 0x02,第二个字节为7bit控制位+1bit字节对齐。
    最后,上述报告描述符(Report Descriptor)构建完毕后,需要在主机的对应接口请求中返回给主机,和接口、类、端点描述符不同,它是可以单独获取的。在开发过程中,也有人习惯称之为:Report Map,它和Report Descriptor是同一个东西。


 

USB原理:从零基础入门到放弃_jimaofu0494的博客-CSDN博客

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/132917.html

(0)
上一篇 2025-07-27 19:33
下一篇 2025-07-27 19:45

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信