Understanding Arduino timer registers is the single most important step you can take to move beyond beginner-level sketches into professional, real-time embedded firmware. The delay() function is convenient for learning, but it blocks your entire program — nothing else can run while it counts milliseconds. Arduino timer registers, by contrast, let you schedule interrupts, generate precise PWM signals, and measure time with microsecond accuracy while the rest of your code runs freely. In this guide, you will learn exactly how the ATmega328P timers work and how to configure them directly for your projects.
Table of Contents
- Why delay() Is Holding You Back
- Arduino Timer Overview: Timer0, Timer1, Timer2
- Key Timer Registers Explained
- Prescalers and Frequency Calculation
- CTC Mode: Generate Precise Timed Interrupts
- Fast PWM Mode: Custom PWM Frequencies
- Practical Examples
- Using millis() and micros() Correctly
- Frequently Asked Questions
Why delay() Is Holding You Back
The delay() function calls _delay_ms() from the AVR libc, which spins the CPU in a tight busy-wait loop. During this time:
- No other code can execute (your sensors go unread, your buttons go unchecked)
- Serial communication can drop bytes if the buffer fills up
- Servo signals and other time-sensitive outputs can glitch
- You cannot respond to external events in real time
The professional alternative is to use hardware timers with interrupts. The timer counts in the background using dedicated hardware, fires an interrupt when it reaches a target value, and your Interrupt Service Routine (ISR) runs — leaving the loop() free to handle everything else. This is fundamentally how real embedded systems work, from thermostats to motor controllers.
Arduino Timer Overview: Timer0, Timer1, Timer2
The ATmega328P (used in the Arduino Uno, Nano, and Mini) has three hardware timers:
Timer0 (8-bit)
Timer0 is an 8-bit timer used by the Arduino core for millis(), micros(), and delay(). It also drives PWM on pins D5 and D6. Avoid reprogramming Timer0 unless you understand that doing so will break millis() and delay(). If you must use it, save and restore its registers or implement your own time tracking.
Timer1 (16-bit)
Timer1 is a 16-bit timer, meaning its counter register (TCNT1) counts from 0 to 65535. This gives far greater resolution for time measurement and PWM generation. It drives PWM on pins D9 and D10. The Servo library also uses Timer1, so if you use that library, Timer1 is already claimed.
Timer2 (8-bit)
Timer2 is an 8-bit timer that drives PWM on pins D3 and D11. Unlike Timer0 and Timer1, Timer2 can operate independently from the main clock using an external 32.768 kHz crystal for real-time clock applications. The Tone library uses Timer2.
Key Timer Registers Explained
Each timer has a set of special function registers (SFRs) that control its operation. Here are the most important ones for Timer1:
TCCR1A and TCCR1B — Timer/Counter Control Registers
These two registers control the timer’s operating mode (CTC, Fast PWM, Phase Correct PWM, etc.) and the clock source / prescaler. TCCR1A holds the Waveform Generation Mode (WGM) bits 0 and 1 plus the Compare Output Mode (COM) bits. TCCR1B holds WGM bits 2 and 3 plus the Clock Select (CS) bits.
TCNT1 — Timer/Counter Register
This is the actual 16-bit counter value. You can read it at any time to get the current count, or write to it to set a starting value. Reading TCNT1 is the basis for microsecond timing without interrupts.
OCR1A and OCR1B — Output Compare Registers
When TCNT1 matches OCR1A or OCR1B, the timer triggers a compare match event. In CTC mode, this resets the counter and fires an interrupt. In PWM modes, it toggles the corresponding output pin.
ICR1 — Input Capture Register
In some modes, ICR1 sets the TOP value (maximum count) instead of OCR1A. This is useful when you want to use both OCR1A and OCR1B for independent PWM outputs while still controlling the period.
TIMSK1 — Timer Interrupt Mask Register
This register enables or disables specific timer interrupts. Set the OCIE1A bit (bit 1) to enable the Output Compare A Match interrupt, which fires when TCNT1 == OCR1A.
Prescalers and Frequency Calculation
The timer clock is derived from the system clock (16 MHz on most Arduinos) divided by a prescaler. Available prescalers for Timer1 are: 1, 8, 64, 256, and 1024.
The timer tick period is: Tick = Prescaler / F_CPU
For a 1 Hz interrupt using CTC mode with Timer1:
OCR1A = (F_CPU / Prescaler) - 1
With Prescaler = 1024:
OCR1A = (16,000,000 / 1024) - 1 = 15,624
The timer counts from 0 to 15,624 (15,625 ticks total at 15,625 Hz), firing the interrupt exactly once per second. The 16-bit Timer1 can hold values up to 65,535, making it perfect for low-frequency interrupts.
For Timer0 and Timer2 (8-bit, max value 255), you cannot achieve 1 Hz directly — you would need to count interrupts in software and act every Nth interrupt.
CTC Mode: Generate Precise Timed Interrupts
CTC (Clear Timer on Compare Match) is the simplest mode for generating regular timed interrupts. Configure it like this:
void setup() {
cli(); // Disable global interrupts during configuration
// Reset Timer1 control registers
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// Set Compare Match Register for 1 Hz
OCR1A = 15624; // = (16*10^6) / (1024*1) - 1
// CTC mode: WGM12 = 1
TCCR1B |= (1 << WGM12);
// Prescaler 1024: CS12=1, CS10=1
TCCR1B |= (1 << CS12) | (1 << CS10);
// Enable compare interrupt
TIMSK1 |= (1 << OCIE1A);
sei(); // Re-enable interrupts
}
ISR(TIMER1_COMPA_vect) {
// This runs exactly once per second
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
void loop() {
// Main loop is completely free!
}
This is a transformative shift in how you think about Arduino programs. The LED blinks at exactly 0.5 Hz (1 toggle per second) and your loop() can do anything else simultaneously — read sensors, handle serial, update a display — with no timing interference.
Fast PWM Mode: Custom PWM Frequencies
The analogWrite() function generates PWM at a fixed frequency determined by the Arduino core (490 Hz on most pins, 980 Hz on D5 and D6). For applications like motor drivers, audio, or LED drivers, you often need a different frequency.
Here is how to set Timer1 for Fast PWM at a custom frequency using ICR1 as TOP:
// Fast PWM on D9 (OC1A) at 25 kHz, 50% duty cycle
void setup() {
pinMode(9, OUTPUT);
// WGM13=1, WGM12=1, WGM11=1 → Fast PWM with ICR1 as TOP
TCCR1A = (1 << COM1A1) | (1 << WGM11);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10); // No prescaler
ICR1 = 639; // TOP = (F_CPU / frequency) - 1 = (16000000/25000) - 1 = 639
OCR1A = 319; // 50% duty cycle = ICR1 / 2
}
void loop() {
// Adjust OCR1A from 0 to ICR1 to change duty cycle
}
At 25 kHz, this PWM frequency is above the audible range — ideal for PC fan speed control (4-pin fans use 25 kHz PWM) and motor drivers where you want silent operation without annoying whine.
Practical Examples
Example 1: Non-Blocking Multi-Rate Task Scheduler
Use Timer2 at 1 kHz to build a simple software task scheduler, calling different tasks at different rates:
volatile uint16_t ticks = 0;
ISR(TIMER2_COMPA_vect) {
ticks++;
}
void setup() {
// Timer2 CTC, 1 kHz
TCCR2A = (1 << WGM21);
TCCR2B = (1 << CS22); // Prescaler 64
OCR2A = 249; // (16000000 / 64 / 1000) - 1
TIMSK2 = (1 << OCIE2A);
sei();
}
void loop() {
if (ticks % 10 == 0) task_10ms(); // 100 Hz task
if (ticks % 100 == 0) task_100ms(); // 10 Hz task
if (ticks % 1000 == 0) task_1s(); // 1 Hz task
}
Example 2: Microsecond Pulse Width Measurement
Use Timer1 at no prescaler (1 tick = 62.5 ns) to measure pulse widths with sub-microsecond resolution:
uint16_t pulseStart, pulseWidth;
void setup() {
TCCR1A = 0;
TCCR1B = (1 << CS10); // No prescaler
Serial.begin(115200);
attachInterrupt(0, isr, CHANGE); // D2
}
void isr() {
if (digitalRead(2)) {
pulseStart = TCNT1;
} else {
pulseWidth = TCNT1 - pulseStart;
// Convert to microseconds: pulseWidth * 0.0625
}
}
Using millis() and micros() Correctly
Even without direct register manipulation, you can achieve non-blocking timing using millis() — which reads a counter updated by Timer0’s ISR. The key pattern is storing a timestamp and comparing elapsed time:
unsigned long lastRun = 0;
const unsigned long INTERVAL = 500;
void loop() {
if (millis() - lastRun >= INTERVAL) {
lastRun = millis();
// Do something every 500 ms
}
// Everything below runs at full speed
}
Note: millis() has a resolution of approximately 1 ms (limited by Timer0’s ISR rate). micros() has a resolution of 4 µs on a 16 MHz Arduino. Neither is suitable for sub-microsecond timing — for that, read TCNT1 directly with prescaler 1 as shown above.
Frequently Asked Questions
Will changing Timer1 settings break the Servo library?
Yes. The Arduino Servo library uses Timer1 on the Uno/Nano. If you reconfigure Timer1’s registers, you must not include the Servo library in the same sketch — or use an alternative servo library that targets a different timer (such as ServoTimer2 on the Mega’s Timer2 or the Mega’s additional timers).
Does reprogramming Timer0 break millis() and delay()?
Yes. Timer0 drives millis(), micros(), and delay(). Changing its prescaler or mode will make these functions return incorrect values or stop working entirely. If you need Timer0 for something else, replace time tracking with Timer1-based code or avoid delay() and millis() entirely.
How do I generate a precise 38 kHz IR carrier for IR remote control?
Set Timer2 in Fast PWM mode with a prescaler of 1 and OCR2A = (16,000,000 / 38,000) – 1 ≈ 420. Use COM2A0=1 to toggle OC2A (pin D11) on each compare match, producing a 38 kHz square wave. The IRremote library handles this for you automatically, but understanding the underlying mechanism helps with debugging.
Can I use timer interrupts on the Arduino Mega for more simultaneous tasks?
Yes. The ATmega2560 has six 16-bit and 8-bit timers, so you can have multiple completely independent timed tasks. Timer3, Timer4, and Timer5 are all 16-bit and are not used by the Arduino core, making them freely available for your own CTC or PWM configurations.
What happens if my ISR takes too long to execute?
If your ISR execution time exceeds the timer period, the next timer interrupt will be queued but the ISR must finish first. This causes the next interrupt to fire immediately after the previous ISR returns, effectively doubling up on interrupts and causing timing drift. Keep ISRs as short as possible — set a flag and do the work in loop() instead of computing inside the ISR.
Mastering Arduino timer registers will permanently change how you write embedded code. Your programs become more responsive, your timing becomes accurate, and your projects can handle multiple tasks that would be impossible with delay(). Explore Arduino boards and accessories on zbotic.in to find the right platform for your timing-critical applications.
Add comment