-
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 represent time. It provides the namespace std::chrono
which defines an abstract concept for durations, time-points and clocks.
Basically, the type std::chrono::duration<rep, period>
combines a value of any arithmetic type (e.g. int64_t, float...) which represents time ticks, with 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
To simplify data entry, 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
To access the stored value, i,e. the time ticks, durations provide the count() member function.
Serial.println(t3.count()); // prints 42010
Of course, to interpret the result we need to know that t3 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 the cycle counter. The constant F_CPU
holds the CPU clock frequency so that one tick of the counter corresponds to 1 / F_CPU seconds. Durations expect this information in the form of a compile time ratio constant std::ratio<1 ,F_CPU>
. Interestingly the ratio information is completely 'baked' into the type and 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 we first need to talk about clocks.
Simply spoken, a clock as defined in std::chrono
codes 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. Opposed to 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 function now()
which returns the current time. The system_clock
additionally provide static functions to 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&)
Thus, std::chrono
defines time points as a combination of a chrono::duration
and a clock type. It also defines reasonable mathematical operations between
time points and durations.
Here a few examples:
using timePoint = system_clock::time_point; // save 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 since
the compiler can't possibly 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 the 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 clock, you need to add the corresponding offset in the now()
function. We will see later how to use the RTC for this.
For now 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), I got 1603443711
from the web page.
Adding 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
To be continued....
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.