|
欢迎大家加入群一起讨论进步 中关村智能手机开发:41995469
作者:付林林
OAL(OEM Adaptation Layer)既OEM 适配层,从逻辑上讲位于Windows CE内核和硬件之间,从物理上讲OAL各个模块代码被编译后(.lib)和其它内核库链接到一起形成Windows CE的内核可执行文件nk.exe。Windows CE内核在OAL层暴露了大量的函数和全局变量,利用这些函数和全局变量OEM可以编写中断处理、RTC、电源管理、调试端口、通用I/O控制代码等。CE安装目录的子目录中包含了OAL的部分源码,大多数情况下开发者对OAL只要修改即可,甚至无需修改。通过阅读本篇文章,开发者能够了解OAL的结构、暴露的接口的功能,可以在此基础上实现甚至增强OAL的功能。因为OAL层代码大多数和CE启动时系统初始化工作有关,所以我们将以CE的启动顺序为线索。
一、在Boot Loader解压CE内核镜像文件(nk.bin)后开始跳转到StartUp(),StartUp函数属于OAL层,此时CE操作系统内核还没有运行。StartUp函数的功能主要有两个,一是初始化CPU为已知状态(known state),二是调用内核初始化函数(x86平台为KernelInitialize,其它平台为KernelStart)。初始化CPU工作因CPU的不同而不同,如果是ARM系列,包括设置CPU为管理员模式、禁止IRQ和FIQ、禁止MMU、清空指令和数据缓冲、检测启动原因、配置GPIO和内存控制器、初始化RTC、保存OEMAddressTable地址等。执行完毕后调用KernetStart。如果是x86系列,包括设置CPU为保护模式、初始化内存控制器、保存OEMAddressTable地址等。执行完毕后调用KernetInitialize。
二、内核初始化函数的功能也因CPU的不同而不同,不过有一些功能是相同的,如初始化串口(为了输出调试信息)、调用OEMInit函数等。对于x86系列,初始化工作除了上述的功能外还包括读取OEMAddressTable内容、确定分页大小、内核重定位、初始化中断分配表、初始化分页表、内存初始化和其它初始化。对于其它系列CPU请参考CE帮助文档。
1. 串口调试:
串口调试函数包括OEMInitDebugSerial、OEMReadDebugByte、OEMWriteDebugByte等。从OEMInitDebugSerial的源码可以看出,系统从BOOT_ARG_PTR_LOCATION为首地址的结构中判断当前连接的串口是哪个,然后配置这个串口。如果你的设备的串口I/O地址设置和CE默认的一致的话,就能在CE内核得到CPU控制权到启动完毕这段时间里通过串口得到调试信息。
2. OEMInit
一般在OEMInit中初始化所有外围的硬件、初始化系统时钟(system tick)和RTC(real time clock)、初始化KITL(Kernel Independent Transport Layer)。例如I486平台的OEMinit函数,它先关联所有的IRQ和中断ID,然后初始化PCI总线、网络适配器、电源管理、PIC(可编程中断控制器)、系统时钟,最后检测是否有扩展内存。另外如果OEM要通过OAL暴露的函数指针或者全局变量来增强功能的话,就要在此函数中实现(在下面详细讲解)。
3. 检测扩展内存
我们都知道在config.bib配置文件中设置CE系统使用RAM总量(如果不知道请参考我的文章Platform Builder之旅系列),注意这个RAM总量不是总的物理内存的大小。PB编译的内核包含一个变量ulRAMEnd,将在config.bib中定义的RAM的起始地址 + RAM大小的和赋值给ulRAMEnd。在CE内核的启动过程中,ulRAMEnd的值赋值给全局变量MainMemoryEndAddress,CE内核通过访问MainMemoryEndAddress得到RAM的总量信息。假如基于CE的设备附加了RAM,而MainMemoryEndAddress的值没有包括这段附加的RAM,结果CE内核无法知道已经附加了RAM。为了让CE内核了解附加RAM的信息,OEM应该编写一个函数检测RAM的总量,并把总量值赋给MainMemoryEndAddress。OAL暴露了一个函数指针pNKEnumExtensionDRAM,OEM应该把编写好的函数地址赋给这个函数指针。如果OEM不准备自己编写内存检测函数的话也可以调用OEMGetExtensionDRAM。从帮助文档中看出OEMGetExtensionDRAM这个函数能够检测内存的总量,但是CE的针对X86 平台的源码中没有具体编写这个函数的实现代码(见%_WINCEROOT%\PUBLIC\COMMON\OAK\CSP\I486\OAL\cfwpc.c)。也就是说在X86平台上调用OEMGetExtensionDRAM是检测不到RAM的。如果OEM有兴趣编写检测RAM总量的函数,可以调用现成的函数IsDRAM。这个函数也保存在cfwpc.c中。
三、内核初始化函数执行完毕后开始按如下步骤执行:
1. 内核创建用于与filesys.exe同步的事件对象SYSTEM/FSReady,之后启动filesys.exe。启动filesys.exe的意义是让filesys.exe读取注册表数据。
2. 内核等待事件SYSTEM/FSReady被触发,这个事件是由filesys.exe在做完一系列工作后触发。这一系列的工作内容如下:
2.1 先检测这是一次冷启动还是热启动,如果是冷启动,那么初始化对象存储内存区域。
2.2 调用OEMIoControl函数,I/O控制代码为IOCTL_HAL_INIT_RTC,也就是初始化RTC。
2.3 初始化数据库子系统和API、文件系统API、消息队列API。
2.4 如果操作系统镜像(nk.bin)包括RAM文件系统,那么读取Initobj.dat文件内容后创建一个RAM文件系统。
2.5 初始化注册表(在内存中形成注册表)。
2.6 如果此时device.exe没有启动,那么读取HKEY_LOCAL_MACHINE\System\StorageManager下"Dll"的值(这个值为存储管理器所在的.dll的文件名)并加载到内存。加载之后创建一个线程专用于初始化存储管理器,初始化之后此线程结束。
2.7 初始化NLS(national language support)。关于NLS请参见我的文章《CE下中文输入法编辑器》。
2.8 为数据库引擎设置本地ID。
2.9 读取Initdb.ini文件,安装在对象存储中的数据库。
2.10 触发SYSTEM/FSReady事件,之后filesys.exe处于等待状态,等待内核发通知给它。
3. 此时注册表已经存在于内存当中,内核开始读取如下位置数据:
4. HKEY_LOCAL_MACHINE\Loader\SystemPath
5. HKEY_LOCAL_MACHINE\SYSTEM\OOM\cbLow and cpLow
6. HKEY_LOCAL_MACHINE\SYSTEM\KERNEL\InjectDLL
7. HKEY_LOCAL_MACHINE\MUI\Enable and SysLang
HKEY_CURRENT_USER\MUI\CurLang
8. 内核设置低内存处理(out of memory)。低内存处理是指当前可用的内存非常少时,内核所做的解决方案(CE帮助文档中有详细说明)。
9. 内核在做好了上述工作后通知filesys.exe,由filesys.exe做其余工作。filesys.exe所做的工作内容如下:
5.1 读取HKEY_LOCAL_MACHINE\System\Events 下包含的所有事件对象名称并一一创建。
5.2 读取HKEY_LOCAL_MACHINE\Init 下包括的所有应用程序名称并一一启动。如果device.exe在列表中并且此时它已经启动了,那么触发SYSTEM/BOOTPHASE2事件,这会使device.exe重新读取注册表数据来完成最后的驱动程序初始化。
5.3 初始化时间区域(time zone)。
正如CE的帮助文档所言,创建OAL是一个非常复杂的任务,而通常的办法是复制原有的相同平台的OAL代码,然后修改来适应平台的特殊要求。也就是说对于没有特殊要求的平台,复制原有相同平台的OAL代码就足够了。由于OAL的复杂性在这篇文章中我只讲解常用的部分。
一、实现ISR
1.ISR的概念
ISR(interrupt service routine)是处理IRQs(interrupt request line)的程序。Windows CE用一个ISR来处理所有的IRQ请求。当一个中断发生时,内核的异常处理程序先调用内核ISR,内核ISR禁用所有具有相同优先级和较低优先级的中断,然后调用已经注册的OAL ISR程序,一般ISR有下列特征:
• 执行最小的中断处理,最小的中断处理指能够检验、答复产生中断的硬件,而把更多的处理工作留给IST(interrupt service thread)。
• 当ISR完成时返回中断ID(中断ID大部分是预定义的)。
2. X86平台的ISR结构
X86平台的ISR保存在%_WINCEROOT%\PUBLIC\COMMON\OAK\CSP\I486\OAL\fwpc.c中,函数名为PeRPISR。下面分析一下此函数的主要代码:
ULONG PeRPISR(void)
{
ULONG ulRet = SYSINTR_NOP; ///返回值,既中断ID(以SYSINTR_为前缀)
UCHAR ucCurrentInterrupt; ///当前中断号
if (fIntrTime) ////// fIntrTime 用于测试SR和IST的延时时间,测试工具为ILTiming.exe。
......
ucCurrentInterrupt = PICGetCurrentInterrupt(); ////返回当前中断IRQ
///IRQ0,IRQ0为系统时钟(system tick)中断,具体见“二、实现系统时钟”
if (ucCurrentInterrupt == INTR_TIMER0)
......
if (dwRebootAddress) ////是否需要重启动
RebootHandler();
......
if(ucCurrentInterrupt == INTR_RTC) ////IRQ8,real-time clock的中断
......
else if (ucCurrentInterrupt <= INTR_MAXIMUM) ///如果中断小于 INTR_MAXIMUM
{
ulRet = NKCallIntChain(ucCurrentInterrupt); ////调用中断链
if (ulRet == SYSINTR_CHAIN) ///如果中断链未包含中断
////在IRQ 和SYSINTR之间转换,此函数返回IRQ对应的SYSINTR
ulRet = OEMTranslateIrq(ucCurrentInterrupt);
......
PICEnableInterrupt(ucCurrentInterrupt, FALSE); ///启用除当前中断以外的所有中断
} ///else if
OEMIndicateIntSource(ulRet); ///通知内核已经发生SYSINTR中断
}
从以上代码不难看出ISR的任务就是返回以“SYSINTR_”为前缀的中断ID,如果不需要进一步执行IST,那么就返回SYSINTR_NOP。
3.中断注册步骤
参考X86平台的代码,中断注册步骤如下:
• 用SETUP_INTERRUPT_MAP宏关联SYSINTR和IRQ。以“SYSINTR_”为前缀的常量由内核使用,用于唯一标识发生中断的硬件。在Nkintr.h文件中预定义了一些SYSINTR,OEM可以在Oalintr.h文件中自定义SYSINTR。
• 用HookInterrupt函数关联硬件中断号和ISR。这里的硬件中断号为物理中断号,而非逻辑中断号IRQ。在InitPICs函数(和上述ISR位于同一文件)的最后调用了HookInterrupt函数,如下:
for (i = 64; i < 80; i++)
HookInterrupt(i, (void *)PeRPISR); ///用ISR关联16个中断号
4. 中断处理步骤
• 调用InterruptInitialize函数关联SYSINTR和IST,具体是关联IST等待的事件。一般在驱动程序中按如下编写:
• hEvent = CreateEvent(...) ///创建一个事件对象
• InterruptInitialize(SYSINTR_SERIAL, hEvent, ...) ///关联一个串口中断ID和这个事件
• hThd = CreateThread(..., MyISTRoutine, hEvent, ...) ///创建一个线程(IST)
CeSetThreadPriority(hThd, 152); ///提高此线程的优先级
• IST执行I/O操作,一般IST按如下编写:
• for(;;) ///驱动程序一直处于服务状态
• {
• WaitForSingleObject(hEvent, INFINITE); ////无限等待事件
• ...... //// I/O操作
• InterruptDone(InterruptId); ///结束当前中断处理
}
• ISR和IST之间数据传输
假如我们要从一个设备频繁的读取数据而每次读取量非常少,那么每次读取都要调用IST会降低性能。作为解决方案,ISR可以做读取工作(存放到缓冲区),并在缓冲区存放满后由IST到缓冲区读取。因为ISR运行在内核模式而IST运行在用户模式,IST不能轻易地访问ISR的缓冲区,为此CE提供了一个办法(参见标题为“Passing Data between an ISR and an IST”的帮助文档),您也可以到天极网嵌入式开发论坛询问。
二、实现系统时钟
1. 系统时钟(system tick)概念
系统时钟是内核需要的唯一中断(IRQ0),系统时钟每毫秒产生一个中断,当发生中断时内核在ISR中累计,到1000的倍数就是过了一秒钟。在处理系统时钟的ISR中不仅要累计计数,还要决定是否通知内核开始重新调度当前所有的线程。要实现一个OAL,系统时钟是第一个必须做的事。
2. X86平台系统时钟中断的处理工作
系统时钟由InitClock函数负责初始化工作,一般是在OEMInit函数中调用。当发生中断时,ISR首先用下列语句累计计数:
CurMSec += SYSTEM_TICK_MS; /////SYSTEM_TICK_MS = 1
然后根据下列语句判断应该返回什么值:
if ((int) (dwReschedTime – CurMSec) >= 0)
return SYSINTR_RESCHED; ///重新调度
else
return SYSINTR_NOP; ///不再执行任何操作
上述代码中全局变量dwReschedTime在schedule.c中定义,也就是由内核的调度模块决定在何时开始重新调度线程。CurMSec累计了从WindowsCE启动到当前总共产生了多少个system tick。实现系统时钟后还要实现OEMIdle函数,当没有线程准备运行时OEMIdle被调用,OEMIdle函数将CPU置于空闲模式,但在空闲模式下仍然要累计系统时钟。
三、I/O控制代码
1. I/O控制代码作用
应用软件或者驱动程序可以调用KernelIoControl函数与OAL层通信,而KernelIoControl在内部调用OEMIoControl函数。OEMIoControl是一个OAL API,OEM可以在OEMIoControl中编写自己的I/O控制代码实现一些功能,或者说与应用软件通信。I/O控制代码常用的例子如重启计算机、得到系统信息、设置RTC、得到设备ID等。还有一些系统程序使用的特殊的I/O控制代码。在这里说明一下,我经过实验证实CE提供的得到设备ID方法并非有效。
2. 编写自己的I/O控制代码步骤
• 在pkfuncs.h或者新编写一个.h文件中按如下格式定义:
#define IOCTL_MY_CONTROL CTL_CODE(FILE_DEVICE_HAL, 3000, METHOD_NEITHER, FILE_ANY_ACCESS)
• 在oemioctl.c中修改OEMIoControl函数,添加如下代码:
case IOCTL_MY_CONTROL:
......
• 在应用程序中调用KernelIoControl函数,具体参数参见帮助文档。 |
|