USB原理:从零基础入门到放弃

您所在的位置:网站首页 汽车usb接口是什么样的 USB原理:从零基础入门到放弃

USB原理:从零基础入门到放弃

2024-07-10 11:48| 来源: 网络整理| 查看: 265

前言

从一无所知到开发USB设备,需要经历怎样的过程?     我刚接触USB模块时,有无从下手的感觉。经过“摸石头过河”式的学习后,才算有了大致概念。虽说USB文档齐全、原理详实,但入门还是有一定的门槛。因此,我把自己从零开始的学习USB的过程记录分享,希望能给USB这条大河搭个桥,以供参考。本文提供一种自上而下的学习过程,无意深刻剖析直达底层原理,只盼所述能使人对完整的USB知识体系有清晰的架构认知。

理论学习

本章将由浅入深介绍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 Hardware Interface     主机端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),“从机=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】 端点号 ≠ \neq ​=端点地址。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; 功能描述符(Functional Descriptor)

    以下功能描述符的通用结构:

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

USB常用的调试工具和SDK

为了印证上述理论和调试开发,以下是我们常用到的USB工具/SDK: 【USB View】     用于查看从机描述符集,Windows SDK Debugger工具之一。 【Bus Hound】     记录主机与从机之间传输的数据(包括枚举)的工具,但它并不统计所有数据,比如部分被NAK回复的主机请求不会被记录。 【Libusb】     用户在主机端直接访问从机的开源库。但Windows下使用libusb访问从机时,需确保从机不是复合设备(windows下libusb没有访问复合设备的权限,且libusb会直接使用复合设备的第一个功能),还要确保windows也给从机分发了驱动“winusb.sys”,可以使用Zadig工具手动给设备安装驱动(当然,从机必须是winusb设备)。 【MichaelTien8901/STM32WINUSB】     WINUSB设备开发,参考这位仁兄把STM32例程中的USB CDC改成WINUSB的做法。 【STM32CubeMx】     用STM32平台开发,可以用官方工具直接生成USB HID/CDC的例程。

例程1:WINUSB设备

    Winusb设备的实现相对简单,也很好理解,初学者可以尝试开发winusb设备,对usb枚举和bulk传输也会有一个比较清晰的印象。     需要注意的是,本文给出的所有USB包结构均按char型对齐,使用这些结构开发时需注意。 以下是winusb设备常见的描述符集:

#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"W20201022") - 2 0x03, // descriptor type: string L"W20201022" }

    上述描述符集合,就是一个winusb设备的简单实现。但有了这些数据,还要将它们按主机枚举的规则来发送,所以我们还要实现它们的通信部分。     一般在各个MCU平台都会实现USB最底层的部分:SOF包同步、接收并处理令牌包、接收并解析请求、配置设备地址、读写IO中断等。要实现winusb设备,我们只需要在这些平台上完成以下事情:     1、在USB中断架构中对不同的描述符请求返回正确的数据;     2、根据端点描述符构建所有端点对应的端点通道;     3、实现端点bulk传输的读写IO;     最后,把它接入到主机,就可以看到主机能够识别到这个设备并显示出对应的文本信息(字符串描述)。如果你想抛弃MCU的USB架构从零开始实现,可以参考圈圈所著的 《圈圈教你玩USB》。     当然,作为一个标准的winusb设备,上述功能还不能算是完整的。在Windows平台,所有的USB设备都需要安装驱动(又一个庞大的知识体系)后才能使用,只不过一些知名的USB设备是支持免驱的(实际上是Windows为USB设备安装了默认的驱动)。Microsoft规定:想要Windows为winusb设备自动派发winusb.sys(即免驱功能),设备应当提供OS描述符。

例程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"K20201022") - 2 0x03, // descriptor type: string L"K20201022" }

    而HID描述符中声明的报告描述符(Report Descriptor)是什么呢?     上文提到,报告标识号(Report ID)可以将键盘输入的按键信息、多媒体控制区分开来。这个Report ID就是在报告描述符中定义的。而Report ID本身,以及按键、多媒体控制、按键状态等数据的输入/输出,统称为报告(Report)。可以说,HID设备所有功能的具体内容、格式、作用,都由报告描述符给出详细、彻底的定义,描述成一个个实际的报告。     这里先给出键盘的报告描述符:

#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 };

    可以看到,报告描述符的长度不是固定的,它随着功能的变化而变化。换句话说,报告描述符详细规定了报告的所有细节。而组成报告描述符的单位,就是短条目(Short Item)。 长条目(Long Item):既然有短条目,当然有长条目。不过长条目当前只是预留的,为了避免未来短条目不够用。 短条目(Short Item):参考《HID Spec 1.1.1》 Chapter 5.2。     短条目是标准的TLV结构,只不过T和L在同一字节(T占6bit、L占2bit)。不过,当Length=11b时,短条目的Data字段长度是4B而非3B。

Byte01,2,3,4字段Tag+LengthData(0B~4B) bit7,6,5,43,21,0PartsbTagbTypebSize

    对于上文的键盘报告描述符,每行都是一个短条目,它们的意义需要在《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(应用集合),这个集合的用途就是键盘;     … …(大家可以试着自己解析一下,再往下看)

    通过一个个短条目,就构建了一个完整的报告描述符。而上文的报告描述符,其实就是声明了三个报告:     第一个报告是Report ID为0x01的输入报告,长度为9B,作用:从机告诉主机按了XX键。比如按了“Ctrl+Alt+W+D”,发送的报告为:0x01, 0x05, 0x00, 0x07, 0x1A, 0x00, 0x00, 0x00, 0x00。其中0x1A为“W”的键值,0x07为“D”的键值。

Byte0123~9字段0x01Sepecial KeyReservedNormal Key(0~6B)

    Special Key:

bit01234567字段LCtrlLShiftLAltLWinRCtrlRShiftRAltRWin

    第二个报告是没有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.org:USB规范的官方组织。 圈圈教你玩USB(第二版):USB原理介绍非常清晰,刚学USB的人都爱它。 USB 2.0 Spec:USB协议的官方标准。 HID Usage Tables 1.2:查询HID设备报告描述符的条目(Item)代码。 HID Spec 1.11:HID设备的定义。 CDC120-20101103-track:CDC设备的定义。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3