Skip to content

Durations Timepoints and Clocks

luni64 edited this page Jun 28, 2023 · 7 revisions

Back to Fun with modern c++


Concepts

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.

Durations

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

Back | Fun with modern c++

Define custom durations

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

Back | Fun with modern c++

Using the cycle counter

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
...

Back | Fun with modern c++

Time points and clocks

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
}

Back | Fun with modern c++

A simple Teensy clock

Please note: On embedded systems, the chrono::xxx_clocks 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

Back | Fun with modern c++

Clone this wiki locally