技术开发 频道

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

 【IT168技术文档】Windows CE.NET 确实通过 QueryPerformanceCounter API 为高分辨率计时器提供了现成的解决方案。如果您必须延迟一小段时间,则使用该 API 非常有效,但如果您希望等待一小段时间,又该如何呢?延迟和等待之间的差别在于:延迟比等待消耗更多的 CPU 时间。等待意味着系统中的其他(优先级较低或相等的)线程可以在等待期间执行。

 LARGE_INTEGER liDelay;

 // Query number of ticks per second

 if (QueryPerformanceFrequency(&liDelay))

 {

 // 1ms delay

 liDelay.QuadPart /= 1000;

 LARGE_INTEGER liTimeOut;

 // Get current ticks

 if (QueryPerformanceCounter(&liTimeOut))

 {

 // Create timeout value

 liTimeOut.QuadPart += liDelay.QuadPart;

 LARGE_INTEGER liCurrent;

 do

 {

 // Get current ticks

 QueryPerformanceCounter(&liCurrent);

 // Delay until timeout

 } while (liCurrent.QuadPart<liTimeOut.QuadPart);

 }

 }

 以最高优先级(优先级 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 源代码文件位于 \WINCE410\PUBLIC\COMMON\OAK\CSP\I486\OAL1 中。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            CurTicks.QuadPart += 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

 // the rainforest...

 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

 \WINCE410\PUBLIC\COMMON\OAK\CSP\I486\INC, 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 request.\r\n")));

 return (TRUE);

 }

 #endif

  生成平台

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

查看原文地址

0
相关文章