-
Notifications
You must be signed in to change notification settings - Fork 3
Handling Time
Synchronizing and keeping track of time is critical for real-time mechatronics hardware control and data acquisition. MEL provides four classes to assist you in this process:
#include <MEL/Core/Time.hpp>
#include <MEL/Core/Frequency.hpp>
#include <MEL/Core/Clock.hpp>
#include <MEL/Core/Timer.hpp>
The Time
class represents a time point or duration. Times
are constructed using the free functions seconds()
, milliseconds()
, and microseconds()
. They can be used with basic arithmetic and logical operations:
Time t1 = seconds(10);
Time t2 = milliseconds(10);
Time t3 = t1 + t2;
if (t1 > t2)
print(t3); // 10.01 s
The advantage to using a Time
over something like double t3 = 10.01
is that the units are automatically kept track of. If you do need to convert Times
to numerical types, the class provides conversions for you:
double t1_s = t1.as_seconds();
int32 t2_ms = t2.as_milliseconds();
int64 t3_us = t3.as_microseconds();
Frequency
represents inverse Time
. Like Time
, it is created with free functions hertz()
, kilohertz()
, and megahertz()
. It can be convert to and from a Time
:
Frequency f1 = hertz(1000);
Frequency f2 = t2.to_frequency();
print(f1,f2);
// 1000 Hz 100 Hz
A Clock
is an object that measures elapsed time. It begins counting upward from Time::Zero
until the member function restart()
is called, at which point it restarts counting upward from Time::Zero
:
Time time1, time2, time3;
Clock clock;
sleep(seconds(1));
time1 = clock.get_elapsed_time();
sleep(seconds(1));
time2 = clock.restart();
sleep(seconds(1));
time3 = clock.get_elapsed_time();
print(time1, time2, time3);
// 1.00016 s 2.00095 s 1.00079 s
A Timer
is a waitable object. It provides a member function wait()
which will wait until the the time since construction or the last call to wait()
is greater than or equal to the Timer
period.
Timer timer1(seconds(1));
time1 = timer1.wait();
time2 = timer1.wait();
time3 = timer1.wait();
print(time1, time2, time3);
// 1 s 2 s 3 s
You may be wondering why there are no trailing digits in the print out. This is because each wait was exactly one second to machine precision. By default, Timer
uses a "busy wait" routine to do its waiting and thus provides a much higher accuracy than sleep()
. While sleep()
tells the OS to allow other processes on the current CPU thread to work, Timer
hogs the entire CPU thread. This is the price we pay to achieve a somewhat deterministic state on non-real-time OS's like Windows. Fortunately, with most modern computers providing 8 or more threads, this isn't really a big deal. If you find Timer to be too CPU intensive, you can construct them with a "hybrid" wait mode, which waits 90% of the remaining time by sleeping the thread, and the last 10% with a "busy wait":
Timer hybrid_timer(milliseconds(10), Timer::WaitMode::Hybrid);
As long as your OS can sleep for relatively short periods, this is actually a decent solution. Unfortunately, Windows can't accurately sleep for much less than 1 millisecond, so in most situations you will use the default WaitMode::Busy
. NI's Linux Real-Time on the other hand is more successful with hybrid Timers
.
Timer
's primary utility is in creating accurately timed loops since achieving a steady loop rate is critical to control and signal processing. Here's an example which creates a 1000 Hz control loop lasting for one minute:
Time t = Time::Zero;
Timer timer(hertz(1000));
while (t < seconds(60)) {
// code which executes in less than one millisecond
t = timer.wait();
}
Assuming your code takes less than a millisecond to finish, each loop iteration should last exactly one millisecond. For example, if your code took 250 microseconds to execute, then Timer
would wait the remaining 750 microseconds, providing you with a consistent 1000 Hz loop rate. If instead your code took 1100 microseconds, the Timer
would not wait at all, and would count the iteration as a missed deadline.
Each Timer
keeps track of how many deadlines have been missed, and will start to log Warnings when its miss rate exceeds 1%. You can lower or increase this value with the member function set_acceptable_miss_rate()
. At anytime, you can also query the Timer
performance using get_misses()
, get_miss_rate()
, and get_wait_ratio()
.
On Windows, you should not have any trouble achieving 1000 Hz time loops. Beyond this up to 2000 Hz is doable, but you may start to see some instability. NI Linux Real-Time should be more performant and reliable overall.
Note: MEL is NOT a hard real-time control software. MEL's timing primitives would be considered soft or firm real-time, but are more than adequate for most mechatronics and research based needs. While MEL does target hard real-time systems using NI Linux Real-Time, these devices only guarantee hard real-time performance on the FPGA. MEL by virtue runs on the CPU, which, despite running real-time Linux, is NOT deterministic.