-
Notifications
You must be signed in to change notification settings - Fork 5
Implementing a high resolution Teensy clock
- Extending the cycle counter to 64bit
- The periodic timer of the real time clock
- Implementing the Teensy clock
- Usage examples
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:
- The 32bit cycle counter of a T4.x @600MHz quickly rolls over (2^32 / 600MHz = 7.6s).
- 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. - To be able to track absolute times a synchronization to the Teensy RTC would useful.
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 IntervalTimer
s to call it periodically, but, wasting one of the 4 timers
would be too expensive.
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
}
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
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
...
Teensy is a PJRC trademark. Notes here are for reference and will typically refer to the ARM variants unless noted.