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。如图所示:
主机端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想访问鼠标设备时,该如何实现呢?
答案是设备地址。主机给所有已连接的从机分配设备地址,并确保不会重复。对刚接入还没来得及分配地址的从机,主机使用默认地址
【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_P