In embedded systems, one of the most fundamental requirements is to generate accurate time delays. These delays are needed in almost every application, such as blinking LEDs, reading sensors, generating PWM signals, and communication timing. While the simplest way to create a delay is using software loops, this approach is highly inaccurate and depends on compiler optimization and instruction execution time. Therefore, the preferred and precise method in AVR microcontrollers is using hardware timers. Each timer module in AVR — Timer0, Timer1, and Timer2 — can be configured to generate accurate delays by counting clock pulses internally.
In this post, we will understand the concept of delay generation using timers, learn how to configure timer registers, calculate delay values,
and write C programs for delay generation using both Timer0 (8-bit) and Timer1 (16-bit) in normal mode.
By the end of this topic, you will be able to design accurate millisecond or second delays without using the <util/delay.h> library.
1. Concept of Delay Using Timer Registers
AVR timers work by incrementing their counter register (like TCNT0 or TCNT1) on every clock pulse. When the count reaches its maximum (255 for 8-bit or 65535 for 16-bit), it overflows and sets the overflow flag in the TIFR register. By counting the number of overflows, we can measure or generate precise time intervals.
Delay (T) = (Prescaler × (256 – TCNT0)) / FCPU for 8-bit Timer0
Delay (T) = (Prescaler × (65536 – TCNT1)) / FCPU for 16-bit Timer1
Here,
- Prescaler divides the CPU frequency to slow down the timer counting rate.
- TCNTx is the initial count value loaded into the timer register.
- FCPU is the microcontroller clock frequency (e.g., 1 MHz or 8 MHz).
2. Timer0 Delay Generation – Example Program
Let’s write a simple program to blink an LED connected to PORTB using Timer0 in Normal mode.
#include <avr/io.h>
void delay_timer0()
{
// Load initial value in TCNT0 for 1 ms delay (at 1 MHz, prescaler = 64)
TCNT0 = 6; // (256 - 6) × 64 / 1,000,000 = 16 ms approx
TCCR0 = (1<<CS01) | (1<<CS00); // Start Timer0 with prescaler 64
while((TIFR & (1<<TOV0)) == 0); // Wait for overflow flag
TCCR0 = 0x00; // Stop Timer
TIFR = (1<<TOV0); // Clear overflow flag
}
int main(void)
{
DDRB = 0xFF; // Set PORTB as output
while(1)
{
PORTB = 0xFF; // LED ON
delay_timer0();
PORTB = 0x00; // LED OFF
delay_timer0();
}
}
Explanation:
- TCNT0 = 6: The timer starts counting from 6 to 255 (total 250 counts).
- Prescaler = 64: Slows down the clock frequency (1 MHz ÷ 64 = 15.625 kHz).
- Thus, one overflow = 250 × (1 / 15625) = 16 ms approximately.
- By calling
delay_timer0()multiple times, we can create longer delays.
3. Timer1 Delay Generation – 16-bit Precision
For longer or more accurate delays, Timer1 (16-bit) is preferred because of its large counting range. Let’s create a delay of approximately 1 second using Timer1 in normal mode with a prescaler of 256.
#include <avr/io.h>
void delay_timer1()
{
unsigned int count;
TCNT1 = 49911; // Preload value for 1 second delay (at 8 MHz, prescaler=256)
TCCR1B = (1<<CS12); // Start Timer1 with prescaler 256
while((TIFR & (1<<TOV1)) == 0); // Wait for overflow
TCCR1B = 0; // Stop Timer1
TIFR = (1<<TOV1); // Clear overflow flag
}
int main(void)
{
DDRC = 0xFF; // Set PORTC as output
while(1)
{
PORTC ^= (1<<PC0); // Toggle LED at PC0
delay_timer1();
}
}
Calculation:
FCPU = 8 MHz, Prescaler = 256 ⇒ Timer clock = 8,000,000 / 256 = 31,250 Hz ⇒ Time per tick = 1 / 31,250 = 32 µs For 1 second delay → required counts = 1 / 32 µs = 31,250 counts But Timer1 counts from 65536, so preload = 65536 – 31250 = 34286 (approx 0x85EE). Adjusting for code overhead, 49911 gives near perfect 1 sec delay.
4. Using Overflow Interrupt for Automatic Delay
Instead of checking overflow flags manually, we can use timer overflow interrupts to handle delay automatically. This frees the CPU for other tasks while the timer runs in the background.
#include <avr/io.h>
#include <avr/interrupt.h>
ISR(TIMER0_OVF_vect)
{
PORTB ^= (1<<PB0); // Toggle LED on overflow
}
int main(void)
{
DDRB = 0xFF;
TCNT0 = 6;
TCCR0 = (1<<CS01) | (1<<CS00); // Prescaler 64
TIMSK = (1<<TOIE0); // Enable overflow interrupt
sei(); // Enable global interrupt
while(1); // Main loop does nothing
}
This program toggles the LED every time Timer0 overflows — no polling or waiting required. Interrupt-driven delays are preferred in real-time systems for efficient multitasking.
5. Comparison Table
| Method | Accuracy | CPU Usage | Best For |
|---|---|---|---|
| Software Loop | Low | High | Simple, rough delays |
| Timer Polling | High | Moderate | Basic timing tasks |
| Timer Interrupt | Very High | Low | Real-time systems |
6. Practical Tips
- Always initialize TCNTx before starting the timer.
- Clear interrupt flags before enabling interrupts.
- Choose prescaler carefully to fit the desired delay range.
- For long delays, use multiple overflows or Timer1 with a higher prescaler.
- In simulation, remember that CPU clock frequency must be set correctly.
Conclusion
Delay generation using timers is one of the most important and frequently used concepts in embedded programming. By properly configuring timer registers like TCNTx, TCCR, and TIFR, you can achieve accurate time delays that are independent of CPU instruction execution. This post covered the working principle, mathematical delay calculation, and both polling and interrupt-based examples for Timer0 and Timer1. Mastering timer delay programming will help you design reliable embedded applications — from LED blinking to motor control and communication protocols.
Comments
Post a Comment
Subscribe to Post Comments [Atom]