Many Arduino projects start with a simple delay(1000) call to blink an LED or pace sensor readings. While delay() works well for basic bench demonstrations, it holds the processor in an idle busy-wait loop. During this time, the sequential progress of your main loop() stops, preventing code from promptly evaluating buttons, processing sensor streams, or servicing state machines.
Software polling with millis() provides a popular cooperative alternative, but its timing consistency depends on how quickly the main loop returns to check the timestamp. If an intervening task takes several milliseconds, scheduled events can drift.
Hardware timer interrupts offer another approach. By offloading timekeeping to an internal hardware counter, the microcontroller can automatically execute a dedicated Interrupt Service Routine (ISR) at regular intervals while the main program continues executing other work.
In this guide, we examine how hardware timers operate, configure Timer1 on an ATmega328P (Arduino Uno) in Clear Timer on Compare Match (CTC) mode, calculate register values for specific intervals, and observe the resulting waveforms in simulation.
---
1. Comparing Timing Approaches on Arduino
Embedded developers working on AVR microcontrollers typically choose between three timing strategies depending on application requirements:
| Strategy | Program Flow Impact | Interrupt Availability | Timing Sensitivity | Best Suited For |
|---|---|---|---|---|
delay() | Halts loop() progress during wait | Interrupts remain active in background | Fixed delays; stalls cooperative tasks | Simple linear scripts, power-up settling |
millis() polling | Non-blocking cooperative flow | Interrupts increment internal tick counter | Execution delayed if other loop tasks run long | General UI updates, button debouncing, telemetry |
| Timer Interrupt (CTC) | Asynchronous hardware preemption | Dedicated ISR fires on hardware compare match | Deterministic counter; small jitter from ISR entry/locks | Precise sampling rates, waveform generation, PID loops |
A common misconception is that delay() turns off the microcontroller entirely. On AVR boards, delay() simply polls the Timer0 tick count in a tight while loop. Background interrupts—such as incoming UART serial bytes on hardware RX pins—continue to fire. However, your own application logic in loop() cannot proceed until the delay expires.
Conversely, polling with millis() lets loop() run freely, but the exact moment a task executes depends on the total execution time of all other tasks in that loop. When an application requires regular, recurring execution independent of loop() workload, hardware timer interrupts provide a dedicated mechanism.
---
2. Understanding Timer1 Architecture on the Arduino Uno
The classic Arduino Uno uses an ATmega328P clocked at 16 MHz with three built-in hardware timers:
- Timer0: 8-bit timer used by the Arduino core for
millis(),micros(), and PWM outputs on pins 5 and 6. - Timer1: 16-bit timer with dual compare units (
OCR1A,OCR1B) and input capture, providing PWM on pins 9 and 10. - Timer2: 8-bit timer with asynchronous clock capability, often used for
tone()and PWM on pins 3 and 11.
Because Timer1 is a 16-bit timer, its counter register (TCNT1) can count from 0 to 65,535. By contrast, 8-bit timers overflow after 255 counts, requiring software prescaling to achieve longer intervals.
In CTC mode (Clear Timer on Compare Match), the timer counter increments on each clock tick until its value matches Output Compare Register A (OCR1A). Upon matching, the counter clears back to zero on the next timer clock cycle, and the hardware sets the compare flag (OCF1A), triggering the TIMER1_COMPA_vect interrupt if enabled.
---
3. Calculating Timer Prescalers and Compare Values
The nominal hardware interrupt interval is determined by the CPU clock frequency ($F_{\text{CPU}} = 16\text{ MHz}$), the prescaler division factor ($N$), and the value loaded into $OCR1A$:
$$T_{\text{interrupt}} = \frac{(OCR1A + 1) \times N}{F_{\text{CPU}}}$$
To find the required register value $OCR1A$ for a desired interrupt interval $T_{\text{interrupt}}$:
$$OCR1A = \frac{F_{\text{CPU}} \times T_{\text{interrupt}}}{N} - 1$$
Example: Configuring a 1 ms (1000 Hz) Compare Match
1. Select a prescaler of $N = 8$. The timer clock frequency is:
$$F_{\text{timer}} = \frac{16\,000\,000\text{ Hz}}{8} = 2\,000\,000\text{ Hz} \quad (0.5\,\mu\text{s per count})$$
2. Substitute $T_{\text{interrupt}} = 0.001\text{ s}$ into the formula:
$$OCR1A = \frac{16\,000\,000 \times 0.001}{8} - 1 = \frac{16\,000}{8} - 1 = 1999$$
3. Loading OCR1A = 1999 configures the timer hardware to reach compare match every 2,000 counts, yielding a nominal 1.000 ms compare interval.
Common Timer1 Compare Values ($F_{\text{CPU}} = 16\text{ MHz}$, Prescaler $N = 8$)
| Target Interrupt Interval | OCR1A Value | Interrupt Event Rate | Square-Wave Output Frequency (Toggled Each ISR) |
|---|---|---|---|
| 0.5 ms (500 $\mu$s) | 999 | 2,000 Hz | 1,000 Hz (1 kHz) |
| 1.0 ms (1000 $\mu$s) | 1999 | 1,000 Hz | 500 Hz |
| 2.0 ms | 3999 | 500 Hz | 250 Hz |
| 10.0 ms | 19999 | 100 Hz | 50 Hz |
| 20.0 ms | 39999 | 50 Hz | 25 Hz |
Interrupt Rate vs. Waveform Frequency: Toggling an output pin once per interrupt produces one edge per event. Because a complete square-wave cycle requires both a rising edge and a falling edge, the resulting signal period is twice the interrupt interval ($T_{\text{period}} = 2 \times T_{\text{interrupt}}$), making the output frequency half the interrupt event rate.
---
4. Complete Code: Timer1 CTC Interrupt on Arduino Uno
The following sketch sets up Timer1 in CTC mode using bare-metal register assignments and toggles the built-in LED on digital pin 13 (PB5) at each compare match:
#include <avr/io.h>
#include <avr/interrupt.h>
void setup() {
// Configure digital pin 13 (Port B, Bit 5) as output
pinMode(13, OUTPUT);
// Temporarily disable global interrupts during timer configuration
cli();
// Reset Timer1 control registers
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// Set compare match register for a nominal 1 ms interval
OCR1A = 1999;
// Set CTC mode (WGM12 bit in TCCR1B)
TCCR1B |= (1 << WGM12);
// Set CS11 bit for clock prescaler of 8
TCCR1B |= (1 << CS11);
// Enable Timer1 Output Compare Match A interrupt
TIMSK1 |= (1 << OCIE1A);
// Re-enable global interrupts
sei();
}
void loop() {
// Application tasks execute here while Timer1 operates independently
}
// Interrupt Service Routine for Timer1 Compare Match A
ISR(TIMER1_COMPA_vect) {
// Toggle PB5 (digital pin 13) using the hardware PIN register toggle feature
PINB = (1 << PB5);
}
Understanding `PINB = (1 << PB5);`
In the AVR architecture, writing a logical 1 to a bit in an input pin register (PINx) toggles the corresponding bit in the output data latch (PORTx). This hardware feature executes as an atomic single-instruction toggle on that specific pin, avoiding the multi-cycle read-modify-write sequence of PORTB ^= (1 << PB5); without disturbing other pins on the port.
---
5. Practical Guidelines for Timer Interrupts
1. Keep ISR Execution Brief: Interrupt handlers should execute only time-critical actions and return as quickly as possible. Extensive computation inside an ISR delays other interrupts and increases latency across the system. Budget execution time according to your application's real-time deadlines.
2. Avoid delay() Inside an ISR: Calling delay() inside an ISR is unsuitable because delay() relies on the Timer0 tick counter, which depends on interrupts that cannot fire while the current ISR is executing.
3. Defer Serial Printing to loop(): Hardware serial transmissions rely on interrupt-driven ring buffers. Instead of calling Serial.print() inside an ISR, set a flag in the ISR and handle logging inside loop().
4. Declare Shared Variables as volatile: Any variable accessed by both an ISR and main loop code must be marked volatile (e.g., volatile bool eventTriggered = false;). This instructs the C++ compiler to read the variable from RAM on each access rather than caching it in a CPU register.
5. Protect Multi-Byte Reads on 8-bit Architectures: On an 8-bit AVR processor, reading or writing a 16-bit or 32-bit variable requires multiple CPU instructions. If an interrupt fires between the byte reads, the main loop may observe an inconsistent intermediate state. Guard multi-byte reads in the main loop using atomic blocks (noInterrupts(); copy variable; interrupts();).
6. Check for Timer Peripheral Conflicts: Reconfiguring Timer1 registers alters the PWM behavior on digital pins 9 and 10, and conflicts with libraries such as Servo.h that allocate Timer1 for servo timing. Review timer allocations if your project combines interrupts with hardware PWM, as detailed in our ATmega32 vs Arduino Uno register guide.
---
6. Simulating Timer Interrupts in CircuPilot
Before deploying firmware to a physical microcontroller, running your code in an embedded simulator helps verify register behavior, timing logic, and pin transitions.
CircuPilot compiles real AVR C++ sketches using arduino-cli and executes the compiled binary in the browser using instruction-level AVR emulation:
1. Open the CircuPilot Circuit Simulator.
2. Connect digital pin 13 to an LED and an oscilloscope channel.
3. Click Run: the emulator advances AVR instructions and hardware peripheral timers. Because the 1 ms interrupt toggles pin 13 every millisecond, the LED switches between HIGH and LOW states at 500 Hz. While this rate appears continuously lit to the human eye, the virtual oscilloscope confirms a 500 Hz square wave with a 2 ms period and 1 ms between transitions.
4. Experiment by modifying OCR1A to 3999 (250 Hz output) or 19999 (50 Hz output) to observe how timer compare values scale the output waveform in real time.
---
Conclusion
Timer interrupts provide a structured mechanism for scheduling recurring operations on Arduino microcontrollers without stalling program execution. By placing Timer1 into CTC mode, you establish a hardware-driven cadence that allows your main loop() to manage user interfaces, sensors, and state machines while time-critical tasks run on schedule.
To explore register mappings across different AVR chips, read our comparative analysis of ATmega32 vs Arduino Uno registers, or learn how timer registers control motor drivers in our Arduino DC motor speed control tutorial.
---
Frequently asked questions
What is an Arduino timer interrupt?
A timer interrupt is a hardware-generated signal triggered when a timer counter reaches a configured condition, allowing an Interrupt Service Routine to run without requiring the main loop to wait in busy-loops.
What is CTC mode on ATmega328P?
Clear Timer on Compare Match (CTC) mode resets the timer counter to zero automatically when it matches the value stored in OCR1A, establishing a predictable, repeating hardware time base.
Why is the output square-wave frequency half the interrupt frequency?
Each interrupt toggles the output pin once (low-to-high or high-to-low). A full square-wave cycle requires two transitions, meaning a 1 kHz interrupt rate generates a 500 Hz square wave with a 2 ms period.
Can Timer1 interrupts conflict with other Arduino features?
Yes. Reconfiguring Timer1 alters hardware PWM on digital pins 9 and 10, and conflicts with libraries such as Servo that depend on Timer1 for pulse generation.