Implementing Rock-Solid Windows CE Timers On Windows CE .NET 4.1 Platforms
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:
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