Implementing a high resolution Teensy clock - TeensyUser/doc GitHub Wiki

Back to Fun with modern c++


As shown here it is easy to set up the chrono::system_clock for using any convenient time base. Instead of the millis() based example we could as well use the cycle counter as underlying time base. Here we already discussed how to use it to define a custom duration type which we could use for the high resolution clock. However, we need to take the following into account:

  1. The 32bit cycle counter of a T4.x @600MHz quickly rolls over (2^32 / 600MHz = 7.6s).
  2. The chrono::system_clock class uses nanosecond based uint64_t time ticks. the cycle counter runs at 1/F_BUS = 1.667ns (T4.x, 600MHz). Mapping this to nanoseconds is possible of course but would introduce a significant rounding error for small times.
  3. To be able to track absolute times a synchronization to the Teensy RTC would useful.

Back | Fun with modern c++

Extending the cycle counter to 64bit

To achieve useful rollover times we first need to extend the 32bit cycle counter to 64bit. This will give us a roll over time of 2^64 / 600MHz = 975 years which seems to be sufficient. Extending is not difficult:

uint32_t oldLow = ARM_DWT_CYCCNT;
uint32_t curHigh = 0;

uint64_t getCnt()
{
    uint32_t curLow = ARM_DWT_CYCCNT;
    if (curLow < oldLow) // we had a roll over
    {
        curHigh++;
    }
    oldLow = curLow;
    uint64_t curVal = ((uint64_t)curHigh << 32) | curLow;

    return curVal;
}

The code above checks if the current value of the cycle counter is smaller than the last one we have seen. This is only possible if the counter rolled over in between and we have to increment its high word by 1 before we return the combined 64bit value.

Obviously, this pattern only works if we can ensure that getCnt() is called at least once per ARM_DWT_CYCCNT overflow period (7.6s @600MHz). We could use one of the IntervalTimers to call it periodically, but, wasting one of the 4 timers would be too expensive.

Back | Fun with modern c++

The periodic timer of the real time clock

Fortunately there is a seldom/never used periodic timer in the real time clock module of the T4.x processors. This timer is perfectly suited to call getCnt() say once per second. Since the RTC registers are already set up by Teensyduino, enabling its periodic interrupt is straight forward:

// disable periodic interrupt
SNVS_HPCR &= ~SNVS_HPCR_PI_EN;
while ((SNVS_HPCR & SNVS_HPCR_PI_EN)){}   // spin until PI_EN is reset...

// set interrupt frequency to 1Hz
SNVS_HPCR = SNVS_HPCR_PI_FREQ(0b1111);

// enable periodic interrupt
SNVS_HPCR |= SNVS_HPCR_PI_EN;
while (!(SNVS_HPCR & SNVS_HPCR_PI_EN)){}  // spin until PI_EN is set...

// attach a callback
attachInterruptVector(IRQ_SNVS_IRQ, SNVS_isr);
NVIC_SET_PRIORITY(IRQ_SNVS_IRQ, 255);     // lowest priority
NVIC_ENABLE_IRQ(IRQ_SNVS_IRQ);

And here the simple callback we attached to the interrupt:

void SNVS_isr(void)
{
   SNVS_HPSR |= 0b11; // reset interrupt flag
   getCnt()           // dummy call to check for overflow of the cycle counter
   asm("dsb");        // wait until flag is synced over the busses to prevent double calls of the isr
}

Back | Fun with modern c++

Implementing the Teensy clock

We now have all building blocks to implement our own teensy_clock. Opposed to the RTC or the C-API functions which are based on seconds, this clock will measure time in increments of 1.667ns (T4@60MHz) since 0:00h 1970-01-01. It can be synced to the built in real time.

Here its interface.

struct teensy_clock
{
    // required typdefs:
    using duration = std::chrono::duration<uint64_t, std::ratio<1, F_CPU>>;  // use a uint64_t representation with a time step of 1/F_CPU (=1.667ns @600MHz)
    using rep = duration::rep;                                               // uint64_t
    using period = duration::period;                                         // std::ratio<1,600>
    using time_point = std::chrono::time_point<teensy_clock, duration>;

    static constexpr bool is_steady = false;                                 // can not be guaranteed to be steady (could be readjusted by syncToRTC)

    static time_point now()
    {
        duration t = duration(t0 + cycles64::get());                         // adds the current 64bit cycle counter to an offset set by syncToRTC() (default: t0=0)
        return time_point(t);                                                // ... and returns the correspoinging time point.
    }

    static void begin(bool sync = true);                                     // starts the 64bit cycle counter update interrupt. Sync=true sycns the clock to the RTC
    static void syncToRTC();                                                 // Sync to RTC whenever needed (e.g. after adjusting the RTC)

    //Map to C API
    static std::time_t to_time_t(const time_point& t);                       // returns the time_t value (seconds since 1.1.1970) to be used with standard C-API functions
    static time_point from_time_t(std::time_t t);                            // converts a time_t value to a time_point

 private:
    static uint64_t t0;                                                      // offset to adjust time (seconds from 1.1.1970 to now).
};

To be compliant with the clock's defined in std::chrono we need to define a few types, provide the is_steady flag and the static function time_point now(). Additionally we need a begin() function to start our RTC periodic timer and provide means to sync to the RTC and convert to and from the C-API.

The now() function simply returns the sum of the 64bit cycle counter and an offset value t0 which will be set by syncToRTC();

Here the implementation of syncToRTC()

void teensy_clock::syncToRTC()
{
    t0 = ((uint64_t)rtc_get()) * F_CPU - cycles64::get();
}

It uses rtc_get() to read out the current value of the built in RTC in seconds, converts it to the 1/F_CPU ticks the clock needs and sets the offset t0 to reflect the new time.

Please note that the PJRC Teensy uploader sets the current PC time during uploading. Thus the clock will run with the correct time after uploading. In case you have no battery attached, this time will be reset to 0:00h 1990-01-01 after power cycling the teensy.

The complete code for the teensy_clock can be found here: https://github.com/luni64/TeensyHelpers

Back | Fun with modern c++

Usage examples

The following example demonstrates the resolution of the clock by using it to time a random delay. Besides this it converts the current time to a time_t value and pretty prints the result. Please note that you don't need a battery attached to the Teensy to try this example since the Teensy loader will update the built in RTC at every firmware upload.

#include "teensy_clock.h"
using namespace std::chrono;

typedef teensy_clock::time_point timePoint;               // just for the sake of less typing
typedef duration<float, std::micro> micros_f;             // float based microseconds (predefined type 'microseconds' is integer based)

void setup()
{
    teensy_clock::begin();                                // t_0 the clock and sync to rtc (works with and without battery)
}

void loop()
{
    // demonstrate clock resolution --------------------------------------------------
    unsigned delay_us = random(10, 5000);
    Serial.printf("delayMicroseconds(%u)\n", delay_us);

    timePoint t_0 = teensy_clock::now();                  // get two timepoints 'delay_us' apart
    delayMicroseconds(delay_us);
    timePoint t_1 = teensy_clock::now();

    auto dt = duration_cast<micros_f>(t_1 - t_0);         // cast delta to microseconds (float)
    Serial.printf("dt = t_1-t_0: %7.2f µs\n", dt.count());

    // convert to C-API ---------------------------------------------------------------
    timePoint currentTime = teensy_clock::now();          // get current time
    time_t ct = teensy_clock::to_time_t(currentTime);     // convert C-API time_t
    Serial.printf("Current Time: %s\n", ctime(&ct));      // pretty print date/time

    delay(1000);
}

Which prints out:

delayMicroseconds(3099)
dt = t_1-t_0: 3099.08 µs
Current Time: Fri Oct 23 17:02:34 2020

delayMicroseconds(4945)
dt = t_1-t_0: 4945.08 µs
Current Time: Fri Oct 23 17:02:35 2020

delayMicroseconds(4714)
dt = t_1-t_0: 4714.08 µs
Current Time: Fri Oct 23 17:02:36 2020

delayMicroseconds(4384)
dt = t_1-t_0: 4384.08 µs
Current Time: Fri Oct 23 17:02:37 2020

delayMicroseconds(806)
dt = t_1-t_0:  806.08 µs
Current Time: Fri Oct 23 17:02:38 2020
...

Back | Fun with modern c++

⚠️ **GitHub.com Fallback** ⚠️