Example Code

High-Precision Application Timing with NI LabWindows™/CVI Real-Time

Products and Environment

This section reflects the products and operating system used to create the example.

To download NI software, including the products shown below, visit ni.com/downloads.

    Software

  • LabWindows/CVI

Code and Documents

Third-Party Code Repository

Some users manage their code in repositories outside of ni.com. Use your best judgment when following links to third-party sites. http://www.ni.com/lwcvi/

Description

Overview


LabWindows/CVI software provides various timing engines that you can use to peform periodically recurring tasks at fixed intervals. For example, in a process control application, you may want to monitor one or more aspects of your system at precise intervals so you can detect changes and react to them in time. In a data acquisition application, you may want to retrieve data from your data acquisition hardware at reliable intervals to reduce the risk of losing data due to buffer overflows. Timed loops are a simple design pattern for solving these and similar problems in real-time applications. This tutorial discusses the different APIs that the LabWindows/CVI Real-Time Module offers for implementing timed loops.

Timed Loops Using Asynchronous Timers

Asynchronous timers provide the simplest way to implement timed loops in LabWindows/CVI. Each timer runs in a separate thread, interrupting the main program to perform its time-sensitive task when necessary. As with multithreaded programs, you have to protect variables that your main program and your asynchronous timers share.

An asynchronous timer is characterized by the timer interval, your timer event function, the thread priority with which it executes your event function, and, optionally, how often it executes your event function. You can also temporarily suspend and resume timers depending on your application needs:

 int NewAsyncTimer (double Interval, int Count,
   int Initial_State, void *Event_Function, void *Callback_Data);

NewAsyncTimer creates asynchronous timers that run in threads with priority THREAD_PRIORITY_HIGHEST. On real-time systems, you specify a different priority by using NewAsyncTimerWithPriority:

 int NewAsyncTimerWithPriority (double Interval, int Count,
   int Initial_State, void *Event_Function, void *Callback_Data, int Priority);

Interval specifies the number of seconds between timer events. Most Windows targets support intervals in the millisecond range, while most real-time targets support intervals in the microsecond range. Call GetAsyncTimerResolution to obtain the maximum clock resolution of your Windows or real-time target.

Count specifies how many times the timer calls your Event_Function. Pass -1 if you want to run your timer indefinitely.

Normally, you want your timers to start running immediately, but you can also delay the start of your timers by passing FALSE as the Initial_State. Use SetAsyncTimerAttribute to suspend and resume individual timers, or use SuspendAsyncTimerCallbacks and ResumeAsyncTimerCallbacks to suspend and resume all timers in your application.

Event_Function specifies the timer event function that the timer calls every Interval. You can pass application-specific data to your event function in the Callback_Data parameter.

Priority specifies the thread priority at which the timer runs. By default, asynchronous timers run in threads with priority THREAD_PRIORITY_HIGHEST. Highest priority is an unfortunate naming choice because there is an even higher thread priority, THREAD_PRIORITY_TIME_CRITICAL, that may be more desirable for your timers.

Timer Event Functions

Event_Function receives the ID of the timer that generated the timer event, the type of the timer event, any application-specific data, and the absolute and relative times at which the event function is called:

int CVICALLBACK Event_Function (int reserved, int timerId,
       int event, void *callbackData, int eventData1, int eventData2);

You have to handle two types of events in your timer callback: EVENT_TIMER_TICK when the timer fires and EVENT_DISCARD when the timer is discarded.

EventData1 is a pointer to a double indicating the absolute time since the timer started. It is zero on the first call to your callback. EventData2 is a pointer to a double indicating the time that has elapsed since the previous call to your callback.

If your event function takes longer to execute than the timer interval, the asynchronous timer calls your event function as fast as possible with EVENT_TIMER_TICK events until it has caught up with its original schedule. Compare the value pointed to by eventData2 against the timer interval to find out if the current EVENT_TIMER_TICK is on schedule or catching up.

Asynchronous Timer Example (RT)

 

#include <windows.h>
#include <cvirte.h>
#include <rtutil.h>
#include <userint.h>
#include "asynctmr.h"

#define LOOP_RATE  0.001 /* 1 millisecond */

static int CVICALLBACK AsyncTimerCB (int reserved, int timerId,
 int event, void *callbackData, int eventData1, int eventData2)
{
 switch (event) {
  case EVENT_TIMER_TICK:
   /* your code */
   break;
 }
 return 0;
}

void CVIFUNC_C RTmain (void)
{
 int timer;
 
 if (InitCVIRTE (0, 0, 0) == 0)
  return;    /* out of memory */

 /* your initialization code */
 timer = NewAsyncTimerWithPriority (LOOP_RATE, -1, TRUE,
  AsyncTimerCB, NULL, THREAD_PRIORITY_TIME_CRITICAL);

 while (!RTIsShuttingDown ())
 {
  /* your main application code */

  Sleep (100);
 }

 /* your cleanup code */
 DiscardAsyncTimer (timer);
 CloseCVIRTE ();
}

 

Timed Loops Using the Real-Time Microsecond Sleep Function 

The microsecond sleep functions (supported on real-time targets) offer more control over the timing of your loops than asynchronous timers. You can run multiple loops with different phase offsets, and you can decide how the loop behaves when it falls behind. Naturally, this flexibility makes the implementation more complex.

To simplify the discussion, implement your timed loops directly in the main program. In a real application, you create separate threads for your timed loops so that you can run several timed loops and other tasks at the same time. Refer to the "Multithreading in LabWindows/CVI" white paper for more information on how to create and manage threads in LabWindows/CVI.

A Simple Timed Loop

The simplest timed loop executes the loop body at a fixed millisecond interval. The timed loop does not attempt to detect if your code takes longer than the loop interval. It ignores missed iterations and it does not attempt to catch up.

 

#include <windows.h>
#include <cvirte.h>
#include <rtutil.h>
#include <utility.h>

#define LOOP_RATE  1000 /* 1 millisecond */

void CVIFUNC_C RTmain (void)
{
 if (InitCVIRTE (0, 0, 0) == 0)
  return;    /* out of memory */

 /* your initialization code */
 SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);

 while (!RTIsShuttingDown ())
 {
  /* sleep until the next iteration */
  SleepUntilNextMultipleUS (LOOP_RATE);
  
  /* your code -- we must not fall behind! */
 }

 /* your cleanup code */
 CloseCVIRTE ();
}

 

A Simple Timed Loop with Phase Offset

Suppose you have two timed loops running at 1 ms intervals. If you implement the loops as outlined in the previous section, both timed loops wait for the same multiple of 1 ms. They wake up at the same time and compete for the processor at the same time. Sometimes one loop wins and runs first, and sometimes the other loop runs
first.

Rather than running your timed loops all at the same time, you want to stagger them to avoid contention and jitter in your application.

In this case, your timed loops run at the same time because they sleep on the same internal clock in the microsecond timing engine. Because the internal clock does not allow you to specify a phase offset, you have to implement your own separate clocks on top of the internal clock if you want to run LabWindows/CVI timed loops at different phase offsets.

 

#include <windows.h>
#include <cvirte.h>
#include <rtutil.h>
#include <userint.h>

#define LOOP_RATE  1000 /* 1 millisecond */
#define PHASE_OFFSET  400 /* 400 microseconds */

void CVIFUNC_C RTmain (void)
{
 unsigned int iterationStart;
 
 if (InitCVIRTE (0, 0, 0) == 0)
  return;    /* out of memory */

 /* your initialization code */
 SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);

 /* synchronize on the next multiple of LOOP_RATE */
 SleepUntilNextMultipleUS (LOOP_RATE);
 
 /* delay by PHASE_OFFSET */
 iterationStart = SleepUS (PHASE_OFFSET);
 /* iterationStart is the time at the start of the current iteration */
 
 while (!RTIsShuttingDown ())
 {
  /* your code -- we must not fall behind! */
  
  /* sleep until next iteration */
  iterationStart += LOOP_RATE;
  SleepUntilUS (iterationStart);
 }

 /* your cleanup code */
 CloseCVIRTE ();
}

In the above example, you synchronize the clocks of the LabWindows/CVI timed loops on the loop rate because you need a common reference point for implementing your phase offsets.

Next, you sleep for the phase offset. The current time when you wake up becomes the new reference clock for your timed loop. Inside your timed loop, you increment the reference clock by the loop rate to find the start of the next loop iteration.

Note: You may have noticed that your reference clock is only 32 bits wide, limiting you to loop intervals of less than 71 minutes.

Even though the internal microsecond timer is 64 bits wide, the microsecond sleep functions expect and return only 32 bit values:

  • SleepUS returns the lower 32 bits of the microsecond timer, and
  • SleepUntilUS compares only the lower 32 bits of the microsecond timer to the input parameter.

For these reasons, you do not have to worry about overflow in your reference clock.

This implementation requires all timed loops to complete their work within their loop intervals. The timed loops may not fall behind.

When Timed Loops Fall Behind

If you cannot guarantee that your timed loop completes its work within the loop interval, you have to decide what to do when it falls behind. You can try to catch up on the missed iterations until you are back on schedule, or you can ignore the missed iterations entirely.

Catching Up on Missed Iterations 

You have to modify the timekeeping in your application. In the previous implementation, you could afford to ignore when the clock for your timed loop overflowed. The lower 32 bits of the microsecond timer are not sufficient if you want to be able to catch up on missed iterations. You have to keep track of time more precisely by changing iterationStart to a 64-bit integer.

 

#include <windows.h>
#include <cvirte.h>
#include <rtutil.h>
#include <userint.h>

#define LOOP_RATE  1000 /* 1 millisecond */
#define PHASE_OFFSET  400 /* 400 microseconds */
#define EPSILON   10 /* adjustment for conditional */

void CVIFUNC_C RTmain (void)
{
 unsigned long long iterationStart;
 
 if (InitCVIRTE (0, 0, 0) == 0)
  return;    /* out of memory */

 /* your initialization code */
 SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);

 /* synchronize on the next multiple of LOOP_RATE */
 SleepUntilNextMultipleUS (LOOP_RATE);
 
 /* delay by PHASE_OFFSET */
 SleepUS (PHASE_OFFSET);
 /* iterationStart is the time at the start of the current iteration */
 iterationStart = GetTimeUS ();
 
 while (!RTIsShuttingDown ())
 {
  /* your code */
  
  /* sleep until next iteration */
  iterationStart += LOOP_RATE;
  if (iterationStart+EPSILON > GetTimeUS ())
   SleepUntilUS (iterationStart);
 }

 /* your cleanup code */
 CloseCVIRTE ();
}

The code at the end of your timed loop has changed subtly. IterationStart specifies the start of the next iteration. If the start of the next iteration still lies in the future, you wait until the start of the next iteration.

If the start of the next iteration lies in the past, that is, your iterationStart reference clock has fallen behind the real-time clock in your system, then you immediately execute the next loop iteration in the hope of catching up to your original loop schedule.

Note the introduction of the adjustment EPSILON. Suppose the current time on your system is very close to the start of the next loop iteration. The start of the next iteration may still be in the future when you compare it against the system time. But the comparison takes a small amount of time itself. The adjustment EPSILON accounts generously for the time it takes to perform the comparison.

If you do not account for the comparison, you risk that when you get to SleepUntilUS, the system clock has advanced just enough to put the start of the next iteration into the past.

You do not have to worry about overflow in iterationStart. The 64 bits can represent more than 500,000 years in microseconds.

Skipping Missed Loop Iterations

Depending on your application, you may decide to ignore the missed loop iterations and continue your timed loop as if it had never fallen behind.

As in the previous case, you have to keep time more precisely because you need to know how many loop iterations you've missed. You "catch up" on these missed iterations by updating the reference clock for your timed loop without executing the body of the timed loop.

Note that while updating the clock in a tight loop, you could miss yet another iteration because the update operation takes some time itself. You also need to introduce an adjustment for the actual comparison as in the previous section.

 

#include <windows.h>
#include <cvirte.h>
#include <rtutil.h>
#include <userint.h>

#define LOOP_RATE  1000 /* 1 millisecond */
#define PHASE_OFFSET  400 /* 400 microseconds */
#define EPSILON   10 /* adjustment for conditional */

void CVIFUNC_C RTmain (void)
{
 unsigned long long iterationStart;
 
 if (InitCVIRTE (0, 0, 0) == 0)
  return;    /* out of memory */

 /* your initialization code */
 SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL);

 /* synchronize on the next multiple of LOOP_RATE */
 SleepUntilNextMultipleUS (LOOP_RATE);
 
 /* delay by PHASE_OFFSET */
 SleepUS (PHASE_OFFSET);
 /* iterationStart is the time at the start of the current iteration */
 iterationStart = GetTimeUS ();
 
 while (!RTIsShuttingDown ())
 {
  /* your code */
  
  /* sleep until next iteration */
  do {
   iterationStart += LOOP_RATE;
  } while (iterationStart < GetTimeUS ()+EPSILON);
  SleepUntilUS (iterationStart);
 }

 /* your cleanup code */
 CloseCVIRTE ();
}

 

Timed Loops Using External Hardware Sources

You can adapt the techniques outlined in the "Timed Loops Using the Microsecond Sleep Functions" section if you want your timed loops to depend on signals from external hardware sources. Call the timing functions of your hardware API instead of the SleepUS functions.

Accuracy of Timed Loops

It is important to verify that your application actually exhibits the timing behavior you want. In addition to writing a simple testing framework, you can use the Real-Time Execution Trace Toolkit to monitor the timing behavior of your application.

A Simple Testing Framework

A simple testing framework measures when your timed loops run by taking timestamps at the beginning of each iteration. Later, you compare the timestamps against your expectations.

The testing framework should be as lightweight as possible. You don't want the testing framework to change the time behavior of your program. Rather than write the timestamp information directly to disk, record the unprocessed timestamps in memory and save them to disk after the program has finished.

 

#include <windows.h>
#include <ansi_c.h>
#include <rtutils.h>

#define MAX_TICKS 1000

static int next_tick;
static LARGE_INTEGER ticks[MAX_TICKS];

void tick (void)
{
 if (next_tick < MAX_TICKS)
  QueryPerformanceCounter (&ticks[next_tick++]);
}

void write_ticks (char *filename)
{
 FILE  *file = fopen (filename, "w");
 int    i;
 double frequency;
 LARGE_INTEGER freq;
 
 QueryPerformanceFrequency (&freq);
 frequency = (double)freq.QuadPart;
 
 fprintf (file, "%3d: %10.6f\n", 0, 0.0);
 for (i = 1; i < next_tick; ++i)
  fprintf (file, "%3d: %10.6f\n", i,
   (double)(ticks[i].QuadPart - ticks[0].QuadPart) / frequency);
 fclose (file);
}

In addition to testing the regular operation of your timed loops, you want to verify that your application behaves correctly when one of the timed loops falls behind. You can simulate these cases by introducing delays in some iterations.

 

void do_work (void)
{
 static int iteration;
 if (++iteration == 10)
  SleepUS (5000); /* 5 ms */
}

Related Links

Creating Multithreaded Applications with LabWindows/CVI

LabWindows/CVI Page

 

The mark LabWindows is used under a license from Microsoft Corporation. Windows is a registered trademark of Microsoft Corporation in the United States and other countries.

Example code from the Example Code Exchange in the NI Community is licensed with the MIT license.