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