SoftRTC

Soft RTC - 4 Methods For A Precision PIC Time Clock

It's common knowledge that PIC's have timers. Timers' incremental counting is synchronized with the internal PIC clock, which is the usual default operating state of timers at the Power On Reset (POR). Further, the internal clock speed is a division by 4 of the external oscillator frequency (Fosc), sometimes shown as "Fosc/4" in the datasheet (Note that many PIC's also have an internal RC oscillator which can be used in lieu of the external oscillator, but for the sake of precision and accuracy, we are only going to recommend an external crystal oscillator for these examples). Implementing PIC timers as a precision high-resolution real-time clock source and taking advantage of the microcontroller's other useful features without problems can be a challenge. This article and following code examples intend to demonstrate 4 methods for creating a precision time base that can run concurrent with managing other tasks, such as outputing data to an LCD for a 24-hour real time clock.

PIC timers have several features which are useful for different applications, and for the sake of brevity this article is only going to consider timers which are synchronized with the internal PIC clock. PIC timers increment, that is, they count up, and how far they increment or count is determined by their size. An 8-bit timer can count from 0 to 255 for a total of 256 counts. A timer that is 16-bits (2 bytes) can count from 0 to 65535 for a total of 65536 counts. Why 256 counts for 0 to 255? Because the timer rolls over (overflows) to 0, one count past 255, and continues incrementing. The same applies to the 16-bit timer. A count of 256 or 65536 results in a timer value of 0 because of this rollover. The values of 256 and 65536 will become important later when discussing loading values into the timers.

Timer overflow is the foundation of a precision time clock because of a potential "side-effect" when it occurs. Overflow sets a bit in a special register related to the timer called the Timer Interrupt Flag. This flag, or bit, can be cleared through software, but it will always set again when the timer count overflows to 0. Although this flag is called the Interrupt Flag, and always sets on overflow, no interrupt events can occur unless the Timer Interrupt Enable bit is set, or enabled. When enabled, the PIC is forced to jump to the Interrupt Service Routine (ISR) at the overflow moment. So making sure that the timer's Interrupt Enable bit is set, and Interrupt Flag bit is cleared, will ensure that real time clock registers are updated at the same exact interval determined by the timer's count.

Since timers increment, preloading a timer requires deducting the desired counts from the overflow point, or 0. For example, a PIC which has a 4MHz external oscillator, has a 1MHz internal clock (Fosc/4) which is equivalent to a 1 microsecond timer clock. Thus, the timer will increment once every microsecond. Using an 8-bit timer, the value of 250, or 250us, makes a nice time base to work with. If the value of 250 is loaded into the timer, 6us later the timer would overflow since the count started at 250 and progressed to 256, or 0. To load the timer correctly, deduct 250 from the rollover point. As 0 minus 256 will create a negative value, use 256 instead, 256 - 250 = 6. 6 is the correct timer load value for a 250us interrupt cycle. The same rule follows for a 16-bit timer, except use 65536 as the rollover value.

Those are some timer basics. Timers also have prescaler and postscaler features which will not be considered. If implemented these features can introduce timing errors and affect the precision of the time base. Timer prescalers and postscalers must be set to 1:1 and synchronized to count with each PIC machine cycle.

Easy. Not yet. There is more to consider. Recall that when a timer has overflowed its count is at 0 when it triggers a jump to the ISR, and then continues to count. There are a number of machine cycles required just for the interrupt vectoring, and then more for the saving of important registers (context saving) so it's possible to resume exactly where the code left off when it was interrupted. It's not unusal for a dozen or more machine cycles, and timer counts, to pass before any attempt is made to manage and reload the timer with the desired time base. Reloading and restarting the timer in itself can require a few more cycles. If a programmer has access to the assembly code at the beginning of the ISR he can make an accurate assessment of how many cycles have passed and compensate by deducting those used cycles from his timer reload amount. When programming in a high level language, such as Basic, the programmer may prefer not to be bothered with cycle counting and would need another method.

ISR timer compensation can be managed in a more clever way than cycle counting. Simply add the present value in the timer to the desired time base reload value. Since timers increment, adding to its value pushes it closer to the overflow point and is essentially deducting time from the timer interrupt cycle. Adding the timer's present count to the reload value when inside the ISR automatcally results in timer compensation for the cycles the timer continued to run after the interrupt. A point to note, there is further compensation of a cycle or two which must be considered with the timer reload. This is commented in the code examples.

The 4 coding methods below show how ISR compensation is not a problem using SF Basic for creating a precision interrupt-driven real time clock using various timers and features found in PIC's. The first example uses a timer stop method, and the other three let the timers run freely. The second example, in paticular, is notable because it also implements the Zero-Sum/Zero Cumulative Error method of reloading the timer as demonstrated by Roman Black on his webpage, http://www.romanblack.com/one_sec.htm. The last 2 examples use interrupt features when a timer count matches the value in another register, namely, Timer2 when it matches the PR2 register, and when Timer1 and the CCPR1 register match. This compare interrupt feature does not require reloading the timers since the timers are automatically reset and will continue to interrupt on the same time interval as long as the respective Interrupt Flag continues to be cleared.

Each of the examples demonstrate a 24-Hour Time Clock using a character LCD.

Warren Schroeder May-2007


TIMER STOP METHOD USING TIMER1

{
****************************************************************
*  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 

FREE-RUN-TIMER USING ZERO-SUM METHOD & TIMER0

{
****************************************************************
*  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

FREE-RUN-TIMER USING TIMER2 & PR2 MATCH/RESET METHOD

{
****************************************************************
*  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

FREE-RUN-TIMER USING TIMER1 SPECIAL EVENT TRIGGER & CCP1 MATCH/RESET METHOD

{
****************************************************************
*  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