USB帧概念
如上图所示,在USB1.1规范当中,把USB总线时间按帧划分,每一帧占用时间是1ms;
每一帧内的最开始处是SOF token,在SOF内包含有11位的帧号;
每一帧的SOF帧号相比前一帧的SOF帧号加1,直到11位最大值以后回到零,如此循环往复;
每一帧的最后是很短时间的EOF Interval,EOF即为end of frame,作为当前帧的结束;
USB帧内传输(transfer)
如上图所示的同步传输和批量传输可以看出,USB规范定义传输的基本单位是transaction,多个transaction构成1个transfer。这些transaction就发生在usb帧内,如下图所示:
图中可以看到有3个USB 帧的数据传输, 每1帧都以SOF开始:
- 在第1帧内,USB主机先和设备1的端点2通信也就是通过前述的transaction或者transfer,接下来再和设备2的端点2通信,然后再和设备5的端点3通信;最后我们可以看到这一帧剩下的时间没有任何通信;
- 在第2帧内,主机先和设备1的端点2通信,接下来再和设备2的端点0通信,然后再和设备5的端点3通信,最后也是剩余的空闲时间;
- 在第3帧内,主机先和设备1的端点2通信,接下来再和设备2的端点0通信,然后是剩余的空闲时间。
从上面的图形我们需要明白的是,每一帧内会发生多少次传输是由主机来决定;USB是一种主从通信的架构,每次传输由主机发起,通过类似查询的机制,被寻址到的端点才会发出数据或者告诉主机现在无数据可发。
另外,同步传输和中断传输的优先级比批量传输和控制传输更高,主机总是先处理前两者,每一帧剩下的时间才会处理后两者;另外,每一帧总会保留一些时间给控制传输使用,防止因为USB带宽耗尽而不能处理其他事务的情况发生比如USB枚举。
USB封包类型
USB通信都以封包的形式进行,例如前面提到的基本通信单位transaction就由多个封包构成。下图是USB封包格式:
下图列出了USB1.1标准的封包类型:
从上面的图片可以看到,封包当中有一个4位宽度的字段PID,也就是packet identifier,上面的表格总共列出了10个PID也就是总共有10个类型,同时又分成4大类:
- Token类,包含OUT/IN/SOF/SETUP封包;OUT/IN token专门用于数据交换,SOF如前所述用于帧起始指示;SETUP专门用于控制传输。
- Data类,包含DATA0/DATA1 共2个封包,与前述OUT/IN token相配合,专门用于数据交换;当通信需要多次transaction的时候,DATA0和DATA1交替发生,以用于可能发生的错误恢复。
- Handshake类,包含ACK/NAK/STALL封包,都专门用于数据交互时的应答;ACK表示DATA0或者DATA1封包已经被收到;NAK表示当前不能够收取Data类封包;STALL表示当前通信发生了错误且需要主机进一步采取措施来恢复通信。
- Special类,只包含PRE封包;这一类封包毋须关心,只需要知道它的作用是用于全速总线兼容低速设备。
什么是HID设备?
HID设备即人机交互设备,常见例子如键盘、鼠标以及游戏控制类设备。
HID设备专门用于主机和人类交互的场景,比如主机根据使用者在键盘按下的键值或者鼠标的移动做出响应。
HID主机必须做到快速响应,以防止使用者可能觉察得到的响应延迟。
HID设备产生的数据以report的形式向主机发送,其格式由USB HID规范定义;HID规范的核心是定义了现实世界的各种物理对象或者物理单位并指明其用途;可以查询HID相关规范比如Usage Page以及Usage ID的定义。
HID Report主要使用中断和控制两种传输;其中,中断传输主要 用于低时延场合比如按键或者鼠标的移动,而控制传输主要用于对时延要求不高的场合比如键盘上的大小写状态灯或者数字键盘锁定状态灯等等。
HID transaction
如前所述,HID Report主要使用中断传输向主机发送有时延要求的数据,下面以中断传输为例看一下具体流程如下图:
从图中可以看到有3个阶段:
- 首先是主机发送token;如果是设备向主机发送数据也就是上图左侧,主机发送IN token,IN token包含设备地址以及端点地址,起到类似查询的作用,意思是询问某个设备端点是否有数据;类似地如图右侧,如果是主机向设备发送数据,主机发出OUT token,其含义是通知某个设备端点即将有数据发送给该端点;
- 接下来是数据阶段;如上图左侧,被寻址到的设备端点交替发送DATA0/DATA1,这是设备有数据发送的情况;如果设备无数据可发,该端点就发送NAK,之后本次的中断传输就结束了;如果设备遇到的某一种错误而期望主机之后进行错误处理,该端点就发送STALL,同样也结束了本次中断传输;如上图右侧,因为是主机向设备发送数据,这个阶段就直接由主机交替发送DATA0/DATA1;
- 最后是握手阶段;如上图左侧,如果主机正确接收了DATA0/DATA1,就回应ACK;如果主机接收到了DATA0/DATA1但是发现CRC错误或者DATA0/DATA1的顺序错误,或者由于线缆质量低劣没有收到DATA0/DATA1,都不会回应ACK,在下一次中断传输该端点会进行数据重发;如上图右侧,如果设备端点回应ACK表示DATA0/DATA1被正确接收,如果设备端点回应NAK表示该端点现在不能接收数据,通常的原因是上一次的数据还没有从端点取走;如果设备端点回应STALL表示该端点遇到错误通信不能继续,需要主机之后进行错误处理;如果设备端点没有任何回应,下一次中断传输主机会重发数据。
从前面可以看出,中断传输都是由主机发起的,以IN token或者OUT token开始,类似一种查询机制;数据要低延迟发送,查询的时间间隔就非常重要,这个参数通常是在端点描述符里进行描述,并且是在枚举阶段主机获取到这个参数,之后主机就按照该设备端点期望的查询间隔周期性地开始查询。
nRF5 HID composite example
现在结合nRF5 SDK HID composite example具体讲述前面的内容。
如上图,在nRF5 SDK v17.1的HID composite例程当中,可以看到端点8被定义成interrupt IN类型的端点,其查询间隔是1ms,即USB1.1定义的最小查询间隔;这意味着主机将会以1ms的时间间隔来查询该端点。
另外,从上图的按键处理代码可以看到,当DK板按键被按下以后,会向主机发出X方向为+3的位移量;同时还会启动溢出时间为5ms的APP_Timer,溢出后同样也是会向主机发出X方向为+3的位移量;这样形成的效果就是当DK板按键被按下并且一直保持,PC桌面的光标会快速向右移动。
下面再结合USB协议分析仪具体看一下和上述代码相对应的USB总线上的传输情况。
可以看到,在USB总线上主机每5ms成功取到一次数据,也可以看到DATA0和DATA1是交替发送;同时还可以看到在主机成功取到数据之前,还有4次也就是4毫秒的时间内该端点回复NAK;这样算下来,HID汇报率是200Hz。
假如我们修改代码如下,也就是把APP_Timer的溢出时间修改成1ms,是否就可以达到1000 Hz的汇报率呢?
和上述代码相对应的USB总线传输情况如下:
从上图可以看到,在多数的USB 1ms帧内,该端点都交替回复了DATA数据包,但是总有少数的USB帧内,该端点回复NAK表示无数据可发,所以总体来看汇报率是低于1000Hz。
nRF52 USB – 批量和中断传输
如果需要理解上述汇报率低于1000Hz的原因,有必要了解nRF52 USB外设工作原理,如下图:
从上图我们需要理解以下几点:
当easy dma还没有写入数据到端点的时候,nRF52 USB外设端点会自动发送NAK;
当启动easy dma并且数据已经被写入端点以后,主机再次查询时,端点就会把数据以DATA封包的形式交替发出;
当DATA封包发送前后,会产生2个事件,分别是EVENTS_ENDEPIN[n] 和EVENTS_EPDATA;EVENTS_ENDEPIN[n]表示easy dma所指向的RAM地址内的数据已经被端点使用完毕,现在用户可以操作这一段RAM比如改写等操作;EVENTS_EPDATA表示端点内数据已经成功发送到了主机。
如何提高汇报率到1000Hz?
如果主机每次查询时候,端点内的数据已经准备好,就可以保证主机每次都可以取到数据,也就可以达到1000Hz汇报率;如何保证端点内的数据及时准备好呢?很明显,可以利用前面提到的端点外设事件EVENTS_ENDEPIN[n] 和EVENTS_EPDATA;即一旦当前的数据已经成功发送,就立即启动easy dma准备好新的端点数据,在下一帧内主机就可以取到新的数据,如此循环往复汇报率就能够达到1000Hz;非常方便的是,USB驱动已经有事件APP_USBD_HID_USER_EVT_IN_REPORT_DONE供应用层代码使用;通过使用该事件,就可以达到上述目的。
在原始的例程代码当中,该事件并无特殊处理,只是翻转DK板上的LED灯;现在只需在这个事件内加入写端点的代码也就是每次向右移动3个位移量。需要指出的是,我们需要使用APP Scheduler模块的API,把写端点的操作从USB事件队列处理循环当中移出,并放到该循环之后。
改进后的代码片段如下:
如上图,当事件APP_USBD_HID_USER_EVT_IN_REPORT_DONE 发生后也就是主机取到数据之后,调用APP Scheduler模块API,将函数HIDMouseReportHandler放入APP Scheduler模块事件队列;
如上图所示,真正写端点的操作发生在USB事件队列处理循环之后,位于APP Scheduler模块API app_sched_execute内。
更改后的USB总线传输情况如下:
从上图可以看出,HID 汇报率的确达到了1000 Hz。在USB每一帧内,主机都成功取到了一次数据,也就是HID Report所定义的X方向和Y方向位移量以及滚轮数据,可以看到每一次X方向的位移量都是+3。
从时间戳上也可以看出,HID Report相隔的时间也是精确的1ms,例如 19.868.554 -> 19.869.554-> 19.870.554 -> ->19.871.554 ……
补充1:USB协议如何保证正确传输?
首先,从前面的讲述以及上面的图片可以看到,USB封包除了Handshake类没有CRC校验以外,其他封包都包含CRC检验码,token类使用CRC5,数据类使用CRC16,这样可以保证USB封包的正确传输;其次,虽然Handshake类没有CRC并不意味数据完整性会丢失,通过以下3个例子我们就可以理解:
1. DATA封包成功传输
左边第i次传输:
发送方sequence bit是0,因此发送方发出DATA0封包,接收方正确接收以后其sequence bit切换到1;
接收方发出ACK,发送方正确接收以后其sequence bit切换到1;
右边第i+1次传输:
发送方sequence bit已经为1,因此发送方发出DATA1封包,接收方正确接收以后其sequence bit切换到0;
接收方发出ACK,发送方正确接收以后其sequence bit切换到0;
如此循环往复,数据封包在发方和收方之间正确传递。
2. DATA封包错误或者未被接受
左边第i次传输:
发送方sequence bit是0,因此发送方发出DATA0封包,接收方收到错误的DATA0封包或者由于端点缓冲区尚未清空因此没有接收该DATA0封包,因此接收方的sequence bit保持不变为0;
接收方发出NAK,发送方正确接收以后发现是NAK而不是ACK,其sequence bit也保持不变为0;
右边第i次重传:
发送方sequence bit仍旧是0,因此发送方仍旧发出上一次的DATA0封包,接收方正确接收以后其sequence bit切换到1;
接收方发出ACK,发送方正确接收以后其sequence bit也切换到1;
因此,某个封包在没有正确传输的情况下可以在下一次重试的时候成功传输。
3. Handshake封包错误
左边第i次传输:
发送方sequence bit是0,因此发送方发出DATA0封包,接收方正确接收以后其sequence bit切换到1;接收方发出ACK但该封包由于信号质量的原因未被发送方正确接收,因此发送方的sequence bit未能切换到1即保持不变为0;
中间第i次重传:
发送方sequence bit仍旧是0,因此发送方仍旧发出上一次的DATA0封包,接收方因sequence bit已经为1而识别到该DATA0封包是重传封包因此将其忽略;接收方仍旧发出ACK,发送方正确接收以后其sequence bit也切换到1;
右边第i+1次传输:
发送方sequence bit已经为1,因此发送方发出DATA1封包,接收方正确接收以后其sequence bit切换到0;接收方发出ACK,发送方正确接收以后其sequence bit切换到0;
如上所述,ACK封包虽然没有CRC检错能力,但是即使在其没有正确传输的情况下,收发双方在这之后还是可以恢复收发同步。
补充2:主机如何调度中断传输和同步传输
如前所述,USB是一种主从通信的架构,每次传输由主机通过类似查询的机制发起,被寻址到的端点发出响应,从机在任何时候都不能够也不可以主动发起通信;另外,同步传输和中断传输优先级比批量传输和控制传输更高,主机总是先处理前两者,每一帧剩下的时间才会处理后两者;所有这一切都是由主机的调度机制来保证的。
总体上,在主机端把USB的四种传输方式按照两类来执行调度,分别是周期性调度和异步调度:周期性调度针对有时间紧迫性的传输也就是同步传输和中断传输,比如HID设备,音频类设备,这些设备必须在一定的时间内完成数据传输,否则用户会感觉到操作延迟或者听到音乐卡顿。
异步调度针对数据传输没有时间要求而只需数据能够正确传递,也就是批量传输和控制传输。这类设备比如优盘等设备,只要用户能够把文件正确复制即可,如果此时USB带宽还有较大冗余用户也不会感觉耗时。
下面我们通过一个实际的USB主机实现也就是EHCI来理解上述机制。
如上图,EHCI标准当中定义了2个寄存器分别是PeriodicListBase和 FRINDEX,前者相当于指向某一段RAM空间的基地址,FRINDEX相当于索引值,两者组合起来的地址叫Periodic Frame List Element Address;该地址指向一个有1024/512/256个元素的数组当中的某个元素,这个数组叫做Periodic Frame List;而每个元素的值将指向一个对象叫做Isochronous Transfer Descriptor,该对象就是专门用于同步传输的描述符包括设备地址,端点地址,端点的maximum packet size,数据长度及地址,以及下一个传输数据结构的地址可以是同步传输也可以是中断传输……根据该描述符的内容,EHCI主机控制器就可以在同一帧内向指定设备传输同步数据和中断数据。
上图还可以看到用于描述中断传输的数据结构interrupt queue head也就是图中的多边形,同样也包括了设备地址,端点地址,端点的maximum packet size,数据长度及地址以及下一个interrupt queue head地址等等参数,借此 EHCI主机控制器就可以向指定设备传输中断数据。
理解了上述内容,就可以理解上面图形的意义:
上面图形表示现在主机维持了1个同步传输和3个中断传输;其中同步传输发生在每1帧也就是相对应的设备端点描述符的polling interval是1;同样的,另外3个中断传输分别是发生在每8帧、每4帧、每1帧,这也是由对应设备的端点描述符来指定。
EHCI主机控制器根据每帧都会自加1的FRINDEX寄存器,结合PeriodicListBase,就会顺序取到Periodic Frame List元素,而每个元素都指向一个Isochronous Transfer Descriptor(iTD);而iTD又会指向interrupt queue head(iQH);所以最终形成和每一帧相对应的链表如下:
iTD -> iQH(8) -> iQH(4) -> iQH(1);
iTD -> iQH(1);
iTD -> iQH(1);
iTD -> iQH(1);
iTD -> iQH(4) -> iQH(1);
iTD -> iQH(1);
iTD -> iQH(1);
iTD -> iQH(1)
iTD -> iQH(8) -> iQH(4) -> iQH(1);
上述链表的执行结果就是1个同步传输和3个中断传输;同步传输每1帧处理1次;中断传输分别每8帧、每4帧、每1帧处理1次。
前面有讲到过,USB主机在每帧的剩下时间才会处理控制和批量传输。这是通过一个异步链表来实现的,链表的头位于AsyncListAddr寄存器内,异步链表是一个首尾相接的环形链表,链表的元素也是前述类似的queue head数据结构。这样主机控制器就可以在每一帧执行完周期性调度以后的剩余时间内执行异步调度也就是控制传输和批量传输了。