-
Notifications
You must be signed in to change notification settings - Fork 5
Durations Timepoints and Clocks
Since c++ 11 the language provides a standard way to handle clocks, time points
and durations. The related classes and templates are declared in the header
<chrono>
and are part of the namespace std::chrono
.
Basically, the type std::chrono::duration<rep, period>
combines a value of any
arithmetic type (e.g. int64_t, float...) which stores time ticks, and
information about the period of those ticks. This combination makes it possible
to easily convert durations from one representation (e.g. weeks) to another
(e.g. microseconds).
Here a few introductory examples using some canned duration
types from std::chrono
:
using namespace std::chrono;
seconds t0 = seconds(42);
milliseconds t1 = milliseconds(10);
milliseconds t3 = t0 + t1; // = 42'010 ms
In order to make the syntax more easy to read, std::chrono
also provides some
predefined literals like ns, us, ms, s..., which can be used like this:
seconds t0 = 42s;
milliseconds t1 = 10ms
milliseconds t3 = t0 + t1; // = 42'010 ms
To simplify further we can let the compiler find out the correct types
auto t0 = 42ms;
auto t1 = 10us
auto t3 = t0 + t1; // = 42'010 ms
The stored value, i,e. the time ticks, can be accessed by the count() member function.
Serial.println(t3.count()); // prints 42010
Of course, if we want to interpret the numeric result, we need to know that t3 actually is a duration of type 'milliseconds'. However, we can always cast to any duration type we like by using e.g.:
auto t_in_us = duration_cast<microseconds> t3;
auto t_in_s = duration_cast<seconds> t3;
Serial.println(t_in_us.count()); // prints 42'010'000
Serial.println(t_in_s.count()); // prints 42
You can easily define your own duration types. If, for example, you need types handling days and weeks you can simply do typedefs like:
while(!Serial){}
constexpr unsigned secPerDay = 60 * 60 * 24;
using days = duration<uint32_t, std::ratio<secPerDay,1>>;
using weeks = duration<uint32_t, std::ratio<7*secPerDay,1>>;
// usage:
auto twoWeeks = weeks(2);
auto d = duration_cast<days>(twoWeeks); // cast to e.g. days
auto ms = duration_cast<milliseconds>(twoWeeks); // or milliseconds
Serial.printf("2 weeks -> %u days\n", d.count());
Serial.printf("2 weeks -> %u ms\n", ms.count());
// output:
// 2 weeks -> 14 days
// 2 weeks -> 1209600000 ms
Now, lets do something more useful. The processors on the T3.x and T4.x boards
maintain a 32bit counter (ARM_DWT_CYCCNT
), which is incremented at every CPU
clock cycle (e.g. at 600MHz for a T4.x). This counter can be seen as the time
base with the highest resolution available on the processor.
We can easily define a duration type based on this cycle counter. The constant
F_CPU
holds the CPU clock frequency so that we can calculate the period of the
counter by the ratio 1 / F_CPU. Durations expect this information in the form
of the compile time constant std::ratio<1 ,F_CPU>
. Interestingly the ratio
information is completely 'baked into the type'. No memory is needed to store
it.
//...
//define a duration type holding cycle counter ticks
using cycles = duration<uint32_t, std::ratio<1 ,F_CPU>>;
void setup(){
}
void loop()
{
cycles current = cycles(ARM_DWT_CYCCNT);
milliseconds ms = duration_cast<milliseconds>(current);
Serial.printf("ARM_DWT_CYCCNT in seconds %.1fs\n", ms.count() / 1000.0f);
delay(100);
}
Which prints:
...
ARM_DWT_CYCCNT in seconds 6.4s
ARM_DWT_CYCCNT in seconds 6.5s
ARM_DWT_CYCCNT in seconds 6.6s
ARM_DWT_CYCCNT in seconds 6.7s
ARM_DWT_CYCCNT in seconds 6.8s
ARM_DWT_CYCCNT in seconds 6.9s
ARM_DWT_CYCCNT in seconds 7.0s
ARM_DWT_CYCCNT in seconds 7.1s
ARM_DWT_CYCCNT in seconds 0.0s
ARM_DWT_CYCCNT in seconds 0.1s
ARM_DWT_CYCCNT in seconds 0.2s
ARM_DWT_CYCCNT in seconds 0.3s
ARM_DWT_CYCCNT in seconds 0.4s
ARM_DWT_CYCCNT in seconds 0.5s
ARM_DWT_CYCCNT in seconds 0.6s
...
Durations are a useful concept to measure time differences. If, however, we want to talk about absolute time or time points we first need to talk about clocks.
Simply spoken, a clock as defined in std::chrono
models any point in time by
its difference (chrono::duration
) to a known time 'zero'. This time 'zero' is
usually called the epoch of a clock. Currently (gnu++14), std::chrono
predefines the following three clocks:
std::chrono::system_clock
std::chrono::high_resolution_clock
std::chrono::steady_clock
On PC's the chrono::system_clock
reports time in nanoseconds since 0:00h
1970-01-01. Currently, the chrono::high_resolution_clock
is just an alias of
the system clock. Other than the system_clock
the chrono::steady_clock
is
guaranteed to provide monotonically rising time, i.e. it can not be set.
The interface of these clocks is surprisingly simple: All clocks provide the
static member function now()
which returns a time_point
defined by
difference of the current time to the epoch of the clock. The system_clock
additionally provides two static member functions which translate time_points to
and from C-API time_t
values.
time_point from_time_t(time_t)
time_t to_time_t(const time_point&)
As we have seen, std::chrono
defines time points as a combination of a
chrono::duration
and a clock type. It also defines the mathematical
operations necessary to do calculations with time points and durations.
Here a few examples:
using timePoint = system_clock::time_point; // saves typing...
timePoint now = system_clock::now(); // system clock measures time in nanoseconds
timePoint then = now + 10h; // time point + duration = time point
nanoseconds delta = then - now; // time point - time point = duration
//nanoseconds x = then + now; // error, adding time points is not defined
timePoint start = system_clock::now(); // using time_points to loop for 1.5 minutes
while (system_clock::now() < start + 1.5min)
{
// do something
}
Please note: On embedded systems, the chrono::xxx_clock
s don't work out of
the box. Obviously, the compiler can't know which time keeping hardware you
have. However, it is quite simple to set it up. All you need to do is provide an
implementation of its static now()
function.
Here a very basic example:
#include <chrono>
using namespace std::chrono;
system_clock::time_point system_clock::now()
{
duration d(1'000'000 * (uint64_t) millis()); // system clock uses uint64_t to measure time in nanoseconds
return time_point(d);
}
void setup()
{
using timePoint = system_clock::time_point;
timePoint times[3];
while(!Serial){}
Serial.println("Wait for 12s");
times[0] = system_clock::now();
delay(2000);
times[1] = system_clock::now();
delay(10'000);
times[2] = system_clock::now();
for (int i = 0; i < 3; i++)
{
time_t cApiTime = system_clock::to_time_t(times[i]); // convert to time_t
Serial.printf("%u: %s", i, std::ctime(&cApiTime)); // pretty print date/time using C-API function
}
}
void loop(){
}
Which prints:
Wait for 12s
0: Thu Jan 1 00:00:00 1970
1: Thu Jan 1 00:00:02 1970
2: Thu Jan 1 00:00:12 1970
As mentioned above, the C-API assumes the clocks epoch is 0:00h 1970-01-01. If
you want to set the chrono::system_clock
you need to add a corresponding
offset in the now()
function. We will see later how to use the RTC for this.
For the time being, we can simply look up the current time here:
https://www.epochconverter.com/. At the time of writing (Fri Oct 23 09:01:51
2020), the converter reported a time_t value of 1603443711
. We can add this
information to the definition of system_clock::now()
:
system_clock::time_point system_clock::now()
{
time_point t0 = from_time_t(1603443711);
return t0 + duration(1'000'000 * (uint64_t)millis());
}
With this change the sketch now prints:
Wait for 12s
0: Fri Oct 23 09:01:51 2020
1: Fri Oct 23 09:01:53 2020
2: Fri Oct 23 09:02:03 2020
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.