High-Precision Application Timing with NI LabWindows™/CVI Real-Time Publish Date: Mar 20, 2008 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 f 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 the LabWindows/CVI Real-Time Module offers for implementing timed loops. Table of Contents 1. 2. 3. 4. 5. Timed Loops Using Asynchronous Timers Timed Loops Using the Real-Time Microsecond Sleep Function Timed Loops Using External Hardware Sources Accuracy of Timed Loops Related Links 1. 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 ( http://zone.ni.com/devzone/cda/tut/p/id/3663), you have to protect variables that your main program and your asynchronous tim 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 rang while most real-time targets support intervals in the microsecond range. Call GetAsyncTimerResolution ( http://zone.ni.com/reference/en-XX/help/370051K-01/toolslib/cvigetasynctimerresolution/) to obtain the maximum clock resolutio 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 a the Initial_State. Use SetAsyncTimerAttribute to suspend and resume individual timers, or use SuspendAsyncTimerCallbacks a 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 y 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 prior 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_DISCAR 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 callbac 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 1/8 www.ni.com 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 (); } 2. 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 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 "Multithread in LabWindows/CVI" white paper (http://zone.ni.com/devzone/cda/tut/p/id/3663) 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. 2/8 www.ni.com #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, bo 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 cloc on top of the internal clock if you want to run LabWindows/CVI timed loops at different phase offsets. 3/8 www.ni.com #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 value 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 behi 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 f 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 clo 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 o missed iterations. You have to keep track of time more precisely by changing iterationStart to a 64-bit integer. 4/8 www.ni.com #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 th 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 nex 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 t comparison. If you do not account for the comparison, you risk that when you get to SleepUntilUS, the system clock has advanced just enou 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 ne 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 bo 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. 5/8 www.ni.com #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 (); } 3. 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 your hardware API instead of the SleepUS functions. 4. 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 (http://www.ni.com/lwcvi/realtime/) to monitor the timing behavio 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 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. 6/8 www.ni.com #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 o 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 */ } 5. Related Links Sign up for the LabWindows/CVI Developer Newsletter (http://www.ni.com/cvinews) to regularly receive technical LabWindows/CVI white papers. Creating Multithreaded Applications with LabWindows/CVI (http://zone.ni.com/devzone/cda/tut/p/id/3663) LabWindows/CVI Real-Time Module (http://www.ni.com/lwcvi/realtime/ ) LabWindows/CVI Resource Page (http://www.ni.com/lwcvi/) 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. PRODUCT SUPPORT COMPANY Order status and history (http://www.ni.com/status/) Submit a service request ( https://sine.ni.com/srm/app/myServiceRequests) Order by part number ( http://sine.ni.com/apps/utf8/nios.store?action=purchase_form Manuals (http://www.ni.com/manuals/) ) 7/8 About National Instruments ( http://www.ni.com/company/) Events (http://www.ni.com/events/) www.ni.com Drivers (http://www.ni.com/downloads/drivers/) Activate a product ( http://sine.ni.com/myproducts/app/main.xhtml?lang=en Alliance Partners (http://www.ni.com/alliance/) ) Careers (http://www.ni.com/careers/) Order and payment information ( http://www.ni.com/how-to-buy/) MISSION NI equips engineers and scientists with systems that accelerate productivity, innovation, and discovery. (http://twitter.com/niglobal) ( http://www.facebook.com/NationalInstruments) ( http://www.linkedin.com/company/3433?trk=tyah) (http://www.ni.com/rss/) ( http://www.youtube.com/nationalinstruments) Contact Us (http://www.ni.com/contact-us/) (http://privacy.truste.com/privacy-seal/National-Instruments-Corporation/validation?rid=bc6daa8f-7051-4eea-b7b5-fb24dcd96d95) Legal (http://www.ni.com/legal/) | © National Instruments. All rights reserved. | Site map ( http://www.ni.com/help/map.htm) 8/8 www.ni.com