Choosing the Right Arduino Timer: A Practical Guide for Precise Timing and PWM
When you need exact timing or custom PWM on an Arduino Uno, you’ll likely touch the hardware timers. This post walks through the three timers, shows a concrete example of using Timer1 for a 1 kHz control loop, and explains how to avoid common pitfalls and conflicts with core libraries.
07 Nov 2025, 18:56 UTC

When Timer Tweaks Go Wrong
Picture this: you’re building a 1 kHz motor control loop and you decide to “just” change the analogWrite frequency on pin 9. After uploading, millis() stops counting, tone() on pin 3 stops, and the board looks dead. The culprit? The three hardware timers that the Arduino core configures at boot. Understanding their roles and how to use them safely is the first step to reliable, high‑precision sketches.
Timer Overview on the ATmega328P
- Timer0 (8‑bit) – drives
millis(),micros(),delay(), andanalogWrite()on pins 5 and 6. Changing its prescaler or mode breaks core timing functions. - Timer1 (16‑bit) – supports CTC, phase‑correct PWM on pins 9/10, input capture, and is used by the
Servolibrary. With a 16 MHz clock and no prescaler, it offers 1 µs resolution. - Timer2 (8‑bit) – runs asynchronously from a 32.768 kHz crystal (optional) for real‑time‑clock use; also drives
analogWrite()on pins 3/11 and thetone()library.
All three timers are initialized in init() before setup() runs. If you need a timer for a custom task, you must first disable its interrupts, re‑configure the registers, and then re‑enable the interrupts.
Hands‑On: 1 kHz Square Wave on Timer1
Goal: generate a 1 kHz square wave on pin 13 (OC1A) using Timer1 in CTC mode, while leaving Timer0 (millis()) and Timer2 (tone()) untouched.
#include <Arduino.h>
// Pin to toggle
const uint8_t outputPin = 13;
void setup() {
pinMode(outputPin, OUTPUT);
// 1. Stop Timer1 and clear its interrupt flag
TCCR1A = 0; // Normal mode, no PWM
TCCR1B = 0; // Stop the timer
TIMSK1 = 0; // Disable all Timer1 interrupts
// 2. Configure CTC mode, prescaler 8
// OCR1A = 15999 gives 1 kHz: 16 MHz / 8 / 16000 = 1 kHz
OCR1A = 15999;
TCCR1B = (1 << WGM12) | (1 << CS11); // CTC, prescaler 8
// 3. Enable compare‑match interrupt
TIMSK1 = (1 << OCIE1A);
}
void loop() {
// Normal code can run here; millis() works because Timer0 is untouched
}
// ISR for Timer1 Compare Match A
ISR(TIMER1_COMPA_vect) {
// Toggle the output pin
PORTB ^= (1 << outputPin); // Fast toggle using PORTB
}
To verify:
- Upload the sketch to an Arduino Uno.
- Open the Serial Monitor and print
millis()every 100 ms to confirm it continues to increment. - Use an oscilloscope or logic analyzer on pin 13; you should see a clean 1 kHz square wave.
Because we never touched Timer0 or Timer2, millis() and tone() remain functional. The ISR is short (<10 µs) and avoids heavy operations, preventing timing overruns.
Common Pitfalls & How to Avoid Them
- Modifying Timer0 – Breaks core timing. If you need custom PWM on pins 5/6, use
Timer1orTimer2instead. - Using Servo Library – On the Uno,
Servoclaims Timer1, disabling PWM on pins 9/10. If you need both Servo and custom PWM on those pins, move to an Arduino Mega (Timer5) or use a dedicated PWM driver. - Asynchronous Timer2 – Requires a 32.768 kHz crystal on pins TOSC1/TOSC2. Without it, the timer stops or runs at an unpredictable rate.
- Long ISRs – Keep ISR code minimal. Floating‑point math or
Serial.print()inside an ISR can cause missed compare matches. - Register Access While Running – Writing to
TCNTnwhile the timer is running can cause glitches. Wrap such writes innoInterrupts()/interrupts()or stop the timer first.
When to Use Which Timer
| Timer | Best Use | Library Conflicts |
|---|---|---|
| Timer0 | Core timing, delay(), millis(), micros() | None – core functions depend on it |
| Timer1 | High‑resolution PWM (9/10), CTC loops, input capture, Servo (Uno) | Servo library disables PWM on 9/10 |
| Timer2 | Async RTC, tone(), PWM (3/11) | None on Uno; on Mega, tone() uses Timer2, Servo uses Timer5 |
Actionable Checklist
- Identify the task: timing loop, PWM, input capture, or tone.
- Map the task to the appropriate timer from the table.
- Check library documentation for timer claims (Servo, IRremote, etc.).
- Disable the timer’s interrupts, re‑configure registers, then re‑enable interrupts.
- Keep ISRs short; use
PORTxtoggles for speed. - Verify with
millis()output and an oscilloscope or logic analyzer.
By following these steps, you can harness the full power of the ATmega328P timers without breaking the Arduino core or running into subtle conflicts.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.