Working: Semi-accurate "RTC" for Teensy LC from internal clock

Status
Not open for further replies.

TMcGahee

Member
Hey all! I decided to get the LC's RTC module "working". Maybe it'll help someone else do something interesting.

Now, to be extremely clear, the reasons Paul and others have decided not to support the LC's RTC are very good - you can't just use an external crystal like with the 3.1/3.2, you actually need a full, power hungry, expensive oscillator. Even if you add that, there is no way to separately power the RTC module (like the VBat connection in 3.1/3.2), so if you lose power, you lose your set time.

With my code, you don't need any external components to be able to sync the RTC and track time, but you will still lose the set time if you lose power, and it's not nearly as accurate as the RTC on the other Teensys.


The way I got this working is to set the RTC module to use the "LPO" clock, which is an internally generated 1 kHz clock, and really not that accurate (way worse than the 20ppm crystal we normally use, anyway). Normally, the RTC is set up so that it increments by one second every 32,768 cycles (which is why we use a 32.768 kHz crystal on the 3.1/3.2). With the LPO, the RTC will run at 1/32.768 times the speed, so a reading of +1 second actually equals +32.768 seconds. This isn't really useful for what I'm doing (though it could be for some), so what I've done is set the prescaler so that the RTC increments by one second every 1000 cycles (so 1 kHz = 1 increment/second). You have to continuously do this, so I used the RTC seconds interrupt to set the prescaler like this every tick, so it might lose a millisecond here and there. If you want, you can certainly write/port compensation functions to try and improve the accuracy.


Overall, it's not pretty, it's not accurate, but it "works".

Here's the library (LC_RTC.h):
Code:
/* Code to create a working, semi-accurate RTC in the Teensy LC
My strategy:
	Make the RTC run off of the 1kHz LPO clock.
	This makes the RTC 1/32.768 * normal speed
	To fix this, we set the RTC prescaler to add 1 second on every 1k clock cycles
		instead of the normal 32768 cycles. Now the RTC is approx. normal speed.
	The prescaler would normally just rollover back to 32768 cycles, so we use
		an interrupt on every RTC second that resets the prescaler.
*/

#ifndef LC_RTC_h
#define LC_RTC_h


#if defined(KINETISL)

unsigned long LC_RTC_get(){
	return RTC_TSR;
}

void LC_RTC_set(unsigned long t){
	RTC_SR = 0;  // status register - disable RTC, only way to write other registers
	RTC_TPR = 31768; // prescaler register, 16bit
	RTC_TSR = t; // inits the seconds
	RTC_SR = RTC_SR_TCE; // status register again - enable RTC
}


void LC_RTC_enable(){
	// Enable write-access for RTC registers
    SIM_SCGC6 |= SIM_SCGC6_RTC;

	// Disable RTC so we can write registers
	RTC_SR = 0;

	// Set the prescaler to overflow in 1k cycles
	RTC_TPR = 31768;

	// Disable the 32kHz oscillator
	RTC_CR = 0;

	// Set the RTC clock source to the 1kHz LPO
	SIM_SOPT1 = SIM_SOPT1 | SIM_SOPT1_OSC32KSEL(3);

	// Enable the interrupt for every RTC second
	RTC_IER |= 0x10;  // set the TSIE bit (Time Seconds Interrupt Enable)
	NVIC_ENABLE_IRQ(IRQ_RTC_SECOND);

	// Enable RTC
	RTC_SR = RTC_SR_TCE;
}

void rtc_seconds_isr(){
	// Disable RTC so we can write registers
	RTC_SR = 0;

	// Set the prescaler to overflow in 1k cycles
	RTC_TPR = 31768;	

	// Enable RTC
	RTC_SR = RTC_SR_TCE;
}

#endif


#endif

Here's some code to see it running:
Code:
#include "LC_RTC.h"

void setup(){
	Serial.begin(115200);
	while(!Serial);

	Serial.println("Enabling RTC");
	LC_RTC_enable();

	Serial.println("Setting t to 100");
	LC_RTC_set(100);

	Serial.print("Value from the RTC: ");
	Serial.println(LC_RTC_get());

	Serial.println("Begin RTC!");
}

unsigned long timer = 0;
void loop(){
	if(LC_RTC_get() != timer){
		timer = LC_RTC_get();
		
		Serial.print("Current time: ");
		Serial.print(timer);
		Serial.println(" seconds.");
	}
}


To use it, you just need to include the library and call LC_RTC_enable(). You can set the time with LC_RTC_set(seconds) (e.g. if you want to sync the clock). I usually just start it at 0.



I'm pretty new to making portable/optimized code, so if anyone has any recommendations/improvements, I'd love to hear them (e.g. if these should be inline/static/private functions, safer ways of banging on hardware registers etc.).
 
Last edited:
Sure enough, your sketch works on LC. As you note the LPO RC oscillator may not be very accurate. The data sheet says frequency could be off by as much as 10% (that's 100,000 ppm). I've measured crystals and RC oscillators on various MCUs (see crystals.txt), for one of my LCs, LPO was off by -18548 ppm. With your sketch, one can compensate for the drift error by changing RTC_TPR value. For my LC, I set TPR in your sketch to
#define LPOTICKS (32768-1000 + 18)
and that reduced the frequency error to 400 ppm.

I'm not sure why you just wouldn't use the Teensy/Arduino Time library which uses the more accurate (<20 ppm) MCU crystal to keep time. Of course, if you wanted to retain power and compensate for temperature frequency variations you might need to invest in a temperatrure-compensated RTC with coin battery.

Reference on crystal temperature vs frequency
https://forum.pjrc.com/threads/24628-Interesting-Temperature-Data
 
Wow, thanks for putting in time to test my work! That's really generous of you! I'm definitely aware of the inaccuracy, and I appreciate the succinct solution you use for compensation. I wonder if we can use the compensation registers to improve the accuracy further, but that's for another time.

I actually developed this as part of a larger solution to be able to time how long the teensy lc was in deepsleep/hibernate modes (working! See https://forum.pjrc.com/threads/32454-All-things-Low-Power?p=117318&viewfull=1#post117318). I also wanted to give a concise explanation of why the lc's RTC isn't supported to intermediate users who still have trouble with datasheets - I couldn't find one on the forums.

Thanks again!
 
This may be great if both TSI and timer should be used as wake up sources in deep sleep/hybernate mode (snooze library). TSI is using LPTMR as a hardware trigger source, so LPTMR cannot be used as a timer (unless scan period is equal to timer period). So it is impossible to have both timer and TSI working independently. If RTC is operational, RTC Alarm interrupt can be used as a timer.
 
I've edited the RTC footnote for Teensy LC on the tech specs page, with a link to this thread.

https://www.pjrc.com/teensy/techspecs.html

Hopefully that will help anyone who *really* wants to try to use the RTC to find your code, and hopefully without PJRC giving people an unrealistic idea of the hardware's actual capability.
 
This may be great if both TSI and timer should be used as wake up sources in deep sleep/hybernate mode (snooze library). TSI is using LPTMR as a hardware trigger source, so LPTMR cannot be used as a timer (unless scan period is equal to timer period). So it is impossible to have both timer and TSI working independently. If RTC is operational, RTC Alarm interrupt can be

I made it working. If both alarm (RTC_TAR) and RTC (RTC_TPR and RTC_TSR) are adjusted every alarm call (alarm.setAlarm) the intervals can be tuned with up to millisecond accuracy. My project (Teency LC) uses touch events to wake up and RTC alarm for periodical wake up for some action (like remeasuring touch reference). Moreover RTC (with proper adjustment after each alarm call) is used to track time.

Duff's SnoozeAlarm.cpp setAlarm function is replaced with:


Code:
void SnoozeAlarm::setAlarm(  uint8_t hours, uint8_t minutes, uint8_t seconds ) {
    isUsed = true;
    alarm = hours*3600 + minutes*60 + seconds;
	RTC_SR = 0; //disable RTC
	RTC_TPR=32768-(alarm*1000%32768);
	RTC_TSR=0; //RTC counter
	RTC_SR = RTC_SR_TCE; //enable RTC
	RTC_TAR = alarm*1000/32768;
	
}
ISR modified to do not increase TAR, just clear flag:
Code:
void SnoozeAlarm::isr( void ) {
    if ( !( SIM_SCGC6 & SIM_SCGC6_RTC ) ) return;
    RTC_TAR = RTC_TAR;  //clear SR 
    if ( mode == VLPW || mode == VLPS ) source = 35;
}

In the wake.h file comment the KINETISK lines to allow Teense LC use alarm code:


Code:
 //   #ifdef KINETISK
        else if ( ( mask->llwuFlag>>16 ) & LLWU_F3_MWUF5 ) mask->wakeupSource = 35;
 //   #endif

and the sketch is like that:

Code:
static SnoozeBlock config_teensyLC(touch,digital,alarm);

uint32_t saved_RTC_TSR, saved_RTC_TPR;

uint8_t alarmValue;

void setup()   { 

  	SIM_SCGC6 |= SIM_SCGC6_RTC;
  	// Disable RTC so we can write registers
  	RTC_SR = 0;
  	// Disable the 32kHz oscillator
  	RTC_CR = 0;
  	// Set the RTC clock source to the 1kHz LPO
  	SIM_SOPT1 = SIM_SOPT1 | SIM_SOPT1_OSC32KSEL(3);
	RTC_SR = RTC_SR_TCE; //enable RTC
	
	//reset RTC
	RTC_SR=0; //disable RTC 
	RTC_TPR=0;
	RTC_TSR=0;
	RTC_SR = RTC_SR_TCE; //enable RTC

      alarmValue=25; //as an example
 // we call setAlarm every time before sleep, and save RTC registers in case we are going to track time:

	saved_RTC_TSR=RTC_TSR;
	saved_RTC_TPR=RTC_TPR;

// digital pins wakeup

   digital.pinMode(21, INPUT_PULLUP, RISING);//pin, mode, type

// TSI wakeup

touch.pinMode(0, touchRead(0) + 250); // pin, threshold

//we use alarmValue variable, which is used to restore time. If time tracking is not needed, just use regular call to alarmSet with e.g. constant

	alarm.alarmSet(0,0,alarmValue);


      //whatever else.....

}

void loop() {
//hibernate example from snooze library:

    int who;
    /********************************************************
     feed the sleep function its wakeup parameters. Then go
     to deepSleep.
     ********************************************************/
    who = Snooze.hibernate( config_teensyLC );// return module that woke processor


//the following needed if we need timestamps, we add extra time (since alarm call to wakeup) to the saved earlier RTC_TSR and RTC_TPR
// this works regardless of the wakeup source, even if not RTC alarm wakeup. The example is for seconds  (alarmValue*1000 is number of milliseconds), it can be easily modified to other units.

		//both RTC_TSR and RTC_TPR are modified by setAlarm, so recalculate to keep time
		uint32_t timeSinceAlarmSet=RTC_TSR*32768+RTC_TPR-(32768-(alarmValue*1000%32768)); //ms
		uint32_t actualTime=saved_RTC_TSR*32768+saved_RTC_TPR+timeSinceAlarmSet;  //ms
		RTC_SR = 0;
		RTC_TPR=actualTime%32768;
		RTC_TSR=actualTime/32768;
		RTC_SR = RTC_SR_TCE;
		

    if (who == 21) { // pin wakeup source is its pin value
        for (int i = 0; i < 1; i++) {
            digitalWrite(LED_BUILTIN, HIGH);
            delay(200);
            digitalWrite(LED_BUILTIN, LOW);
            delay(200);
        
        }

// whatever....

    if (who == 35) { // rtc wakeup value
        for (int i = 0; i < 4; i++) {
            digitalWrite(LED_BUILTIN, HIGH);
            delay(200);
            digitalWrite(LED_BUILTIN, LOW);
            delay(200);
        }
    }

    if (who == 37) { // tsi wakeup value
        for (int i = 0; i < 6; i++) {
            digitalWrite(LED_BUILTIN, HIGH);
            delay(200);
            digitalWrite(LED_BUILTIN, LOW);
            delay(200);
	    };

// we call setAlarm every time before sleep, and save RTC registers in case we are going to track time (see above):

	saved_RTC_TSR=RTC_TSR;
	saved_RTC_TPR=RTC_TPR;

//we use alarmValue variable, which is used in the above restore time. If time tracking is not needed, just use regular call to alarmSet with e.g. constant

	alarm.alarmSet(0,0,alarmValue);


//and at any moment actual time (from program start, where we reset counters) is: timeEllapsed=(RTC_TSR*32768+RTC_TPR)  //in milliseconds; 


    }
 
Last edited:
Bumping this thread to get some more clarification and discussion. My personal goal is an LC based, long term data logger, so power efficiency is the focus at the expense of RTC accuracy (GPS will update RTC every 30 minutes anyway).

So the LC RTC module can source its clock from OSC32KCLK (is this 32.00 or 32.768kHz?) or the 1kHz LPO, or External RTC_CLKIN.

The LPO is great since its always running and readily configured with the above code.

The OSC32KCLK should work all the way down to Low Leakage Stop mode.

And External CLKIN would be great but adds power consumption in the form of TCXO or other.

From power consumption side:
From the datasheet using RTC module is 357 nA in STOP mode. Includes ERCLK32K module (32 kHz external crystal) power consumption.
From the datasheet using LPO isn't specified, maybe this is included with the regular stop mode numbers?
From the datasheet using 32 kHz internal reference clock (IRC) is 52 uA in STOP mode.

SO for RTC consumption sources LPO<TCXO<OSC32KHZ?
 
Another update for anyone reading these.

There seems to be a small glitch with TMcGahee code and maybe the LC chip. The RTC wont tick unless the RTC_TSR value is set to a none zero value. I solved this by setting RTC_TSR to 300 inside the LC_RTC_enable() function.
 
Another update for anyone reading these.

There seems to be a small glitch with TMcGahee code and maybe the LC chip. The RTC wont tick unless the RTC_TSR value is set to a none zero value. I solved this by setting RTC_TSR to 300 inside the LC_RTC_enable() function.

Thanks, that's good to note. I'll check into why that's happening to see if there's another solution.

I can't speak to your questions about power draw, but I'll happily dig into the datasheet and try to verify what you found. My experience with other chips lines up with what you wrote.
 
Status
Not open for further replies.
Back
Top