VxWorks驱动开发原理 USB驱动 6

6.2.7 函数库usbKeyboardLib

USB的应用层是指各种各样USB设备的驱动,这些设备通过USBD层的接口来实现与USB设备的通信,实现了USB设备驱动。本节将以USB键盘为例来说明USB应用层的实现。

USB键盘驱动主要包括两部分:USB键盘驱动核心、USB键盘初始化。其中USB键盘驱动核心是由函数库usbKeyboardLib来完成,USB键盘初始化是由函数库usrUsbKbdInit来完成。

函数库usbKeyboardLib提供了一些API函数,这些API函数在实现标准SIO驱动接口之外还针对USB的热插拔特性做了一些扩展。作为一个USB设备,USB键盘驱动主要是由系统向键盘发送IRP来完成按键的获取以及状态指示灯的控制。同时和标准SIO驱动一样,这个模块的驱动也可以通过调用usbKeyboardDevInit()函数完成初始化。usbKeyboardDevInit()函数初始化了与USBD的连接及其他一些内部资源。与其他SIO驱动不同的是,在调用usbKeyboardDevInit()函数进行初始化之前,不需要对模块内部的任何变量进行初始化。

需要注意的是,在调用usbKeyboardDevInit()函数之前,必须要首先调用 usbdInitialize()函数以确保USBD的初始化。而且还必须保证至少一个USB HCD(USB Host Controller Driver)已经attach到USBD函数——这一步可以通过调用usbdHcdAttach()来完成。

USB键盘设备统一向USBD层注册一个USBD_CLIENT结构变量,并通过类注册函数usbdDynamicAttachRegister向USBD层注册回调函数usbKeyboardAttachCallback。对每个USB键盘设备,都会注册一个interrupt pipe,并通过该pipe向设备发送interrupt Irp。

函数库usbKeyboardLib为上层提供了一系列通用的函数接口,这个结构封装在数据结构sio_drv_funcs中,对每个USB键盘,都会在函数库中对应一个USB_KBD_SIO_CHAN结构变量,该结构变量是从结构SIO_CHAN派生而来的,而结构SIO_CHAN中有一个元素SIO_DRV_FUNCS * pDrvFuncs则是指向该USB键盘的一些基本的操作函数,这些函数都封装在结构sio_drv_funcs中。如图6.42。

图6.42 结构USB_KBD_SIO_CHAN与结构SIO_CHAN的关系

函数库usbKeyboardLib中有如下关键数据:

LOCAL LIST_HEAD sioList:sioList链记录了系统管理的各个USB键盘的信息,每个键盘信息都用一个USB_KBD_SIO_CHAN结构变量来描述。函数库usbKeyboardLib支持多个USB键盘,当系统检测到一个USB键盘时,就在sioList链中添加一个USB_KBD_SIO_CHAN结构。

LOCAL LIST_HEAD reqList:由于USB设备属于即插即用设备,为了支持USB键盘的即插即用特性,需要在系统启动的时候注册一个USB键盘驱动的动态挂载和卸载程序。对USB键盘来说,这个注册过程由函数usbKeyboardDynamicAttachRegister (usbKbdDrvAttachCallback,(void *) NULL)来完成,它的作用就是在reqList链中添加一个ATTACH_REQUEST结构,告诉系统当系统中发现了新插入或者拔掉一个USB键盘的时候,可以调用函数usbKbdDrvAttachCallback来完成USB键盘驱动程序的加载和卸载。

下面分析各个函数。


1. LOCAL UINT16 cvtScanCodeToKeyCode
    (
    pUSB_KBD_SIO_CHAN pSioChan,

    UINT16 scanCode,

    UINT16 modifiers
    )

将扫描码转换成键码。扫描码在不同状态下对应的键码是不同的,如大小写等等。

  • scanCode:输入的扫描码。
  • modifiers:当前键盘的状态,如SHIFT、ALT、CTRL键等。
  • 返回值:ASCII码以及CAPLOCK,SCRLOCK,NUMLOCK指示,否则返回NOKEY。

2. LOCAL BOOL isKeyPresent
    (
    pUINT16 pKeyArray,

    UINT16 key
    )

查看key是否存在于pKeyArray的阵列中。


3. LOCAL VOID setLedReport
    (
    pUSB_KBD_SIO_CHAN pSioChan,

    UINT8 ledReport
    )

设定键盘的LED指示。


4. LOCAL VOID changeKbdState
    (
    pUSB_KBD_SIO_CHAN pSioChan,

    UINT16 scanCode,

    pBOOL pKeyState
    )

该函数主要完成两个动作:翻转参数pKeyState为变量pSioChan->capsLock、pSioChan->scrLock或者pSioChan->numLock,并将其反应到LED指示灯上。


5. LOCAL void interpScanCode
    (
    pUSB_KBD_SIO_CHAN pSioChan,

    UINT16 scanCode,

    UINT16 modifiers
    )

处理CAP、NUM、SCR扫描码。如果当前扫描码已经激活(存在于pSioChan->activeScanCodes数组中)说明已经处理过则直接退出;否则如果扫描码为CAP、NUM、SCR,则需要调用changeKbdState函数修改键盘的状态。


6. LOCAL VOID putInChar
    (
    pUSB_KBD_SIO_CHAN pSioChan,

    char putChar
    )

存放一个字符到接收队列中。

inQueue [KBD_Q_DEPTH]是一个环状输入字符缓冲区。从USB键盘读入一个字符就将他放在该inQueue [inQueueIn++]中,如果要取出一个字符,就需要取出inQueue [inQueueOut++]处的字符。inQueueCount则是记录了每个该缓冲区中保存的字符的数量(最大KBD_Q_DEPTH个)。

注意:存入数据时函数putInChar已经确保缓冲区还有空间可存放,如果没有空间则自动丢掉当前的字符。


7. LOCAL char nextInChar
    (
    pUSB_KBD_SIO_CHAN pSioChan
    )

取出下一个字符。

取出inQueue [inQueueOut++]处的字符。

注意:该函数在取出前对缓冲区中是否有数据不做判断,因此在调用函数nextInChar之前必须确保取出函数调用前缓冲区是有数据的。


8. LOCAL VOID updateTypematic
    (
    pUSB_KBD_SIO_CHAN pSioChan
    )

当一个键按下持续超过TYPEMATIC_DELAY(500毫秒),则解释为重复输入该字符(重复频率为66毫秒),并存放到pSioChan->inQueue []中。之后要调用pSioChan->putRxCharCallback进行处理,也就是取出接收buffer中的字符通过回调函数交给上层。


9. LOCAL VOID interpKbdReport
    (
    pUSB_KBD_SIO_CHAN pSioChan
    )

首先要调用函数检查是不是有SCR、NUM、CAP键按下,如果有这些键,那么就需要改变键盘指示灯。

其次是判断键盘输入模式,如果pSioChan->scanMode == SIO_KYBD_MODE_RAW,则读取分三步走:

  • l 将pReport->modifiers(主要包含CTRL、ALT、SHIFT键)放入缓冲区;
  • l 依次检查pReport->scanCodes[i](这里存放扫描码,最多可以存放6个),非0则将其存入缓冲区;
  • l 将0xff写入缓冲区。

如果pSioChan->scanMode == SIO_KYBD_MODE_ASCII,对每个pReport->scanCodes[i],如果键码不为0,则

  • l 先调用cvtScanCodeToKeyCode将pReport->scanCodes的各个扫描码转换成为键码
  • l 如果扫描码不在pSioChan->activeScanCodes中,则直接将该键码放入缓冲区中。如果是扩展键,要首先存放0,再存放键码的低8位。
  • l 如果pReport->scanCodes [i]只有一个非0字符,则说明可能是由于该键被持续按下,下面要进行进一步判断,即当前仅有的一个非0字符和原来的字符pSioChan->typematicChar是否一致,一致则说明确实被持续按下,则需要调用updateTypematic (pSioChan)函数;如果不一致说明输入了新的字符,记录下当前时刻,计数器pSioChan->typematicCount清0。

第三步就是如果缓冲区有接收数据且处于中断模式下,则调用pSioChan->putRxCharCallback函数,取出缓冲区的数值交给上层。

注意:pSioChan->typematicChar的作用主要是记录上次函数interpKbdReport执行时检测到的键码,用于判断是否持续按下该按键(连续500ms都按下同一个按键才能够最终确认)。


10. LOCAL int usbKeyboardIoctl
    (
    SIO_CHAN *pChan,

    int request,

    void *someArg
    )

通过该函数可以设定键盘的工作模式:中断/轮询(mode)、RAW/ASCII(scanMode)以及LED的控制。


11. LOCAL int usbKeyboardTxStartup
    (
    SIO_CHAN *pChan
    )

因为USB键盘不支持向外部输出字符,因此直接返回EIO。


12. LOCAL int usbKeyboardCallbackInstall
    (
    SIO_CHAN *pChan,

    int callbackType,

    STATUS (*callback) (),

    void *callbackArg
    )

向pChan安装回调函数。当函数库usbKeyboardlib收到USB键盘数据时需将其存放到队列pSioChan->inQueue[]并调用回调函数(*pSioChan->putRxCharCallback)()将队列中的数据取走;回调函数(*pSioChan->getTxCharCallback)()并没使用。


13. LOCAL int usbKeyboardPollOutput
    (
    SIO_CHAN *pChan,

    char outChar
    )

因为USB键盘不支持向外部输出字符,因此直接返回EIO。


14. LOCAL int usbKeyboardPollInput
    (
    SIO_CHAN *pChan,

    char *thisChar
    )

调用NextInChar函数获取一个char。


15. LOCAL BOOL initKbdIrp
    (
    pUSB_KBD_SIO_CHAN pSioChan
    )

初始化一个USB_IRP结构,并调用usbdTransfer函数以进行interrupt pipe的侦听。

注意这里pIrp->userCallback=usbKeyboardIrpCallback,也就是说当IRP收到新的数据时,将会调用函数usbKeyboardIrpCallback进行处理。

这个函数正是利用USB接口实现键盘的中断处理功能的一个核心,就是首先向USB键盘发送一个按键状态查询的IRP,这个IRP的timeout时间为无穷大,因此只要USB按键按下就立刻通过USB向系统报告状态,并调用回调函数usbKeyboardIrpCallback将其发送给计算机系统。

注意:按键的键盘是由USBD填入到pSioChan->pBootReport的。


16. LOCAL VOID usbKeyboardIrpCallback
    (
    pVOID p
    )

函数库usbKeyboardLib向USB键盘发送一个中断IRP,当USB键盘有按键按下后会完成该IRP,此时USBD函数库会调用usbKeyboardIrpCallback回调函数进行处理。

usbKeyboardIrpCallback函数调用interpKbdReport获取键码,并调用initKbdIrp函数重新提交一个IRP以侦听下一次按键数据。


17. LOCAL VOID typematicThread
    (
    pVOID param
    )

对函数库usbKeyboardLib中的USB键盘链sioList中的每个USB键盘,调用updateTypematic函数检查重复按键,之后休眠66毫秒开始下次循环。


18. LOCAL BOOL configureSioChan
    (
    pUSB_KBD_SIO_CHAN pSioChan
    )

配置USB键盘。配置过程如下:

  • l 调用usbdDescriptorGet函数获取该USB键盘设备的配置描述符(配置序号为0)信息并调用usbDescrParse函数进行分析得到其配置描述符pCfgDescr;
  • l 调用usbDescrParseSkip查找配置号为0的配置描述符下interface号等于pSioChan->interface的interface描述符pIfDescr。注意,usbdDescriptorGet函数得到的buf中存在很多interface描述符,只有第pSioChan->interface个interface描述符才是属于该usb键盘的描述符;
  • l 调用usbDescrParseSkip得到endpoint描述符(一个interface可以包含多可endpoint);
  • l 调用usbdConfigurationSet选择配置;
  • l 调用usbdInterfaceSet选择interface;
  • l 调用usbHidProtocolSet选择为该interface指定protocol;
  • l 调用usbHidIdleSet将键盘空闲时间设定为无限制;
  • l 调用setLedReport (pSioChan, 0)关掉LED;
  • l 调用usbdPipeCreate创建interrupt pipe;
  • l 调用initKbdIrp初始化IRP。

19. LOCAL VOID destroyAttachRequest
    (
    pATTACH_REQUEST pRequest
    )

调用usbListUnlink (&pRequest->reqLink)删除reqList链中一个ATTACH_REQUEST结构。


20. LOCAL VOID destroySioChan
    (
    pUSB_KBD_SIO_CHAN pSioChan
    )

删除pUSB_KBD_SIO_CHAN及其相关结构。


21. LOCAL pUSB_KBD_SIO_CHAN createSioChan
    (
    USBD_NODE_ID nodeId,

    UINT16 configuration,

    UINT16 interface
    )

根据参数创建一个USB_KBD_SIO_CHAN结构并将其加入到sioList链当中。

这个函数在发现插入的新的USB键盘设备的时候通过函数usbKeyboardAttachCallback来调用,也就是说,当USBD发现了USB键盘设备插入,则自动发送消息给client,clientThread的任务就是不断读消息队列,当发现新的设备插入后就会调用函数usbKeyboardAttachCallback,该函数则是调用createSioChan创建USB_KBD_SIO_CHAN结构并完成结构的初始化,同时还要完成对USB设备的配置。


22. LOCAL pUSB_KBD_SIO_CHAN findSioChan
    (
    USBD_NODE_ID nodeId
    )

根据nodeId从sioList链当中找到USB_KBD_SIO_CHAN。


23. LOCAL VOID notifyAttach
    (
    pUSB_KBD_SIO_CHAN pSioChan,

    UINT16 attachCode
    )

当usbKeyboardAttachCallback发现一个USB键盘设备插拔之后,就调用notifyAttach函数遍历reqList链,并调用链中所有的pRequest->callback。


24. LOCAL VOID usbKeyboardAttachCallback
    (
    USBD_NODE_ID nodeId, 

    UINT16 attachAction, 

    UINT16 configuration,

    UINT16 interface,

    UINT16 deviceClass, 

    UINT16 deviceSubClass, 

    UINT16 deviceProtocol
    )

增加或删除USB键盘设备的时候调用的回调函数。

如果是增加一个USB键盘设备,则需要根据参数判断,如果nodeId对应的结构已经存在,则不做处理,否则要调用createSioChan新建一个USB_KBD_SIO_CHAN结构,并调用notifyAttach通知reqList中所有其他USB_KBD_SIO_CHAN结构;

如果是要删除一个设备,则先调用notifyAttach通知reqList中所有其他USB_KBD_SIO_CHAN结构;检查—pSioChan->lockCount,如果为0则调用destroySioChan删除USB_KBD_SIO_CHAN结构。

注意删除的时候有一个技巧:调用notifyAttach通知reqList中所有其他USB_KBD_SIO_CHAN结构时要首先pSioChan->lockCount++,这样防止函数usbKeyboardSioChanUnlock删掉该USB_KBD_SIO_CHAN结构。

注意和函数usbKeyboardIrpCallback区分,usbKeyboardIrpCallback是在收到字符的时候才调用;usbKeyboardAttachCallback是在发现USB键盘插拔的时候调用,二者的作用是完全不同的。


25. LOCAL STATUS doShutdown
    (
    int errCode
    )

关闭usbKeyBoardLib库。usbKeyboardDevInit的逆过程。


26. STATUS usbKeyboardDevInit (void)

初始化usbKeyBoardLib库。整个初始化过程主要完成了如下工作:

  • l 建立线程typematicThread,该线程的主要作用是:当持续按下某个按键的时候要进行特殊处理,比如当持续按下字符按键时等同于重复输入等等;
  • l 调用usbdClientRegister创建USBD_CLIENT结构并创建clientThread线程,这说明USB键盘设备公用一个clientThread线程;
  • l 调用usbdDynamicAttachRegister在新创建的USBD_CLIENT结构变量中注册一个USBD_NOTIFY_REQ结构变量,当USB系统中出现了符合USBD_NOTIFY_REQ结构变量的设备(USB键盘)插拔时,通过回调函数通知client。同时也要立刻对现有的USB设备仅进行检查,如果发现有符合USBD_NOTIFY_REQ变量的设备,马上通知client。这样既保证了当有USB键盘插拔时通知client又保证了即使在该函数被invoke之前插入的USB设备也能正确处理。

27. STATUS usbKeyboardDevShutdown (void)

当initCount==0时调用函数doShutdown。


28. STATUS usbKeyboardDynamicAttachRegister
    (
    USB_KBD_ATTACH_CALLBACK callback,

    pVOID arg
    )

VxWorks操作系统可以支持多个USB键盘,在函数库usbKeyboardLib中的sioList链中记录了所有的USB键盘结构。由于USB设备是支持热插拔的,因此当新插入一个设备时就需要对其进行初步识别并调用相应的管理程序以完成初始化等操作,因此需要在系统中首先进行注册,告诉系统“如果发现了某USB键盘设备,调用这个函数通知我”,正如生活中的“我是xxx,这是我的名片,我的职责是XXX,如果有什么问题打这个电话通知我”,如果多个程序都对USB键盘的插入感兴趣,则可以向reqList链中存放多个ATTACH_REQUEST结构。函数usbKeyboardDynamicAttachRegister的作用正是在reqList链中增加一个ATTACH_REQUEST结构。

注意:函数usbKeyboardDynamicAttachRegister只是在名片夹(reqLink链)里放了一张名片(ATTACH_REQUEST结构变量);而每张名片又有自己的回调函数(如usbKbdDrvAttachCallback);usbKeyboardAttachCallback回调函数的作用则是遇到USB键盘设备插拔的时候根据名片夹(reqLink链)的每张名片(每个ATTACH_REQUEST结构变量)挨个通知;而usbdDynamicAttachRegister函数的作用则是向USBD层注册了一个名片夹,当然如果需要可以注册多个名片夹。

在调用这个函数之前,可能就有匹配的设备存在了,因此需要注册reqList后检查sioList,如果有USB键盘设备则调用回调函数。


29. STATUS usbKeyboardDynamicAttachUnRegister
    (
    USB_KBD_ATTACH_CALLBACK callback,

    pVOID arg
    )

将reqLIst链中的回调函数为callback和arg的ATTACH_REQUEST结构释放。注意,reqLIst链中可能保存着两个具有相同回调函数及参数的ATTACH_REQUEST结构变量,因此在删除一个匹配的ATTACH_REQUEST结构变量之后并不马上返回,而是要继续查找。


30. STATUS usbKeyboardSioChanLock
    (
    SIO_CHAN *pChan
    )

结构pUSB_KBD_SIO_CHAN中保留了一个元素lockCount,当一个任务需要使用该USB键盘的时候,它需要调用函数usbKeyboardSioChanLock()将变量lockCount加1,表明当前有设备在使用;当它不再使用时将该变量减1,当所有的任务都不使用时,该USB键盘就可以从软件中删除了。


31. STATUS usbKeyboardSioChanUnlock
    (
    SIO_CHAN *pChan
    )

参见函数usbKeyboardSioChanLock。需要注意的时,如果系统检测到USB键盘被拔掉那么该函数会自动将该键盘的数据结构从系统中删除。

6.2.8 函数库usrUsbKbdInit

usrUsbKbdInit函数库实现了用文件系统访问的方法实现对USB键盘的访问控制。函数usbKbdDevCreate安装了一个USB键盘并将相应的驱动函数装进相应的访问函数表,这样就可以通过标准的fopen、close、read、write等类型的访问接口对USB键盘进行访问。


1. STATUS usbKbdDevCreate
    (
    char * name,

    SIO_CHAN * pSioChan
    )

为一个USB键盘创建一个文件系统访问接口,以方便后面的文件系统访问函数进行访问。比如当系统发现新插入了一个USB键盘之后,就可以调用函数usbKbdDevCreate (“/usbKb/0”, pSioChan)来创建一个USB键盘的驱动。

关于文件系统可以参考串口驱动一章的分析,这里不再过多描述。

这个函数存在一个bug,就是每调用一次系统都会对其中的一些初始化变量初始化一次,这些变量主要有usbKbdMutex、usbKbdListMutex、usbKbdList,如果有多个USB键盘那么就会调用多次,这显然也对这些初始化变量初始化了多次,这显然是不对的。修正的办法是将这几个初始化过程保存在函数usrUsbKbdInit中。


2. LOCAL STATUS usbKbdDevDelete
    (
    USB_KBD_DEV * pUsbKbdDev
    )

主要删除USB_KBD_DEV结构的链:将pUsbKbdDev->ioDev从iosDvList中删除;将pUsbKbdDev->pUsbKbdNode节点从usbKbdList链中删除;以及usbKbdDevCreate函数专门为该设备建立的数据结构:pUsbKbdDev->pUsbKbdNode和pUsbKbdDev。其delete的内容仅仅与当前参数指定的设备有关,并不修改其他的USB键盘设备,而usbKbdDevCreate函数则是初始化了和所有USB键盘设备有关的变量,因此不能简单地理解为usbKbdDevCreate函数的逆过程。


3. LOCAL STATUS usbKbdDevFind
    (
    SIO_CHAN * pChan,

    USB_KBD_DEV ** ppUsbKbdDev
    )

从usbKbdList链中找到其元素pSioChan等于参数pChan的USB_KBD_DEV结构。


4. LOCAL void usbKbdDrvAttachCallback
    (
    void * arg,

    SIO_CHAN *pChan,

    UINT16 attachCode
    )

回调函数,当有USB键盘插入或拔掉的时候调用。

该函数的执行分为两种情况:如果是新插入了USB键盘,则调用usbKbdDevCreate函数进行该键盘的驱动初始化;如果是USB键盘设备被拔掉了,则调用usbKbdDevDelete函数删除与该设备相关的结构,然后调用函数usbKeyboardSioChanUnlock(pChan)删除pChan结构。

图6.43表明了pChain和结构指针pUsbKbdDev的关系。更详细的分析可以结合图6.42以及i8250串口驱动一章。

图6.43 USB键盘对应的IO数据结构关联

注意区分回调函数本身和回调注册函数。对于USB键盘来说,共有两类回调函数,一类是USB键盘接收到数据之后需要调用回调函数处理数据,如回调函数usbKeyboardIrpCallback,该回调函数的注册由函数initKbdIrp在在调用函数usbKeyboardDynamicAttachRegister来完成的。

第二类回调函数就是USB设备的插拔时需要调用的回调函数,这个回调函数又分为两个层次:USBD层将USB设备(对应一个client)注册为一个回调函数usbKeyboardAttachCallback,当USBD层发现这一类设备插拔时调用回调函数usbKeyboardAttachCallback,这个回调函数是由函数usbdDynamicAttachRegister在USBD层注册的;另一个层次是对USB键盘设备来说,当发现一个USB键盘设备插拔时可能需要调用多个回调函数,每个回调函数则是由usbKeyboardDynamicAttachRegister函数注册到reqList链中,函数库usrUsbKbdinit来说则只需要注册一个回调函数即可。图6.44描述了USB键盘各层热插拔回调函数之间的关系。

图6.44 USB键盘各层热插拔回调函数之间的关系

5. LOCAL int usbKbdOpen
    (
    USB_KBD_DEV * pUsbKbdDev,

    char     * name,

    int flags,

    int         mode
    )

打开一个usbKbdDrv设备。


6. STATUS usbKbdDrvUnInit (void)

usrUsbKbdInit函数的逆过程。


7. LOCAL int usbKbdClose
    (
    USB_KBD_DEV * pUsbKbdDev
    )

关闭一个usbKbdDrv设备。


8. LOCAL int usbKbdIoctl
    (
    USB_KBD_DEV * pUsbKbdDev,

    int request,

    void * arg
    )

直接调用函数usbKeyboardIoctl。


9. LOCAL int usbKbdRead
    (
    USB_KBD_DEV * pUsbKbdDev,

    UCHAR *buffer, 

    UINT32 nBytes    
    )

用USB键盘轮询的方式对USB键盘获取的键码进行访问,每调用函数usbKbdRead一次只能获取一个字符,因此nBytes是无效的。


10. int usbKbdWrite
    (
    USB_KBD_DEV * pUsbKbdDev,

    UCHAR * buffer,

    UINT32   nBytes
    )

USB键盘不支持写操作。


11. void usrUsbKbdInit (void)

初始化函数库。主要是

  • l 调用函数usbKeyboardDevInit初始化函数库usbKeyboardLib;
  • l 调用函数usbKeyboardDynamicAttachRegister对回调函数usbKbdDrvAttachCallback进行注册。

注意:这里存在bug,参考函数usbKbdDevCreate,需要把三个变量的初始化过程放在函数usrUsbKbdInit 中来执行。