2024年3月5日发(作者:)

在 Windows CE .NET 4.1 平台上实现坚固的 Windows CE 计时器

发布日期: 11/10/2004 | 更新日期: 11/10/2004

Michel Verhagen

Microsoft Windows Embedded MVP

PTS Software, The Netherlands

适用于:

Microsoft Windows CE .NET 4.1

摘要本文为系统开发人员提供了使用仅限于软件的解决方案在 Windows CE 平台上实现坚固的 CE 计时器的指导原则。

本页内容

简介

QueryPerformanceCounter

对计时器进行重新编程

关于作者

缩略语和术语

简介

Windows CE .NET 是来自 Microsoft 的嵌入式组件化 OS,它具有 1 微秒 (ms) 的内部计时器信号分辨率。对于大多数项目而言,2 ms 的精确度就足够了,但某些项目却需要较高分辨率的非阻塞计时器。CE API 没有提供此类现成功能,但通过对 OAL 加以略微修改,我们可以获得分辨率高于 2ms 的坚固的非阻塞计时器。

返回页首

QueryPerformanceCounter

Windows 确实通过 QueryPerformanceCounter API 为高分辨率计时器提供了现成的解决方案。如果您必须延迟一小段时间,则使用该 API 非常有效,但如果您希望等待一小段时间,又该如何呢?延迟和等待之间的差别在于:延迟比等待消耗更多的 CPU

时间。等待意味着系统中的其他(优先级较低或相等的)线程可以在等待期间执行。

LARGE_INTEGER liDelay;

// Query number of ticks per second

if (QueryPerformanceFrequency(&liDelay))

{

// 1ms delay

rt /= 1000;

LARGE_INTEGER liTimeOut;

// Get current ticks

if (QueryPerformanceCounter(&liTimeOut))

{

// Create timeout value

rt += rt;

LARGE_INTEGER liCurrent;

do

{

// Get current ticks

QueryPerformanceCounter(&liCurrent);

// Delay until timeout

} while (rt

}

}

以最高优先级(优先级 0)运行上述代码将在延迟期间阻塞整个 OS。

// !!! PSEUDO CODE !!!

HANDLE hTimer = CreateHighResolutionTimer();

SetHighResolutionTimeout(hTimer, GetHighResolutionTimer() + DELAY);

WaitForSingleObject(hTimer, INFINITE);

当我们在优先级 0 运行第二个示例时,线程会在等待期间释放 CPU。因此,在这些小段时间内,其他线程可以取得 CPU 的拥有权并完成它们的工作。遗憾的是,在 Windows

CE .NET 中未实现上述 HighResolutionTimer API。

休眠分辨率

如果您已经阅读本文档的简介,则可能会认为它含有一个打字错误:

“Windows CE (...) 具有 1 ms 的内部计时器信号分辨率。对于大多数项目而言,2 ms

的精确度就足够了,(...)”。

如果 CE 具有 1 ms 的分辨率,则您可能认为那也是您可以等待的最小时间。可惜的是,事实并非如此,因为如果我们在系统计时器滴答(重新安排时间滴答)10 μs 之后发出“Sleep(1),”,则休眠计数器会在下一次滴答时启动,并且在接下来的那次滴答时终止。这使我们获得了 1.90 ms 的休眠,而不是预期的 1 ms。一般说来,Sleep(N) 将休眠 N

到 (N+1) ms。

硬件解决方案

PC 硬件体系结构只提供 1 个计时器,它在物理上连接到某根中断线,并且该计时器已经由 Windows CE 内核使用。CE 内核对该计时器进行编程,使其每毫秒生成一个中断,并且将该中断主要用于线程计划程序和其他一些功能。如果 PC 体系结构能够加入一些备用的中断计时器,则我们在应用 x86 CEPC 的时候就不会如此困难。当然,您可以在 ISA 或

PCI 总线上的某个位置添加一个简单的可编程计时器芯片,但为什么不尝试用软件实现高分辨率计时器呢?

返回页首

对计时器进行重新编程

生成硬实时 1 ms 中断的唯一方法是对 PIT(可编程间隔计时器,在 PC 硬件中通常为

82C54 或衍生物)进行重新编程,使其快于 1ms。OAL 中的分析代码使用了类似的技术(参见 OEMProfileTimerEnable)。Windows CE 用于对 PIT 进行编程的代码位于

OAL(OEM 适应层)中。OAL 源代码文件位于

WINCE410PUBLICCOMMONOAKCSPI486OAL1 中。Windows CE 使用

timer.c 内部的 InitClock 函数对 PIT 进行编程:

//

// Setup Timer0 to fire every TICK_RATE mS and generate

// interrupt

//

SetTimer0(TIMER_COUNT);

PICEnableInterrupt(INTR_TIMER0, TRUE);

dwReschedPeriod = TIMER_COUNT;

1. 我使用原始路径来指向 OAL 源代码,当然您应该将 OAL 代码从 PUBLIC 树移动到您自己的 BSP,并且在那里修改它。绝对不要在 PUBLIC 树中修改任何代码;Microsoft

可能使用 QFE 更新它。

创建 1 ms 中断的最简便方法是将中断速度加倍并切换该行为。该行为是在主中断服务例程(将在下面对其进行讨论)中编码的。

要使计时器中断的速度加倍,可以用 TIMER_COUNT / 2 加载该计时器,如下所示:

//

// Setup Timer0 to fire every TICK_RATE mS and generate

// interrupt

//

// Twice as fast for software 1ms timer

#define USE_SOFT_1MS

#ifdef USE_SOFT_1MS

SetTimer0(TIMER_COUNT / 2);

#else

SetTimer0(TIMER_COUNT);

#endif

PICEnableInterrupt(INTR_TIMER0, TRUE);

dwReschedPeriod = TIMER_COUNT;

现在,timer0 中断将每 500 微秒 (0.5 ms) 发生一次。

我已经在修改过的代码前后添加了 #ifdefs,以使其能够稍微容易一些返回到原始 CE 代码。

修改 ISR

主 ISR 位于 fwpc.c 内部:

001 ULONG PeRPISR(void)

002 {

003 ULONG ulRet = SYSINTR_NOP;

004 UCHAR ucCurrentInterrupt;

005

006 if (fIntrTime)

007 {

008 //

009 // We're doing interrupt timing. Get Time to ISR.

010 //

011 #ifdef EXTERNAL_VERIFY

012 _outp((USHORT)0x80, 0xE1);

013 #endif

014 dwIntrTimeIsr1 = _PerfCountSinceTick();

015 dwIntrTimeNumInts++;

016 }

017

018 ucCurrentInterrupt = PICGetCurrentInterrupt();

019

020 if (ucCurrentInterrupt == INTR_TIMER0)

021 {

022 if (PProfileInterrupt)

023 {

024 ulRet= PProfileInterrupt();

025 }

026 else

027 {

028 #ifdef SYSTIMERLED

029 static BYTE bTick;

030 _outp((USHORT)0x80, bTick++);

031 #endif

032

033 CurMSec += SYSTEM_TICK_MS;

034 #if (CE_MAJOR_VER == 0x0003)

035 DiffMSec += SYSTEM_TICK_MS;

036 #endif

037 rt += TIMER_COUNT;

038

039 if (fIntrTime)

040 {

041 //

042 // We're doing interrupt timing. Every nth tick is a

043 // SYSINTR_TIMING.

044 //

045 dwIntrTimeCountdown--;

046

047 if (dwIntrTimeCountdown == 0)

048 {

049 dwIntrTimeCountdown = dwIntrTimeCountdownRef;

050 dwIntrTimeNumInts = 0;

051 #ifdef EXTERNAL_VERIFY

052 _outp((USHORT)0x80, 0xE2);

053 #endif

054 dwIntrTimeIsr2 = _PerfCountSinceTick();

055 ulRet = SYSINTR_TIMING;

056 }

057 else

058 {

059 #if (CE_MAJOR_VER == 0x0003)

060 if (ticksleft || (dwSleepMin && (dwSleepMin <=

DiffMSec))

061 || (dwPreempt && (dwPreempt <= DiffMSec)))

062 #else

063 if ((int) (CurMSec - dwReschedTime) >= 0)

064 #endif

065 ulRet = SYSINTR_RESCHED;

066 }

067 }

068 else

069 {

070 #if (CE_MAJOR_VER == 0x0003)

071 if (ticksleft || (dwSleepMin && (dwSleepMin <=

DiffMSec)) ||

072 (dwPreempt && (dwPreempt <= DiffMSec)))

073 #else

074 if ((int) (CurMSec - dwReschedTime) >= 0)

075 #endif

076 ulRet = SYSINTR_RESCHED;

077 }

078 }

079

080 //

081 // Check if a reboot was requested.

082 //

083 if (dwRebootAddress)

084 {

085 RebootHandler();

086 }

087 }

088 else if (ucCurrentInterrupt == INTR_RTC)

089 {

090 UCHAR cStatusC;

091 // Check to see if this was an alarm interrupt

092 cStatusC = CMOS_Read( RTC_STATUS_C);

093 if((cStatusC & (RTC_SRC_IRQ|RTC_SRC_US)) ==

(RTC_SRC_IRQ|RTC_SRC_US))

094 ulRet = SYSINTR_RTC_ALARM;

095 }

096 else if (ucCurrentInterrupt <= INTR_MAXIMUM)

097 {

098 // We have a physical interrupt ID, but want to return a

SYSINTR_ID

099

100 // Call interrupt chain to see if any installed ISRs handle

this

101 // interrupt

102 ulRet = NKCallIntChain(ucCurrentInterrupt);

103

104 if (ulRet == SYSINTR_CHAIN)

105 {

106 ulRet = OEMTranslateIrq(ucCurrentInterrupt);

107 if (ulRet != -1)

108 PICEnableInterrupt(ucCurrentInterrupt, FALSE);

109 else

110 ulRet = SYSINTR_NOP;

111 }

112 else

113 {

114 PICEnableInterrupt(ucCurrentInterrupt, FALSE);

115 }

116 }

117 if (ucCurrentInterrupt > 7 || ucCurrentInterrupt == -2)

118 {

119 __asm

120 {

121 mov al, 020h ; Nonspecific EOI

122 out 0A0h, al

123 }

124 }

125 __asm

126 {

127 mov al, 020h ; Nonspecific EOI

128 out 020h, al

129 }

130 return ulRet;

131 }

所有硬件中断都被映射到该 ISR 并在该 ISR 中进行处理。线 018 得到当前的中断号。线 020 和 088 分别处理计时器 0 中断和 RTC(实时时钟)中断。如果该中断是其他某种中断,则线 096 会执行快速验证,调用任何链化的 ISR(参见 MSDN 中的函数

NKCallIntChain),将中断号转换为 SYSINTR_ 值,禁用该中断,并且最后在 ulRet 中返回 SYSINTR_ 值。如果找不到 SYSINTR_ 映射的 Irq,则用 SYSINTR_NOP 填充

ulRet。任何已注册的 IST(中断服务线程)事件都按照 ISR 的 SYSINTR_ 返回值进行设置。通过调用 InterruptInitialize 来注册 IST:

InterruptInitialize(SYSINTR_SOFT1MS, hEvent, NULL, 0);

在上述函数中,事件 hEvent 被映射到 ISR 返回值 SYSINTR_SOFT1MS。

最后,ISR 通过向可编程中断控制器写 EOI(中断结束)值 (0x20),通知它中断已被处理。如果中断号大于 7,则必须首先通知第二个 PIC(两个 PIC 控制器通过中断线 2 级联)。

因为我们调整了计时器频率,所以我们还必须调整上述 ISR,原因是现在 ISR 通常被调用两次,因此计划程序也工作两倍的次数(对于每个线程,计划次数被除以 2)。

首先,我们必须声明一个静态布尔值,以便能够在 timer0 中断发生时切换 ISR 行为:

001 ULONG PeRPISR(void)

002 {

003 ULONG ulRet = SYSINTR_NOP;

004 UCHAR ucCurrentInterrupt;

#define USE_SOFT_1MS

#ifdef USE_SOFT_1MS

static BOOL bToggle = FALSE;

#endif

005

006 if (fIntrTime)

007 {

// Append rest of code here

我们必须只为 timer0 中断切换该行为:

020 if (ucCurrentInterrupt == INTR_TIMER0)

021 {

#ifdef USE_SOFT_1MS

bToggle = !bToggle; // Toggle value

if (bToggle)

{

#endif

022 if (PProfileInterrupt)

023 {

024 ulRet= PProfileInterrupt();

025 }

026 else

027 {

// Lines 028 to 077 are unchanged, and not showed here

to save

//

078 }

079

080 //

081 // Check if a reboot was requested.

082 //

083 if (dwRebootAddress)

084 {

085 RebootHandler();

086 }

#ifdef USE_SOFT_1MS

}

else

{

ulRet = SYSINTR_SOFT1MS;

}

#endif

087 }

088 else if (ucCurrentInterrupt == INTR_RTC)

089 {

// Append rest of code here

现在,发生 timer0 中断时的行为在“运行正常的 CE ISR 代码”和“返回

SYSINTR_SOFT1MS”之间切换。我们现在可以通过 SYSINTR_SOFT1MS 值使用

InterruptInitialize,以便将某个事件绑定到 timer0 中断。然后,该事件将每 1 ms 产生一次。

修改 oalintr.h

Before we can use the SYSINTR_SOFT1MS value

we have to define it in oalintr.h, which resides in

WINCE410PUBLICCOMMONOAKCSPI486INC, like this:

#define USE_SOFT_1MS

#ifdef USE_SOFT_1MS

#define SYSINTR_SOFT1MS (SYSINTR_FIRMWARE+6)

#endif

只要您按照下面的说明修改 OEMInterruptEnable 函数,就可以随便使用任何基于

SYSINTR_FIRMWARE 的值(就像 SYSINTR_FIRMWARE+20 一样)。

修改 OEMInterruptEnable 函数

我们还必须更改 cfwpc.c 内部的 OEMInterruptEnable 函数,以确保对于我们的

timer0 中断,该函数总是成功。如果我们不这样做,则对于 SYSINTR_SOFT1MS 中断,InterruptInitialize 函数将失败。将下面的代码行添加到该函数:

#define USE_SOFT_1MS

#ifdef USE_SOFT_1MS

if (idInt == SYSINTR_SOFT1MS)

{

DEBUGMSG (1, (TEXT("Accepting the soft 1ms interrupt enable

")));

return (TRUE);

}

#endif

生成平台

因为我们更改了一些内核代码,所以必须对内核进行完整的生成,包括重新生成依赖项树。首先,保存所有已更改的文件,然后在 Platform Builder 的“Tools”菜单中选择“Options”,并单击“Build”选项卡。现在,确保“Enable Deptree Build”被选中。此时,就可以通过单击“Build”菜单中的“Rebuild Platform”重新生成整个平台了。完

成所有工作以后,请从“Tools”->“Options”菜单的“Build”选项卡中取消选中“Enable

Deptree Build”。

返回页首

关于作者

Michel Verhagen 自 2000 年以来一直担任 PTS Software bv 的 Windows

顾问,致力于为荷兰的客户生成用于工业设备的复杂的 Windows CE 平台和设备驱动程序。因此,他是荷兰仅有的几名专门研究 Windows 的开发人员之一,并且是荷兰唯一的 eMVP。过去,他已经参与了对 Windows CE 3.0 的实时行为的评估。最近,Michel 已经混合使用托管代码和非托管代码并结合使用 Windows 4.1,对 .NET Compact Framework 的实时行为进行了评估。有关该主题的白皮书最近已被

Microsoft 授予 2003 年度技术卓越奖。当您需要 Michel 的专业知识时,您总是可以在

Microsoft 嵌入式新闻组之一中向他求教。如果 Michel 没有及时答复,那么他很可能正在驾着自己的滑翔伞翱翔于云端。

其他资源:

/windows/embedded//

反馈:

要提供有关该白皮书的反馈,请向 en@ 发送电子邮件。

更多信息

有关 Windows CE .NET 的详细信息,请参阅 Windows CE .NET 主页。

有关 Windows CE .NET 中包含的联机文档资料和上下文相关帮助,请参阅 Windows

CE .NET 产品文档资料。

返回页首

缩略语和术语

μs 微秒(1 / 1000000 秒)

API 应用程序编程接口

CPU 中央处理器

EOI 中断结束

ISA 行业标准体系结构

ISR 中断服务例程

IST 中断服务线程

ms 毫秒(1 / 1000 秒)

OAK OEM 适应工具包

OAL OEM 抽象层

OS 操作系统

PC 个人计算机

PCI 外围组件互连

PIC 可编程中断控制器

PIT 可编程间隔计时器

RTC 实时时钟