Export (0) Print
Expand All
Expand Minimize

Implementing Rock-Solid Windows CE Timers On Windows CE .NET 4.1 Platforms

Windows CE .NET
 

Michel Verhagen,
Microsoft Windows Embedded MVP
PTS Software, The Netherlands

July 2003

Applies to:
    Microsoft® Windows® CE .NET 4.1

Abstract

This paper provides guidelines for system developers to implement rock-solid CE timers on Windows CE platforms using a software-only solution.

Contents

Abstract
Introduction
QueryPerformanceCounter
Re-Programming the Timer
About the Author
Acronyms and Terms

Introduction

Windows CE .NET, the embedded componentized OS from Microsoft, has an internal timer tick resolution of 1 microsecond (ms). For most projects, 2 ms accuracy is enough, but some projects just need a higher resolution non-blocking timer. The CE API does not provide such functionality out of the box, but by modifying the OAL a little bit, we can get rock-solid non-blocking timers with resolutions higher than 2ms.

QueryPerformanceCounter

Windows CE. NET does supply an out-of-the-box solution for high-resolution timers by means of the QueryPerformanceCounter API. This API is great if you have to delay for a small period of time, but what if you want to wait for a small period of time? The difference between delaying and waiting is that delaying is much more CPU consuming than waiting. Waiting implies that other (lower or equal priority) threads in the system can execute during the wait.

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);
   }
}

Running the code shown above at the highest priority (priority 0) will block the entire OS during the delay time.

// !!! PSEUDO CODE !!!
HANDLE hTimer = CreateHighResolutionTimer();
SetHighResolutionTimeout(hTimer, GetHighResolutionTimer() + DELAY);
WaitForSingleObject(hTimer, INFINITE);

When we run the second example at priority 0, the thread frees the CPU during the wait. Therefore, during these small periods of time, other threads can take ownership of the CPU and do their work. Unfortunately, the above HighResolutionTimer API's are not implemented in Windows CE .NET.

Sleep resolution

If you've read the introduction of this document, you might think there is a typo in it:

"Windows CE (...) has an internal timer tick resolution of 1 ms. For most projects 2 ms accuracy is enough, (...)".

If CE has a resolution of 1 ms, you would expect that's also the smallest time you can wait. Unfortunately, that's not the case because if we issue a Sleep(1), 10 µs after the system timer tick (the reschedule tick), the sleep counter starts at the next tick and will end on the following tick. This gives us a sleep of 1.90 ms, and not the 1 ms as expected. Generally speaking, a Sleep(N) will sleep somewhere in between N and (N+1) ms.

Hardware solution

The PC hardware architecture provides only 1 timer, which is physically connected to an interrupt line, and this timer is already in use by the Windows CE kernel. The CE kernel programs the timer to generate an interrupt every millisecond, and uses this interrupt primarily for the thread scheduler and some other functions. Our lives in x86 CEPC land wouldn't be so difficult if the PC architecture would incorporate some spare interrupt timers. Of course, you could add a simple programmable timer chip somewhere on the ISA or PCI bus, but why not try to accomplish high-resolution timers in software?

Re-Programming the Timer

The only way to generate a hard real-time 1 ms interrupt is to reprogram the PIT (Programmable Interval Timer, in PC hardware usually an 82C54 or derivate) faster than 1ms. A similar technique is used by the profiling code in the OAL (see OEMProfileTimerEnable). The code Windows CE uses to program the PIT is located in the OAL (OEM Adaptation Layer). The OAL source code files reside in \WINCE410\PUBLIC\COMMON\OAK\CSP\I486\OAL1. Windows CE uses the InitClock function inside timer.c to program the PIT:

   //
   // Setup Timer0 to fire every TICK_RATE mS and generate 
   // interrupt
   //
   SetTimer0(TIMER_COUNT);

   PICEnableInterrupt(INTR_TIMER0, TRUE);

   dwReschedPeriod = TIMER_COUNT;

1. I use the original paths to point to OAL source code, but of course you should move the OAL code from the PUBLIC tree to your own BSP and modify it there. Never modify any code in the PUBLIC tree; Microsoft might update it using a QFE!

The easiest way to create the 1 ms interrupt is to double the interrupt speed and toggle the behavior. The behavior is coded in the main Interrupt Service Routine, which will be discussed below.

To double the speed of the timer interrupts, load the timer with TIMER_COUNT / 2, like this:

   //
   // 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;

Now, timer0 interrupts will occur every 500 microseconds (0.5 ms).

I've added the #ifdefs around the modified code to make it slightly easier to go back to the original CE code.

Modifying the ISR

The main ISR is located inside 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    }

All hardware interrupts are mapped to and handled in this ISR. Line 018 gets the current interrupt number. Line 020 and 088 handle the timer 0 interrupt and the RTC (Real Time Clock) interrupt respectively. If the interrupt is some other interrupt, line 096 does a quick validation, calls any chained ISRs (see function NKCallIntChain in the MSDN), translates the interrupt number into a SYSINTR_ value, disables the interrupt and finally returns the SYSINTR_ value in ulRet. If the Irq to SYSINTR_ mapping could not be found, ulRet is filled with SYSINTR_NOP. Any registered IST (Interrupt Service Thread) event is set according to the SYSINTR_ return value of the ISR. An IST is registered by calling InterruptInitialize:

InterruptInitialize(SYSINTR_SOFT1MS, hEvent, NULL, 0);

In the above function, the event hEvent is mapped to the ISR return value SYSINTR_SOFT1MS.

Finally the ISR let's the programmable interrupt controller know the interrupt is handled by writing the EOI (End Of Interrupt) value (0x20) to it. If the interrupt number is bigger then 7, the second PIC has to be notified first (the two PIC controllers are cascaded through interrupt line 2).

Since we adjusted the timer frequency, we also have to adjust the above ISR, because now, the ISR is called twice as often as normal, and thus the scheduler is also working double times (scheduled times are divided by 2 per thread).

First of all, we have to declare a static Boolean, to be able to toggle the ISR behavior when a timer0 interrupt occurs:

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

We have to toggle the behavior for the timer0 interrupt only:

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

The behavior when a timer0 interrupt occurs now toggles between 'running normal CE ISR code' and 'returning SYSINTR_SOFT1MS'. We can now use InterruptInitialize with the SYSINTR_SOFT1MS value to bind an event to the timer0 interrupt. This event will then be pulsed every 1 ms.

Modifying 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

You are free to use any SYSINTR_FIRMWARE based value (like (SYSINTR_FIRMWARE+20), as long as you modify the OEMInterruptEnable function as described below.

Modifying the OEMInterruptEnable function

We also have to change the OEMInterruptEnable function inside cfwpc.c to make sure this function always succeeds for our timer0 interrupt. If we don't do this, the InterruptInitialize function will fail for the SYSINTR_SOFT1MS interrupt. Add the following lines to the function:

#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

Building the platform

Because we changed some kernel code, we have to do a complete build of the kernel, including a rebuild of the dependency tree. First save all changed files, then choose Options in the Tools menu of the Platform Builder, and click on the Build tab. Now make sure Enable Deptree Build is selected. You can now start rebuilding the entire platform by clicking Rebuild Platform from the Build menu. When everything is done, deselect Enable Deptree Build from the Build tab of the Tools->Options menu.

About the Author

Michel Verhagen has been a Windows CE. NET consultant for PTS Software bv since 2000, specializing in building complex Windows CE platforms and device drivers for industrial appliances for customers in the Netherlands. As such he is one of the few Dutch developers specializing in Windows CE.NET and the only eMVP in the Netherlands. In the past he has been involved in evaluating Windows CE 3.0 as far as real-time behavior is concerned. Recently Michel has evaluated the real-time behavior of the .NET Compact Framework, using a mix of managed- and unmanaged code in combination with Windows CE.NET 4.1. The whitepaper about this subject has recently been awarded by Microsoft with the Technical Excellence Award 2003. When you need Michel's expertise, you can always count on him in one of the Microsoft embedded newsgroups. When Michel is not responding in real-time, he can most likely be found at cloudbase under his paraglider.

Additional Resources:

www.dotnetfordevices.nl

Feedback:

To provide feedback about this whitepaper, please send e-mail to michel.verhagen@pts.nl

For Additional Information

For more information about Windows CE .NET, see the Windows CE .NET home page.

For online documentation and context-sensitive Help included with Windows CE .NET, see Windows CE .NET product documentation.

Acronyms and Terms

µs   microsecond (1 / 1000000 second)

API   Application Programming Interface

CPU   Central Processing Unit

EOI   End Of Interrupt

ISA   Industry Standard Architecture

ISR   Interrupt Service Routine

IST   Interrupt Service Thread

ms   millisecond (1 / 1000 second)

OAK   OEM Adaptation Kit

OAL   OEM Abstraction Layer

OS   Operating System

PC   Personal Computer

PCI   Peripheral Component Interconnect

PIC   Programmable Interrupt Controller

PIT   Programmable Interval Timer

RTC   Real Time Clock

Show:
© 2014 Microsoft