Search 8052.com...


User (Email)

Password

Remember Logon

Forgot password?
Create Account


8052.com Online Store
Back to 8052.com Main Page



























Software-Based Real Time Clock (RTC)

This webpage is part of The 8051/8052 Microcontroller book which was authored by Craig Steiner, the author of this tutorial. If you find this tutorial useful and easy to understand, you may wish to consider obtaining the book which includes many additional chapters not contained in this online tutorial.

This tutorial is copyrighted by the author--do not copy/distribute without permission from the author.



What is a Real Time Clock? (RTC)

A Real-Time-Clock (RTC) is, as the name suggests, a clock which keeps track of time in a "real mode." While there are a number of 8051-compatible microcontrollers that have built-in, accurate real-time clocks (especially from Dallas Semiconductor), some simple applications may benefit from a software RTC solution that uses the built-in capabilitites of an 8051 microcontroller.

This page will go through the development of a simple software-base RTC solution using 8051 Timer 1 (T1). Thus, your software application will have the benefit of an RTC without requiring any additional hardware.

What are the drawbacks of a software-based RTC?

The drawback to this or any other similar software-based RTC is accuracy: This software RTC is based on the 8051 Timer. The 8051 Timer, in turn, is based on the crystal speed used in your application. Thus there are two potential (and real) issues that you need to take into consideration:

  • Your software will require a known crystal speed. If you change the crystal speed connected to your 8051, you will have to modify the software accordingly.

  • The accuracy of our RTC will only be as accurate as the crystal you use.

The variation of an RTC compared to the current time is called "drift" and is often measured in "seconds of drift per month." A specification may indicate that a given hardware RTC is accurate "+/- 10 seconds per month." If you are going to use a software-based RTC, such as this one, be sure your crystal is rated with minimal variation.

Step 1: Our Variables

Before we start developing the code, lets get a few variables established. These variables will be used frequently within interrupts, so it is a good idea to put them in Internal RAM. To make this code as non-instrusive as possible, we'll locate our variables at the end of Internal RAM (07Ch-07Fh).

    HOURS EQU 07Ch MINUTES EQU 07Dh SECONDS EQU 07Eh TICKS EQU 07Fh
Our interrupt will use these four variables to keep track of time. Additionally, your main program may access these variables whenver it wishes to determine the "current time" from the RTC.

Step 2: The Crystal Frequency

The next thing we need to take into account is the speed of the crystal being used. Keep in mind that with a crystal of 11.0592Mhz, Timer 1 will increment 11,059,200/12=921,600 times per second.

    NOTE: The standard 8051 Timer increments every 12 crystal cycles. However, some derivative chips increment their timers after a different number of crystal cycles: For example, Dallas microcontrollers can be programmed to increment every 4 cycles. If you are using a derivative that uses some value other than 12, you will have to make the appropriate changes to this code.
Let's establish some more equates to make our code more portable:
    CRYSTAL EQU 11059200 ;The crystal speed TMRCYCLE EQU 12 ;The number of crystal cycles per timer increment TMR_SEC EQU CRYSTAL/TMRCYCLE ;The # of timer increments per second
Thus, should our crystal frequency change or should we move our code to a derivative microcontroller that uses some other value than 12, we simply need to modify our constants.

Step 3:Calculating the Timer 1 Overflow Frequency

Remember, a 16-bit timer will count from 0 to 65,535 before resetting. This is important when you consider that Timer 1 will be incremented 921,600 times per second. Obviously it will overflow it's 65,535 maximum value a number of times in the course of one second-to be exact, it will overflow 921600/65536=14 times per second. If we were to use the timer in 8-bit or auto-reload mode, the timer would end up overflowing 3599 times per second, which is a lot harder to keep track of.

So we will have timer 1 running in 16-bit mode. However, we have a problem: Timer 1 will actually overflow 921600/65536= 14.0625 times per second. Obviously it's not possible for it to overflow .0625 times. This means we can't simply count the number of overflows from it counting from 0 to 65536. We'll be introducing even more inaccuracy. In the case of an 11.0592Mhz crystal, this inaccuracy will be about 0.44%, but if we were to use this same program with a 12.000Mhz crystal, the inaccuracy would be 1.70% which is much worse. Other crystal frequencies could result in even less accuracy. Generally, the slower the crystal, the more pronounced the error will be-and the error can easily become significant.

That being the case, we're going to need to have timer 1 overflow at some frequency that adds up nicely to 1 second intervals. For example, 65536 timer 1 cycles is 65536/921600 = .071 seconds. In other words, for timer 1 to start counting at 0, count up to 65,535, and overflow back to 0 will take .071 seconds. The problem is that 1.00 seconds divided by .071 seconds does not produce an integer result, thus we have inaccuracy. Our goal is to have timer 1 overflow at a frequency that can be multiplied by an integer to arrive at 1.00 seconds.

For example, if instead of overflowing every .071 seconds, timer 1 were to overflow every .05 seconds, we would know that after 20 overflows exactly one second had passed. How long is .05 seconds in terms of timer cycles? Simple: 921600 * .05 = 46080. In other words, after timer 1 has been incremented 46080 times, 1/20th of a second (.05 seconds) have passed.

So the trick is to have our timer overflow every .05 seconds instead of every .071 seconds. Remember that the timer overflows when it reaches 65,535 and is incremented to 0. We calculated above that we want the timer to overflow every 46,080 cycles. To do that, we need to have the counter start counting at some value other than 0. In fact, we need to have timer 0 start counting at 65536-46080=19456. In other words, if we initialize timer 1 to 19456, it will then take 46,080 cycles for it to reset to 0. When it resets to 0, we need to once again reset it to 19456.

Again, we want our code to be portable, so first let's define an equate that indicates how many timer cycles will pass in .05 seconds. We already have an equate TMR_SEC which indicates how many timer cycles pass in a second, so to determine how many cycles make up 1/20th of a second is just a matter of multiplying the first value by .05.

    F20TH_OF_SECOND EQU TMR_SEC * .05
Thus, F20TH_OF_SECOND indicates how many cycles our timer will count in 1/20th of a second. However, we need an "initialization value" for our timer. The initialization value, as we discussed above, is actually the number 65536 less the constant we just calculated:
    RESET_VALUE EQU 65536-F20TH_OF_SECOND
Now, armed with these equates, we can really start coding.

Step 4: Starting Timer 1

We will use Timer 1 in 16-bit mode as our basic underlying timer. You could also choose to use Timer 0 by making the necessary changes to the program.

First, we need to initialize timer 1 to the reset value that we calculated in our equates in the last section. We do that with the following instructions:

    MOV TH1,#HIGH RESET_VALUE ;Initialize timer high-byte MOV TL1,#LOW RESET_VALUE ;Initialize timer low-byte
Now that timer 1 has been initialized with a reset value, we need to configure timer 1 for 16-bit mode and get it running:
    MOV TMOD,#10h ;Set timer 1 to 16-bit mode SETB TR1 ;Start timer 1 running
We're set: The timer will now overflow in 46,079 timer cycles. But then what? We need to use the 8051 interrupt facility so that whenever timer 1 overflows, our special RTC clock code will be executed.

One other thing: We should initialize our clock variables. We do this with the following instructions:

    MOV HOURS,#00 ;Initialize to 0 hours MOV MINUTES,#00 ;Initialize to 0 minutes MOV SECONDS,#00 ;Initialize to 0 seconds MOV TICKS,#20 ;Initialize countdown tick counter to 20
This initializes our clock to 0 hours 0 minutes 0 seconds. The tick counter is initialized to 20; more on that later.

Step 5:Configuring the Timer 1 Interrupt

Configuring the Timer 1 interrupt is very easy. We just need to enable interrupt (set the EA bit) and enable timer 1 interrupt (set the ET1) bit. We do that with the following code:

    SETB EA ;Initialize interrupts SETB ET1 ;Initialize Timer 1 interrupt
That done, whenever timer 1 overflows (i.e., is incremented from 65536 to 0), an interrupt will be immediately triggered and the interrupt service routine (ISR) at 001Bh will be executed. So our task is to write the ISR that will be executed each time 1/20th of a second has passed.

Step 6: Writing the Timer 1 Interrupt Service Routine (ISR)

Before we write our code, let's consider what we need to do every 20th of a second:

  • We need to reset timer 1 to the reset value of 19456.
  • We need to increment our variable TICKS.
  • If TICKS is equal to 20, it means a second has passed and we need to increment the SECONDS variable.
  • If SECONDS is equal to 60, it means an entire minute has passed and we need to increment the MINUTES variable.
  • If MINUTES is equal to 60, it means an entire hour has passed and we need to incrmenet the HOURS variable.
  • Exit the interrupt routine.
We'll take it one step at a time.

Step 6.1: Reset Timer 1

The first thing we need to do is reset timer 1 to our reset value. If we don't, timer 1 will take the necessary .05 seconds to overflow the first time, but subsequent overflows will occur every .071 seconds as the timer counts from 0 up to 65,535.

Thus whenever our interrupt is triggered, we need to reset the timer to RESET_VALUE that we calculated earlier. Also remember that we need to make sure our interrupt leaves the main working variables in the same state they were in when the interrupt started, so we start by pusing the registers we will change onto the stack so we can restore them when we finish the interrupt.

Our interrupt service routine starts with:

    ORG 001Bh ;This is where Timer 1 Interrupt Routine starts PUSH ACC ;We'll use the accumulator, so we need to protect it PUSH PSW ;We may modify PSW flags, so we need to protect it CLR TR1 ;Turn off timer 1 as we reset the value MOV TH1,#HIGH RESET_VALUE ;Set the high byte of the reset value MOV TL1,#LOW RESET_VALUE ;Set the low byte of the reset value SETB TR1 ;Restart timer 1 now that it has been initialized

Step 6.2: Countdown TICKS variable

Now that we've reset timer 1, we need to "do what needs to be done." We need to count this interrupt as a "tick." When 20 ticks have passed, we know that a second has passed. If 20 ticks have not yet passed, we need not do anything else: we simply exit the interrupt service routine. We can do this with the following code:

    DJNZ TICKS,EXIT_RTC ;Decrement TICKS, if not yet zero we exit immediately
This will decrement the TICKS countdown-timer and, if it hasn't reached zero yet, will exit. You will recall from above (Step #4) that we initialized the TICKS variable to 20. Thus each time our interrupt is triggered, TICKS will be decremented. If it hasn't reached 20, a second has not yet passed and we simply exit to EXIT_RTC.

Step 6.3: One Second has Passed

Once TICKS is decremented to 0, the DJNZ instruction above will fail and execution will continue with this section of code meaning that a full second has passed.

We must first reset TICKS to 20 so that the countdown is ready for another second to pass, and we must increment the number of seconds. We do that with the following code:

    MOV TICKS,#20 ;Reset the ticks variable INC SECONDS ;Increment the second varaiable

Step 6.4: Have 60 seconds passed?

After we increment SECONDS, we must obviously make sure that seconds has not overflowed. It would not make sense to indicate 85 seconds. Rather, we wish to indicate 1 minute and 25 seconds. Thus we must check to see if the value of SECONDS is equal to 60. If it isn't, that means we have not yet counted 60 seconds and we may simply exit the interrupt routine.

    MOV A,SECONDS ;Move the seconds variable into the accumulator CJNE A,#60,EXIT_RTC ;If we haven't counted 60 seconds, we're done.

Step 6.5: Have 60 minutes passed?

If the above test fails, it means we've counted 60 seconds. Thus we need to reset the SECONDS variable to 0, increment MINUTES, and if 60 MINUTES have passed we need to reset MINUTES to 0 and increment the HOURS variable.

    MOV SECONDS,#0 ;Reset the seconds varaible INC MINUTES ;Increment the number of minutes MOV A,MINUTES ;Move the minutes variable into the accumulator CJNE A,#60,EXIT_RTC ;If we haven't counted 60 minutes, we're done MOV MINUTES,#0 ;Reset the minutes variable INC HOURS ;Increment the hour variable

Step 6.6: Exit the Interrupt Routine

Finally, we need to do the standard housekeeping of any interrupt service routine: we need to restore the values that we protected on the stack in step 6.1. Then we simply finish the interrupt routine with a RETI instruction.

    EXIT_RTC: POP PSW ;Restore the PSW register POP ACC ;Restore the accumulator RETI ;Exit the interrupt routine

Step 7: Puting it All Together

That's really about all there is to it. We've written all the code fragments, so let's put it all together in a single program:

    HOURS EQU 07Ch ;Our HOURS variable MINUTES EQU 07Dh ;Our MINUTES variable SECONDS EQU 07Eh ;Our SECONDS variable TICKS EQU 07Fh ;Our 20th of a second countdown timer CRYSTAL EQU 11059200 ;The crystal speed TMRCYCLE EQU 12 ;The number of crystal cycles per timer increment TMR_SEC EQU CRYSTAL/TMRCYCLE ;The # of timer increments per second F20TH_OF_SECOND EQU TMR_SEC * .05 RESET_VALUE EQU 65536-F20TH_OF_SECOND ORG 0000h ;Start assembly at 0000h LJMP MAIN ;Jump to the main routine ORG 001Bh ;This is where Timer 1 Interrupt Routine starts PUSH ACC ;We'll use the accumulator, so we need to protect it PUSH PSW ;Protect PSW flags CLR TR1 ;Turn off timer 1 as we reset the value MOV TH1,#HIGH RESET_VALUE ;Set the high byte of the reset value MOV TL1,#LOW RESET_VALUE ;Set the low byte of the reset value SETB TR1 ;Restart timer 1 now that it has been initialized DJNZ TICKS,EXIT_RTC ;Decrement TICKS, if not yet zero we exit immediately MOV TICKS,#20 ;Reset the ticks variable INC SECONDS ;Increment the second varaiable MOV A,SECONDS ;Move the seconds variable into the accumulator CJNE A,#60,EXIT_RTC ;If we haven't counted 60 seconds, we're done. MOV SECONDS,#0 ;Reset the seconds varaible INC MINUTES ;Increment the number of minutes MOV A,MINUTES ;Move the minutes variable into the accumulator CJNE A,#60,EXIT_RTC ;If we haven't counted 60 minutes, we're done MOV MINUTES,#0 ;Reset the minutes variable INC HOURS ;Increment the hour variable EXIT_RTC: POP PSW ;Restore the PSW register POP ACC ;Restore the accumulator RETI ;Exit the interrupt routine MAIN: MOV TH1,#HIGH RESET_VALUE ;Initialize timer high-byte MOV TL1,#LOW RESET_VALUE ;Initialize timer low-byte MOV TMOD,#10h ;Set timer 1 to 16-bit mode SETB TR1 ;Start timer 1 running MOV HOURS,#00 ;Initialize to 0 hours MOV MINUTES,#00 ;Initialize to 0 minutes MOV SECONDS,#00 ;Initialize to 0 seconds MOV TICKS,#20 ;Initialize countdown tick counter to 20 SETB EA ;Initialize interrupts SETB ET1 ;Initialize Timer 1 interrupt .... Your main program continues here ...

Step 8: Using The RTC

Once you've included the above code in your program, you may simply add your "main" program to the end. Your program can set the RTC by setting the HOUR, MINUTE, and SECONDS variables, or may obtain the current time by reading them. Other than that, you can pretty much forget about the RTC because it will be running all by itself in the background using timer 1 interrupt.

Some Additional Comments

It's probably a good idea to point out a few shortcomings and observations about the above solution-because if I don't, I'll receive lots of email! First, there is a slight error introduced in the ISR. As you can see in the code, the ISR turns off timer 1 while it resets TH1 and TL1. In all, the timer is turned off for three instructions: It is turned off for the two MOV instructions, and it is turned off until the end of the SETB instruction. On a standard 8051, each MOV instruction requires 2 clock cycles to operate, and the SETB instruction requires 1. Thus the clock effectively loses 5 cycles due to the ISR implementation. If you wish to take this into account, you may simply replace the ISR code with the following. which will take into account these 5 "lost" cycles.

    CLR TR1 MOV TH1,#HIGH (RESET_VALUE-5) MOV TL1,#LOW (RESET_VALUE-5) SETB TR1
Second, this solution is based on interrupts. If you use other interrupts in your program, the timer 1 interrupt may not necessarily execute right away. If another interrupt of the same priority is executing when timer 1 overflows, our RTC interrupt will not execute into the other interrupt has finished. This will introduce inaccuracy. The only way to guarantee that our RTC interrupt will always execute immediately is to give it an interrupt priority of "1" and give all other interrupts a priority of "0". This can be done with the following instruction:
    MOV IP,#8 ;Timer 1 Priority=1, all others = 0
Finally, another disadvantage is the fact that the solution requires dedicated use of timer 1. Your main program isn't allowed to change the value of the timer-doing so will cause the RTC to become completely inaccurate. Your program can read the timer, but it may never change it.

Conclusion

As mentioned at the beginning, a software-based RTC is a simple solution that can be implemented instead of using RTC hardware in your design. This is a reasonable solution if you don't require tremendous accuracy or if you already have hardware in the field that doesn't have RTC hardware, but new requirements include some kind of clock. This is a neat way to avoid recalling or replacing all the hardware.


(C) Copyright 1997 - 2008 by Vault Information Services LLC. All Rights Reserved.
Information provided "as-is" without warranty. Please see details.
Contact us for usage and copy permission.