To execute a time interrupt on an Arduino Uno, you must configure the 16-bit Timer1 registers (TCCR1A, TCCR1B) with a specific prescaler and compare value (OCR1A) to trigger an Interrupt Service Routine (ISR). Unlike the blocking delay() function, a hardware timer interrupt allows your microcontroller to maintain precise timing in the background while the main loop continues to process sensors, communications, and user inputs.
Why Hardware Timers Beat the delay() Function
When you call delay(1000), the ATmega328P halts all main-line code execution for one second. It cannot read a sensor, update a display, or check a serial buffer during this window. Think of delay() like closing a one-lane bridge for scheduled maintenance; no other traffic can cross until the work is done. A hardware timer interrupt, by contrast, acts as a dedicated bypass lane. The main highway (your loop()) keeps flowing, while a toll booth operator (the ISR) is triggered at exact intervals to handle time-critical tasks without stopping the rest of the system.
This distinction is critical when building projects that require simultaneous operations, such as reading a rotary encoder while pulsing a stepper motor, or sampling an analog sensor at exactly 1kHz while streaming data over UART.
Project Spec Sheet and Pin Mapping
This guide targets the Arduino Uno R3 (ATmega328P-PU) running at 16MHz. Code written for these specific 8-bit AVR registers will not compile on ESP32 or ARM-based boards without significant modification.
Estimated Build Time: 15 minutes.
Parts List
- 1x Arduino Uno R3 (Rev3, official or high-quality clone with a 16MHz crystal, not a ceramic resonator)
- 1x 5mm LED (any color)
- 1x 220Ω through-hole resistor (1/4W)
- 1x 6x6mm tactile pushbutton switch
- 1x 10kΩ pull-down resistor for the button
- Solderless breadboard and 22 AWG solid jumper wires
Pin Mapping Table
| Component | Arduino Pin | ATmega328P Port | Notes |
|---|---|---|---|
| LED Anode | D13 | PB5 | Current limited by 220Ω resistor to GND |
| Pushbutton Output | D2 | PD2 | Wired to 5V, pulled down to GND via 10kΩ |
The Math: Calculating Prescaler and OCR1A Values
The ATmega328P runs on a 16MHz clock, meaning it executes 16,000,000 cycles per second. Timer1 is a 16-bit counter, meaning it can count from 0 up to 65,535 before overflowing. To get a 1-second interrupt, we cannot count to 16,000,000 directly. We must use a prescaler to divide the clock speed.
The formula for the Output Compare Register (OCR1A) in Clear Timer on Compare (CTC) mode is:
OCR1A = (f_clk / (prescaler * f_target)) - 1
Worked Example (1Hz Blink):
- Clock (
f_clk): 16,000,000 Hz - Target Frequency (
f_target): 1 Hz (1 second period) - Choose Prescaler: 1024
- Calculation: (16,000,000 / (1024 * 1)) - 1 = 15625 - 1 = 15624
Since 15624 is well below the 16-bit maximum of 65535, this prescaler works perfectly. We load 15624 into OCR1A.
Complete Compilable Code with ISR Overrun Protection
The code below configures Timer1 for a 1Hz interrupt. It includes an ISR overrun guard. In embedded systems, 'error handling' inside an interrupt means preventing system lockups. If your ISR takes longer to execute than the timer interval, the interrupt will fire again before the first one finishes, causing a stack overflow and a frozen board. This code detects that condition and flags an error.
// Pin Definitions
const int LED_PIN = 13;
const int BUTTON_PIN = 2;
// Volatile variables shared between ISR and main loop
volatile bool isr_executing = false;
volatile bool isr_overrun_error = false;
volatile uint32_t button_press_count = 0;
volatile bool led_state = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT); // External 10k pull-down used
Serial.begin(115200);
Serial.println(F("Timer1 Interrupt Initialized."));
// --- Timer1 Configuration (16-bit) ---
cli(); // Disable global interrupts during setup
TCCR1A = 0; // Clear Timer/Counter Control Register A
TCCR1B = 0; // Clear Timer/Counter Control Register B
TCNT1 = 0; // Initialize counter value to 0
// Set compare match register for 1Hz increments
OCR1A = 15624; // (16,000,000 / (1024 * 1)) - 1
// Turn on CTC mode (Clear Timer on Compare)
TCCR1B |= (1 << WGM12);
// Set CS12 and CS10 bits for 1024 prescaler
TCCR1B |= (1 << CS12) | (1 << CS10);
// Enable timer compare interrupt
TIMSK1 |= (1 << OCIE1A);
sei(); // Enable global interrupts
}
void loop() {
// Non-blocking button read
if (digitalRead(BUTTON_PIN) == HIGH) {
button_press_count++;
delay(50); // Simple software debounce (acceptable here as it doesn't block the ISR)
}
// Check for ISR overrun errors
if (isr_overrun_error) {
Serial.println(F("CRITICAL ERROR: ISR Overrun detected! Execution time exceeds timer period."));
isr_overrun_error = false; // Reset flag
}
// Print status every 2 seconds (non-blocking)
static uint32_t last_print = 0;
if (millis() - last_print >= 2000) {
last_print = millis();
Serial.print(F("Button presses: "));
Serial.println(button_press_count);
}
}
// --- Interrupt Service Routine ---
ISR(TIMER1_COMPA_vect) {
// Overrun Guard: Check if previous ISR is still running
if (isr_executing) {
isr_overrun_error = true;
return; // Exit immediately to prevent stack corruption
}
isr_executing = true;
// Toggle LED state
led_state = !led_state;
digitalWrite(LED_PIN, led_state ? HIGH : LOW);
isr_executing = false;
}
Debugging: When Your Timer Interrupt Fails
If you are integrating this code into a larger project, you may encounter the following exact compiler error string in the Arduino IDE output console:
c:/users/.../avr8-gnu-toolchain/bin/../lib/gcc/avr/7.3.0/../../../../avr/bin/ld.exe: wiring.c.o (symbol from plugin): in function `__vector_11':
(.text+0x0): multiple definition of `__vector_11'
This error means two different pieces of code are trying to claim the exact same hardware interrupt vector. For the ATmega328P, __vector_11 corresponds to Timer1 Compare Match A.
Ranked Causes for Vector Conflicts
- The Servo Library: The standard Arduino
Servo.hlibrary hijacks Timer1 to generate precise PWM pulses for RC servos. You cannot useServo.hand raw Timer1 interrupts simultaneously. - The IRremote Library: Older versions of
IRremote.hdefault to Timer1 for decoding infrared signals. - The Tone Function: While
tone()typically uses Timer2, some third-party audio libraries force it onto Timer1.
The First Three Things to Check When It Fails
- Audit your included libraries: Check if any library claims Timer1. If you need servos and Timer1 interrupts, switch your custom timer code to use Timer2 (an 8-bit timer) or use the
ServoTimer2library to free up Timer1. - Verify the
volatilekeyword: If your main loop isn't reacting to variables changed inside the ISR, ensure every shared variable is declared asvolatile. Without it, the GCC compiler optimizes the code by caching the variable in a CPU register, completely ignoring the SRAM updates made by the ISR. - Check for Global Interrupt Enable: Ensure
sei()is called at the end of yoursetup(). If you forget this, the timer will count and compare, but the global interrupt mask will prevent the CPU from jumping to the ISR.
How to Extend or Simplify the Build
To Simplify: If raw register manipulation feels brittle, use the TimerOne library. It abstracts the prescaler math and register flags into simple commands like Timer1.initialize(1000000) and Timer1.attachInterrupt(blinkLED). The trade-off is a slight increase in compiled flash size and a minor overhead in ISR latency.
To Extend: If you need to migrate this architecture to an ESP32, the AVR register code will fail. The ESP32 uses a completely different hardware timer API. You will need to use hw_timer_t *timer = timerBegin(0, 80, true); and timerAttachInterrupt(). Furthermore, on the ESP32, ISRs should ideally be placed in IRAM using the IRAM_ATTR attribute to prevent cache-miss panics during flash operations.
Frequently Asked Questions
Can I use a time interrupt Arduino setup on an ESP32?
No, the code provided above is strictly for 8-bit AVR microcontrollers (ATmega328P/ATmega2560). The ESP32 uses a 32-bit Xtensa architecture with a dedicated timer group peripheral. You must use the ESP32's hw_timer_t API or the newer Arduino-ESP32 core ESP32TimerInterrupt library to achieve the same non-blocking timing results.
What happens if my ISR takes longer than the timer interval?
If your ISR execution time exceeds the timer period, the interrupt flag will remain set. As soon as the ISR finishes and re-enables interrupts, it will immediately fire again. This creates an infinite loop inside the interrupt vector, effectively freezing your main loop() and locking up the microcontroller. This is why the overrun guard included in the code above is critical for production firmware.
Why must variables shared with an ISR be declared as volatile?
The C++ compiler aggressively optimizes code. If it sees a variable in the loop() that is never explicitly modified inside the loop() itself, it will cache that variable's value in a CPU register to save time. When the ISR updates the actual memory address in SRAM, the main loop never sees the change because it keeps reading the stale cached register. The volatile keyword forces the compiler to read the variable directly from SRAM every single time it is referenced. For more details, see the official Arduino volatile reference.
How do I pause and resume a hardware timer interrupt?
To pause the timer without losing your configuration, clear the clock select bits in the TCCR1B register: TCCR1B &= ~((1 << CS12) | (1 << CS11) | (1 << CS10));. This disconnects the clock source, freezing the counter. To resume, simply write the prescaler bits back to TCCR1B exactly as you did in the setup() function. For deeper register-level details, consult the Microchip ATmega328P datasheet.






