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 实时时钟
发布评论