If you need a hardware timer interrupt on an Arduino Uno (ATmega328P) running at exactly 10Hz without relying on millis() drift, you must configure Timer1 in Clear Timer on Compare (CTC) mode using a 256 prescaler and set OCR1A = 6249. This bare-metal approach guarantees microsecond-level precision, bypassing the software overhead of the Arduino core.

Relying on millis() inside the loop() works for blinking LEDs, but it fails the moment your main loop blocks on a sensor read or a heavy serial print. A true hardware timer interrupt fires independently of your code's execution state. Below is the complete jobsite-tested guide to setting up, verifying, and debugging AVR timer interrupts.

Project Spec Sheet & Difficulty Rating

Difficulty: Intermediate | Time to Complete: 30 Minutes | Target Board: Arduino Uno R3 (ATmega328P, 16MHz Crystal)
Component Exact Variant / Specification Estimated Cost (2026)
Microcontroller Arduino Uno R3 (ATmega328P-PU DIP-28) $24.00 - $28.00
Verification Tool Digital Storage Oscilloscope (or Logic Analyzer) $60.00+ (e.g., Rigol DS1054Z or Saleae Logic)
Wiring 22 AWG solid core hookup wire $5.00 / spool

Why Bypass millis() for Hardware Timers?

The Arduino millis() function is driven by Timer0, which overflows every ~1ms. While convenient, it requires the CPU to constantly poll the timer flag inside the main loop. If your loop takes 50ms to execute a blocking delay() or a slow I2C transaction, your timing drifts.

By contrast, configuring Timer1 (a 16-bit timer) in CTC mode offloads the timing to dedicated silicon. The hardware counts clock pulses, and when the counter matches the value in the Output Compare Register (OCR1A), it triggers an interrupt vector. The CPU pauses the main loop, executes the Interrupt Service Routine (ISR) in microseconds, and resumes. This is critical for applications like:

  • Generating precise stepper motor step pulses.
  • Sampling ADC data at a fixed rate for DSP (Digital Signal Processing).
  • Maintaining real-time state machines in robotics where loop latency varies.

For a deep dive into the underlying silicon registers, Microchip's official ATmega328P datasheet (Section 16: 16-bit Timer/Counter1) is the definitive reference.

Pin Mapping & Hardware Setup

While Timer1 can output hardware PWM directly to specific pins, we are using it to trigger a software ISR. We will toggle Pin 13 (the onboard LED) inside the ISR to verify the 10Hz frequency visually, and expose Pin 9 for oscilloscope verification.

Arduino Uno Pin ATmega328P Port Timer1 Function Role in This Build
D13 PB5 (PORTB5) None (General I/O) ISR toggled LED (Visual 10Hz verification)
D9 PB1 (OC1A) Timer1 Output Compare A Reserved for hardware PWM/scope probe (Optional)
D8 PB0 (ICP1) Input Capture Unit Unused (Reserved for future pulse-width measurement)

Complete AVR Timer1 Interrupt Code

This code targets the Arduino Uno R3 (ATmega328P). It calculates a 10Hz interrupt (100ms period) using the 16MHz system clock and a 256 prescaler.

The Math: 16,000,000 Hz / 256 = 62,500 ticks per second. For a 10Hz signal (0.1s period), we need 6,250 ticks. Since the counter starts at 0, we set the compare register to 6,249.

#include <Arduino.h>
#include <avr/interrupt.h>

// --- Pin Definitions ---
const uint8_t LED_PIN = 13;    // Onboard LED (PB5)
const uint8_t SCOPE_PIN = 9;   // OC1A Hardware Pin (PB1)

// --- Shared State Variables ---
// MUST be volatile to prevent compiler optimization from caching them in registers
volatile bool isrTriggered = false;
volatile uint32_t isrExecutionCount = 0;

void setupTimer1() {
  cli(); // 1. Disable global interrupts during setup

  // 2. Reset Timer1 control registers to default
  TCCR1A = 0; 
  TCCR1B = 0;
  TCNT1  = 0; // Reset counter value

  // 3. Set compare match register for 10Hz increments
  // 16MHz / 256 prescaler / 10Hz - 1 = 6249
  OCR1A = 6249;

  // 4. Turn on CTC mode (Clear Timer on Compare Match)
  // Set WGM12 bit in TCCR1B
  TCCR1B |= (1 << WGM12);

  // 5. Set CS12 bit for 256 prescaler
  TCCR1B |= (1 << CS12);

  // 6. Enable timer compare interrupt A
  TIMSK1 |= (1 << OCIE1A);

  sei(); // 7. Re-enable global interrupts
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(SCOPE_PIN, OUTPUT);
  Serial.begin(115200);
  
  // Error handling: Wait for Serial monitor with a timeout
  unsigned long start = millis();
  while (!Serial && (millis() - start < 2000)) {
    // Yield to hardware
  }
  
  setupTimer1();
  
  if (Serial) {
    Serial.println(F("Timer1 Interrupt Initialized at 10Hz."));
    Serial.println(F("Verify Pin 13 visual blink or probe Pin 9 with oscilloscope."));
  }
}

// --- Interrupt Service Routine ---
// Fires exactly every 100ms (10Hz)
ISR(TIMER1_COMPA_vect) {
  isrTriggered = true;
  isrExecutionCount++;
  
  // Direct port manipulation for ultra-fast, jitter-free pin toggle
  // PORTB5 corresponds to Arduino Pin 13
  PORTB ^= (1 << PORTB5); 
}

void loop() {
  // Handle the interrupt flag in the main loop to avoid blocking the ISR
  if (isrTriggered) {
    isrTriggered = false; // Clear flag immediately
    
    // Safe to use Serial.print here, NOT inside the ISR
    if (Serial) {
      Serial.print(F("ISR Count: "));
      Serial.println(isrExecutionCount);
    }
    
    // Execute non-time-critical tasks here
    // e.g., updating a display, reading slow sensors
  }
  
  // Main loop is free to do other work or sleep
}

Debugging: Fixing "expected constructor... before 'void'" & Other Failures

When working with bare-metal AVR interrupts, the compiler errors can be cryptic. The most common syntax error occurs when defining the ISR.

Exact Error String:
error: expected constructor, destructor, or type conversion before 'void'
Root Cause: You wrote void ISR(TIMER1_COMPA_vect) { ... }. The ISR() macro in <avr/interrupt.h> already injects the return type and compiler attributes. Prepending void results in invalid C++ syntax.
Fix: Remove the void keyword. Use ISR(TIMER1_COMPA_vect) { ... }.

The First Three Things to Check When It Fails

  1. Missing Headers: Ensure #include <avr/interrupt.h> is at the top of your sketch. Without it, cli(), sei(), and ISR() are undefined.
  2. Vector Name Typos: The vector name must exactly match the datasheet. TIMER1_COMPA_vect is correct for Timer1 Compare A. Using TIMER1_COMP_vect or TIM1_COMPA_vect will throw a "not declared in this scope" error or silently fail to link.
  3. Missing volatile Keyword: If your ISR updates a variable read in the loop(), it must be declared volatile. Without it, the GCC compiler optimizes the main loop to read the variable from a CPU register only once, completely ignoring the ISR's updates.

Dealing with Vector Collisions

If you see multiple definition of '__vector_11', another library (like Servo.h or TimerOne.h) has already claimed Timer1's Compare A interrupt. You cannot have two ISRs for the same vector. You must either disable the conflicting library or switch your bare-metal code to use Timer2 (TIMER2_COMPA_vect).

Extending and Simplifying the Build

How to Simplify: If bare-metal register math feels brittle across different board variants, use the TimerOne library. It abstracts the prescaler calculations into a simple Timer1.initialize(100000); (microseconds) call. However, note that libraries add slight overhead and obscure the underlying hardware behavior.

How to Extend: To turn this from a simple timer into a precision measurement tool, enable the Input Capture Unit (ICU). By setting the ICES1 bit in TCCR1B and routing an external signal to Pin 8 (ICP1), the hardware will instantly latch the exact TCNT1 timestamp of a rising or falling edge into the ICR1 register. This allows you to measure PWM duty cycles and pulse widths with zero CPU jitter, a technique heavily used in RC receiver decoding and ultrasonic anemometry.

Frequently Asked Questions

Can I use this exact Arduino timer interrupt code on the ESP32?

No. The ESP32 uses an Xtensa (or RISC-V) architecture, not AVR. It does not have TCCR1A or OCR1A registers. Instead, the ESP32 utilizes a dedicated hardware timer peripheral accessed via the ESP-IDF timerBegin(), timerAttachInterrupt(), and timerAlarmWrite() functions. Attempting to compile AVR-GCC register code on an ESP32 will result in immediate "not declared in this scope" errors.

Why is my Arduino timer interrupt skipping beats or causing USB disconnects?

This happens when your ISR takes too long to execute, or worse, when you put Serial.print() inside the ISR. The Arduino serial port relies on its own interrupts (UART TX/RX buffers). If your Timer ISR disables global interrupts or blocks waiting for the serial buffer to clear, you create a deadlock or cause the main loop to starve, leading to USB watchdog resets. Always keep ISRs under 5 microseconds: toggle a pin, set a volatile flag, and exit immediately.

How do I calculate the prescaler and OCR value for a custom frequency?

Use the formula: OCR = (Clock_Speed / (Prescaler * Target_Frequency)) - 1. The ATmega328P Timer1 supports prescalers of 1, 8, 64, 256, and 1024. Choose the smallest prescaler that keeps the resulting OCR value under 65,535 (the 16-bit maximum). For example, for a 50Hz interrupt: 16,000,000 / (256 * 50) - 1 = 1249. A smaller prescaler (like 64) would yield an OCR of 4999, which also fits and provides slightly higher timing resolution.