Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 7 of 7

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

  1. #1
    Junior Member
    Join Date
    Nov 2015
    Posts
    11

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

    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 by TMcGahee; 09-30-2016 at 08:53 PM.

  2. #2
    Senior Member manitou's Avatar
    Join Date
    Jan 2013
    Posts
    957
    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...mperature-Data

  3. #3
    Junior Member
    Join Date
    Nov 2015
    Posts
    11
    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...l=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!

  4. #4
    Junior Member
    Join Date
    Feb 2017
    Posts
    2
    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.

  5. #5
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    14,483
    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.

  6. #6
    Junior Member
    Join Date
    Feb 2017
    Posts
    2
    Quote Originally Posted by obelix66200 View Post
    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 by obelix66200; 03-02-2017 at 03:08 PM. Reason: forgot to add modifications in the wake.h file

  7. #7
    Senior Member duff's Avatar
    Join Date
    Jan 2013
    Location
    Las Vegas
    Posts
    811
    Quote Originally Posted by obelix66200 View Post
    Duff's SnoozeAlarm.cpp setAlarm function is replaced with:
    This is great! I want to include it in the Snooze library?

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •