How to use a timer interrupt to blink an LED at a fixed rate?

ampheoampheo
3 min read

Here’s a real-world pattern: use a hardware timer interrupt to blink an LED at a fixed rate while the main loop keeps doing other work (e.g., reading sensors, talking over serial). I’ll show an Arduino Uno (AVR) version first, then quick notes for ESP32/RP2040.


1) Arduino Uno/Nano (ATmega328P) — Timer1 interrupt + non-blocking main loop

What it does

  • Timer1 fires every 500 ms (2 Hz).

  • ISR just sets a flag (keeps ISR short).

  • loop() toggles the LED when it sees the flag, while still reading a sensor and printing data.

// --- Pins ---
const uint8_t LED_PIN = 13;   // On-board LED
const uint8_t SENSOR_PIN = A0;

// --- Shared state between ISR and loop ---
volatile bool tick500ms = false;   // set by ISR, read/cleared in loop

// Optional: data accumulation in loop
unsigned long lastPrintMs = 0;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(SENSOR_PIN, INPUT);
  Serial.begin(115200);

  // -------- Timer1 setup (CTC mode) --------
  // Goal: 500 ms interrupt (2 Hz)
  // F_CPU = 16 MHz, prescaler = 256
  // OCR1A = (16e6 / (256 * 2 Hz)) - 1 = 31249
  noInterrupts();
  TCCR1A = 0;                       // normal port operation
  TCCR1B = 0;
  TCCR1B |= (1 << WGM12);           // CTC mode (clear timer on compare)
  TCCR1B |= (1 << CS12);            // prescaler = 256
  OCR1A = 31249;                    // compare match value for 500 ms
  TCNT1 = 0;                        // reset counter
  TIMSK1 |= (1 << OCIE1A);          // enable compare A match interrupt
  interrupts();
}

void loop() {
  // --- Do regular work (non-blocking) ---
  int sensor = analogRead(SENSOR_PIN); // e.g., potentiometer or sensor
  unsigned long now = millis();

  // Print sensor every 250 ms (independent of LED blink)
  if (now - lastPrintMs >= 250) {
    lastPrintMs = now;
    Serial.print(F("Sensor="));
    Serial.println(sensor);
  }

  // --- Handle the 500 ms "tick" generated by the timer ISR ---
  if (tick500ms) {
    tick500ms = false;              // consume the event
    // Toggle LED
    static bool led = false;
    led = !led;
    digitalWrite(LED_PIN, led ? HIGH : LOW);
  }

  // The CPU is free to do more things here (parse serial, handle state machines, etc.)
}

// -------- Interrupt Service Routine --------
ISR(TIMER1_COMPA_vect) {
  // Keep it short: just set a flag
  tick500ms = true;
}

Why this is good practice

  • ISR is tiny (sets a flag only) → minimal jitter, safer for complex systems.

  • loop() stays responsive (reads sensors, prints, handles state machines).

  • Timing is rock solid (true hardware timer, not delay()).


2) ESP32 quick version (hardware timer)

#include <driver/timer.h>

const uint8_t LED_PIN = 2;
volatile bool tick500ms = false;
hw_timer_t* timer0 = nullptr;

void IRAM_ATTR onTimer() {
  tick500ms = true;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);

  // 80 MHz APB clock / 80 = 1 MHz timer -> 500,000 ticks = 500 ms
  timer0 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer0, &onTimer, true);
  timerAlarmWrite(timer0, 500000, true);  // 500 ms periodic
  timerAlarmEnable(timer0);
}

void loop() {
  // Do other work...
  if (tick500ms) {
    tick500ms = false;
    static bool led = false;
    led = !led;
    digitalWrite(LED_PIN, led);
  }
}

3) Raspberry Pi Pico (RP2040) quick version (hardware alarm)

#include "pico/stdlib.h"
#include "hardware/timer.h"

const uint LED_PIN = 25;
volatile bool tick500ms = false;

bool on_timer(repeating_timer_t *t) {
  tick500ms = true;
  return true; // keep repeating
}

int main() {
  stdio_init_all();
  gpio_init(LED_PIN);
  gpio_set_dir(LED_PIN, GPIO_OUT);

  repeating_timer_t timer;
  add_repeating_timer_ms(500, on_timer, nullptr, &timer);

  bool led = false;
  while (true) {
    // Do other work here (read sensors, UART, etc.)
    if (tick500ms) {
      tick500ms = false;
      led = !led;
      gpio_put(LED_PIN, led);
    }
  }
}

Pro tips

  • Keep ISRs short: set flags, push bytes to ring buffers, acknowledge hardware — nothing heavy.

  • No delay() in ISRs, avoid Serial.print() there.

  • Share data via volatile variables; for multi-byte data, disable interrupts or use atomic access.

  • Use hardware timers for precise periodic tasks; use millis() for coarse scheduling in loop().

0
Subscribe to my newsletter

Read articles from ampheo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ampheo
ampheo