引言
几乎没有人会否认USB(Universal Serial Bus)接口的使用方便性。对许多USB设备开发者而言,除了像一般用户那样感受到连接方便外,还会青睐许多USB接口芯片配有的较成熟的枚举程序和固件程序。这些程序如果全部要开发者从头编写,决非一朝一夕之功,难度要远大于I2C和SPI等嵌入式总线。但是,对于USB原理的学习者来讲,不尝试一下USB芯片的底层编程,什么都用别人编好的程序,恐怕也说不过去。
底层编程首先要过枚举这一关。众多公司成熟的枚举程序采用中断方式编程。事实上,为了跟踪、显示和学习枚举过程的每一细节,采用查询方式编程更为可取。本文以Microchip公司的PIC18F2455单片机为例[1],采用查询方式,对枚举过程的前5个控制传输给出了较为详细的编程流程。有关思路对其他型号的单片机也具一定参考价值。
1PIC18F2455中的USB底层编程
1.1CPU与SIE
CPU与USB总线连接示意图如图1所示。USB SIE(Serial Interface Engine)称为串行接口引擎,CPU通过它跟USB总线打交道。1 KB的USB RAM是双端口存储器,USB各端点的缓冲区描述符及缓冲区本身都设在这一部分。虽然硬件上允许CPU和SIE同时访问USB RAM,但编程时要尽量避免,否则会出现不可预料的结果。
图1CPU与USB总线连接示意图
图2中BDnSTAT是端点n的缓冲区描述符状态寄存器。当UOWN位=0时,CPU拥有相关RAM区;而当UOWN位=1时,RAM区为SIE拥有,当某事务完成时SIE将UOWN清零。
图2重要寄存器及标志位
1.2缓冲区及其描述符
USB可允许4种传输方式,设备中又可以支持多个端点[2]。为了便于循序渐进地学习,把握主要原理,在下面的实验流程中只对应控制传输方式,并且只设一个端点0(简称EP0)。图3中,EP0 OUT描述符服务于主机往设备发送数据包(SETUP事务或OUT事务时)。其中,BD0STAT是对应图2的状态寄存器,BD0CNT是主机通过SIE送来的实际字节数,BD0ADRH和BD0ADRL分别是该描述符所指缓冲区首地址的高低字节。类似地,EP0 IN描述符服务于设备往主机发送数据包(IN事务时),其中BD0CNT代表设备发送给主机的字节数。
图3EP0OUT和IN描述符
1.3事务与中断
对单片机PIC18F2455来说,USB通信既可以采用查询方式又可以采用中断方式。在实际应用系统中多考虑中断方式,但是,为了学习USB枚举过程,查询方式优势明显,便于跟踪程序状态变化。图2中UIR为8位中断状态寄存器,重要的一个标志位是TRNIF(事务完成标志位)。当待处理的事务完成时,TRNIF被置1。例如:枚举一开始,主机往设备发送一SETUP令牌包,接着主机再往设备发送一数据包(取设备描述符的请求),设备再发给主机一个ACK应答包。整个过程完毕时标志一个SETUP事务完成,TRNIF被置1,此时可以中断CPU(如果单片机的中断已被允许的话)。如果中断被禁止,程序可以等待并查询TRNIF,一旦发现等于1,马上继续后续枚举过程,这正是本文采用的方式。
2枚举编程流程
因篇幅有限,下面只给出枚举过程的前5次控制传输,从首次取设备描述符开始,到全部读出配置信息为止。
2.1第1次控制传输:SETUP-IN-OUT(STATUS)
(1) 建立阶段(一个SETUP事务)
① 为SETUP事务预期一个OUT型数据包(8字节来自主机的GetDescriptor(Device)请求),且请求设备在数据阶段传输8个字节。实验中EP0(端点0)为OUT型数据准备的缓冲区首地址设为500H(该地址放在402H 403H单元)。
② SIE 拥有OUT缓冲区,等待SETUP事务。
③ 判断是否有 TRNIF 中断。如果有,说明出现了一个SETUP事务,即设备已收到GetDescriptor(Device)请求。
(2) 数据阶段(一个IN事务)
① 为IN事务准备一个IN型数据包(8字节Descriptor(Device)),实验中EP0为IN型数据准备的缓冲区首地址设为510H(该地址放在406H 407H单元)。
② SIE拥有IN缓冲区,等待IN事务。
③ 判断是否有 TRNIF中断。如果有,说明出现了一个IN事务,即主机已收到8字节Descriptor(Device)。
(3) 状态阶段(一个OUT事务)
① 为OUT事务预期一个OUT型数据包(任意字节来自主机的STATUS空数据)。
② SIE 拥有OUT缓冲区,等待OUT事务。
③ 判断是否有 TRNIF 中断。如果有,说明出现了一个OUT事务,即设备已收到STATUS空数据。
2.2第2次控制传输:SETUP-IN(STATUS)
(1) 建立阶段(一个SETUP事务)
与第1次控制传输的建立阶段类似,但本次是8字节来自主机的SetAddress请求,无数据阶段,分配给设备的Address在本阶段捎带传递。缓冲区与前同。
(2) 状态阶段(一个IN事务)
① 为IN事务准备一个IN型数据包(0字节送往主机的STATUS空数据)。
② SIE拥有IN缓冲区,等待IN事务。
③ 判断是否有 TRNIF 中断。如果有,说明出现了一个IN事务,即主机已收到0字节STATUS。
2.3第3次控制传输:SETUP-IN-OUT(STATUS)
(1) 建立阶段(一个SETUP事务)
与第1次控制传输的建立阶段类似,但本次是8字节来自主机的“GetDescriptor(Device) with new address”请求,且请求设备在数据阶段传输18字节。
(2) 数据阶段(一个IN事务)
与第1次控制传输的数据阶段类似,但本次的IN型数据包是18字节送往主机的Descriptor(Device) with new address。
(3) 状态阶段(一个OUT事务)
与第1次控制传输的状态阶段相同。
2.4第4次控制传输:SETUP-IN-OUT(STATUS)
(1) 建立阶段(一个SETUP事务)
与第1次控制传输的建立阶段类似,但本次是8字节来自主机的“GetDescriptor(Configuration)”请求,且请求设备在数据阶段传输9字节。
(2) 数据阶段(一个IN事务)
与第1次控制传输的数据阶段类似,但本次的IN型数据包是9字节送往主机的Descriptor(Configuration)。
(3) 状态阶段(一个OUT事务)
与第1次控制传输的状态阶段相同。
2.5第5次控制传输:SETUP-IN-OUT(STATUS)
(1) 建立阶段(一个SETUP事务)
与第1次控制传输的建立阶段类似,但本次是8字节来自主机的“GetDescriptor(Configuration)”请求,且请求设备在数据阶段传输18字节。
(2) 数据阶段(一个IN事务)
与第1次控制传输的数据阶段类同,但本次的IN型数据包是18字节Descriptor送往主机,包括9字节Descriptor(Configuration)和9字节Descriptor(Interface)。实验中只设EP0一个端点,故不要送Descriptor(Endpoint)。
(3) 状态阶段(一个OUT事务)
与第1次控制传输的状态阶段相同。
3结论
采用汇编语言编程。运行程序时,每到一个阶段,可以设置断点变量,以便观察中间结果,如果借助于编程工具(如ICD2)则更方便。例如,第1次控制传输的建立阶段结束时,可以看看OUT缓冲区中有没有 GetDescriptor(Device)请求;第2次控制传输的建立阶段结束时,可以看看OUT缓冲区中有没有主机分配给设备的USB新地址。如此可以极大地提高学生的学习兴趣,将较复杂的USB实际过程摆到前台。当然这里未考虑通信出错等异常情况,但学习首先应学主干,即使某次出现通信异常,将程序从头运行再试即可。