Timing is at the heart of almost every Arduino project. Blinking an LED, debouncing a button, reading sensors at fixed intervals, generating PWM signals — all of these depend on accurate time measurement and scheduling. Yet many beginners reach for delay() and quickly discover its painful limitation: the whole sketch freezes while it waits. In this comprehensive Arduino timer tutorial you’ll master everything from the simple millis() pattern to hardware timer interrupts that fire with microsecond precision.
Why delay() Is Almost Always Wrong
delay(n) blocks the CPU for n milliseconds. During that time, nothing else happens: no button reads, no sensor checks, no serial data received. For simple blink demos it’s fine. For any real project with multiple tasks, it’s a trap.
Consider a project that blinks an LED every 500 ms AND reads a button. With delay(500), the button is only checked twice per second — you’ll miss fast presses. The solution is non-blocking timing using millis().
millis() — Non-Blocking Millisecond Timing
millis() returns the number of milliseconds elapsed since the board was last reset or powered on. It’s driven by Timer0 hardware and updated in a background interrupt — no CPU blocking involved. The return type is unsigned long (32-bit), which means it overflows back to zero after approximately 49.7 days.
Classic Blink Without delay()
const int LED_PIN = 13;
const unsigned long BLINK_INTERVAL = 500; // ms
unsigned long lastBlinkTime = 0;
bool ledState = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - lastBlinkTime >= BLINK_INTERVAL) {
lastBlinkTime = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
// Other code runs here every loop iteration — not blocked!
}
The key expression is now - lastBlinkTime >= BLINK_INTERVAL. Using subtraction rather than direct comparison makes this overflow-safe — it continues to work correctly even when millis() wraps around at 49 days.
Running Multiple Timed Tasks
The real power of millis() is running several independent timers simultaneously:
unsigned long lastSensorTime = 0;
unsigned long lastHeartbeatTime = 0;
unsigned long lastDisplayTime = 0;
const unsigned long SENSOR_INTERVAL = 200; // 5 Hz
const unsigned long HEARTBEAT_INTERVAL = 1000; // 1 Hz
const unsigned long DISPLAY_INTERVAL = 100; // 10 Hz
void loop() {
unsigned long now = millis();
if (now - lastSensorTime >= SENSOR_INTERVAL) {
lastSensorTime = now;
readSensors();
}
if (now - lastHeartbeatTime >= HEARTBEAT_INTERVAL) {
lastHeartbeatTime = now;
blinkHeartbeatLED();
}
if (now - lastDisplayTime >= DISPLAY_INTERVAL) {
lastDisplayTime = now;
updateDisplay();
}
checkButtons(); // Runs every loop — maximum responsiveness
}
This pattern scales to as many independent tasks as you need, with no impact on each other’s timing.
micros() — Microsecond Resolution
micros() works identically to millis() but returns elapsed time in microseconds, providing 4 µs resolution on a 16 MHz Arduino. It overflows after approximately 70 minutes.
// Measure pulse width manually
unsigned long pulseStart = micros();
while (digitalRead(SIGNAL_PIN) == HIGH); // Wait for pulse end
unsigned long pulseWidth = micros() - pulseStart;
Serial.print("Pulse: ");
Serial.print(pulseWidth);
Serial.println(" µs");
micros() is ideal for:
- Ultrasonic distance measurement (HC-SR04 pulse timing)
- IR remote decoding
- Bit-banging custom protocols
- Benchmarking code execution time
Hardware Timers: Timer0, Timer1 and Timer2
The ATmega328P (Arduino Uno/Nano) has three hardware timers, each with specific roles and capabilities:
| Timer | Bits | PWM Pins | Notes |
|---|---|---|---|
| Timer0 | 8-bit | D5, D6 | Used by millis(), micros(), delay() — modify with caution |
| Timer1 | 16-bit | D9, D10 | Best for precise timing; used by Servo library |
| Timer2 | 8-bit | D3, D11 | Used by tone() function |
Timer Interrupts: Triggering Code at Exact Intervals
Hardware timer interrupts let you execute code at precise intervals completely independent of what the main loop is doing. Here’s how to configure Timer1 to fire every 1 second:
void setup() {
Serial.begin(9600);
// Configure Timer1 for 1-second interrupt
cli(); // Disable interrupts during config
TCCR1A = 0; // Clear Timer1 control register A
TCCR1B = 0; // Clear Timer1 control register B
TCNT1 = 0; // Reset timer counter
// Set compare match value for 1 Hz at 16 MHz with 1024 prescaler
// Formula: OCR1A = (F_CPU / prescaler / freq) - 1
// = (16,000,000 / 1024 / 1) - 1 = 15624
OCR1A = 15624;
TCCR1B |= (1 << WGM12); // CTC mode (clear timer on compare match)
TCCR1B |= (1 << CS12) | (1 << CS10); // 1024 prescaler
TIMSK1 |= (1 << OCIE1A); // Enable Timer1 compare interrupt
sei(); // Enable interrupts
}
ISR(TIMER1_COMPA_vect) {
// This runs exactly every 1 second, regardless of loop() activity
Serial.println("One second tick!");
}
void loop() {
// Main loop code — runs independently
delay(100);
}
Timer1 Frequency Formula
To calculate OCR1A for a desired interrupt frequency:
OCR1A = (F_CPU / prescaler / desired_frequency) - 1
Examples at 16 MHz:
100 Hz, prescaler 256: OCR1A = (16000000/256/100) - 1 = 624
10 Hz, prescaler 256: OCR1A = (16000000/256/10) - 1 = 6249
1 Hz, prescaler 1024: OCR1A = (16000000/1024/1) - 1 = 15624
1 kHz, prescaler 8: OCR1A = (16000000/8/1000) - 1 = 1999
PWM Generation with Hardware Timers
Arduino’s analogWrite() function uses hardware timers to generate PWM signals automatically. The default PWM frequency varies by pin:
- D5, D6 (Timer0): ~977 Hz
- D9, D10 (Timer1): ~490 Hz
- D3, D11 (Timer2): ~490 Hz
To change the PWM frequency, modify the timer prescaler bits. For example, to change Timer2’s frequency to approximately 31 kHz (useful for audio or motor drivers that need high-frequency PWM):
// Set Timer2 prescaler to 1 → ~31 kHz PWM on D3 and D11
TCCR2B = TCCR2B & B11111000 | B00000001;
// Set Timer2 prescaler to 64 → ~490 Hz (default)
TCCR2B = TCCR2B & B11111000 | B00000100;
// Set Timer2 prescaler to 1024 → ~30 Hz (very slow PWM)
TCCR2B = TCCR2B & B11111000 | B00000111;
Warning: changing Timer0’s prescaler also changes the rate at which millis() and micros() count — they will no longer return real time. Always prefer Timer1 or Timer2 for custom PWM frequencies.
Using the TimerOne and TimerThree Libraries
If direct register manipulation feels overwhelming, the popular TimerOne library wraps Timer1 with a clean API:
#include <TimerOne.h>
void timerCallback() {
// Called every 500 ms
digitalWrite(13, !digitalRead(13));
}
void setup() {
pinMode(13, OUTPUT);
Timer1.initialize(500000); // Period in microseconds (500 ms)
Timer1.attachInterrupt(timerCallback);
}
void loop() {
// Free to do other things
}
Install via the Arduino Library Manager: search for “TimerOne”. The TimerThree library works identically for Timer3 on Mega-class boards.
Common Timer Pitfalls and How to Avoid Them
- millis() overflow: Always use
unsigned longfor time variables and the subtraction pattern (now - lastTime). Never use==comparison or you’ll miss the exact tick. - ISR code size: Keep ISRs short and fast. No
Serial.print(), nodelay(), no blocking calls. Set a flag in the ISR and handle the work inloop(). - Volatile variables: Variables shared between ISRs and main code must be declared
volatileto prevent compiler optimisation from caching them in registers. - Servo + Timer1 conflict: The Servo library uses Timer1. You cannot use Timer1 for custom interrupts while Servo is active. Use Timer2 or the Mega’s Timer3/4/5 instead.
- tone() + Timer2 conflict: The
tone()function uses Timer2. Callingtone()will override any custom Timer2 configuration.
Frequently Asked Questions
What is the resolution of millis() on Arduino?
On a 16 MHz Arduino Uno, millis() has a resolution of approximately 1.024 ms (due to Timer0 prescaler settings). It does not tick exactly every 1.000 ms — there’s a small accumulated drift. For timing requirements tighter than ~1 ms, use micros() which has 4 µs resolution.
How do I measure elapsed time accurately in Arduino?
Use unsigned long start = millis(); before your operation, then unsigned long elapsed = millis() - start; after. The subtraction is overflow-safe for periods up to ~49 days. For sub-millisecond measurements use micros() with the same pattern, which overflows after ~70 minutes.
Can I use hardware timer interrupts alongside delay() and millis()?
Yes, with one constraint: avoid using Timer0 for custom interrupts (it drives millis() and delay()). Use Timer1 (16-bit, highest precision) or Timer2 (8-bit) for your interrupt. Both can coexist with the Timer0-based functions without any issue.
Why does my Arduino run slow when I use too many hardware timer interrupts?
If ISRs fire very frequently (e.g., every microsecond) and take more than a few CPU cycles each, the CPU spends most of its time in ISR context and the main loop barely gets to run. Reduce ISR frequency, minimise ISR code, or consider moving time-critical processing to the main loop using flags set by the ISR.
What’s the difference between CTC mode and Fast PWM mode?
CTC (Clear Timer on Compare Match) mode resets the counter when it matches OCR1A, creating precise periodic interrupts. Fast PWM mode continuously counts up and uses OCR values to toggle output pins, ideal for generating PWM waveforms. CTC is best for timer interrupts; Fast PWM is best for analog output and motor control.
Level Up Your Arduino Timing Skills
Mastering Arduino’s timer system — from the simple elegance of millis() to the precision of hardware timer interrupts — fundamentally changes what you can build. Multi-tasking sketches, microsecond-accurate measurements, custom PWM frequencies: all become possible once you move beyond delay().
Find all the Arduino hardware you need to put these techniques into practice at Zbotic.in — India’s Arduino specialists with fast nationwide delivery.
Add comment