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.
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).
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.
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.
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:
One other thing: We should initialize our clock variables. We do this with the following instructions:
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:
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.
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:
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:
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:
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.
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.
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.
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:
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!
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.
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.