USB原理学习:1

理论学习
本章将由浅入深介绍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。如图所示:


    主机端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想访问鼠标设备时,该如何实现呢?

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

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

    答案是用端点(Endpoint,EP)加以区分。主机通过设备地址找到从机后,再通过端点访问从机的指定功能的指定用途。端点具有唯一性,它们和从机的功能及用途一一对应,按照端点的属性构建专用的端点通道(Pipe)来通信。另外,端点还标识了特定用途的数据传输方向。因此,对于USB复合设备A,通过端点号可区分键盘功能的发送或接收。
【Q】有多少个功能/用途就分配多少个设备地址不就可以了吗?
【A】 如果这么做,当主机接入多个USB设备,而每个USB设备又支持多种功能、每个功能又包含多个用途时,主机需要分配的地址数量非常之多,且每次拔插设备需要多次分配地址,最终通信效率变低了。
【Q】主机未识别从机的功能之前用什么端点通信?
【A】 与默认地址0一样,从机也会有默认端点0(Default Endpoint, EP0)。准确来讲,对初次接入的从机,双方通过<Addr0,EP0>进行通信。

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

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

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

 

    主机发送令牌包(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;

       主机可以通过请求包指定要访问的从机,发起请求过程,配置从机或指示从机准备发送/接收数据。在枚举过程中,主机使用SETUP包请求从机的信息。枚举成功后,主机使用IN包请求输入数据,OUT包请求输出数据。
    枚举时,在SETUP包的后面会紧跟一个8B长度的请求(Request),用于描述主机的具体意图,结构如下:

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

        在“请求过程”阶段,被访问的从机会接收并解析请求,若wLength字段不为0,则进入数据过程,否则进入状态过程。
【Q】从机收到不支持的请求怎么办?
【A】
可以直接进入状态过程,从机发送STALL包。
【Q】有了IN/OUT包,为什么还要在请求中声明传输方向(Direction)?
【A】
IN/OUT包后面不会带有请求。从机在收到IN/OUT包后直接进入数据过程,发送数据或回复NAK(没有数据要发送)。

数据过程(数据包)
    请求的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;

       没有数据过程时,握手包的发送方是从机;
    数据过程为Data Out时,握手包的发送方是从机;
    数据过程为Data In时,握手包的发送方是主机;
    当然,除了上述USB包,还有特殊包(Special Packets):PING(0100B) / SPLIT(1000B) / PRE(1100B) / ERR(1100B)。这些特殊包的作用参考《USB 2.0 Spec》Chapter 8。

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

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

描述符集的层次结构

    一个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;

功能描述符(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设备接入后,主机复位从机,使用<addr0, EP0>构建端点通道(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。


    对于上文的键盘报告描述符,每行都是一个短条目,它们的意义需要在《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”的键值。


    第二个报告是没有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设备的定义。

版权声明:本文为CSDN博主「mnstr」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:

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

  • 11
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值