AVR Timer0 in CTC Mode: Accurate 1 ms Ticks and Non-Blocking Delays
The _delay_ms() function is handy, but while it runs the CPU can do nothing else. Real programs need to blink an LED, scan buttons and send serial data at the same time. The standard solution is a hardware timer that generates a regular interrupt, for example every millisecond, and a counter that the main loop compares against.
This tutorial configures Timer0 of the ATmega328P in Clear Timer on Compare Match (CTC) mode to produce an exact 1 ms tick at 16 MHz. You will build a millis() function and use it to blink two LEDs at different rates without any blocking delays.
How Timer0 Counts
Timer0 is an 8-bit counter held in the register TCNT0. Its clock comes from the system clock through a prescaler that divides it by 1, 8, 64, 256 or 1024. Every prescaled clock pulse adds one to TCNT0.
In Normal mode the counter runs from 0 to 255 and overflows back to 0. Getting an exact period this way requires reloading TCNT0 in the ISR, which is awkward and slightly inaccurate. In CTC mode the counter is cleared to 0 automatically when it equals the value in OCR0A. You choose OCR0A to set the period exactly, and the hardware sets the flag OCF0A each time a match happens.
Tick period = N x (1 + OCR0A) / f_clk OCR0A = (f_clk x period / N) - 1 Example: f_clk = 16 MHz, N = 64, period = 1 ms OCR0A = (16 000 000 x 0.001 / 64) - 1 = 250 - 1 = 249
Because 249 fits in eight bits, a prescaler of 64 works perfectly. With a prescaler of 8 the required value would be 1999, which is too large for Timer0, so always check that your result is between 0 and 255.
Registers Used
| Register | Bits used here | Purpose |
|---|---|---|
| TCCR0A | WGM01 = 1, WGM00 = 0 | Selects CTC mode (with WGM02 = 0 in TCCR0B) |
| TCCR0B | CS01 = 1, CS00 = 1 | Starts the timer with clock divided by 64 |
| OCR0A | 249 | Compare value, defines the period |
| TIMSK0 | OCIE0A = 1 | Enables the compare match A interrupt |
| TIFR0 | OCF0A | Compare match flag, set by hardware |
| TCNT0 | - | The counter itself |
The clock select bits CS02, CS01 and CS00 give: 001 for no prescaling, 010 for divide by 8, 011 for 64, 100 for 256 and 101 for 1024. Writing 000 stops the timer. The timer starts counting the moment a non-zero clock select is written, so configure OCR0A and the mode first and set the clock last.
Program: millis() and Two Independent Blink Rates
LED 1 is on PB5 (Uno D13) and LED 2 on PB0 (Uno D8), each with a 330 Ω resistor to ground.
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <stdint.h>
static volatile uint32_t ms_ticks = 0;
/* Runs every 1 ms */
ISR(TIMER0_COMPA_vect)
{
ms_ticks++;
}
static void timer0_init(void)
{
TCCR0A = (1 << WGM01); /* CTC mode, TOP = OCR0A */
OCR0A = 249; /* 16 MHz / 64 / 250 = 1 kHz */
TIMSK0 = (1 << OCIE0A); /* compare A interrupt on */
TCCR0B = (1 << CS01) | (1 << CS00); /* prescaler 64, timer starts */
}
/* Returns milliseconds since start; reading 4 bytes must be atomic */
static uint32_t millis(void)
{
uint32_t now;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
now = ms_ticks;
}
return now;
}
int main(void)
{
uint32_t last1 = 0, last2 = 0;
DDRB |= (1 << DDB5) | (1 << DDB0);
timer0_init();
sei();
while (1) {
uint32_t now = millis();
/* LED 1 every 500 ms; subtraction is safe across wrap-around */
if (now - last1 >= 500) {
last1 += 500;
PINB = (1 << PINB5);
}
/* LED 2 every 120 ms */
if (now - last2 >= 120) {
last2 += 120;
PINB = (1 << PINB0);
}
/* other tasks can run here without being blocked */
}
}
Why It Is Written This Way
The tick counter is 32 bits wide so it lasts about 49.7 days before wrapping. The AVR needs four separate instructions to read it, and the ISR could update it between them, giving a corrupted value. ATOMIC_BLOCK(ATOMIC_RESTORESTATE) disables interrupts while copying and then restores the previous state.
The comparison now - last1 >= 500 uses unsigned subtraction, which gives the correct elapsed time even when ms_ticks wraps from its maximum value to zero. Writing now >= last1 + 500 would fail at the wrap. Adding the interval to last1 instead of setting last1 = now keeps the average period exact even if the main loop is occasionally late.
Polled delay without interrupts
If you just want a precise blocking delay without an ISR, you can wait for the OCF0A flag instead. Each loop iteration below waits for one compare match and then clears the flag by writing a one to it.
/* Assumes Timer0 is already in CTC mode with OCR0A = 249, prescaler 64,
and that the OCIE0A interrupt is NOT enabled. */
void delay_ms_timer(uint16_t ms)
{
TCNT0 = 0;
TIFR0 = (1 << OCF0A); /* clear old flag */
while (ms--) {
while (!(TIFR0 & (1 << OCF0A)))
; /* wait 1 ms */
TIFR0 = (1 << OCF0A); /* clear by writing 1 */
}
}
Generating a Square Wave on OC0A
CTC mode can also toggle the OC0A pin (PD6, Uno D6) in hardware on every match by setting COM0A0 in TCCR0A and making PD6 an output. The output frequency is f_clk / (2 x N x (1 + OCR0A)). With the 1 ms settings above the pin toggles every millisecond, giving a 500 Hz square wave you can check with a multimeter's frequency range or an oscilloscope. This is a good way to verify that your clock really is 16 MHz.
Troubleshooting
- Timing is eight times slow: the CKDIV8 fuse is still programmed, so the CPU runs from the internal 8 MHz RC divided by 8 rather than the crystal.
- The program restarts repeatedly: the interrupt is enabled but the ISR name is misspelt, so the default handler runs.
- LEDs freeze after some time: a non-atomic read of a multi-byte counter or a comparison that fails at wrap-around.
- Using Arduino core code alongside: the Arduino core already uses Timer0 for its own millis(), so this register-level code is meant for plain avr-gcc projects.
Summary
CTC mode clears Timer0 when it matches OCR0A, giving an exact period set by OCR0A and the prescaler. With 16 MHz, prescaler 64 and OCR0A = 249, you get a 1 kHz interrupt that drives a millis() counter. Read multi-byte shared variables atomically and compare times by subtraction, and the main loop can run many tasks at independent rates.