RTC Registers and Millisecond Precision from Teensy 4.0?

Status
Not open for further replies.

awbmiln

New member
Hello!:D

I have been working on a project recently and I am in need of millisecond precise time stamps.
I have a GPS providing time codes, but they are so infrequent that I would like to rely on something more regular.

The internal RTC for the Teensy 4.0 seems to be the solution.

I am looking at how to get millisecond precise reading by reading the raw data registers for the RTC and interpreting it manually. I was wondering how possible this approach is.

I have done some digging and found a couple things:
A Thread on Millisecond precision for the Teensy 3
This post with comments about handling register update issues
The RTC get and set functions in the Teensy 4 core
And various other less-useful information...

My questions are....
  1. What exactly do the SNVS_HPRTCMR and SNVS_HPRTCLR registers store? (Source code below1)
  2. How are the registers updated (So any potential issues can be handled)
  3. Is there a better way to do this that I dont know of (carry-over with power loss, poor GPS signal, no network, Millisecond precision)

Overall, it seems like the registers store the RTC crystals cycles, where the 16th bit should be the actual second (32,768 cycles per second).
This doesnt really line up with my tests2:
Code:
// RTC register reading taken 1000 ms apart using delay(1000);
// |─────────  SNVS_HPRTCMR ─────────| |────────── SNVS_HPRTCLR ─────────|
1: 00000000 00000000 00000000 00000000 01110001 10010110 00011001 00001001 
2: 00000000 00000000 00000000 00000000 01110010 00010110 00011010 00001001 
3: 00000000 00000000 00000000 00000000 01110011 10010110 00011010 00001001 
4: 00000000 00000000 00000000 00000000 01110100 00010110 00011011 00001001 
5: 00000000 00000000 00000000 00000000 01110110 10010110 00011011 00001001 
6: 00000000 00000000 00000000 00000000 01110111 00010110 00011100 00001001 
7: 00000000 00000000 00000000 00000000 01111000 10010110 00011100 00001001 
8: 00000000 00000000 00000000 00000000 01111001 00010110 00011101 00001001 
9: 00000000 00000000 00000000 00000000 01111011 10010110 00011101 00001001 
10:00000000 00000000 00000000 00000000 01111100 00010110 00011110 00001001
11:00000000 00000000 00000000 00000000 01111101 10010110 00011110 00001001

It seems that the 24th bit (from the right) is very consistent in cycling every second, The 25th bit seems to count the seconds, but skips a second irregulary. (See lines 4 & 5)

So, That leaves me curious about how it works and what extra precision I can squeeze.

THANKS FOR YOUR INPUT IN ADVANCE!!

───────────────────────────────────────────────────────────────────
1: Source code for SNVS_HPRTCMR and SNVS_HPRTCLR values in Teensy 4 core:
Code:
#include "imxrt.h"
#include "debug/printf.h"


unsigned long rtc_get(void)
{
	uint32_t hi1 = SNVS_HPRTCMR;
	uint32_t lo1 = SNVS_HPRTCLR;
	while (1) {
		uint32_t hi2 = SNVS_HPRTCMR;
		uint32_t lo2 = SNVS_HPRTCLR;
		if (lo1 == lo2 && hi1 == hi2) {
			return (hi2 << 17) | (lo2 >> 15);
		}
		hi1 = hi2;
		lo1 = lo2;
	}
}

void rtc_set(unsigned long t)
{
	// stop the RTC
	SNVS_HPCR &= ~(SNVS_HPCR_RTC_EN | SNVS_HPCR_HP_TS);
	while (SNVS_HPCR & SNVS_HPCR_RTC_EN); // wait
	// stop the SRTC
	SNVS_LPCR &= ~SNVS_LPCR_SRTC_ENV;
	while (SNVS_LPCR & SNVS_LPCR_SRTC_ENV); // wait
	// set the SRTC
	SNVS_LPSRTCLR = t << 15;
	SNVS_LPSRTCMR = t >> 17;
	// start the SRTC
	SNVS_LPCR |= SNVS_LPCR_SRTC_ENV;
	while (!(SNVS_LPCR & SNVS_LPCR_SRTC_ENV)); // wait
	// start the RTC and sync it to the SRTC
	SNVS_HPCR |= SNVS_HPCR_RTC_EN | SNVS_HPCR_HP_TS;
}

void rtc_compensate(int adjust)
{
}

2: Source code used to read RTC registers:
Code:
#include "Arduino.h"
//#include "TimeLib.h"

void BinaryStrZeroPad(int Number,char ZeroPadding);



void setup(){
    Serial.begin(9600);

    rtc_set(0);
}

void loop(){
   delay (1000);

   for (int i = 3; i > -1; i--){
       BinaryStrZeroPad((SNVS_HPRTCMR >> (8*i)) & 0xFF, 7);
       Serial .print(" ");
   }
   for (int i = 0; i < 4; i++){
       BinaryStrZeroPad((SNVS_HPRTCLR >> (8*i)) & 0xFF, 7);
       Serial .print(" ");
   }
   Serial.println();
}

/** Binary Value Padding Function
 * Credit: https://forum.arduino.cc/index.php?topic=475435.0
 * 
 * Pads output with 0's
 */
void BinaryStrZeroPad(int Number,char ZeroPadding){
//ZeroPadding = nth bit, e.g for a 16 bit number nth bit = 15
signed char i=ZeroPadding;

	while(i>=0){
	    if((Number & (1<<i)) > 0) Serial.write('1');
	    else Serial.write('0');
	    --i;
	}
}
 
Last edited:
i think the SNVS_HPRTCMR and SNVS_HPRTCLR registers create a 64-bit counter of the RTC crystal ticks (32768 ticks/second). Reading the pair is not "atomic" so as you noted rtc_get() in hardware/teensy/avr/cores/teensy4/rtc.c reads the pair over and over to account for rollover. The low-order 15 bits of SNVS_HPRTCLR are subseconds (1/32768). A simple float example to get subseconds from RTC would be
Code:
void setup() {
 while(!Serial);

}

void loop() {
  static float prev = 0;
  float s;

  s=SNVS_HPRTCLR/32768.;
  Serial.println(s-prev,3);
  prev = s;
  delay(1234);
}

or you could calculate an integer approximation as described in https://forum.pjrc.com/threads/38304-millisecond-precision-from-Teensy-3*-RTC
or extend that to return a 64-bit millisecond result.

EDIT: Your test sketch is badly flawed. Not only are you not sync'ing the two RTC regs as rtc_get() does, but you are reading each RTC register 4 times to extract bytes, BUT the RTC register might change during those 4 reads :( If you want a warm fuzzy feeling that the RTC is properly ticking you can print millis() every odd second of the RTC with
Code:
void setup() {}

void loop() {
  if (SNVS_HPRTCLR & 0x8000) {
    Serial.println(millis());
    while (SNVS_HPRTCLR & 0x8000) ;
  }
}
 
Last edited:
Ahhhhhhh....

I now see the Doofus move I made that caused me to be so confused about the registers (it seems you have looked through it @manitou).
I have accidentally output the SNVS_HPRTCLR backwards.

In regards to your (@manitou) point about multiple register reads and not error checking,
THIS IS A REALLY GREAT POINT!
Fortunately, this is simply code to show me how the registers look in the memory, so I can write proper code for it.
I have updated the code to avoid multiple reading, but left out the error checking.

Here is the corrected data output format:
Code:
// RTC register reading taken 1000 ms apart using delay(1000);
// |─────────  SNVS_HPRTCMR ─────────|  |────────── SNVS_HPRTCLR ─────────|
1: 00000000 00000000 00000000 00000000  00001001 00011001 10010110 01110001
2: 00000000 00000000 00000000 00000000  00001001 00011010 00010110 01110010
3: 00000000 00000000 00000000 00000000  00001001 00011010 10010110 01110011
4: 00000000 00000000 00000000 00000000  00001001 00011011 00010110 01110100
5: 00000000 00000000 00000000 00000000  00001001 00011011 10010110 01110110
6: 00000000 00000000 00000000 00000000  00001001 00011100 00010110 01110111
7: 00000000 00000000 00000000 00000000  00001001 00011100 10010110 01111000
8: 00000000 00000000 00000000 00000000  00001001 00011101 00010110 01111001
9: 00000000 00000000 00000000 00000000  00001001 00011101 10010110 01111011
10:00000000 00000000 00000000 00000000  00001001 00011110 00010110 01111100
11:00000000 00000000 00000000 00000000  00001001 00011110 10010110 01111101

And the Correlated Corrected source code:
Code:
#include "Arduino.h"
//#include "TimeLib.h"

void BinaryStrZeroPad(int Number,char ZeroPadding);



void setup(){
    Serial.begin(9600);

    rtc_set(0);
}

void loop(){
   delay (1000);
   uint32_t CMR = SNVS_HPRTCMR;
   uint32_t CLR = SNVS_HPRTCLR;
   for (int i = 3; i > -1; i--){
       BinaryStrZeroPad((CMR >> (8*i)) & 0xFF, 7);
       Serial .print(" ");
   }
   for (int i = 3; i > -1; i--){
       BinaryStrZeroPad((CLR >> (8*i)) & 0xFF, 7);
       Serial .print(" ");
   }
   Serial.println();
}

/** Binary Value Padding Function
 * Credit: https://forum.arduino.cc/index.php?topic=475435.0
 * 
 * Pads output with 0's
 */
void BinaryStrZeroPad(int Number,char ZeroPadding){
//ZeroPadding = nth bit, e.g for a 16 bit number nth bit = 15
signed char i=ZeroPadding;

	while(i>=0){
	    if((Number & (1<<i)) > 0) Serial.write('1');
	    else Serial.write('0');
	    --i;
	}
}
Noteable changes:
- Changing the for loop parameters to count down from 3 instead of up to 3 on the SNVS_HPRTCLR print loop.
- Read the Registers only once instead of every loop (Thanks @manitou)

───────────────────────────────────────────────────────────────

Thanks for the help.

Do you also have any experience with the registers referenced in the set_rtc() function?
I am also looking to set the RTC registers to millisecond precision (hopefully safely and to values other than 0).

set_rtc():
Code:
void rtc_set(unsigned long t)
{
	// stop the RTC
	SNVS_HPCR &= ~(SNVS_HPCR_RTC_EN | SNVS_HPCR_HP_TS);
	while (SNVS_HPCR & SNVS_HPCR_RTC_EN); // wait
	// stop the SRTC
	SNVS_LPCR &= ~SNVS_LPCR_SRTC_ENV;
	while (SNVS_LPCR & SNVS_LPCR_SRTC_ENV); // wait
	// set the SRTC
	SNVS_LPSRTCLR = t << 15;
	SNVS_LPSRTCMR = t >> 17;
	// start the SRTC
	SNVS_LPCR |= SNVS_LPCR_SRTC_ENV;
	while (!(SNVS_LPCR & SNVS_LPCR_SRTC_ENV)); // wait
	// start the RTC and sync it to the SRTC
	SNVS_HPCR |= SNVS_HPCR_RTC_EN | SNVS_HPCR_HP_TS;
}

Namely:
- SNVS_HPCR
- SNVS_HPCR_RTC_EN
- SNVS_HPCR_HP_TS
- SNVS_LPCR
- SNVS_LPCR_SRTC_ENV

Based off my understanding from just reading the code:
It seems like the time could be set to any desired value by modifying the
Code:
SNVS_LPSRTCLR = t << 15;
SNVS_LPSRTCMR = t >> 17;
To contain the Millsecond precise RTC count value.

Is there something im missing?

Thanks again!
 
yep, just add in your sub-second value (ms for milliseconds) SNVS_LPSRTCLR = t << 15 + 32768*ms/1000;
i've never tried it though. I don't know what you are using for setting time, but note the RTC crystals could have a frequency error of 10 ppm.

some anecdotal frequency-error numbers
Code:
  crystal drift (ppm)
         24MHz   32KHz
T4beta1   -3.2    -4.7   1052
T4beta1-2 -4.1    -4.3
T4beta2     1     -6     1062
T4beta2-2 -11     10     1062
T4-1      -11     -3     1062
T4-2      -10     -5
T4.0-1    -23     -0.5
T4.0-2    -26     -2.5
T4.1       -7     -2
T4.1-1    -11     -5.5
T4.1-2    -12     -3
T4.1-3    -10      1
EVK       -14    -54   1052
EVKB       -6    -52
NXP1062    -9     9
 
Last edited:
> I have a GPS providing time codes, but they are so infrequent that I would like to rely on something more regular.

I'm curious why you wouldn't take a micros() reading whenever you get a GPS value, and then calculate future absolute times from more micros() readings. No rtc and no low level code.
 
> I'm curious why you wouldn't take a micros() reading whenever you get a GPS value

In my code, there is currenty a large amount of calculation and communication happening. All of which is very speed sensitive, and the fast the better.
As part of that, I want to offload time handling as much as possible onto another system. I would also like to avoid interrupts and heavy time value processing.

Sub-millisecond accuracy/precision is not of high concern due to the large amount of cross-system communication over closed networks and Serial connections.

Also, As this project is a team effort, it should make the code a bit more readable.

I will have to look into the issues with crystal drift and frequency adjustment for temperatures, mentioned by @manitou
 
Status
Not open for further replies.
Back
Top