PJRC.COM Offline Archive, February 07, 2004
Visit this page on the live site

skip navigational linksPJRC
Shopping Cart Checkout Shipping Cost Download Website
Home MP3 Player 8051 Tools All Projects PJRC Store Site Map
You are here: 8051 Tools Development Board Example Code Timers Search PJRC

PJRC Store
8051 Dev Board, $79
LCD 20x2 Display, $11
Serial Cable, $5
12 Volt Power, $8
More Components...
8051 Tools
Main Page
Software
PAULMON Monitor
Development Board
Code Library
89C2051 Programmer
Other Resources

Using the 8051's Built-in Timers

The 87C52 chip on the development board includes three built-in timers, two of which can you easily use in your applications (Timer 1 generates the serial port baud rate and usually can't be used). This page will show you how to utilize these timers in your code. It is assumed that you are already familiar with developing code. If not, the LED Blink example is best for new users before attempting to use the timers.

No time to read all this?? Jump directly to the downloadable example code.

Understanding Timer 0

Timer 0 can function in four different modes, each useful for certain types of applications, but the basic operation is the same in all modes. Registers hold a number which is incremented until it reaches a maximum value and then becomes zero. This roll-over back to zero is called timer overflow, and it is the condition that makes the timer useful.

The TF0 bit is automatically set to 1 when Timer 0 overflows. Your code can read the TF0 bit at any time to find out if the timer has overflowed. You can also configure the Timer 0 interrupt to automatically execute code when the timer overflows. When the interrupt code starts, the 8051 automatically clears TF0 back to zero. If you do not use an interrupt, your code must clear TF0 in order to be able to detect the next time Timer 0 overflows.

Timer can be started and stopped with the TR0 bit. When this bit is set to 1, the timer runs, and when its cleared to zero the timer is stopped. This allows you to create one-shot delays easily, or measure elapsed time between two events. Applicatons that need regular intervals usually just set this bit and never stop the timer.

Modes of Operation for Timer 0

Timer 0 can operate in 4 different modes, each of which is useful for certain types of applications. This table briefly summarizes the modes.

Mode Description Typical Application
0 8 Bit + div by 32 Repetitive Timeout, 225 Hz
1 16 Bit Longer Delay or Elapsed Time Measurement
2 8 Bit Auto-Reload Repetitive Timeout, 7200 Hz and faster
3 Two 8 Bit Limited timer and counter (not commonly used)

Mode 0 is the simplest to use. A single 8 bit value is incremented at 57600 Hz, which results in 225 Timer 0 overflows per second (if the value is not modified by your code). Mode 0 is commonly used to execute an interrupt routine at a regular interval.

Mode 1 provides the greatest precision and longest timeout. The timer value is 16 bits and it increments at 1.8432 MHz. Mode 1 is useful for measuring elapsed time between events. It also provides the flexability for generating delays and timeouts with more precision, at the cost of additional complexity of handling 16 bits instead of 8.

Mode 2 provides short repetitive intervals. When the timer overflows, the 8 bit value is automatically reloaded with a value you define, rather than rolling over to zero. Because the value increments at 1.8432 MHz, the timeout interval is adjustable from 0.543 µs to 139 µs. Timer 1 is commonly used in mode 2 to configure the serial port baud rate. Timer 0 is not usually used in mode 2 because the timeout interval is so quick.

Mode 3 turns timer 0 into two 8 bit timers. One is incremented at 1.8432 MHz (no auto-reload), and the other is incremented by external pulses. Mode 3 is not commonly used.

How To Configure Timer 0

Timer 0 is usually configured using these basic steps:

  1. Stop the timer and clear the overflow flag
  2. Set the mode of operation
  3. Write the timer's initial starting value
  4. Enable the interrupt (if interrupts to be used)
  5. Start the timer

The first step is to clear the TR0 and TF0 bits. This can be done directly on the bits, or by modifying the TCON register which contains both of them. Writing directly to TCON also effects Timer 1. If Timer 1 is already in use generating baud rates, clearing TR1 will stop all serial port communication. In assembly, "ANL TCON, #0x5F" will clear TR0 and TF0 safely. In C, "TCON &= 0x5F" has the same effect. Manipulating the two bits directly takes one extra byte of code and makes the source code more readable. If you do this, clear TR0 first.

Next, you will usually set the Timer 0 mode using TMOD. Writing directly to TMOD also effects Timer 1. If Timer 1 is already generating baud rates, you should AND it with 0xF0 to set Timer 0 to mode 0, and then OR it with 1, 2 or 3 if one of those modes is desired instead of mode 0. TMOD also can configure counter options to the timer, which we won't discuss further to keep things simple.

To set the timer's initial value, you will write to TH0 and TL0 as necessary. For mode 0, TH0 is the 8-bit value. In mode 1, both are used to hold the 16 bits. In mode 2, TL0 holds the 8 bit value, and TH1 holds the automatic reload value.

If you intend to use interrupts, you will usually enable the interrupt (discussed below in the interrupt section), and then to complete the timer setup you would set TR0 to start it running. What you do with the timer after it is running depends on your application. The next section will look at some common applications and how to make use of the Timer 0 in each.

Simple Timer 0 Examples (Without Interrupts)

Many simple programs do not need to use the Timer 0 interrupt. Instead, they can simply check the TF0 bit and test for events as the timer runs. This testing is called polling, and the loop within the code that does this is called the polling loop.

Polling is simpler and easier than using interrupts for applications that essentially wait for the timer to complete or do not need to perform other non-related tasks while the timer is running. It is possible to perform some work while the timer is running by placing it inside the polling loop. Generally, when the polling loop does not need to contain code to preform other tasks unrelated to the timer activity, polling is usually the best approach to using Timer 0. The examples in this section show how polling is done in a few useful Timer 0 applications. In each case, we will briefly look at the limitations of the simpler polling approach.

Elapsed Time Measurement

To measure the time that elapses between two events, Timer 0 is configured in 16 bit mode, with an initial value of zero, and it is started when the first event occurs. The polling loop must check for the second event, and for overflow on the timer. At each overflow, a 16 bit variable is incremented, allowing a total time measurement of 4294967296 cycles, or approximately 2330 seconds with 2 µs resolution (the typical execution time of the polling loop).

This example uses the serial port's RI bit (reception of a byte) as the event. It can easily be modified to test port pins to measure time between hardware-level events.

begin:
        clr     tr0             ;make sure timer 0 is stopped
        clr     tf0             ;clear the overflow flag
        anl     tmod, #0xF0
        orl     tmod, #0x01     ;set to mode 1 (without touching timer 1)
        mov     tl0, #0
        mov     th0, #0         ;clear the timer 0 value
        mov     r3, #0
        mov     r2, #0          ;clear the overflow count
        mov     dptr, #msg_begin
        lcall   pstr
        lcall   cin             ;wait for the user to start the test
        setb    tr0             ;start the timing
waiting_loop:
        jb      ri, done_waiting  ;did we receive another byte ??
        jnb     tf0, waiting_loop ;did timer 0 overflow?
        mov     a, r2
        add     a, #1           ;increment the overflow count
        mov     r2, a
        mov     a, r3
        addc    a, #0
        mov     r3, a
        mov     a, #'.'
        lcall   cout            ;print a dot, so the user sees something
        clr     tf0
        sjmp    waiting_loop    ;keep waiting until the press another key
done_waiting:
        clr     tr0             ;stop timer 0
Complete code is available in the download section at the bottom of this page.
Most of the time, the two conditional branches at the top of the polling loop will execute over and over. So in most cases, Timer 0 will be stopped within 4 cycles (2 µs) of the occurance of the second event. However, if the RI bit becomes set while the 16 bit increment code is executing, several mode cycles can elapse, leading to an additional error in the measurement.

The worst case error is the longest path executed by the polling loop between tests of the stop condtition. Though this error is small (and can be made smaller with some careful re-writing of the polling loop), it can not be avoided. Worse yet, if some other work must be done within the polling loop, the normal and longest execution paths between tests of the stop event will grow, thereby increasing the typical and worst-case measurement errors.

Later, we will revisit this elapsed time measurement using interrupts. The interrupt approach will allow this potential error to be eliminated (and allow other code unrelated code to execute while the timer is counting), but at the cost of a much more complex and difficult implementation.

Timeout Waiting For Input Data

When receiving data from an external source, it is commonly required to implement a timeout. If the data does not arrive after some allowed time, the wait is abandoned and some other action is taken. Perhaps a request is retransmitted or some error message is sent to a user. This simple example waits for the user to type 10 characters. The timeout for the first character is 5 seconds, and then 1 second for each character thereafter. If the user does not respond within this time, a timeout message is printed.

TODO: explain how this code works

begin:
        mov     r0, #0          ;r0 counts number of bytes received
        clr     tr0             ;make sure timer 0 is stopped
        clr     tf0             ;clear the overflow flag
        anl     tmod, #0xF0
        orl     tmod, #0x01     ;set to mode 1 (without touching timer 1)
        mov     tl0, #0
        mov     th0, #0         ;clear the timer 0 value
        mov     r2, #140        ;140 overflows is 5 seconds
        mov     dptr, #msg_begin
        lcall   pstr
        setb    tr0             ;start the timing
waiting_loop:
        jb      ri, get_input   ;did we receive another byte ??
        jnb     tf0, waiting_loop ;did timer 0 overflow?
        clr     tf0
        djnz    r2, waiting_loop
timeout:
        mov     dptr, #msg_timeout
        lcall   pstr
        sjmp    ending
get_input:
        lcall   cin             ;get the user input
        lcall   cout            ;echo it back to their screen
        mov     r2, #28         ;reset timeout to 1 second
        inc     r0
        cjne    r0, #10, waiting_loop
got_all_input:
        mov     dptr, #msg_ok
        lcall   pstr
ending:
Complete code is available in the download section at the bottom of this page.

TODO: polling vs interrupts....

Timekeeping

TODO: intro and explain how the code works

begin:
        mov     r7, #0          ;zero hours, minutes, seconds
        mov     r6, #0
        mov     r5, #0
        clr     tr0             ;make sure timer 0 is stopped
        clr     tf0             ;clear the overflow flag
        anl     tmod, #0xF0     ;set to mode 0 (without touching timer 1)
        mov     th0, #0         ;clear the timer 0 value
        mov     tl0, #0
        mov     r2, #225        ;start overflow countdown (1 second)
        mov     dptr, #msg_begin
        lcall   pstr
        setb    tr0             ;start the timing
        lcall   print_time
timekeeping_loop:
        jnb     tf0, timekeeping_loop ;did timer 0 overflow?
        clr     tf0             ;reset overflow flag
        djnz    r2, timekeeping_loop  ;is this 225 overflows (exactly 1 second)
        lcall   inc_time
        lcall   print_time
        mov     r2, #225        ;reset countdown for another second
        sjmp    timekeeping_loop
Complete code is available in the download section at the bottom of this page.

TODO: this code restructured as subroutines

TODO: pitfalls of the polling approach

How Interrupts Work And Typical Issues Using Them

The timer interrupt is an 8051 hardware feature where the CPU will execute special code, called an interrupt service routine, when the overflow flag bit is set by the timer. The advantage is that your program does not need to constantly check that flag as it runs, as the three polling examples above must do. Another advantage is that the interrupt service routine executes almost immediately in responds to the timer overflow, rather than with a delay determined by how often the polling can check the flag.

However, these advantages do not come for free. The intrrupt service routine code must be written very carefully to avoid interfering with the main program, and great care must be used when exchanging data between the interrupt routine and the main program. Interrupts should be avoided when simple polling is adaquete.

To define a timer0 interrupt routine in assembly, simply using an ORG directive to place the code at location 0x200B. The 8051 will jump to 0x000B, and the code inside PAULMON2 has an LJMP instruction which jumps to 0x200B in your code. In some cases, another LJMP is placed at 0x200B to jump to your code, if it is not possible to place the interrupt service routine at 0x200B.

        .org    0x200B
timer0_isr:
        ; interrupt service routine code            
        reti
        
Defining an ISR in Assembly (AS31)

Using C language, an interrupt service routine is declared using a special "interrupt" keyword. SDCC requires that the interrupt routine or its function prototype must be included in the same file as the main() function.

void timer0_isr(void) interrupt 1
{
       /* interrupt service routine code */        
}
Defining an ISR in C Language (SDCC)

TODO: Properly saving context.

TODO: Enabling, disabling, priority, and latency

TODO: Atomic access to shared data issues

Timer 0 Examples Using Interrupts

Reliable Timekeeping

begin:
	mov	ie, #0		;turn off all interrupts
	mov	sp, #stack-1
	mov	hours, #0	;zero hours, minutes, seconds
	mov	minutes, #0
	mov	seconds, #0
	mov	ov_countdown, #225
	clr	tr0		;make sure timer 0 is stopped
	clr	tf0		;clear the overflow flag
	anl	tmod, #0xF0	;set to mode 0 (without touching timer 1)
	mov	th0, #0		;clear the timer 0 value
	mov	tl0, #0
	mov	dptr, #msg_begin
	lcall	pstr
	clr	time_changed_flag
	setb	tr0		;start the timing
	mov	ip, #0		;set interrupt priorities (all low)
	mov	ie, #0x82	;enable timer0 interrupt
	lcall	print_time
timekeeping_loop:
	lcall	esc
	jc	abort
	jnb	time_changed_flag, timekeeping_loop
	clr	time_changed_flag
	lcall	print_time
	sjmp	timekeeping_loop


	;this interrupt service routine will run every time timer0 sets
	;the TF0 flag.  The 8051 hardware automatically clears TF0 for
	;us.  As with all interrupts, we must be very careful to save
	;any registers that get changed.
timer0_isr:
	djnz	ov_countdown, timer0_end   ;have 225 interrupts (1 second) elapsed?
	mov	ov_countdown, #225
	push	psw		;save psw (cjne changes status bits)
	push	acc		;save accumulator, used inside inc_time
	acall	inc_time	;actually increment the time
	setb	time_changed_flag ;set a flag to alert the main program to change
	pop	acc
	pop	psw
timer0_end:
	reti
Complete code is available in the download section at the bottom of this page (also in C language).

Accurate Elapsed Time Measurement

Implementing Many "Software" Timers

Simple Multitasking

Download Example Code



Using the 8051's Built-in Timers, Paul Stoffregen
http://www.pjrc.com/tech/8051/board5/timers.html
Last updated: February 7, 2004
Status: finished
Suggestions, comments, criticisms: <paul@pjrc.com>