Soft RTC - 4 Methods For A Precision PIC Time Clock

Coding and general discussion relating to user created compiler modules

Moderators: David Barker, Jerry Messina

Post Reply
xor
Posts: 286
Joined: Sun Nov 05, 2006 1:15 pm
Location: NYC
Contact:

Soft RTC - 4 Methods For A Precision PIC Time Clock

Post by xor » Fri May 25, 2007 4:05 am

I couldn't figure out how to create a new page in the WIKI (a little late right now), so I will post here for the time being.

In some of the forums there are several requests for creating a real time clock using a PIC. With some ensuing discussion and looking around I found a great webpage by Roman Black (http://www.romanblack.com/one_sec.htm) which discussed a theory and method to actually achieve a precision time clock using a PIC timer. There he implements a method of Zero Sum or Zero Cumulative Error to achieve a zero error 1 second time base. It's a great method and for the explanation I will defer to him on that webpage. I further discovered that more techniques can be used to also achieve a very accurate time base. Below are 4 sample programs that demonstrate using each of the methods.

The first 2 coding techniques are implementations of the Zero-Sum method. One uses Timer1 and the other Timer0 with no prescalers. The first stops the timer, adds the load value to the timer's present value, and then restarts the timer. The Stop-Load-Start takes exactly 6 cycles and is thus added to the load value to compensate for the timer's stoppage. The second uses Timer0 and is free-running and never stops.

The last 2 techniques do not use zero-sum but utilize the PR2 match/reset feature of Timer2, and the Special Event Trigger/Interrupt of the CCP1 module when Timer1 matches CCPR1. In both cases, the respective timer is reset and continues to run, and so creates a regular and precise time base.

Each of the following examples outputs a 24 Hour Clock on a character LCD. Enjoy.

TIMER1 - Zero-Sum With Timer Stop

Code: Select all

{
****************************************************************
*  Name    : Soft_RTC_TimerStop.BAS                            *
*  Author  : Warren Schroeder alias "xor"                      *
*  Notice  : Copyright (c) 2007 Warren Schroeder               *
*          : All Rights Reserved                               *
*  Date    : 5/23/2007                                         *
*  Version : 1.0                                               *
*  Notes   : Real Time Clock - Timer Stop Method               *
*            Precision 24H Time Clock Example on LCD           *
*            18F452 @ 8MHz                                     *
*            Uses Timer1 and No Prescaler                      *
*          :                                                   *
****************************************************************
}

Device = 18F452
Clock = 8

#option LCD_RS = PORTB.2
#option LCD_EN = PORTB.3
#option LCD_DATA = PORTB.4

Include "lcd.bas"
Include "convert.bas"

{
   For One Second Update:
   
   8MHz Fosc = 2MHz internal clock = 0.5us per cycle (timer count)
   Use 16-bit Timer1; No Prescaler
   50000 counts = 50000 x 0.5us = 25000us
   40 x 25000us = 1000000us = 1 second
}
   
Const T1Period As Word = 65536-50000+6    ' 25000us interrupt cycle + 6 cycles for TIMER1 stoppage
Const Int_Total As Byte = 40              ' 40 x 25000us = 1 second  
Dim T1 As Word Absolute $0FCE             ' TMR1L + TMR1H
Dim T1IE As PIE1.0
Dim T1IF As PIR1.0
Dim T1ON As T1CON.0
Dim update As Boolean
Dim int_counter As Byte
Dim secs,mins,hrs As Byte

interrupt RTC()
   T1ON = 0                               ' stop timer
   T1 = T1 + T1Period                     ' reload timer + include cycles after jump to ISR
   T1ON = 1                               ' restart timer
   Dec(int_counter)                        
   If int_counter = 0 Then
      int_counter = Int_Total             ' count 40 interrupts @ 25000us = 1 second
      update = true                       ' update LCD clock
   End If
   T1IF = 0                               ' clear int flag
End interrupt

Sub Clock24()
 Dim clk As String
    Inc(secs)
    If secs = 60 Then                     ' check each tally for rollover
       secs = 0
       Inc(mins)
       If mins = 60 Then
          mins = 0
          Inc(hrs)
          If hrs = 24 Then 
             hrs = 0 
          End If
       End If
    End If
    clk = DecToStr(hrs,2)                ' output to LCD
    LCD.WriteAt(2,5,clk)
    clk = DecToStr(mins,2)
    LCD.WriteAt(2,8,clk)
    clk = DecToStr(secs,2)
    LCD.WriteAt(2,11,clk)
    update = false 
End Sub

Sub Initialize()
   ADCON1 = 15
   secs = 0
   mins = 0
   hrs = 0
   int_counter = Int_Total   
   update = false
   LCD.Command(130)
   LCD.Write("24-HOUR CLOCK")
   LCD.Command(196)
   LCD.Write("00:00:00")      
   INTCON = 192                           ' enable GIE & PEIE
   T1CON = 0                              ' no prescaler and Timer1 is Off
   T1 = 65536-50000                       ' load Timer1
   T1IE = 1                               ' enable Timer1 Interrupt
   T1IF = 0                               ' clear Timer1 Interrupt Flag
   T1ON = 1                               ' start Timer1
   Enable(RTC)                            ' enable jump to RTC ISR
End Sub

   Initialize

   While 1=1
      If update = true Then
         Clock24                          ' update 24H Clock output
      End If
   Wend
TIMER0 - Zero-Sum Free Running Timer

Code: Select all

{
****************************************************************
*  Name    : Soft_RTC_FreeRunTimer.BAS                         *
*  Author  : Warren Schroeder alias "xor"                      *
*  Notice  : Copyright (c) 2007 Warren Schroeder               *
*          : All Rights Reserved                               *
*  Date    : 5/23/2007                                         *
*  Version : 1.0                                               *
*  Notes   : Real Time Clock - Free Running Timer              *
*            Precision 24H Time Clock Example on LCD           *
*            18F452 @ 8MHz                                     *
*            Timer0 and No Prescaler                           *
*  Program implements Zero-Sum/Zero Cumulative Error Method    *
*  by Roman Black .. http://www.romanblack.com/one_sec.htm     *:                                                  
****************************************************************
}

Device = 18F452
Clock = 8

#option LCD_RS = PORTB.2
#option LCD_EN = PORTB.3
#option LCD_DATA = PORTB.4

Include "lcd.bas"
Include "convert.bas"

{
   For One Second Update:
   
   8MHz Fosc = 2MHz internal clock = 0.5us per cycle (timer count)
   Use 8-bit Timer0; No Prescaler
   2000000 counts = 1 Second
   Timer0 reload requires 2 cycle compensation = 2000000-2
}
   
Const OneSecond As LongWord = 2000000-2   ' 1 Second total clock cycles - 2 for timer relaod 
Dim Cyc_Counter As LongWord Absolute $30  ' Timer0 cycle counter
Dim T0Load As Byte Absolute $30           ' Timer0 reload value
Dim Countdown As Word Absolute $31        ' # of 8-bit cycles 
Dim update As Boolean
Dim secs,mins,hrs As Byte

interrupt RTC()
   Dec(Countdown)
   If Countdown = 0 Then
      Cyc_Counter = OneSecond             ' reload timer
      TMR0L = TMR0L + T0Load              ' include additional TMR0 counts
      If STATUS.0 = 0 Then
         Inc(Countdown)                   ' if no carry increment "countdown" counter once
      End If
      update = true                       ' flag to update clock output
   End If
   INTCON.2 = 0                           ' clear interrupt flag  
End interrupt

Sub Clock24()
 Dim clk As String
    Inc(secs)
    If secs = 60 Then                     ' check each tally for rollover
       secs = 0
       Inc(mins)
       If mins = 60 Then
          mins = 0
          Inc(hrs)
          If hrs = 24 Then 
             hrs = 0 
          End If
       End If
    End If
    clk = DecToStr(hrs,2)                 ' output to LCD
    LCD.WriteAt(2,5,clk)
    clk = DecToStr(mins,2)
    LCD.WriteAt(2,8,clk)
    clk = DecToStr(secs,2)
    LCD.WriteAt(2,11,clk)
    update = false 
End Sub

Sub Initialize()
   ADCON1 = 15
   secs = 0
   mins = 0
   hrs = 0
   Cyc_Counter = OneSecond
   update = false
   LCD.Command(130)
   LCD.Write("24-HOUR CLOCK")
   LCD.Command(196)
   LCD.Write("00:00:00")      
   INTCON = 192                           ' enable GIE & PEIE
   T0CON = 72                             ' 8-bit; no prescaler; Timer0 off
   TMR0L = 0                              ' clear timer0
   INTCON.5 = 1                           ' interrupt enabled
   INTCON.2 = 0                           ' interrupt flag cleared
   T0CON.7 = 1                            ' start Timer0
   Enable(RTC)                            ' enable jump to RTC ISR
End Sub

   Initialize

   While 1=1
      If update = true Then
         Clock24                          ' update 24H Clock output
      End If
   Wend
TIMER2 - PR2 Match Free Running Timer

Code: Select all

{
****************************************************************
*  Name    : Soft_RTC_PR2_FreeRunTimer.BAS                     *
*  Author  : Warren Schroeder alias "xor"                      *
*  Notice  : Copyright (c) 2007 Warren Schroeder               *
*          : All Rights Reserved                               *
*  Date    : 5/23/2007                                         *
*  Version : 1.0                                               *
*  Notes   : Real Time Clock - PR2 Free Running Timer          *
*            Precision 24H Time Clock Example on LCD           *
*            18F452 @ 8MHz                                     *
*            Timer2, PR2, and No Prescaler                     *
*      Implements PR2 match reset of Free Running Timer2       *
*                                                              *                                                 
****************************************************************
}

Device = 18F452
Clock = 8

#option LCD_RS = PORTB.2
#option LCD_EN = PORTB.3
#option LCD_DATA = PORTB.4

Include "lcd.bas"
Include "convert.bas"

{
   For One Second Update:
   
   8MHz Fosc = 2MHz internal clock = 0.5us per cycle (timer count)
   Use 8-bit Timer2, No Prescaler
   Set PR2 = 250; Timer2 resets on match every 250 counts = 125us
   Each Timer2 reset requires 1 cycle compensation... so set PR2 = 249
   8000 interrupts x 125us each = 1 second
}

Dim Int_Counter As Word               
Dim update As Boolean
Dim secs,mins,hrs As Byte

interrupt RTC()
   Dec(Int_Counter)
   If Int_Counter = 0 Then
      Int_Counter = 8000                  ' each interrupt = 125us x 8000 int's = 1 second
      update = true                       ' update LCD output
   End If
   PIR1.1 = 0                             ' clear interrupt flag  
End interrupt

Sub Clock24()
 Dim clk As String
    Inc(secs)
    If secs = 60 Then                     ' check each tally for rollover
       secs = 0
       Inc(mins)
       If mins = 60 Then
          mins = 0
          Inc(hrs)
          If hrs = 24 Then 
             hrs = 0 
          End If
       End If
    End If
    clk = DecToStr(hrs,2)                ' output to LCD
    LCD.WriteAt(2,5,clk)
    clk = DecToStr(mins,2)
    LCD.WriteAt(2,8,clk)
    clk = DecToStr(secs,2)
    LCD.WriteAt(2,11,clk)
    update = false 
End Sub

Sub Initialize()
   ADCON1 = 15
   secs = 0
   mins = 0
   hrs = 0
   Int_Counter = 8000
   update = false
   LCD.Command(130)
   LCD.Write("24-HOUR CLOCK")
   LCD.Command(196)
   LCD.Write("00:00:00")      
   INTCON = 192                           ' enable GIE & PEIE
   T2CON = 0                              ' no prescaler or postscaler: timer2 OFF
   TMR2 = 0                               ' clear TMR2
   PR2 = 249                              ' set match value
   PIE1.1 = 1                             ' enable interrupt
   PIR1.1 = 0                             ' clear interrupt flag
   T2CON.2 = 1                            ' Timer2 ON
   Enable(RTC)                            ' enable jump to RTC ISR
End Sub

   Initialize

   While 1=1
      If update = true Then
         Clock24                          ' update 24H Clock output
      End If
   Wend
TIMER1 - CCP1 match Free Running Timer

Code: Select all

{
****************************************************************
*  Name    : Soft_RTC_CCP1_FreeRunTimer.BAS                     *
*  Author  : Warren Schroeder alias "xor"                      *
*  Notice  : Copyright (c) 2007 Warren Schroeder               *
*          : All Rights Reserved                               *
*  Date    : 5/23/2007                                         *
*  Version : 1.0                                               *
*  Notes   : Real Time Clock - PR2 Free Running Timer          *
*            Precision 24H Time Clock Example on LCD           *
*            18F452 @ 8MHz                                     *
*            Timer1, CCPR1, and No Prescaler                   *
* Implements Special Event Trigger when Timer1 matches CCPR1   *
*                                                              *                                                 
****************************************************************
}

Device = 18F452
Clock = 8

#option LCD_RS = PORTB.2
#option LCD_EN = PORTB.3
#option LCD_DATA = PORTB.4

Include "lcd.bas"
Include "convert.bas"

{
   For One Second Update:
   
   8MHz Fosc = 2MHz internal clock = 0.5us per cycle (timer count)
   Use 16-bit Timer1, No Prescaler
   Set CCPR1 = 50000; Timer1 resets on match every 50000 counts = 25000us
   Each Timer1 reset requires 1 cycle compensation... so set CCPR1 = 49999
   40 interrupts x 25000us each = 1 second
}

Dim C1 As Word Absolute $0FBE             ' CCPR1L + CCPR1H
Dim Int_Counter As Byte               
Dim update As Boolean
Dim secs,mins,hrs As Byte

interrupt RTC()
   Dec(Int_Counter)
   If Int_Counter = 0 Then            
      Int_Counter = 40                    ' each interrupt = 25000us x 40 int's = 1 second
      update = true                       ' update LCD output
   End If
   PIR1.2 = 0                             ' clear CCP1 interrupt flag  
End interrupt

Sub Clock24()
 Dim clk As String
    Inc(secs)
    If secs = 60 Then                     ' check each tally for rollover
       secs = 0
       Inc(mins)
       If mins = 60 Then
          mins = 0
          Inc(hrs)
          If hrs = 24 Then 
             hrs = 0 
          End If
       End If
    End If
    clk = DecToStr(hrs,2)                 ' output to LCD
    LCD.WriteAt(2,5,clk)
    clk = DecToStr(mins,2)
    LCD.WriteAt(2,8,clk)
    clk = DecToStr(secs,2)
    LCD.WriteAt(2,11,clk)
    update = false 
End Sub

Sub Initialize()
   ADCON1 = 15
   secs = 0
   mins = 0
   hrs = 0
   Int_Counter = 40
   update = false
   LCD.Command(130)
   LCD.Write("24-HOUR CLOCK")
   LCD.Command(196)
   LCD.Write("00:00:00")      
   INTCON = 192                           ' enable GIE & PEIE
   T1CON = 0                              ' no prescaler timer OFF
   TMR1H = 0                              ' clear TMR1
   TMR1L = 0
   CCP1CON = 11                           ' enable special trigger event                    
   C1 = 49999                             ' set match value
   PIE1.2 = 1                             ' enable CCP1 interrupt
   PIR1.2 = 0                             ' clear CCP1 interrupt flag
   T1CON.0 = 1                            ' Timer1 ON
   Enable(RTC)                            ' enable jump to RTC ISR
End Sub

   Initialize

   While 1=1
      If update = true Then
         Clock24                          ' update 24H Clock output
      End If
   Wend

MichaelM
Posts: 57
Joined: Sun May 13, 2007 6:52 pm
Location: Michigan, USA

Post by MichaelM » Sun May 27, 2007 4:00 pm

Warren (xor),

Thank you for the nice examples.

Other factors that effect the accuracy of an RTC include crystal aging, temperature drift, and crystal frequency (tolerance). Of those three, crystal frequency usually proves the biggest headache.

You might consider implementing a soft timer "trim" algorithm to compensate for crystals which may be slightly off frequency. The method I use (untested Swordfish code below) simply adds 1 or -1 to the TMR2 register each 1-msec interrupt for the first 'N' number of 1-msec interrupts during each 1-second period. This method can be used to trim the 1-second period to within 1 instruction cycle (1 Tcy) timing with a theoretical accuracy to within plus or minus 7 seconds per year, not including crystal aging or temperature drift, when using a 16-MHz crystal (Tcy = 250-nsecs).

I normally use 1-msec Timer 2 interrupts with an 8-MHz clock (pre 1:1, post 8:1) or 16-MHz clock (pre 1:1, post 16:1) and PR2 = 250-1 for my RTC code. Please note one caveat of this method is that modifying the TMR2 register will clear the prescaler so it's important to use a 1:1 prescaler value and to implement the "trim" routine early in the ISR.

After several months of correction count adjustments I've found that I'm pretty much stuck with about 1/2 second per month accuracy and it seems further adjustments are simply chasing temperature drift and crystal aging.

I suspect this "trim" method would be much easier to implement on the "Timer 1 Special Event Trigger" algorithm though you might need to make the interrupt intervals smaller to improve "trim" resolution.

Have fun. Kind regards, Mike

Code: Select all

Interrupt TMR2_Interrupt()
  '
  ' Timer "trim" code is used to adjust the 1-second period to within 1 Tcy or 
  ' 1 instruction cycle (250-nsecs when using a 16-MHz clock) with theoretical
  ' accuracy to within plus or minus 7 seconds per year, not including crystal
  ' aging or temperature drift.
  '
    If CorrectionCount <> 0 Then        // adjustments left this 1-sec period?
        TMR2 = TMR2 + CorrectionValue   // add or subtract 1 (1 Tcy) from TMR2
        Dec (CorrectionCount)           // decrement correction counter
    EndIf
  '
  ' Our 1-millisecond "Heartbeat"
  '
    Inc (Milliseconds)                  // increment our millisecond counter
  ' 
  ' Test for 1-second period and perform once-per-second functions
  '
    If Milliseconds = 1000 Then         // if 1-second has elapsed then
        Milliseconds = 0                // reset millisecond counter
      '
      ' Reset Correction counter for next 1-second period
      '
        CorrectionCount = SavedCount    // reset 1-second correction counter

        update = true                   // flag 1-second interval for MAIN

        PIR1.1 = 0                      // clear Timer 2 interrupt flag

    EndIf    
 
Last edited by MichaelM on Sun May 27, 2007 9:56 pm, edited 1 time in total.

xor
Posts: 286
Joined: Sun Nov 05, 2006 1:15 pm
Location: NYC
Contact:

Post by xor » Sun May 27, 2007 6:08 pm

Thanks for the suggestions. I will post a small article and the code in the WIKI later and allow others to include their alterations.

Prescalers should be avoided because of the errors they can introduce into the intervals due to the lost reload/restart cycles of the timers. Compensation for these cycles is accurately done if prescalers are 1:1.

Post Reply