Compact Framework

Getting a Millisecond-Resolution DateTime under Windows CE

November, 2007
Chris Tacke
OpenNETCF Consulting, LLC

 

Download the Source Code
 Download the PDF

 
Introduction

One seemingly strange behavior that Windows CE devices (including Pocket PC, Smartphone and Windows Mobile devices) exhibit is that when you query the device time, you get back either a zero of some constant but invalid value for the milliseconds field. This is true whether you call GetLocalTime or GetSystemTime in native code or DateTime.Now in managed code. Why is it that devices running an OS that has real-time capabilities can’t provide us as developers with a clock that has simple millisecond resolution? The answer lies not in the OS – which is quite capable of giving that resolution – but in how the time function is implemented by the device OEM.

 

Typically Windows CE devices contain a piece of hardware internally called a real time clock or RTC. The RTC is very accurate, but often has a resolution of only 1 second. When the OS was implemented, most OEMs simply return the value that the RTC holds and therefore when you query the time that’s all you get.

 

So the question remains – how can we as developers get a millisecond-resolution time? If you’re simply looking to get the time short tasks take, then often calling GetTickCount or Environment.TickCount (which is the number of milliseconds since the OS started and tied to a separate oscillator in the processor) is sufficient, but what if we want a true time stamp with a millisecond field? The answer is that we simply have to calculate it. In this white paper we’ll look at code that does that calculation and to make our code reusable, we’ll create a simple class called DateTime2 that will expose a Now property that has millisecond resolution.

 

Calculating the millisecond field

Since we know that the Environment.TickCount returns the number of milliseconds since startup, we know that we have access to data that can become our millisecond field. By using modulo division on it we can get a “current millisecond” piece of data:

 

int tick = Environment.TickCount % 1000;

 

However the problem with this is that there’s only a 1 in 1000 chance that it will be synchronized with the system clock, so it’s more than likely that this millisecond tick will roll over to zero at a time when the system clock is not rolling over. All we have to do then is manually synchronize that rollover. To do that we simply need to find what the value of the above tick is when the clock rolls its second value over. So during initialization of our class, we simply poll the clock repeatedly and quickly until we see the second roll over, then we calculate the offset. Bear in mind that this initialization can take up to 1 second to complete.

 

private static int m_offset = 0;

 

static DateTime2()

{

   int s = DateTime.Now.Second;

   while (true)

   {

     int s2 = DateTime.Now.Second;

 

     // wait for a rollover

     if (s != s2)

     {

       m_offset = Environment.TickCount % 1000;

       break;

     }

   }

}

 

So now that we have our offset, we simply use it to calculate our millisecond field every time the Now property is retrieved.

 

public static DateTime Now

{

   get

   {

     // find where we are based on the os tick

     int tick = Environment.TickCount % 1000;

 

     // calculate our ms shift from our base m_offset

     int ms = (tick >= m_offset) ? (tick - m_offset) : (1000 - (m_offset - tick));

 

     // build a new DateTime with our calculated ms

     // we use a new DateTime because some devices fill ms with a non-zero garbage value

     DateTime now = DateTime.Now;

     return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Month, now.Second, ms);

   }

}

 

Testing our code

So now that we have a function that is logically sound, how do we check that it really works? First, toss out any idea that you might test or use this on the emulator. The emulator emulates hardware, including the RTC and the system tick and is therefore completely unreliable for any type of time testing (I actually tested in with this code and saw major creep of the millisecond field across the second rollover period).

 

To test the function on actual hardware, we use logic that is pretty much the same as our offset initialization code. We simply want to watch the system clock roll over, and when it does, we want to get the millisecond field of our DateTime. It should be at or near zero, and more importantly it should be consistent. So let’s look at what a test function might look like.

 

private void Test()

{

   int seconds = 5;

   int s = DateTime2.Now.Second;

   while (seconds > 0)

   {

     DateTime dt = DateTime2.Now;

     int s2 = dt.Second;

     if (s != s2)

     {

       System.Diagnostics.Debug.WriteLine("Test ms=" + dt.Millisecond);

       s = dt.Second;

       seconds--;

     }

   }

}

 

When I ran this on a Dell Axim x51 I found that the tick repeatedly came out around 998 or so, meaning that my millisecond field would actually roll to zero a couple milliseconds before the actual second field. For many applications this might be fine, but of course we can do better, right? If we can test the inaccuracy, we can always account for it. So let’s add a Calibrate function to our DateTime2 class that will run our test code logic for a period of time, calculate the average offset from zero over that period and then adjust our global offset by that average.

 

public static void Calibrate(int seconds)

{

   int s = DateTime2.Now.Second;

   int sum = 0;

   int remaining = seconds;

   while (remaining > 0)

   {

     DateTime dt = DateTime2.Now;

     int s2 = dt.Second;

     if (s != s2)

     {

       System.Diagnostics.Debug.WriteLine("ms=" + dt.Millisecond);

       remaining--;

       // store the offset from zero

       sum += (dt.Millisecond > 500) ? (dt.Millisecond - 1000) : dt.Millisecond;

       s = dt.Second;

     }

   }

 

   // adjust the offset by the average deviation from zero (round to the integer farthest from zero)

   if (sum < 0)

   {

     m_offset += (int)Math.Floor(sum / (float)seconds);

   }

   else

   {

     m_offset += (int)Math.Ceiling(sum / (float)seconds);

   }

}

 

After I ran Calibrate for 5 seconds on the same device, I found that Test spit out an offset of zero very repeatedly, which is what we want.

 

Conclusion

So we now understand that most CE devices don’t fill the milliseconds field of their system time because it’s not readily available, and we’ve seen that with a little extra code we can derive a reasonably accurate replacement that does. Our new DateTime2 class provides a reasonable utility class that can be used in a lot of applications where you’d like to have a millisecond field.

 

Keep in mind, however, that what we have here is still based on two separate oscillators on the device and those two will drift apart over time. Your offset might be zero today, but if you run the app for a week I would expect you’ll find it to have drifted a few ticks - how much depends on the hardware, its layout and the temperature the device is at. Also keep in mind that managed code is still nondeterministic. Just because you get a millisecond accurate time doesn’t mean that you can reliably do tasks with millisecond accuracy.

Comments

 

ctacke said:

So someone asked a question that is now obvious, but wasn't when I slapped this article together this morning: what happens if the device time is changed?

The answer is simple - you need to reset the offset.  To do this we'd need to add a new call for initialization - so move the code in the constructor to a new public static function called Reset() and then have the constructor call Reset.  You can the call Reset whenever the device time changes.

For real easy use, I would then extend the DateTime2 class by adding a reference to the OpenNETCF.WindowsCE.dll assembly and then add an event handler for TimeChanged and have it automatically call Reset when the even fires.

November 20, 2007 1:53 PM
 

Linz said:

Personally, I question the need to have millisecond accuracy in the DateTime class.  I think the designers did the right thing leaving it out.  Nobody really wants to look at fractions of a second of the current time, unless it's perhaps a stopwatch.

The most common reason for using milliseconds is for timing small intervals for polling, refreshing, games, and the like.  In all these cases the actual time is irrelevant and it's use is computationally expensive.  

Time Intervals should be calculated as a difference in Environment.TickCount and avoid the use of the expensive DateTime class altogether.  This also avoids the problem of the user changing the time.  What happens if you construct a time at some point, the user alters the time, and then you calculate a difference from that time a few seconds later.  It may be minutes, or negative - values sure to upset your application.

March 16, 2008 6:59 PM
 

ahmad.shahwan said:

Me too I avoid using the DateTime class in my applications when it comes to calculating time intervals, mainly because it puts my app accuracy under the mercy a user changing system date/time.

Anyway, using Environment.TickCount is no better solution neither. Unfortunately, the counter returns the ticks elapsed while the system was on, ignoring the time when system was -for example- suspended! It's a big issue by itself, indeed, I was searching for a reliable (some how accurate) way to calculate the intervale between two particular points, but i still can't find any.

April 23, 2008 4:56 AM
 

bugmenot said:

Found a typo in the second code fragment. Must be

public static DateTime Now

{

  get

  {

    // find where we are based on the os tick

    int tick = Environment.TickCount % 1000;

    // calculate our ms shift from our base m_offset

    int ms = (tick >= m_offset) ? (tick - m_offset) : (1000 - (m_offset - tick));

    // build a new DateTime with our calculated ms

    // we use a new DateTime because some devices fill ms with a non-zero garbage value

    DateTime now = DateTime.Now;

    return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, ms);

  }

}

June 17, 2008 11:44 AM