1、问题的提出

通用串行总线(USB)作为一种中高速的数据方式,已经很普遍地应用于外设与主机的通信中。VxWorks是当今十分流行的实时操作系统,在通信、国防、工业控制、医疗设备等嵌入式实时应用领域。很多VxWorks系统中都有USB设备,可是关于USB的驱动实现并没有相关的资料可以参考,给实际工作带来了难题。本文通过详细地分析VxWorks下的USB驱动栈,具体提出了其实际应用的方法,为USB在应用VxWorks的嵌入式系统的开发扫清了技术障碍。

2、VxWorks简介

VxWorks是WindRiver公司开发的具有工业领导地位的高性能实时操作系统(Real Tim Operation System,RTOS)内核,具有先进的网络功能。VxWorks的开放式结构和对工业标准的支持,使得开发人员易于设计高效的嵌入式系统,并可以很小的工作量移植到其它不同的处理器上。

作为一种先进的实时操作系统,它具有以下特点:

  • 可裁剪微内核结构
  • 高效的任务管理
  1. 多任务,具有256个优先级
  2. 具有优先级排队和循环调度
  3. 快速的、确定性的上下文切换
  • 灵活的任务间通讯
  1. 三种信号灯:二进制、计数、有优先级继承特性的互斥信号灯
  2. 消息队列
  3. 套接字(Socket)
  4. 共享内存
  5. 信号(Signals)
  • 微秒级的中断处理
  • 支持POSIX 1003.1b实时扩展标准
  • 支持多种物理介质及标准的、完整的TCPIP网络协议
  • 灵活的引导方式。支持从ROM、flash、本地盘(软盘或硬盘)或网络引导
  • 支持多处理器并行处理
  • 快速灵活的l/O系统
  • 支持MS-DOS和RT-11文件系统
  • 支持本地盘,flash,CD-ROM 的使用
  • 完全符合ANSI C标准
  • 多个系统调用

3、USB驱动程序的结构概述

图1提供了一个USB主驱动栈结构的简单概括

VxWorks USB HC

图2显示了USB主驱动栈的各模块之间的功能联系

VxWorks USB SW HW

在栈的最底部是USB主控制器(USB HC, 即USB Host Controller), 这是主系统中控制每一个USB设备的硬件部分。

目前,市场上主要有两大类USB主控制器,一种是支持由ime1公司最先提出的通用主控制器接口(Universal Host Controller Interface,简称UHCI),另一种是支持由微软、康柏和国家半导体公司联合设计提出的开放主控制器接口(Open Host Controller Interface,简称OHCI)。硬件厂商一般根据这两个规范设计他们的USB主控制器。

对于每一类型的主控制器都有一个与硬件独立的USB主控制器驱动(Host Controller Driver,简称HCD)。WindRiver提供了两个驱动:usbHcdUhciLib (UHCI 主控制器库)和usbHcdOhciLib (OHCI主控制器库)。USB主驱动(USB host driver,简称USBD)和HCD之间的接口允许一个或超过一个的底层主控制器。而且,WindRiver的USBD能够同时连接多个USB HCD。这样的设计特点可以使开发者建立复杂的USB系统。

USBD是在HCD之上的与硬件独立的模块。USBD管理每一个与主机相连的USB设备, 向更高层次提供了可与USB设备通信的路径。它还负责自动处理USB电力管理以及USB带宽管理。而且,USBD还管理USB hub,Hub功能是一个驱动能否对USB正确操作的评价之一。因此WindRiver的USBD设计者要使USBD透明地处理hub的功能。这意味着,USBD 还能处理USB hub和设备的动态插拔。

USB Client模块在USB主驱动栈的顶端。USB类驱动(USB Class Driver)是Client模块的典型例子。USB类驱动负责管理连接到USB上的不同类型的设备; 它们依靠USBD来提供与每个设备的通信路径。USB client模块的其他例子就是那些利用USBD与USB设备通信的应用程序。

4、 USBD驱动详解

这一部分将要描述USBD(USB Host Driver)的典型应用。例如初始化,client注册,动态连接注册,设备配置,数据传输,同时还探讨了USBD内部设计的关键特性。这部分是VxWorks下USB驱动的核心。

4.1 初始化USBD:分为两步

(1) 必须至少调用一次函数usbdInitialize()。在一个给定的系统中,usbdlnifialize()初始化内部USBD数据结构,并依次调用其它USB驱动栈模块的入口。usbdinitialize()可以在启动时调用一次,也可以对每一个设备各调用一次。USBD 自己记录了调用usbdInitialize()(‘+’)和usbdShutDown()(‘-’)的次数。只有大于等于1时才是真正初始化了,而等于0是关闭了。

(2) 用USBD 的lisbdHedAttaeh()函数来把至少一个HCD连接到USBD上。这一过程既可以在VxWorks启动时,也可以在运行时把HCD 连接到USBD 上去。后一种机制可以支持“热插拔”,而不用象前一种那样需要重新启动。

4.2 HCD的连接(attaching)与断开(detaching)

当HCD连接到USBD 时,调用者为usbdHedattaeh函数传递HCD执行入口(表HCD_EXEC_FUNC)和HCD连接参数(HCD attach parameter)。USBD用HCD FNC ATYACH 服务请求依次激活HCD的执行入口,传递同样的HCD attach参数。

需要强调虽然可以改变用HCD定义的参数,但是最好不应该有所改变。对于WindRiver提供的UHCI和OHCI的HCD,HCI attach参数是一个指向结构PCI_CFG_HEADER (定义在pciConstants.h) 的指针。

该结构用UHCI和OHCI主控制器的PCI配置头来初始化,而HCD用这个结构中的信息来定位,管理特定的主控制器。典型的,调用者用usbPeiClassFind ()和usbPciConfigHeaderGet()来得到想要的主控制器的PCI配置头- 这两个函数定义在usbPciLib 中(stubUsbarchPciLib.h中)。如果有UHCI或OHCI要连接到USBD,就要调用这些函数来获得每一个主控制器的PCI_CFG_HEADER。然后利用usbdHedAttaeh来激活已鉴别出的每一个主控制器。

注意:底层BSP可能不支持USB的HCD断开,因为当中断向量表重新使能时,如果还应用的是过期的向量表,会导致错误
4.3 启动顺序

必须在所有USBD函数前执行函数usbdInitialize()。存在以下两种调用方式:

(1) 传统的“启动”初始化。执行顺序与其意义如下:

  • usbdInitialize()
  • usbdPciClassFind():定位一个USB主控制器
  • usbdPeiConfigHeaderGet():读USB主控制器配置头
  • usbdHedAttaeh():连接HCD,将其作为特定的主控制器
  • 调用USB class driver初始化入口点
  • USB class driver调用usbdlnitialize()

(2) “热插拔”调用。执行顺序与其意义如下:

Boot Code里调用:

  • USB class driver初始化入口点
  • USB class driver调用usbdlnitialize() Hot-Swap code调用
  • Hot-Swap 鉴别USB主控制器的连接或断开
  • Usbdlnitialize()
  • UsbdPciConfigHeaderGet():读USB主控制器配置头
  • UsbdHedAttaeh():连接HCD,将其作为特定的主控制器

因为热插拔可以在任何时刻发生,所以USBD和其Client都必须被写成可以动态识别USB设备被插入还是被拔出。当主控制器连接到系统时,USBD 自动地鉴别与其相连的设备,并通知相关的client;同样,拔出设备时,也要通知相关设备。重要的是,USBD 的client,比如USB class driver,在client初始化时,从不设想特定的设备已经出现;而在其他时候,这些驱动随时检查设备是否已经连接到系统上。

4.4 总线任务

对每一个连接到USBD 的主控制器,例如插入或拔出设备,USBD都会产生一个总线任务,来监控总线事件。一般情况下,这些任务是休眠的(不消耗CPU),只有当USB hub报告它的一个端口有变化时,它们才被唤醒。每一个USBD总线任务有VxWorks任务名:UsbdBus。

虽然HCD委托USBD来管理,但有可能HCD 亲自监视主控制器事件。例如WindRiver提供了UHCI和OHCI的HCD来创造这样的任务。对于WindRiver的UHCI模块(usbHcdUheiLib),后台任务只是被周期地唤醒,目的是为了检查超时IRP(用一个中断来通知OHCI根hub发生改变)。

用以在USBD和USB之问进行通信的client模块,除了调用usbdlnitialize()外,必须调用usbClientRegister()使其在USBD注册。当一个client注册到USBD时,USBD把每一个以后将要用到的client的数据结构定位,并跟踪那个client的请求。

对于每一个client,在client注册过程中,USBD还创建了一个callback任务。在成功注册client后,USBD返回一个句柄USBD_CLIENT_HANDLE。以下对USBD的调用,将会用到这个句柄。当所有句柄都不需要时,可以调用usbdClientUnregister()来释放每一个client的数据结构和callback任务。注意:此时所有client要求的任务都会被取消。

例如:注册一个叫USBD_TEST的client,再注销。

注册:usbdClientRegister(USBD_TEST,&usbdClientHandle);

注销:usbdClientUnregister(usbdClientHandle);

4.5 client回调(callback)任务

USB操作是严格遵守时序的。例如为使中断传输和同步传输正确工作.需要依靠时钟中断。在一个有几个不同client出现的主系统中.总是有可能出现一个client打断其它client传输事件的发生。WindRiver USBD建议用client callback任务来解决这个问题。许多USB事件可以导致一个USB client的callback任务。例如, 每当USBD 完成USB IRP后,client的IRP callback函数被激活。同样,当USBD识别出一个动态连接事件后,会激活一个或更多的动态attach callback操作。但不是马上激活这些回调操作, 而是安排合适的相应的USBD client的回调任务来执行callback。

一般的情况下,每一个client的callback任务处于“休眠”态(阻塞态)。每一个client的callback,继承了usbdClientRegister()产生的VxWorks任务优先级。这确保了每一个callback按其client的任务优先级来执行,而且可以利用优先级来写client,保证对时间要求严格的USB传输。由于每一个client有它自己的callback任务,因此在callback期间,它们有很大的灵活性决定可以做什么。例如,允许在不破坏USBD或其它USBD client性能的条件下,使callback执行代码运行至阻塞态。

Client callback task有VxWorks任务名:tUsbdCln。

4.6 USBD内部Client

当第一次初始化USBD时,由USBD产生并注册一个内部client,以跟踪USB请求。

USBD 可以产生什么类型的USB请求呢? 所有USBD与USB设备的传输,均利用调用USBD client的形式来完成。例如, 当一个设备第一次连接到系统时.USBD用一个控制管道(control pipe) 自动地创建设备需要的所有的control pipe,即USBD client要用usbdPipeCreate()来创建一个与USB endpoint0通话的通道,然后所有USBD 内部、外部client通过这个管道来发送诸如usbdDescriptorGet()或usbdFeatureGet()等的函数,进行操作。

所以,USBD 的一个机制就是USBD 循环利用它自己的entry point,而内部chent跟踪这些请求。

4.7 动态连接的注册

每当一个特定类型的设备插入或拔出时,USBD client都通知上一层。利用调用usbdDynamicAttachRegister()操作,client可以指定一个callback操作,以便可以获取这样的通知。

USB设备类型用class,subclass,protocol来区别。标准的USB 类在usb.h 中定义为USB_CLASS_XXXX。Subclass和protocol根据class来定义, 因此这些常数根据特定的class在头文件中定义。

有时,一个client当利用usbdDynamicAttachRegister()进行注册时,只对特定的class,subclass,protocol感兴趣。例如,USB键盘类驱动usbkeyboardLib, 注册了Human Device Interface (HID) 类,subclass 是USB_SUBCLASS_HID_ BOOT,protocol是USB_PROTOCOL_HID_BOOT _KEYBOARD。通过callback机制的响应,每当一个设备完全符合这样的标准, 从设备上插入或拔出时,SBD便通知给keyboard class driver。而在其它情况下,client关注的范围更广泛了。常量USBD_NOTIFY(定义在usbdLib.h)可以替代任意的class,subclass,protocol。例如,USB打印机USB驱动,usbPrinterLib, 其class等于USB_CLASS_PRINTER,subclass 等于USB_SUBCLASS_PRINTER (usbPrinter.h),protocol等于USBD_ NOTIFY_ ALL。典型的,当一个client只调用一次usbdDynamicAttachRegister()时,对一个client能拥有的并发通知请求数目没有限制。

4.8 Node ID

USB设备一般用USBD_NODE_ID来区别。从其作用来看,USBD_ NODE_ ID 是USBD 用来跟踪一个设备的句柄。它与USB设备真正的USB地址无关。这表明client并不真正关心想要了解设备是物理上与哪一个USB主控制器相连。应用为每个设备抽象定义的Node ID, 使client可以不用考虑物理设备的连接细节以及USB地址分配, 并允许USBD 在其内部对这些进行详细的管理。

当一个client通知有一个设备连接或断开时,USBD经常通过USBD_NODE_ID来定位设备。同样,当一个client想通过USBD与一个特定的设备通信时,它必须向USBD传递那个设备的USBD_NODE_ID。

4.9 总线编号(bus enumeration)操作

usbdLib模块提供了usbdBusCountGet(),usbdRootNodeldGet(),usbdHubPortCountGet(),usbdNodldGet()操作。它们被一起称作总线编号操作。它们使USBD Client对连接到每一个主控制器上的设备进行编号。

这些操作对于诊断程序和测试工具很有用,例如usbTool(WindRiver提供的一个测试工具)。但是,利用它们编号之后,调用者无法知道USB的拓扑结构是否变化。因此, 建议USB class driver的开发者不要用这些操作。

4.10 数据传输

一旦client配置完成一个设备,就开始利用USBD提供的管道和传输功能与设备进行数据交换。传输种类(分为控制、块、中断和同步传输)用一个USB_IRP数据结构来描述。 USB_IRP 的具体描述请参见HCD_FUNC_IRP_SUBM1T。USB数据传输被定位于每一个设备的特定endpoint。在USBD client和特定的设备endpoint之间的通道被称作管道(pipe)。每一个管道有以下若干特性:

  • USBD_NODE_ID
  • 设备的endpoim 数目
  • 数据传输方向
  • 带宽需求
  • 延时需求

为了和设备交换数据,client必须先创建管道。作为结果,USBD得到了一个USBD_PIPE_HANDLE,它被用于随后对这个管道的所有client操作。

当client企图创建一个管道时,USBD会检查是否有足够的可用带宽。对于中断和同步传输,带宽限制是必需的。USBD不允许把90% 以上的可用带宽分配给中断和同步管道;而对于控制和块传输,则没有带宽的限制。同时,保证至少10% 的带宽用于控制传输,对块传输则不保证会提供任何可用带宽。

数据传输的具体过程:

  1. 创建pipe :usbdPipeCreate(usbdClient Handle,nodeld,endpoint,configvalue,interface,USB_XFRTYPE_BULK,USB_ DIR_OUT,maxPacketSize,0,0,&outPipeHandle)
  2. 定义callback:ourlrpCallback(pvoid P)
  3. 初始化IRP的数据结构
  4. 发送IRP:usbdTransfer(usbdChentHandle,outPipeHandle,&irp)

5 、小结

USB在VxWroks下的从下至上驱动栈分为HC、UCD、USBD和Client Module四层,每一层都相对独立,并为上一层提供了屏蔽该层次具体特征的接口。作者所说的USB驱动,实际上主要在USBD这一层次上完成。具体分为Chent注册,注销,创建pipe ,配置,数据发送,以及各回调函数。当正确地依次调用时, 会根据回调函数的状态和返回值,按正确的时序进行完整的数据传输。

上述设计思想构成了VxWorks下USB设备应用的基础。作者的研究详细地分析了VxWorks的USB协议栈,证明了该方案的可行性,同时又给出了合理的实现方法。作为实践成果,作者已在VPN网关证书读取系统中,利用该思想编写的驱动,顺利读出存储在USB设备中的设备证书和管理员证书,且运行情况良好。作者认为,文中提到的模型完全可以胜任解决USB设备在VxWorks下的应用所面临的技术难题。