Wince5.0键盘驱动分析及开发

来源:互联网 发布:project画图软件 编辑:程序博客网 时间:2024/05/17 01:38

 本文将对wince5.0键盘驱动进行详细的分析,分析以SMDK2440 BSP包中的键盘驱动为对象(PDD)。并在此基础上,对嵌入式系统中定制矩阵键盘驱动程序的开发做详细介绍。最终的目的是掌握wince5.0下键盘驱动的开发与移植。

 

一、    Wince5.0键盘驱动基本框架

         在本节将对wince5.0键盘驱动的整体框架做较为深入的介绍,虽然开发一个Wince5.0键盘驱动不用实现它的整个框架,但是了解wince5.0键盘驱动的整体框架将对该驱动的开发大有裨益。

1.1Wince5.0键盘驱动的分类

       wince5.0中,键盘驱动从驱动接口上分它是一个本地驱动(专用接口);从驱动层次结构上分它是一个分层驱动,从驱动宿主上分它是一个由GWES.exe加载并调用的驱动。

1.1.1、  Wince5.0键盘驱动是一个本地的、分层的驱动分层驱动都由两个独立的层组成,分别是:模型设备层(model device driverMDD)和平台依赖(platform-dependent driverPDD)。

MDD层:实现与具体硬件平台无关的功能,通常描述了一类设备的通用共同特性或通用操作。比如:串口。尽管不同厂家不同型号的串口控制器硬件及其工作机制各不相同,但在驱动程序中对于串口的一些操作总是相通的。如:初始化串口、打开关闭串口、读写串口、串口流控制等等。再比如:键盘。尽管在嵌入式系统中,键盘的硬件实现方式及与系统链接方式各不相同,如矩阵键盘、PS2键盘、CPLD键盘;如GPIO口连接、USB连接、SPI连接等等。但是只要是wince下的键盘,它总要将键盘输出的硬件扫描码映射为虚拟键值、总要向GWES发送键盘事件。微软将驱动程序中,描述这些同类设备中通用操作或共同特性的代码抽取并组织起来,这就是分层驱动中的MDD层。

PDD层:包含了针对特定硬件设备的的专用代码,对各设备的底层操作都在该层完成。对于不同的硬件设备,有不同的实现。如:从键盘中读取硬件扫描码;设置某串口控制器的寄存器,以打开串口。

在分层驱动程序中,有两类接口函数:在操作系统与MDD之间DDI接口(Device Drive Interface);MDD层与PDD层之间的DDSI接口(Device Driver Service Interface)。当操作系统访问硬件时,是通过DDI接口与驱动程序交互的,然后再驱动程序内部MDD层再通过DDSI接口与PDD层交互,最后PDD层完成真正的硬件设备访问操作。

通常对于某个类型的驱动程序,wince会自带此类型驱动程序的MDD代码,且无需另外修改。实现分层驱动,驱动开发者只需完成PDD层代码的编写或移植。MDD层的代码与PDD层的代码会被编译成独立的静态LIB库,然后进行链接形成驱动程序的DLL动态链接库。注:MDDPDD层之分,是针对驱动程序的代码结构而言,驱动程序链接成DLL后并没有MDDPDD层之分。

Wince5.0下,键盘驱动是分层驱动。故驱动程序由MDD层及PDD层组成:MDD层完成矩阵键盘硬件扫描码到虚拟键的映射,产生与虚拟键编码相关的字符。然后封装键盘事件发送到GWESPDD层:初始化键盘硬件、从硬件平台上获得硬件扫描码,并实现键盘布局及输入语言。

 

1.1.2、  Wince5.0键盘驱动是一个由GWES.exe加载并调用的驱动

键盘驱动由GWES.exe加载,而不是device.exe。通常一些与图形界面相关的I/O设备驱动由GWES.exe加载。如鼠标驱动、键盘驱动、显示驱动、触摸屏驱动等与图形界面功能紧密耦合的驱动。这些驱动一个重要的共同特征是:用户在应用程序中不会调用这些驱动,这些驱动是由GWES调用的。如键盘驱动,用户不可能在图形框架应用程序中调用该驱动的接口函数(除非是该驱动以流接口的形式实现,并在WIN32 API应用程序中调用)去获取键值,而是由GWES调用,把一次键盘事件封装成一个消息(包含虚拟键值、按键时间等信息)并投递到系统的消息队列。

 

1.2Wince5.0键盘驱动的平台无关结构

与一般的分层驱动程序不同,Wince5.0下键盘驱动的MDD成可同时管理多个PDD层。也就是说:在一个键盘驱动程序中可包含多个PDD层。每个PDD层对应一个键盘硬件(在一个嵌入式系统中存在多个键盘是可能的)。所以在键盘驱动中必须有个参数来唯一标识每个PDD

每个PDD层又可对应多个键盘布局(device layout)。所谓的键盘布局就是硬件扫描码(scand code)到虚拟键值(virtual key)的映射。不同的键盘布局会使得同一键盘硬件输出的相同扫描码对应不同的虚拟键值。比如在标准的PS2/AT 101键盘中数字键8PS2_AT_00000409键盘布局( us键盘布局)中,对应的shfit虚拟键值为‘*’;而在PS2_AT_00000411 jnp1键盘布局)中对应的则是‘(’。在嵌入式系统中,每个键盘都有自己的键盘布局,所以驱动开发者需要为自己的键盘在PDD层实现键盘布局。键盘布局的具体映射过程由MDD层完成(因此键盘布局被划分为了平台无关结构的一部分)。为了各种不同的键盘布局与MDD层无缝对接,键盘布局的实现应遵循统一的数据结构。在2.4节中详细介绍了键盘布局的实现。

Wince5.0键盘驱动的平台无关结构中还包含一个被称为输入语言(Input Language)的组件。输入语言(Input Language)是虚拟键值(virtual key)到Unicode字符的映射。不同的输入语言将同一虚拟键值映射成不同的Unicode字符。

GWES发现当前窗口存在输入焦点,此时GWES会在发送按键消息(WM_KEYDOWN)后,再次返回到键盘驱动程序的函数KeybdDriverVKeyToUnicode中。在该函数中,将根据当前虚拟键值从输入语言(Input Language)中映射出对应的Unicode字符,最后GWES通过字符消息(WM_CHAR)发送当前输入焦点。

说到底,输入语言(Input Language)就是在必要的时候完成虚拟键值到unicode字符的映射。与键盘布局类似,输入语言的具体映射过程也同样在MDD层完成;同样,输入语言也是在PDD层遵循统一的数据结构在PDD层中的实现。在wince5.0的驱动示例代码:C:\WINCE500\PUBLIC\COMMON\OAK\DRIVERS\KEYBD\INPUTLANGS下实现了多个输入语言,其中在0409子目录下实现的标准PS2_AT键盘的输入语言,它包含了所有虚拟键值到Unicode字符的映射。所以在为定制键盘实现输入语言时,可以在此基础上修改。

Wince5.0键盘驱动平台无关结构中,其核心是布局管理器(layout manager)。它由键盘驱动的MDD层实现。布局管理器统筹管理键盘驱动中各PDD层,输入语言以及键盘布局。布局管理器是怎么管理这些平台无关组件的呢?

首先GWES从注册表中读取键值[HKEY_LOCAL_MACHINE\Hardware\DeviceMap\KEYBD]来获得键盘驱动的DLL名。

如:[HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\KEYBD]

                 "DriverName"="matrixkbd.dll"

然后GWES将加载该键盘驱动DLL,并调用键盘驱动DDI函数KeybdDriverInitializeEx()(这是该DLL的一个导出函数,在def文件中做了标记)来初始化布局管理器(layout manager)。之后布局管理器将依次调用各PDD层的入口函数PFN_KEYBD_PDD_ENTRY(如:Matrix_Entry())初始化键盘驱动中各PDD层并获得硬件扫描码。然后,布局管理器将读取注册表:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Layouts\下的子键,如:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Layouts\00000409]来获得该键盘驱动的键盘布局和输入语言,并通过它们将硬件扫描码映射成虚拟键值或Unicode字符,然后通过键盘事件发送到GWES。至于布局管理器通过相关注册表键值获得键盘布局与输入语言的详细过程,后面会有详细描述。

以下是键盘驱动(KBDMOUSE.DLL)平台无关结构图:

    从上图看出:该键盘驱动包含了两个PDD层,分别是PS/2Matrix。每个PDD层都对应一个当前键盘布局,分别是PS/2 device layoutMatrix device layout。事实上,每个PDD层可对应多个键盘布局,并在运行进行热键切换。如图中,两个PDD层公用一个输入语言Current input language,这是很常见的情况。

嵌入式系统中定制键盘的驱动在一般情况下,平台无关结构只会包含1PDD层,1个键盘布局,1个输入语言。不过了解一下通常意义上的平台无关结构还是很有必要的。

 

1.3Wince5.0下键盘驱动原理

键盘的驱动一般有两种方式:扫描法和中断法。扫描法实现简单,但实时性差,占用CPU时间多;中断法实现复杂,但实时性好,占用CPU时间少。这节将以wince5.0中断法驱动键盘的原理做具体分析。

1.3.1wince5.0下的中断处理概述

中断法驱动的键盘每当有键按下或松开时都会触发中断,此时键盘驱动的核心当然就是中断处理了。在wince5.0中其中断处理有其独特的方式。

wince中存在逻辑中断(SYSINTR)的概念。当中断发生时,OAL中的ISR(中断服务例程)须把物理中断信号映射成OEM定义的逻辑中断信号,返回给内核。内核自动激活与该逻辑中断信号相关联的同步事件,被该事件阻塞的IST(中断服务线程)获得同步事件,开始执行并处理中断。这样在wince中对中断的处理就转化成IST获得同步事件开始执行。

wince下,中断的处理分为两个过程:ISRISTISR负责把IRQ映射成逻辑中断并返回给内核;IST是一个普通的用户线程,负责中断处理的真正工作,IST大多时候是空闲的、被阻塞的。只有操作系统用同步事件通知IST有中断发生时它才开始工作。

关于wince5.0下中断处理的详细情况可参见《Windows CE嵌入式系统》(何宗键著,P225)。

 

1.3.2Wince5.0下键盘驱动工作过程

   在这里我们将分两条主线来分析键盘驱动的工作过程:一条主线是系统启动时;一条是按键中断发生时(即有键按下或松开)。

1) 系统启动时键盘驱动工作过程

系统启动时,操作系统内核NK首先运行,然后NK根据注册表的HKEY_LOCAL_MACHINE\init的内容陆续启动系统正常运行必须一些进程,GWES.exe就在此时被启动。GWES.exe启动后会先从注册表中读取键值[HKEY_LOCAL_MACHINE\Hardware\DeviceMap\KEYBD]来获得键盘驱动的DLL名。然后GWES将加载该键盘驱动DLL,并调用键盘驱动DDI函数KeybdDriverInitializeEx()(这是该DLL的一个导出函数,在def文件中做了标记)来初始化布局管理器(layout manager)。之后布局管理器将依次调用各PDD层的入口函数PFN_KEYBD_PDD_ENTRY(如:Matrix_Entry())初始化键盘驱动中各PDD(如GPIO口、EINT口键盘相关硬件初始化)然后创建一个键盘中断的IST线程(在该线程中,创建同步事件、关联逻辑中断号与同步事件等),该线程的作用为获取硬件扫描码并调用键盘事件函数KEYBD_EVENT发送键盘事件至MDD层。同时布局管理器将通过相关注册表键值获得当前键盘驱动的键盘布局与输入语言。

 

2) 按键中断发生时键盘驱动工作过程

当键盘有键按下或松开时,将触发外部IRQ中断。此时系统将自动跳转到中断处理程序(Interrupt Handler)处执行。中断处理程序位于操作系统内核中,负责中断现场的保存与恢复,并调用ISRISR将读取SOC芯片中中断控制器的相应寄存器获得到底是哪个IRQ中断发生了,并将其映射成逻辑中断(一般是SYSINT_KEYBD),然后返回给内核。内核自动激活与该逻辑中断关联的同步事件,同时IST获得同步事件后开始执行,去获取硬件扫描码并调用键盘事件函数KEYBD_EVENT发送键盘事件至MDD层。MDD层将对该键盘事件进行进一步处理,把硬件扫描码映射成虚拟键值或Unicode字符,封装成消息(WM_KEYDOWNWM_CHARSWM_KEYUP)投递到系统的消息队列中。

总的说起来:键盘驱动的工作过程可以分为两个方面:系统启动时初始化键盘硬件,执行一些准备工作;中断发生时,获得硬件扫描码发送键盘事件,并封装成消息发送出去

 

二、    Wince5.0键盘驱动源码分析

在该节,将对SMDK2440 BSP包中的键盘驱动源码做详细分析。在BSP包中实现的键盘驱动的PDD层代码,MDD层代码C:\WINCE500\PUBLIC\COMMON\OAK\DRIVERS\KEYBD中。键盘驱动的MDD层为公用代码,在键盘驱动开发时通常不需修改。所以对佳品驱动源码的分析主要是针对BSP包中的PDD层代码。

2.1Wince5.0键盘驱动的PDD

开发一个wince键盘驱动实际上就是实现它的PDD层,那么键盘驱动的PDD层到底需要实现些什么东西呢?很简单,就是3个函数。在PDD层有4DDSI函数,其中需要用户实现的有3个。这三个函数分别是:

2.1.1、键盘驱动入口函数

typedef BOOL (*PFN_KEYBD_PDD_ENTRY)(

  UINT uiPddId,

  PFN_KEYBD_EVENT pfnKeybdEvent,

  PKEYBD_PDD *ppKeybdPdd

 );

    该函数就是前面提到的PDD层的入口函数,这个函数是整个键盘驱动PDD层的核心。在该函数中最重要的两项工作是:初始化键盘硬件及PDD层;创建IST线程。后面将对该函数进行详细分析。

 

2.1.2、键盘电源管理函数

typedef void (*PFN_KEYBD_PDD_POWER_HANDLER)(

  UINT uiPddId,

  BOOL fTurnOff

);

  该函数为键盘电源管理的接口函数,如果系统实现了电源管理。则须实现该函数配合电源管理器来实现键盘的电源管理。该函数在PDD层定义,MDD层被调用。该函数在本文中不作详细介绍。

 

2.1.3、键盘灯管理函数

typedef void (*PFN_KEYBD_PDD_TOGGLE_LIGHTS)(

  UINT uiPddId,

  KEY_STATE_FLAGS KeyStateFlags

);

  该函数为键盘灯管理函数,如果键盘有背景灯,则须实现该函数。该函数根据不同的按键状态,对背景灯进行操作。该函数被在PDD层定义,在MDD层被调用。该函数在本文中不作详细介绍。

 

PDD层除了上述的三个DDSI函数外,还有一个DDSI函数:键盘事件函数

typedef void (*PFN_KEYBD_EVENT)(

  UINT uiPddId,

  UINT32 uiScanCode,

  BOOL fKeyDown

 );

  该函数比较特殊,它不需要驱动开发者去实现,它在MDD层实现。但是它在PDD层被调用。(该函数的函数指针将作为键盘驱动入口函数的一个参数,从而实现在PDD层被调用)其功能为:IST线程获得硬件扫描码及按键状态后,该函数负责包装及发送键盘事件至MDD层。该函数在C:\WINCE500\PUBLIC\COMMON\OAK\DRIVERS\KEYBD\LAYMGR\laymgr.cpp中实现。

声明原型是:

void

KeybdEventCallback(

   UINT    uiPddId,

   UINT32  uiScanCode,

   BOOL    fKeyUp

)

注:laymgr.cpp源文件中实现了多个函数,这些函数就组成了所谓的布局管理器,它是键盘驱动平台无关结构中的核心组件。可以说:laymgr.cpp实现了一个布局管理器。

及描获得硬件是细的分析,分析将

2.2SMDK2440 BSP包中键盘驱动源码组织结构

键盘驱动的PDD层源码在BSP包中位置是\SMDK2440A\Src\Drivers\Keybd。在该目录下有以下几个子目录:

\ Kbdcommon;

\ Kbds3c2440jpn1

\ Kbds3c2440jpn2

\ Kbds3c2440kor

\ Kbds3c2440us

\ Matrix_0409

\ Pddlist

还有一个DIRS文件dirs。下面将对各目录做简要介绍:

\ Kbdcommon:实现接口函数、IST线程及其调用的相关函数;

\ Kbds3c2440jpn1\ Kbds3c2440jpn2\ Kbds3c2440kor\ Kbds3c2440us:这四个目录类似,均包含一个.def文件,用来描述键盘驱动动态链接库的导出函数。   

\ Matrix_0409:实现了一个键盘布局函数,该函数须在上述的.def文件中导出;

\ Pddlist:实现了PDD层入口函数列表。

 

DIRS文件dirs:描述\SMDK2440A\Src\Drivers\Keybd须递归进入的文件夹,其内容如下:

DIRS= \

      kbdcommon \

      matrix_0409 \

      pddlist \

      kbds3c2440jpn1 \

      kbds3c2440jpn2 \

      kbds3c2440kor \

      kbds3c2440us \

由上可知,编译键盘驱动是会进入\Keybd目录下的所有子目录。但事实上,对于kbds3c2440jpn1 \kbds3c2440jpn2 \kbds3c2440kor \kbds3c2440us \这四个目录我们只需要进入其中一个即可。应为我们只需要一个.def文件来描述键盘驱动动态链接库的导出函数。开发驱动是此处需要修改。

 

2.3Kbdcommon目录下源码及相关函数分析

   分析SMDK2440的键盘驱动,只为了搞清楚键盘驱动PDD层源码实现框架。并不会涉及到SMDK2440开发板上的键盘硬件驱动细节。

在该目录下包含3个源文件:kbd.cpps3c2440kbd.cpps3c2440kbd.hpp。在源文件kbd.cpp实现了PDD层的入口函数Matrix_Entry(),该函数在\ppdlist目录下声明。如何声明一个PDD层的入口函数,在分析\ppdlist目录时,详细分析。我们先来关注入口函数Matrix_Entry()

其函数原型是:

BOOL

WINAPI

Matrix_Entry(

   UINT uiPddId,

   PFN_KEYBD_EVENT pfnKeybdEvent,

   PKEYBD_PDD *ppKeybdPdd

   )

2.3.1、入口函数Matrix_Entry()调用及参数分析

前面提到过“布局管理器将依次调用各PDD层的入口函数PFN_KEYBD_PDD_ENTRY(如:Matrix_Entry())初始化键盘驱动中各PDD层”。那么Matrix_Entry()到底怎么被布局管理器调用?我们知道布局管理器事实上就是laymgr.cpp源文件中实现的一系列函数。而Matrix_Entry()正是被在laymgr.cpp源文件中实现的函数KeybdDriverInitializeEx()所调用,在该函数中有如下代码:

        BOOL fNoErr = (*g_rgpfnPddEntries[uiCurrPdd])

           (uiCurrPdd, KeybdEventCallback, &pKeybdPddInfo->pKeybdPdd);

其中g_rgpfnPddEntries[]是在源文件pddlist.cpp中定义并初始化的一个全局数组,如下:

PFN_KEYBD_PDD_ENTRYg_rgpfnPddEntries[] = {

   PS2_NOP_Entry,

   Matrix_Entry,

   NULL

};

在该数组中包含了键盘驱动中各PDD层的入口函数的函数指针。当uiCurrPdd == 1时,显然Matrix_Entry()函数被调用。再来看看它被调用时的参数。

uiCurrPdd显然为1,该参数是Matrix_Entry()所在PDD层的唯一标识(还记得吗?前面说过“在键盘驱动中必须有个参数来唯一标识每个PDD层”)。PDD层的标识值事实上就是它的入口函数在g_rgpfnPddEntries数组中的下标值。该参数由MDD层传递给PDD层。

 

KeybdEventCallback该参数正是同样在laymgr.cpp源文件中实现的键盘事件函数的函数指针。该参数有MDD层传递给PDD层。

 

pKeybdPdd这个参数须有PDD层由驱动开发者初始化并传给MDD层的&pKeybdPddInfo->pKeybdPdd,该参数类型是PKEYBD_PDD,它MSDN中给出的唯一一个PDD层接口结构体KEYBD_PDD的指针,结构体KEYBD_PDD它用来描述一个PDD层的完整信息。包括PDD层的键盘布局、电源管理、背景灯管理、调试输出标识字符。该结构体定义:

typedef struct tagKEYBD_PDD {

  WORD wPddMask;

  LPCTSTR pszName;

  PFN_KEYBD_PDD_POWER_HANDLER pfnPowerHandler;

  PFN_KEYBD_PDD_TOGGLE_LIGHTS pfnToggleLights;

} KEYBD_PDD, *PKEYBD_PDD;

该结构体中第一个成员wPddMask PDD层键盘布局掩码,它的值为该PDD层支持的键盘布局的键盘布局(一个PDD层可能有多个键盘布局)结构体DEVICE_LAYOUT中第二个参数wPddMask的或值。当PDD层仅支持一种键盘布局是该值应和她支持的键盘布局结构体DEVICE_LAYOUT中第二个参数wPddMask的值相同且不为0。要保证下面代码fMatchTRUE

if ((wPddMask & dl.wPddMask) != 0) {

                   fMatch = TRUE;

               }

dl.wPddMaskDEVICE_LAYOUT中第二个参数;

wPddMaskKEYBD_PDD第一个参数;

该结构体中第二个成员pszName:向用户描述PDD层,它将在调试信息中出现。不对键盘驱动本身有影响。

pfnPowerHandler:键盘电源管理函数指针;

pfnToggleLights:键盘背景灯管理函数指针;

 

2.3.2、入口函数Matrix_Entry()功能实现分析

1)先将MDD层通过Matrix_Entry()参数传递过来的PDD层标识值uiPddId及键盘事件函数指针pfnKeybdEvent分别保存到全局变量中,如下:

v_uiPddId = uiPddId;

   v_pfnKeybdEvent = pfnKeybdEvent;

它们将赋值给KeybdIstLoop()函数形参:PKEYBD_IST结构体的成员。

 

2)将在PDD层中初始化的KEYBD_PDD结构体变量MatrixPdd通过Matrix_Entry()的第三个参数ppKeybdPdd传递给MDD层,如下:

*ppKeybdPdd = &MatrixPdd;

 

3)新建一个PDD层类,这个类在kbdcommon目录下的s3c2440kbd.hpp头文件中声明,s3c2440kbd.cpp源文件中定义。

v_pp2k = new Ps2Keybd

这个类的类名为Ps2Keybd,通过类名可知,这是个针对PS2键盘PDD层的类。在该类中包含了一些PS2键盘PDD层实现需要的函数,这样对于不同键盘的PDD层可以通过继承该类来生成针对各PDDPDD类。但是实际操作中不一定会这样做,可能是按照Ps2Keybd类的模式重建一个PDD类。

Ps2Keybd类在s3c2440kbd.hpp头文件中声明如下:

class Ps2Keybd

      {

      HANDLE       m_hevInterrupt;

public:

BOOL Initialize(void);

BOOL IsrThreadStart(void);

BOOL IsrThreadProc(void);

BOOL KeybdPowerOff(void);

        BOOL  KeybdPowerOn(void);

friend

      void  KeybdPdd_PowerHandler(BOOL bOff);

friend

      int WINAPI KeybdPdd_GetEventEx(

             UINT32                VKeyBuf[16],

             UINT32                ScanCodeBuf[16],

             KEY_STATE_FLAGS   KeyStateFlagsBuf[16]

             );

friend

      void WINAPI

      KeybdPdd_ToggleKeyNotification(KEY_STATE_FLAGS  KeyStateFlags );

};

下面对一些重要的成员做一下介绍:

   成员变量m_hevInterrupt:表示与键盘逻辑中断相关联的同步事件句柄;

成员函数Initialize(void):在这个函数中将做一些初始化工作,注意:这些初始化工作并不是针对键盘硬件的初始化而是除此之外的初始化工作,例如一些获得扫描码相关全局变量的初始化等。在SMDK2440BSP包中该函数是空实现。

成员函数KeybdPowerOn(void):在该函数中执行与键盘硬件相关的初始化工作。如键盘连接的GPIO口,EINT口的初始化。

成员函数KeybdPdd_GetEventEx():是个空实现,它的存在是为了与wince4.2之前的版本键盘驱动兼容。要注意它与KeybdPdd_GetEventEx2()的区别,后者是对键盘驱动来说是一个非常重要的函数,它的功能是获取键盘硬件扫描码及案件状态,并按规定的数据结构组织起来。

 

4)执行键盘硬件之外的初始化工作,并创建键盘驱动的IST线程。

if (v_pp2k->Initialize()) {

       v_pp2k ->IsrThreadStart();

创建IST线程是通过PDD类中的成员函数IsrThreadStart()函数来完成的,该函数在s3c2440kbd.cpp源文件中定义。实现很简单,如下:

BOOL Ps2Keybd::IsrThreadStart()

{

HANDLE  hthrd;

DEBUGMSG(1,(TEXT("IsrThreadStart:\r\n")));

hthrd = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Ps2KeybdIsrThread,this,0,NULL);

// Since we don't need the handle, close it now.

CloseHandle(hthrd);

return TRUE;

}

   在该函数中就是调用CreateThread()创建IST线程,由于我们并不需要IST线程的句柄,所以调用CloseHandle(hthrd)关闭IST线程句柄。注意:关闭线程句柄,并不会是线程退出,这里关闭线程句柄是出于节省内存的考虑。IST线程线程函数是Ps2KeybdIsrThread,它实际上就是PDD类中的成员函数IsrThreadProc()。该函数在s3c2440kbd.cpp源文件中定义。对于该函数,我们将在后面详细分析。

 

5)完成键盘硬件相关物理地址的内存空间申请及映射,最后对键盘硬件进行初始化。

if (!KeybdDriverInitializeAddresses()) {

             goto leave;

      }

if (v_pp2k)

   {

          v_pp2k->KeybdPowerOn();

   }

键盘硬件相关物理地址的内存空间申请及映射是通过KeybdDriverInitializeAddresses()函数来完成的,该函数在s3c2440kbd.cpp源文件中定义,它分别调用VirtualAlloc()函数及VirtualCopy()函数来完成内存空间空间的申请及映射。最后调用PDD类成员函数KeybdPowerOn()完成键盘硬件的初始化。该函数在s3c2440kbd.cpp源文件中定义。

至此,PDD层的入口函数Matrix_Entry()分析完毕,总结一下该函数到底干了些什么。

1)接收MDD层通过Matrix_Entry()参数传递过来的PDD层标识值uiPddId及键盘事件函数指针pfnKeybdEvent

2)将在PDD层中初始化的KEYBD_PDD结构体变量MatrixPdd通过Matrix_Entry()的第三个参数ppKeybdPdd传递给MDD层;

3)新建一个PDD层类;

4)执行键盘硬件之外的初始化工作,并创建键盘驱动的IST线程;

5)完成键盘硬件相关物理地址的内存空间申请及映射,最后对键盘硬件进行初始化。

 

2.3.3IST线程函数IsrThreadProc()分析

键盘驱动最重要的工作是中断处理,而IST线程函数IsrThreadProc()实现的功能正是中断处理。所以IsrThreadProc()对键盘驱动程序的重要性不言而喻,所以在该节将对该函数进行详细分析。

1)  读取注册表相关键值,赋值IST线程优先级参数dwPriority

      ReadRegDWORD( TEXT("HARDWARE\\DEVICEMAP\\KEYBD"), _T("Priority256"), &dwPriority );

      if(dwPriority == 0) {

             dwPriority = 240;   // default value is 145

      }

2)  创建同步事件m_hevInterrupt

m_hevInterrupt = CreateEvent(NULL,FALSE,FALSE,NULL);

3)申请键盘物理中断映射的逻辑中断;

if (!KernelIoControl(IOCTL_HAL_REQUEST_SYSINTR, &dwIrq_Keybd, sizeof(DWORD), &g_dwSysIntr_Keybd, sizeof(DWORD), NULL))

   {

       RETAILMSG(1, (TEXT("ERROR: Failed to obtain sysintr value for keyboard interrupt.\r\n")));

       g_dwSysIntr_Keybd = SYSINTR_UNDEFINED;

       goto leave;

   }

  事实上,在键盘中断发生时,键盘驱动IST之前就通过OAL层实现的OEMInterruptHandler()函数完成了键盘物理中断到逻辑中断的映射,并返回给内核。但是IST线程运行在用户态,并不能直接使用已映射好的逻辑中断,所以须通过KernelIoControl()函数申请该逻辑中断。

4)关联逻辑中断与同步事件

             if (!InterruptInitialize(g_dwSysIntr_Keybd,m_hevInterrupt,NULL,0)) {

             DEBUGMSG(1, (TEXT("IsrThreadProc: KeybdInterruptEnable\r\n")));

             goto leave;

      }

5)设置IST线程优先级

      CeSetThreadPriority(GetCurrentThread(), (int)dwPriority);

6)调用  KeybdIstLoop()完成键盘中断处理;        

      KeybdIstLoop(&keybdIst);

可以发现上述的前5步:创建同步事件、申请逻辑中断、关联逻辑中断与同步事件、设置IST优先级都是创建IST线程的必须初始化工作。到现在为止,并没有设计任何键盘中断处理的内容。

事实上,在IST线程函数IsrThreadProc()中,真正完成键盘中断处理工作是函数KeybdIstLoop(&keybdIst),下面将详细分析该函数。

 

2.3.4、中断处理函数KeybdIstLoop()分析

该函数并不在kbdcommon目录下的源文件中定义,但放在这里分析是应为它跟该目录下源文件有着十分紧密的关系。该函数在C:\WINCE500\PUBLIC\COMMON\OAK\DRIVERS\KEYBD\IST目录下的kbdist.cpp源文件中定义(该源文件中也只有这一个函数)。

该函数原型如下:

BOOL KeybdIstLoop(PKEYBD_IST pKeybdIst)

该函数有一个参数,其类型声明在MSDN中找到,如下:

typedef struct tagKEYBD_IST {

 HANDLE hevInterrupt;

 DWORD dwSysIntr_Keybd;

 UINT uiPddId;

 PFN_KEYBD_PDD_GET_KEYBD_EVENT pfnGetKeybdEvent;

 PFN_KEYBD_EVENT pfnKeybdEvent;

} KEYBD_IST, *PKEYBD_IST;

该结构体类型包含了键盘中断处理所需的全部要素。

hevInterrupt:为同步事件句柄;

dwSysIntr_Keybd:键盘中断的逻辑中断号;

uiPddIdPDD标识号

pfnGetKeybdEvent:键盘扫描函数指针,其类型声明在MDSN中找得到,如下:

typedef UINT (*PFN_KEYBD_PDD_GET_KEYBD_EVENT)(

 UINT uiPddId,

 UINT32 rguiScanCode[16],

 BOOL rgfKeyUp[16]

);

 

pfnKeybdEvent:键盘事件发送函数指针,其类型声明在MDSN中找得到,如下:

typedef void (*PFN_KEYBD_EVENT)(

 UINT uiPddId,

 UINT32 uiScanCode,

 BOOL fKeyDown

);

该函数也是PDD层四个DDSI函数之一。

在分析KeybdIstLoop()之前,我们先来看看它被调用情况,在IsrThreadProc()中它被调用如下:

extern UINT v_uiPddId;

      extern PFN_KEYBD_EVENT v_pfnKeybdEvent;

 

      KEYBD_IST keybdIst;

      keybdIst.hevInterrupt = m_hevInterrupt;

      keybdIst.dwSysIntr_Keybd = g_dwSysIntr_Keybd;

      keybdIst.uiPddId = v_uiPddId;

      keybdIst.pfnGetKeybdEvent = KeybdPdd_GetEventEx2;

      keybdIst.pfnKeybdEvent = v_pfnKeybdEvent;

             

      KeybdIstLoop(&keybdIst);

 首先声明了一个KeybdIstLoop()的形参类型KEYBD_IST变量,然后对该变量进行了初始化赋值。我们重点关注对pfnGetKeybdEventpfnKeybdEvent的赋值:

keybdIst.pfnGetKeybdEvent =KeybdPdd_GetEventEx2;

pfnGetKeybdEvent被赋给的值KeybdPdd_GetEventEx2是一个键盘扫描函数指针,该函数在在s3c2440kbd.cpp源文件中定义。后面对该函数会有详细分析,尤其对他的接口。

 

keybdIst.pfnKeybdEvent =v_pfnKeybdEvent;

pfnKeybdEvent被赋给的值v_pfnKeybdEvent是个全局变量,该变量在入口函数Matrix_Entry()中被初始化入口函数的第二个参数pfnKeybdEvent2.3.1节做过分析,它实际上就是MDD层定义的键盘事件函数KeybdEventCallback()

下面对KeybdIstLoop()做详细分析:

1)  重新设置IST优先级

SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);

2)  Goto语句构建while1)(每个线程都是while1)形式的)

wait_for_keybd_interrupt:

…….

goto wait_for_keybd_interrupt;

3)  等待同步事件,实际上就是等待中断的发生。如果等待不到同步事件则IST线程被阻塞。

if (WaitForSingleObject(pKeybdIst->hevInterrupt, INFINITE) == WAIT_OBJECT_0)

4 调用*pKeybdIst->pfnGetKeybdEvent,实际上就是调用KeybdPdd_GetEventEx2()函数,扫描键盘获得硬件扫描码,并按装入规定的数据结构。

cEvents = (*pKeybdIst->pfnGetKeybdEvent)

           (pKeybdIst->uiPddId, rguiScanCode, rgfKeyUp);

5)调用*pKeybdIst->pfnKeybdEvent,实际上就是调用KeybdEventCallback(),逐个发送键盘事件。

for (UINT iEvent = 0; iEvent < cEvents; ++iEvent) {

           (*pKeybdIst->pfnKeybdEvent)(pKeybdIst->uiPddId,

               rguiScanCode[iEvent], rgfKeyUp[iEvent]);

       }

6)调用InterruptDone()结束IST线程,重新开启键盘中断并使同步事件置于无信号状态。

InterruptDone(pKeybdIst->dwSysIntr_Keybd);

 

2.3.5、键盘扫描函数KeybdPdd_GetEventEx2()分析

该函数在s3c2440kbd.cpp源文件中定义,分析该函数并不是分析它的具体实现。因为对不同的键盘而言其扫描过程是有很大的差别的,单单分析SMDK2440开发板的键盘扫描实现是没有意义的。这里重点关注的是该函数的参数、键盘事件的按键动作分解及键盘事件数据。

KeybdPdd_GetEventEx2()函数原型如下:

typedef UINT (*PFN_KEYBD_PDD_GET_KEYBD_EVENT)(

 UINT uiPddId,

 UINT32 rguiScanCode[16],

 BOOL rgfKeyUp[16]

);

1KeybdPdd_GetEventEx2()的三个参数

uiPddId:表示PDD层的唯一标识码;

rguiScanCode[16]:该数组中每个元素都是按键的硬件扫描码,注:该数组中每个元素的硬件扫描码都是当前按下或松开的单一按键的硬件扫描码。而不是在组合键时,同时按下键扫描时得出的硬件扫描码。这个形参的实参应是个全局变量。

rgfKeyUp[16]:该数组中每个元素都是按键的状态TRUE为按键松开。FALSE为按键按下。这个形参的实参应是个全局变量

KeybdPdd_GetEventEx2()函数有一个返回值,它表示按键动作的个数,键按下和松开都算按键动作。

 

2)键盘事件的按键动作分解

KeybdPdd_GetEventEx2()函数的功能实现不但要求在该函数中获得硬件扫描码和按键状态(通常硬件扫描码及按键状态的获取都封装成一个函数在KeybdPdd_GetEventEx2()的调用),而KeybdPdd_GetEventEx2()更重要的工作是:将键盘事件的按键动作分解,并按规定的组织形式准备好键盘事件数据。

下面将以表格的形式来描述典型键盘事件的按键动作分解。

单击’F’键按键动作分解:(假设F键的硬件扫描码为61)

 数组名↓       

按键动作→

0

1

rguiScanCode

61

61

rgfKeyUp

False

True

   从上表可以看出rguiScanCode rguiScanCode数组配合,可组成一个按键动作。上表中按键动作为0时,表示按键动作为字符键F按下;为1时,表示按键动作为字符键F松开。这两个按键动作组合在一起才成为一次按键单击动作。如果少了按键动作0,表示键没按下就有松开动作,不合逻辑。如果少了按键动作1,则系统认为该按键一直按下,MDD层此时会自行做自动重复按键处理。这样都不是一次正确的单击按键动作。

 

组合键shift+F按键动作分解:(假设shift的硬件扫描码为18

 数组名↓       

按键动作→

0

1

2

3

rguiScanCode

18

61

61

18

rgfKeyUp

False

False

Ture

Ture

动作0表示shift键按下;动作1表示shift键按下同时F键按下;动作2表示F键松开,但此时shift键依然按下;动作3表示shift键松开。这是一次完整的组合按键按下和松开的动作。

 

3)键盘事件数据

键盘事件数据中的硬件扫描码和按键状态的载体分别是前面提到的两个数组: rguiScanCode[16]rgfKeyUp[16]。之所以定义为数组,明显是为了容纳一个按键事件中的多个按键动作。但是,在实际操作中由于按键动作的中断触发方式,用数组来容纳按键事件中的多个按键动作的情况并不很多。这句话好像很难理解,请看下面的解释。

以单击按键事件为例:它的按键动作有两种方式按下是触发中断和按下松开都触发中断。当采用按下触发中断时,那么一次中断处理(即KeybdPdd_GetEventEx2()函数执行一次)要有两个按键动作才有一次完整的单击,此时KeybdPdd_GetEventEx2()函数用数组来容量两个按键动作是必要的。用伪代码描述: KeybdPdd_GetEventEx2()函数中,对于按下触发中断的单击键盘事件的键盘事件数据处理如下:

cEvents = 0;

scandCode = GetScandCode( );

rguiScanCode[cEvents] = scandCode;

rgfKeyUp[cEvents] = FALSE;

++ cEvents

rguiScanCode[cEvents] = scandCode;

rgfKeyUp[cEvents] = TURE;

++ cEvents

returncEvents;

    其中加粗的代码部分是因为单击按键事件只有一个按下触发的中断处理,明显必须在该次处理时模拟一次按键松开的按键动作,就是说把两个按键动作都装入到数组中,单击按键事件才会完整。此时数组就派上了用场。

   对于按下松开都触发中断的单击按键事件,那么该事件将触发两次中断,每次中断将处理一个按键动作,此时数组都只会容纳一个按键动作。数组没多大必要。

现在可以总结了:当按键按下松开时都触发按键中断时,rguiScanCode[16]rgfKeyUp[16]数组中都需要容纳一个按键动作。只有在按下或松开才触发按键中断时,在rguiScanCode[16]rgfKeyUp[16]中还需模拟一个松开按键动作,此时数组要容纳两个按键动作。当然,如果纯粹为模拟按键时,如一次单击模拟组合键,那这两个数组将容纳更多的按键动作。

另外需要特别注意的一点是:键盘事件中的按键动作数据发送时是通过for语句逐个发送的,如下代码。

for (UINT iEvent = 0; iEvent < cEvents; ++iEvent) {

           (*pKeybdIst->pfnKeybdEvent)(pKeybdIst->uiPddId,

               rguiScanCode[iEvent], rgfKeyUp[iEvent]);

       }

不要认为一次键盘事件的按键动作数据必须在一次按键中断处理中就全部发送完毕。一次按键事件中的按键动作数据可以在多次按键中断处理中发送。MDD层接收到键盘动作数据后会自行组合这些数据形成按键事件。

 

对于KeybdPdd_GetEventEx2()函数的总结:

1、该函数功能为获取硬件扫描码和按键状态,及按按键中断的触发方式分解按键动作,并按规定的组织形式准备好键盘事件数据。

2、要写好该函数要搞清楚按键中断触发方式、按键动作分解及键盘事件数据之间的关系。具体来说是:有按键中断触发方式来决定一次按键中断处理中是否需要发送一个键盘事件中的所有按键动作数据。

3、一个键盘事件包括多个按键动作。键盘事件数据包含多个键盘动作数据。

4、一个键盘事件中的多个按键动作不是必须要求在一次按键中断处理中全部发送出去。

 

2.4Matrix_0409目录下源码分析

该目录只包含了一个源文件:s3c2440.cpp。它实现了一个键盘布局,所以该节的重点是如何实现一个键盘布局及键盘布局与键盘驱动的对接。

2.4.1、键盘布局的实现

键盘布局包括两方面内容:Scand code到Virtual Key的映射及重映射。这里我们重点关注前一项内容。毕竟重映射功能(还记得笔记本上的那个Fn键吗,他的作用就是将不能与shift组合的按键,与Fn组合产生一个新的虚拟键值,该键值一般做控制用,而不在input language中映射为可打印字符,这就是一种典型的重映射功能。NumLock键也是一种典型的重映射)在嵌入式中用得极少。

SDMK2440ABSP包中键盘布局的映射实现在Matrix_0409子目录中实现。具体实现细节所需的结构体类型及函数原型定义都在C:\WINCE500\PUBLIC\COMMON\OAK\INC目录下的DeviceLayout.h头文件中,以下为该头文件完整代码:

#ifndef _DEVICE_LAYOUT_H_

#define _DEVICE_LAYOUT_H_

#include <windows.h>

 

typedef struct {

    UINT32  uPrefixMask;

    UINT32  uMin;

    UINT32  uMax;

    UINT8  *puScanCodeToVKey;

} ScanCodeToVKeyData;

 

typedef struct _KEYBD_EVENT {

    UINT32 uiVk;

    UINT32 uiSc;

    KEY_STATE_FLAGS KeyStateFlags;

} KEYBD_EVENT, *PKEYBD_EVENT;

 

// Remapping function typedef

// Returns the number of remapped events placed in pRmpKbdEvents

// Call with pRmpKbdEvents == NULL anc cMaxRmpKbdEvents == 0 to get the

// maximum size necessary for pRmpKbdEvents buffer.

typedef UINT (*PFN_KEYBD_REMAP)(

    const KEYBD_EVENT *pKbdEvents,      // List of events to remap

    UINT               cKbdEvents,      // Count of events in pKbdEvents

    KEYBD_EVENT       *pRmpKbdEvents,   // Buffer where remapped events will be placed

    UINT               cMaxRmpKbdEvents // Maximum number of remapped events

    );

 

typedef struct tagDEVICE_LAYOUT {

    DWORD dwSize;   

    WORD wPddMask; // Matches the device layout with its PDD

    ScanCodeToVKeyData **rgpscvk;  // Scan code to virtual key

    UINT cpscvk;

    PFN_KEYBD_REMAP     pfnRemapKey;  // Remapping functions

} DEVICE_LAYOUT, *PDEVICE_LAYOUT;

 

typedef BOOL (*PFN_DEVICE_LAYOUT_ENTRY)(PDEVICE_LAYOUT pDeviceLayout);

 

#endif // _DEVICE_LAYOUT_H_

Typedef定义的ScanCodeToVKeyData类型结构体用来描述键盘布局映射表的完整属性,其中的成员UINT8  *puScanCodeToVKey即为键盘布局映射表的指针,键盘布局映射表使用一个UINT8数组类型来表示,如下是一个范例(摘自某北斗二代导航手持机BSP包):

UINT8 ScanCodeToVKeyTable[] =

{

    //矩阵键盘

    '1',        //0

    '4',        //1

    '7',        //2

    VK_F8,      //3

   

    '2',        //4

    '5',        //5

    '8',        //6

    '0',        //7

   

    '3',        //8

    '6',        //9

    '9',        //10

    VK_F9,      //11       

 

    VK_UP,      //12,VK_UP是有微软定义的虚拟键,在MSDN中可查到。一般定制矩阵键盘,可借鉴这些虚拟键值的定义。毕竟自己定义是不必要的,而且这些虚拟键值意义也不一定需要与微软定义的一致。比如VK_NUMPAD0这个虚拟键值表征在微软标准键盘中为数字键盘的1,如果是自定义键盘的话大可定义为电源键什么的,接下来需要的只是在应用程序中的配合。

    VK_LEFT,    //13

    VK_NUMPAD1, //14

    VK_ESCAPE,  //15

    VK_DOWN,    //16

    VK_RIGHT,   //17

   

    VK_RETURN,  //18

    VK_F2,      //19

 

    VK_DELETE,  //20

    VK_RETURN,  //21

 

    //功能键

    VK_LWIN,    //22

    VK_SHIFT,   //23

    VK_CAPITAL, //24

    VK_CONTROL, //25

};

数组ScanCodeToVKeyTable中每一个UNIT8成员均为一个虚拟键值,按其数组下标依次与Scand code(注释部分标注了硬件扫描码的值)一一对应,这就是Scand code到Virtual Key的映射了。

接下来再来关注一下ScanCodeToVKeyData类型结构体中其它的成员:

UINT32  uPrefixMask 用途暂时不明,为0即可;

UINT32  uMin 为Scand code硬件扫描码的最小值;

UINT32  uMax为Scand code硬件扫描码的最大值,注意硬件扫描码必须在uMin与uMax之间连续存在;

到现在,ScanCodeToVKeyData类型结构体描述了键盘布局中cand code到Virtual Key映射的完整属性,是不是键盘布局就完成了呢?当然没有,我们还需要重映射、还需要将这个映射表告知键盘驱动的MDD层中实现的布局管理器,毕竟最是终布局管理器(layout manager)把Scand code映射到Virtual Key的。重映射先不讨论,那么怎么把ScanCodeToVKeyData描述的键盘布局告知布局管理器的呢?是通过在DeviceLayout.h中定义的如下函数

typedef BOOL (*PFN_DEVICE_LAYOUT_ENTRY)(PDEVICE_LAYOUT pDeviceLayout);

如上定义了一个函数指针,该函数指针指向的函数功能就是将ScanCodeToVKeyData描述的键盘布局告知布局管理器。该函数有一个参数PDEVICE_LAYOUT pDeviceLayout,其类型为DEVICE_LAYOUT的指针,而DEVICE_LAYOUT描述一个PDD层中所有键盘布局映射的完整属性(其中还包括重映射),DEVICE_LAYOUT定义如下:

typedef struct tagDEVICE_LAYOUT {

    DWORD dwSize;   

    WORD wPddMask; // Matches the device layout with its PDD

    ScanCodeToVKeyData **rgpscvk; // Scan code to virtual key

    UINT cpscvk;

    PFN_KEYBD_REMAP     pfnRemapKey;  // Remapping functions

} DEVICE_LAYOUT, *PDEVICE_LAYOUT;

DWORD dwSize 描述了DEVICE_LAYOUT类型结构体的大小,取sizeof(DEVICE_LAYOUT)即可;

WORD wPddMask:键盘布局掩码,我们知道在键盘驱动中,一个PDD层可以对应多个键盘布局。每个键盘布局都要有个唯一的标识码,而键盘布局掩码正式这个标识码。键盘布局掩码的取值应为2的n次方,如2、4、8、16等等。这样的取值是保证任意键盘布局掩码之间位或运算后能得到唯一的值,该值将作为KEYBD_PDD结构体的第一个参数。事实上wPddMask还会在结构体:

typedef struct tagKEYBD_PDD {

  WORD wPddMask;

  LPCTSTR pszName;

  PFN_KEYBD_PDD_POWER_HANDLERpfnPowerHandler;

  PFN_KEYBD_PDD_TOGGLE_LIGHTSpfnToggleLights;

} KEYBD_PDD, *PKEYBD_PDD;

中出现, 该结构体的作用是,表示wince键盘驱动中一个PDD层的完整属性。而它的成员wPddMask则是该PDD层支持键盘布局标识码的位或值。

ScanCodeToVKeyData **rgpscvk 为键盘布局映射中Scand code到Virtual Key的映射表的指针的指针。这里为什么要定义一个ScanCodeToVKeyData的指针的指针,而不是一个指针?这是因为定义成ScanCodeToVKeyData指针的指针是为了方便指向ScanCodeToVKeyData类型的指针数组。一般的在一个PDD层中可能存在多个键盘布局映射,那么描述多个键盘布局映射最好的方法就是把它放在一个ScanCodeToVKeyData数组中。以下代码(摘自创越北斗二代导航手持机BSP包)有助于我们理解:

static ScanCodeToVKeyData scvkEngUS =   //定义一个键盘布局映射表的Sc->Vk(不包含重映射)

{

0,

    ScanCodeTableFirst,

    ScanCodeTableLast,

    ScanCodeToVKeyTable

};

 

static ScanCodeToVKeyData *rgscvkMatrixEngUSTables[ ] = { &scvkEngUS };

  //定义了一个指针数组,只不过这里定义的数组只有一个成员,就是之前定义的键盘布局映射表scvkEngUS的指针

 

static DEVICE_LAYOUT dlMatrixEngUs =

{

    sizeof(DEVICE_LAYOUT),

    MATRIX_PDD,  //宏定义为8

    rgscvkMatrixEngUSTables,  //这里的rgscvkMatrixEngUSTables是一个指针数组的首指针,那么它自然是指针的指针了

    dim(rgscvkMatrixEngUSTables),

    MatrixUsRemapVKey,

};

 

UINT cpscvk 表示该PDD层有多少个键盘布局;

PFN_KEYBD_REMAP  pfnRemapKey 定义了一个函数指针,该函数指针类型PFN_KEYBD_REMAP也在DeviceLayout.h中定义,该函数指针指向键盘布局重映射函数,不实现重映射是直接指定为Null即可。

到这里终于分析完了:某个PDD层键盘布局映射描述结构体DEVICE_LAYOUT了。现在我们回过头来看看,PFN_DEVICE_LAYOUT_ENTRY函数,该函数将键盘驱动某个PDD层键盘布局(可能有多个个键盘布局)告知布局管理器。以下为一个PFN_DEVICE_LAYOUT_ENTRY函数示例: 

extern "C" BOOL Matrix( PDEVICE_LAYOUT pDeviceLayout )

{

    DEBUGCHK(pDeviceLayout != NULL);

    BOOL fRet = FALSE;

    if (pDeviceLayout->dwSize != sizeof(DEVICE_LAYOUT)) {

        RETAILMSG(1, (_T("Matrix: data structure size mismatch\r\n")));

        goto leave;

    }

    // Make sure that the Sc->Vk tables are the sizes that we expect

    DEBUGCHK(dim(ScanCodeToVKeyTable) == (1 + ScanCodeTableLast - ScanCodeTableFirst));

    *pDeviceLayout = dlMatrixEngUs;

    fRet = TRUE;

leave:

    return fRet;

}

该函数很简单,就是将该PDD层键盘布局描述DEVICE_LAYOUT结构体dlMatrixEngUs赋给该函数参数pDeviceLayout指向的DEVICE_LAYOUT结构体即可。这个函数在写wince键盘驱动时可以拿过来就用。

 

2.4.2、键盘布局的对接

至此,我们已经完整实现了一个键盘布局,接下来要做的就是将该键盘布局对接到键盘驱动。即MDD层怎么加载该键盘布局,在键盘驱动MDD层中会读取相关的注册表项来决定加载哪个键盘布局。所以设置这些注册表项称为键盘布局对接的重要步骤,具体键盘布局对接步骤如下:

1)导出键盘布局

在该键盘驱动动态链接库的导出函数文件.def(位于\Keybd\Kbds3c2440us目录中的kbdus.def文件)添加键盘布局函数名。如上面分析过的键盘布局函数名——Matrix,如下加粗部分:

LIBRARY         LAYOUTMANAGER

EXPORTS

 

    KeybdDriverInitializeEx

    …………………

    LayoutMgrActivateKeyboardLayout

 

    IL_00000409

    Matrix

    PS2_AT_00000409

其中PS2_AT_00000409也是一个键盘布局函数名,它是在MDD层实现的一个PS2_AT标准键盘的键盘布局。可见在一个键盘驱动中可导出多个键盘布局函数,这也表明在一个键盘驱动中可有多个键盘布局。

2)设置相关注册表项

[HKEY_CURRENT_USER\Keyboard Layout\Preload]

    @="00000409"

该注册表项表示当前键盘布局注册表子键名,其设置的值00000409表明在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Layouts\注册表项中名为00000409的子键设置本地键盘驱动的当前键盘布局。如上设置的值,则为注册表项:

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Layouts\00000409]

    "Layout File"=" KbdS3C2440Us.dll"

    "Layout Text"=" KbdS3C2440Us "

    " Matrix "=" KbdS3C2440Us.dll"

"Layout File"表示键盘布局在那个键盘驱动动态链接库导出,它的值必须和\Keybd\Kbds3c2440us目录下source文件中的TARGETNAME宏的值一样;

"Layout Text":向用户描述键盘布局的标识文本,该值仅仅起到一个标识作用;

" Matrix "=" KbdS3C2440Us.dll"中,左值必须和.def文件中导出的键盘布局函数名一致;右值即键盘驱动的动态链接库名(即TARGETNAME宏)。

 

注:当PDD层有多个键盘布局是还可设置如下

[HKEY_CURRENT_USER\Keyboard Layout\Preload\1]

    @="xxxxxxxxxx"

 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Layouts\xxxxxxxx]

  按下设置好的热键可在多个键盘布局之间切换。

 

2.5pddlist目录下源文件分析

前面提到过在该目录下的pddlist.cpp源文件定义了PDD层的入口函数。该源文件代码十分简单,所以在这里全贴出来:

#include <windows.h>

#include <keybdpdd.h>

 

// Add NOP driver for USB HID and RDP support.

BOOL WINAPI

PS2_NOP_Entry(

   UINT uiPddId,

   PFN_KEYBD_EVENT pfnKeybdEvent,

   PKEYBD_PDD *ppKeybdPdd

   );

 

BOOL WINAPI

Matrix_Entry(

   UINT uiPddId,

   PFN_KEYBD_EVENT pfnKeybdEvent,

   PKEYBD_PDD *ppKeybdPdd

   );

 

PFN_KEYBD_PDD_ENTRY g_rgpfnPddEntries[ ] = {

   PS2_NOP_Entry,

   Matrix_Entry,

   NULL

};

在该源文件中定义了两个PDD层入口函数分别是PS2_NOP_EntryMatrix_Entry。其中PS2_NOP_Entry是为USB键盘及RDP键盘保留的入口函数,该函数没有实现。Matrix_Entry则是我们实现的矩阵键盘驱动PDD层的入口函数。除了两个入口函数外,还定义并初始化了一个数组,这个数组在2.31节提到过,它是一个全局数组,其中成员都为入口函数,在KeybdDriverInitializeEx()函数正式遍历该数值来,来调用其中的每个入口函数初始化键盘驱动中的PDD层。

需要注意的是:该数值的最后一个元素必须是NULL,表示KeybdDriverInitializeEx()遍历该数组是它将作为一个结束标志。

 

2.6Kbds3c2440us目录下源码分析

在该目录下并没有源码文件,但是它包含了两个对键盘驱动的动态链接库而言非常重要的文件:kbdus.def文件和sources文件。前者描述了键盘驱动动态链接库的导出函数,后者描述了生成该动态链接库一些重要信息。

2.6.1kbdus.def文件分析

    代码注释如下:

…..

EXPORTS 

      KeybdDriverInitializeEx  //以下均为导出的DDI函数

      KeybdDriverPowerHandler

      KeybdDriverGetInfo

      KeybdDriverSetMode

      KeybdDriverInitStates

      KeybdDriverVKeyToUnicode

      KeybdDriverMapVirtualKey

 

      LayoutMgrGetKeyboardType

      LayoutMgrGetKeyboardLayout

      LayoutMgrGetKeyboardLayoutName

      LayoutMgrGetKeyboardLayoutList

      LayoutMgrLoadKeyboardLayout

      LayoutMgrActivateKeyboardLayout   //到这里依然是导出的DDI函数

 

      IL_00000409  //导出的输入语言函数

      Matrix  //导出的键盘布局函数1

      PS2_AT_00000409 //导出的键盘布局函数2

导出的DDI函数对WINCE下的键盘驱动而言都是相同的,所以在开发键盘驱动是.def文件的这一部分是不需要修改的。重点关注的是导出的输入语言函数和键盘布局函数。键盘布局函数的导出在前面做了详细分析,这里不再赘述。

重点分析一下输入语言的导出,SMDK2440实现的键盘驱动并没有在BSP包中的键盘驱动目录中实现在该文件中导出的输入语言函数。而是直接移植了WinceMDD层实现的示例输入语言函数(移植的方法十分简单:在Kbds3c2440us目录下的source文件中添加  $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\InputLang_0409.lib \即可),在目录C:\WINCE500\PUBLIC\COMMON\OAK\DRIVERS\KEYBD\INPUTLANGS\0409下,该目录下实现了标准的101key US键盘的输入语言函数IL_00000409。但是这样做有一个很大的弊端,如果需要并修改了该目录下的源文件,那么将对其他使用该源文件的键盘驱动造成不可预料的影响。所以推荐的做法是将该输入语言函数移植到BSP包中,具体移植方法是:

1)  复制该目录到BSP包中键盘驱动的跟目录;

2)  在BSP包中键盘驱动的根目录下的DIRS文件中添加该目录;

3)  在键盘驱动动态链接库的.def文件中导出该输入语言的函数;

4)  在Kbds3c2440us目录下的source文件中的SOURCELIBS宏中添加$(_TARGETPLATROOT)\lib\$(_CPUINDPATH)\InputLang_0409.lib \

它表示将输入语言库文件链接到键盘驱动的动态库文件,其中_TARGETPLATROOT_CPUINDPATH是两个环境变量,分别为路径C:\WINCE500\PLATFORM\SMDK2440A\ARMV4I\retail。编译BSP包时,输入语言库文件生成在$(_TARGETPLATROOT)\lib\$(_CPUINDPATH)\目录下。

 

2.6.2source文件分析

该文件是生成的键盘驱动提供了必不可少的信息,这些信息包括:键盘驱动名称、键盘驱动的形式(动态链接库)、生成驱动程序文件目录、动态链接库的导出文件、导入库文件列表、静态连接到目标文件的库文件列表等。

!if "$(BSP_NOKEYBD)" == "1"

SKIPBUILD=1

!endif

 

SYNCHRONIZE_DRAIN=1

 

RELEASETYPE=PLATFORM

 

TARGETNAME=KbdS3C2440Us

DEFFILE=KbdUS.def

TARGETTYPE=DYNLINK

DLLENTRY=_DllMainCRTStartup

TARGETLIBS= \

   $(_COMMONSDKROOT)\lib\$(_CPUINDPATH)\coredll.lib \

   $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\ceddk.lib

 

SOURCELIBS=\

   $(_TARGETPLATROOT)\lib\$(_CPUINDPATH)\S3C2440KBD.lib \

   $(_TARGETPLATROOT)\lib\$(_CPUINDPATH)\S3C2440_Layout.lib \

   $(_TARGETPLATROOT)\lib\$(_CPUINDPATH)\PddList.lib \

   $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\Nop_KbdCommon.lib \

   $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\kbdus_lib.lib \

   $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\LayoutManager.lib      \

   $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\KeybdIst.lib \

   $(_COMMONOAKROOT)\lib\$(_CPUINDPATH)\NumPadRmp.lib \

   $(_TARGETPLATROOT)\lib\$(_CPUINDPATH)\InputLang_0409.lib \

 

WINCETARGETFILE=dummy

 

SOURCES=

 

RELEASETYPE宏指定驱动程序文件生成目录,PLATFORM表示为C:\WINCE500\PLATFORM\SMDK2440A\lib\ARMV4I\retail

TARGETNAME宏指定驱动程序的名称,不包含后缀名;

TARGETTYPE宏指定驱动程序的形式,即文件的后缀名。DYNLINK表示动态链接库文件;LIBRARY表示静态库文件;PROGRAM表示可执行文件;

DEFFILE宏指定动态链接库的导出函数列表文件;

TARGETLIBS宏指定要动态连接到导入库文件列表;

SOURCELIBS宏指定要静态连接到目标文件的库文件列表;

需要重点关注的是SOURCELIBS,它指定了键盘驱动的动态链接库所依赖的所有静态库文件,在编译键盘驱动动态链接库之前一定要确认它所依赖的静态库文件都在这里被指定了。

 

 

三、   OAL层支持中断

 键盘驱动的核心是中断,可除了在驱动程序中对中断进行处理外。要使键盘驱动正常工作,还必须使OAL层支持中断。在OAL层支持中断可总结为三个方面的内容:

1)  实现OALIntrinit()函数,初始化SOC中的中断管理器;

2)  实现BSPIntrInit( )函数初始化板级中断系统配置、完成逻辑中断静态映射;

3)  实现ISR(即函数OEMInterruptHandler),在中断发生时,把物理中断信号映射成OEM定义的逻辑中断;

4)  实现几个重要的OEM函数,这几个函数将在驱动程序调用中断相关管理函数(InterruptDisableInterruptInitializeInterruptDone)时,在这些中断管理函数中被调用。

仍以SMDK2440 BSP包为例,分析OAL层对中断的支持。

3.1、初始化SOC的中断管理器(OALIntrinit()函数)

   对于S3C2440的中断管理器参见相关datasheet

wince启动时,将调用OAL层实现的OEMinit()函数对硬件平台初始化。在该函数中则将调用OALIntrinit()函数对中断管理器初始化。OEMinit()函数定义在C:\WINCE500\PLATFORM\SMDK2440A\Src\Kernel\Oal\init.c文件中。如下:

if(!OALIntrInit()) {

       OALMSG(OAL_ERROR, (

           L"ERROR: OEMInit: failed to initialize interrupts\r\n" ));

   }

 

OALIntrinit()函数定义在:C:\WINCE500\PLATFORM\SMDK2440A\Src\Common\Intr\intr.c中定义,详细分析如下:

1)调用OALIntrMapInit()函数初始化逻辑中断映射数组,OALIntrMapInit()函数在C:\WINCE500\PLATFORM\COMMON\SRC\COMMON\INTR\BASE\map.c文件中定义,它是一个硬件平台无关函数。它实现的功能很简单:就是将逻辑中断映射全局数组g_oalIrq2SysIntr[ ]中每个元素赋值为SYSINTR_UNDEFINED特别注意全局数组g_oalIrq2SysIntr[ ],它实际上完成硬件中断号到逻辑中断号的映射,下标为硬件中断号,对应的元素为逻辑中断。后面将多次提到该数组。

 

2屏蔽并清空所有中断,如下:

    // Mask and clear external interrupts

   OUTREG32(&g_pPortRegs->EINTMASK, 0xFFFFFFFF);

   OUTREG32(&g_pPortRegs->EINTPEND, 0xFFFFFFFF);

 

   // Mask and clear internal interrupts

   OUTREG32(&g_pIntrRegs->INTMSK, 0xFFFFFFFF);

OUTREG32(&g_pIntrRegs->SRCPND, 0xFFFFFFFF);

// S3C2440A developer notice (page 4) warns against writing a 1 to any

// 0 bit field in the INTPND register. Instead we'll write the INTPND

// value itself.

 OUTREG32(&g_pIntrRegs->INTPND, INREG32(&g_pIntrRegs->INTPND));

通过对EINTMASK寄存器(注意:该寄存器在GPIO寄存器组)全写1,先暂时屏蔽所有外部中断,对EINTPEND全写1清空所有外部中断。对于内部中断亦如此。

注意加粗的部分:这里使用了一个技巧向INTPND寄存器写入自身值,来清空仲裁后的中断。

 

3打开定时器time4中断,定时器4中断用于产生系统时钟周期,作为整个系统运行一致的步伐。定时器4中断时内核运行必须的也是唯一的中断,所以必须打开。其他中断均可在外设驱动初始化时再打开。如下:

// Unmask the system tick timer interrupt

   CLRREG32(&g_pIntrRegs->INTMSK, 1 << IRQ_TIMER4);

 

4)如果定义了OAL_BSP_CALLBACKS宏,则调用BSPIntrInit()函数如下:

ifdef OAL_BSP_CALLBACKS

   // Give BSP change to initialize subordinate controller

      OALMSG(1, (L"Give BSP change to initialize subordinate controller\r\n"));

   rc = BSPIntrInit();

#else

   rc = TRUE;

#endif

 

上述的内容只是在SMDK2440 BSP包中实现的OALIntrinit()函数,用户在定制wince操作系统时可根据具体情况对SOC的中断管理器做进一步的初始化,如设置仲裁管理器、中断优先级等等。总之,只要是对SOC中断管理器的初始化都可在该函数中完成。

 

3.2、初始化板级中断系统配置、完成静态逻辑中断静态映射(BSPIntrInit()函数)

OALIntrinit()函数如果定义了OAL_BSP_CALLBACKS宏,则会调用BSPIntrInit()函数。

OAL_BSP_CALLBACKS宏在源文件对应的source文件中的CDEFINE宏中定义,CDEFINE宏专门指定需要传给编译器的宏开关,如:CDEFINES=$(CDEFINES) -DCEDDK_USEDDKMACRO -DOAL_BSP_CALLBACKS

   BSPIntrInit()函数在C:\WINCE500\PLATFORM\SMDK2440A\Src\Kernel\Oal\intr.c文件中,完成板级中断系统配置初始化及静态(硬件中断到)逻辑中断映射。分析如下:

1)配置外部中断7

// Set GPF7 as EINT7

   value = INREG32(&pOalPortRegs->GPFCON);

   OUTREG32(&pOalPortRegs->GPFCON, (value & ~(3 << 14))|(2 << 14));

      

   // Disable pullup

   value = INREG32(&pOalPortRegs->GPFUP);

   OUTREG32(&pOalPortRegs->GPFUP, value | (1 << 7));

 

   // High level interrupt

   value = INREG32(&pOalPortRegs->EXTINT0);

   OUTREG32(&pOalPortRegs->EXTINT0, (value & ~(0xf << 28))|(0x1 << 28));

配置内容为:配置外部中断7复用GPIOGPF7,禁止上拉,中断触发方式设置为高电平。

 

2)配置外部中断9

  与外部中断7 基本一致。

 

3)完成静态逻辑中断映射

   // Add static mapping for Built-In OHCI

   OALIntrStaticTranslate(SYSINTR_OHCI, IRQ_USBH);

调用OALIntrStaticTranslate()函数完成硬件中断IRQ_USBH到逻辑中断SYSINTR_OHCI的静态映射。OALIntrStaticTranslate()C:\WINCE500\PLATFORM\COMMON\SRC\COMMON\INTR\BASE\map.c文件中定义,它是一个硬件平台无关函数。函数很简单:如下:

VOID OALIntrStaticTranslate(UINT32 sysIntr, UINT32 irq)

{

   OALMSG(OAL_FUNC&&OAL_INTR, (

       L"+OALIntrStaticTranslate(%d, %d)\r\n", sysIntr, irq

   ));

   if (irq < OAL_INTR_IRQ_MAXIMUM && sysIntr < SYSINTR_MAXIMUM) {

       g_oalSysIntr2Irq[sysIntr] = irq;

       g_oalIrq2SysIntr[irq] = sysIntr;

   }       

   OALMSG(OAL_FUNC&&OAL_INTR, (L"-OALIntrStaticTranslate\r\n"));

}

   就是对全局数组g_oalIrq2SysIntr[ ]进行一次赋值,事实上就是将硬件中断号irq映射到逻辑中断sysIntr

注:硬件中断号IRQ_USBHC:\WINCE500\PLATFORM\SMDK2440A\Src\Inc\s3c2440a_intr.h头文件中定义。逻辑中断号SYSINTR_OHCIC:\WINCE500\PLATFORM\SMDK2440A\Src\Inc\bsp_cfg.h头文件中定义,OEM时可自行添加。

 

上述的内容只是在SMDK2440 BSP包中实现的BSPIntrInit()函数,用户在定制wince操作系统时可根据开发板中断配置的具体情况,来在该函数中完成开发板中断系统的具体配置。如:设置外部中断口、设置中断触发方式将开发板上存在的硬件中断全部映射到逻辑中断等等。

 

3.3、中断发生时,把物理中断信号映射成OEM定义的逻辑中断(函数OEMInterruptHandler

OEMInterruptHandler()函数实际上就是通常所说的ISR。它负责在中断发生时从SOC的中断管理器的相关寄存器中读出物理中断号,并计算出当前中断的硬件中断号,然后对照全局数组g_oalIrq2SysIntr[ ]映射成逻辑中断号,并返回给内核。OEMInterruptHandler()函数在C:\WINCE500\PLATFORM\SMDK2440A\Src\Common\Intr\intr.c中定义。分析如下(注:这里涉及到的中断管理器的知识比较多,尤其是对中断管理器中的各寄存器作用要有非常的了解):

1)读取中断的物理中断号

     注意物理中断号与硬件中断号的区别,物理中断号是从中断管理器INTOFFSET寄存器中读出的数值。它表示INTPND寄存器置1位的位数。INTPND寄存器中的每一位对应唯一一个中断源。硬件中断号是OEM时,对每个中断源包括子中断源的标号。对于含子中断源的中断源,只有一个物理中断号,却有多个硬件中断号,所以读出物理中断号后,还需读出相关寄存器计算出硬件中断号。

// Get pending interrupt(s)

      irq = INREG32(&g_pIntrRegs->INTOFFSET);

  从中断管理器的INTOFFSET寄存器读出物理中断号。

 

2)如果发生的中断时定时器4中断,则直接在ISR中处理该中断。

   if (irq == IRQ_TIMER4)

      {

 

             // Clear the interrupt

             OUTREG32(&g_pIntrRegs->SRCPND, 1 << IRQ_TIMER4);

             OUTREG32(&g_pIntrRegs->INTPND, 1 << IRQ_TIMER4);

 

             // Rest is on timer interrupt handler

             sysIntr = OALTimerIntrHandler();

      }    

   定时器中断时系统时钟中断,它直接在ISR中处理。以加快响应速度。

 

3)如果发生EINT4_7EINT8_23外部中断,含子中断源,计算出硬件中断号并分别屏蔽清空子中断源及中断源

if (irq == IRQ_EINT4_7 || irq == IRQ_EINT8_23) { // 4 or 5

      // Find external interrupt number

                    mask = INREG32(&g_pPortRegs->EINTPEND);

                    mask &= ~INREG32(&g_pPortRegs->EINTMASK);

                    mask = (mask ^ (mask - 1)) >> 5;

                    irq2 = IRQ_EINT4;

                    while (mask != 0) {

                           mask >>= 1;

                           irq2++;

                    }

                    // Mask and clear interrupt

                    mask = 1 << (irq2 - IRQ_EINT4 + 4);

                    SETREG32(&g_pPortRegs->EINTMASK, mask);

                    OUTREG32(&g_pPortRegs->EINTPEND, mask);

 

                    // Clear primary interrupt

                    mask = 1 << irq;

                    OUTREG32(&g_pIntrRegs->SRCPND, mask);

                    OUTREG32(&g_pIntrRegs->INTPND, mask);

 

                    // From now we care about this irq

                    irq = irq2;

             }

S3C2440的中断管理器中外部中断47公用一个中断源,823公用一个中断源。因此它们都含有子中断源。因此处理这两个物理中断相对于处理不含子中断源的中断源要复杂得多。幸运的是,smdk2440BSP包中提供了完整的处理方式。大致过程如下:

a)       读取EINTPENDEINTMASK寄存器,计算出硬件中断号irq2

b)      屏蔽并清空外部中断子中断源;

c)       屏蔽并清空EINT4_7EINT8_23中断源;

 

4)如果发生的中断不含子中断源,屏蔽清空中断源

      // Mask and clear interrupt

      mask = 1 << irq;

      SETREG32(&g_pIntrRegs->INTMSK, mask);

      OUTREG32(&g_pIntrRegs->SRCPND, mask);

      OUTREG32(&g_pIntrRegs->INTPND, mask);

    对于不含子中断源的物理中断,其物理中断号与硬件中断号一致,所以不需要再去计算硬件中断号了。

 

5)如果没有可安装ISR对该硬件中断号进行另外的处理,则调用OALIntrTranslateIrq()函数查找全局数组g_oalIrq2SysIntr[ ],获得该硬件中断的逻辑中断号,并返回给内核。

sysIntr = NKCallIntChain((UCHAR)irq);

             if (sysIntr == SYSINTR_CHAIN || !NKIsSysIntrValid(sysIntr)) {

                    // IRQ wasn't claimed, use static mapping

                    sysIntr = OALIntrTranslateIrq(irq);

             }

NKCallIntChain()函数用来查询某硬件中断号是不是有可安装的ISR对其进行另外的处理,如果没有则返回一个非SYSINTR_CHAIN有效逻辑中断号。对于该函数,如果开发板上没有可安装ISR的情况,则不必深究。OALIntrTranslateIrq()函数在C:\WINCE500\PLATFORM\COMMON\SRC\COMMON\INTR\BASE\map.c文件中定义,它是一个硬件平台无关函数。它实现的功能很简单:查找全局数组g_oalIrq2SysIntr[ ],以硬件中断号为下标,返回逻辑中断号。

总结一下:OEMInterruptHandler()函数的功能不仅仅在于将当前发送的物理中断映射成逻辑中断,更重要的还需要将该当前发生的中断屏蔽并清空。屏蔽中断的目的是防止在该中断的处理过程中再次发生该中断而产生的中断嵌套。清空中断的原因是:S3C2440的中断管理器中SUBINTPNDINTPNDSRCPND等寄存器被一个发生的中断值 1后,必须人工将相关位清0。否则,下次即使中断不发生,也会产生发生了中断的假象。清0的方式为:向置1位写1。且在中断处理过程中,中断屏蔽并清空应安排在中断处理程序的前端。对于wince系统来说,可以在ISR中完成,也可以在驱动程序IST线程中通过调用InterruptDisable()函数来完成。下面会分析到该函数。

可安装ISR这个概念从wince4.0之后开始引入。它是为了解决应用程序开发商面临的一个尴尬。如果应用程序开发商在通用总线上需要加载外设并处理该外设的中断,那么他们将不得不依靠他们所用的wince系统的OEM商,请求他们在OAL层中为自己的外设添加逻辑中断支持。然而有了可安装ISR应用程序开发商可以不再依赖操作系统OEM商了,他们只需要完成一个可安装ISR,并在其中为他们的外设添加一个逻辑中断号即可。说白了:可安装ISR无非是为通用总线外设的物理中断映射成逻辑中断提供了一个新的在应用程序层面的途径。

3.4、实现几个重要的函数。这几个函数在驱动程序调用中断管理相关API函数(InterruptDisableInterruptInitializeInterruptDone)时,将在这些API函数中被调用。

在分析这几个中要的函数之前,先介绍一下在驱动程序中常调用的中断管理相关API函数:

BOOL InterruptInitialize( DWORD idInt, HANDLE hEvent, LPVOID pvData, DWORD cbData );

该函数负责把某个逻辑中断号与一个同步事件对象关联起来,这一点很多人都知道。但是该函数还有另外一个很重要的功能就是:初始化使能中断,在OAL层的OALIntrinit()函数初始化中断管理器时,所有中断都是被屏蔽的。所以必须在驱动程序中调用该函数初始化使能中断(当然,同时也关联逻辑中断)。在该函数中将调用OAL层实现的函数OALIntrEnableIrqs( )来初始化使能中断。

 

VOID InterruptDone( DWORD idInt );

该函数负责在对中断处理已完成时,告诉操作系统可重新使能该中断(中断在中断处理过程中式被屏蔽的)并同时使与该中断关联的同步事件置为无信号状态。在该函数中将调用OAL层实现的函数OALIntrDoneIrqs( )函数来重新使能中断。

 

VOID InterruptDisable( DWORD idInt );

该函数负责在中断处理时屏蔽中断,由于wince下通常在中断处理的ISR阶段就会将中断屏蔽。所以在驱动程序中不必再调用该函数来再次屏蔽中断。当然,如果在ISR阶段并没有将中断屏蔽掉,可在驱动程序中直接调用该函数将中断屏蔽。在该函数中将调用OAL层实现的函数OALIntrDisableIrqs( )函数来屏蔽中断。

 

再来看看上面提到的三个在OAL层实现的函数,它们均在C:\WINCE500\PLATFORM\SMDK2440A\Src\Common\Intr\intr.c中定义。

BOOL OALIntrEnableIrqs(UINT32 count, const UINT32 *pIrqs)

参数count:要使能多少个IRQpIrqs:要被初始化使能的IRQ数组,这个数组中每个元素都是中断对应的硬件中断号。该函数用于初始化时使能IRQ中断,分析如下:

1)  如果定义了OAL_BSP_CALLBACKS宏,则调用BSPIntrEnableIrq()函数,如下:

#ifndef OAL_BSP_CALLBACKS

             irq = pIrqs[i];

#else

             // Give BSP chance to enable irq on subordinate interrupt controller

             irq = BSPIntrEnableIrq(pIrqs[i]);

#endif

该函数在C:\WINCE500\PLATFORM\SMDK2440A\Src\Kernel\Oal\intr.c文件中定义。该函数的作用是:如果开发板上在SOC的中断管理控制器外设置了一个辅助中断管理控制器(最简单的例子就是将几个外设的中断通过或门电路后在接入外部中断引脚)则在该函数中初始化使能辅助中断控制器中的中断。在SDMK2440中并没有这样的分控制器,所以BSPIntrEnableIrq()为空实现。

2)按硬件中断号从051,分IRQ_EINT0~ IRQ_ADCIRQ_EINT4~ IRQ_EINT7IRQ_EINT8~ IRQ_EINT23三个段,分别作不同的使能中断的操作。对于IRQ_EINT0~ IRQ_ADC只要清除INTMASK相应位即可使能中断,如下:

CLRREG32(&g_pIntrRegs->INTMSK, 1 << irq);

对于IRQ_EINT4~ IRQ_EINT7IRQ_EINT8~ IRQ_EINT23段,除了清除INTMASK相应位还须清除EINTMASK相应位才能使能这一段的中断。

 

VOID OALIntrDoneIrqs(UINT32 count, const UINT32 *pIrqs)

参数count:要重新使能多少个IRQpIrqs:要被重新使能的IRQ数组。该函数用于在中断处理结束时,重新使能IRQ中断。在该函数中依然是清除INTMASKEINTMASK相应位并软件清除SRCPNDEINTPEND相应位来手动清除中断(S3C2440的中断管理器是不会再中断发生后自动清除中断发生标志位的,如果不手动清除则系统会认为该中断在不断发生)。

 

VOID OALIntrDisableIrqs(UINT32 count, const UINT32 *pIrqs)

    参数count:要重新使能多少个IRQpIrqs:要被禁止的IRQ数组。该函数用于在某中断处理时禁止该中断。方式是置INTMSKEINTMASK相关位。

 

注:你或许在本节中看到了太多的屏蔽中断、使能中断、清除中断等等,那么到底应该在什么时候、在什么地方来执行这些操作呢?把握几条原则:

1、  在驱动程序中中断初始化时(不是在初始化中断管理器是),使能中断;

2、  中断处理程序开始时屏蔽中断;

3、  中断处理程序结束时重新使能中断;

4、  在中断处理程序中必须清除中断(对不能自动清除中断的中断管理器而言)。

wince中,屏蔽中断、使能中断、清除中断的对象都是硬件中断号。

至此,对OAL层支持中断的分析全部就结束了。为某一个开发板移植wince操作系统时,使OAL层支持中断的重点应该放在:

1、 OALIntrinit()中初始化板上SOC的中断管理器;

2、 BSPIntrInit()设置开发板上的中断系统配置;

3、 发生中断时,在OEMInterruptHandler()中读出中断的硬件中断号;

4、 OALIntrStaticTranslate()静态映射硬件中断号和逻辑中断号;

 

原创粉丝点击