The Short Answer: Why Using TimerOne with ESP32 Fails
If you are attempting to port an Arduino Uno project to the ESP32 and your code includes #include <TimerOne.h>, your compilation will fail. You cannot use the standard Arduino TimerOne library on an ESP32.
When you attempt to compile, the IDE will throw one of two exact error strings:
fatal error: TimerOne.h: No such file or directory(if the library isn't installed for the ESP32 architecture).#error "This library only supports AVRs with Timer1."(if the library is installed but the pre-processor catches the architecture mismatch).
The technical reason: The TimerOne library is hardcoded to manipulate AVR-specific 16-bit Timer/Counter1 registers (like TCCR1A, TCCR1B, and OCR1A). The ESP32 uses a completely different Xtensa LX6 (or RISC-V on newer C-series chips) architecture. It does not have AVR hardware timers. Instead, the ESP32 utilizes its own Timer Group peripherals, which are managed via the ESP-IDF and the ESP32 Arduino Core's native hw_timer_t API.
To achieve the exact same microsecond-precision hardware interrupts you used TimerOne for on an AVR, you must switch to the native ESP32 hardware timer API. The rest of this guide shows you exactly how to do that.
Parts List and Board Variant Specifications
This guide and the provided code specifically target the most common ESP32 development board on the market. Using a different variant (like the ESP32-S3 or ESP32-C3) requires minor adjustments to GPIO strapping pins, which we will cover in the FAQ.
| Component | Specification / Model | Notes |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (30-pin) | Target board for this code. Ensure you select 'DOIT ESP32 DEVKIT V1' or 'ESP32 Dev Module' in the IDE. |
| Arduino Core | Espressif ESP32 Core v2.0.14 or v3.0.x | The modern timerBegin() API used in our code requires v2.0.11 or higher. Do not use legacy v1.0.x. |
| Measurement Tool | Digital Oscilloscope or Logic Analyzer | Required to verify the 1kHz square wave timing. Multimeters cannot capture microsecond ISR jitter. |
| Wiring | 22 AWG Silicone Wire / Jumper Dupont | For routing GPIO 2 to external loads if not using the onboard LED. |
The Native ESP32 Alternative: Hardware Timer API
In older versions of the ESP32 Arduino Core (v1.x), setting up a timer required configuring a prescaler and a counter limit manually. In the modern v2.x and v3.x cores, Espressif simplified this to mimic standard frequency-based setups, making the migration from TimerOne much easier.
Pin Mapping and GPIO Selection
When routing timer outputs or toggling pins inside an Interrupt Service Routine (ISR), you must avoid strapping pins and boot-mode pins. Here is the safe pin mapping for this build:
| Function | GPIO Pin | Hardware Notes & Restrictions |
|---|---|---|
| Timer Output / LED | GPIO 2 | Connected to the built-in blue LED on most DevKits. Safe for output. Pulled down on boot. |
| Serial TX | GPIO 1 | Do not use. Routed to USB-UART bridge. |
| Serial RX | GPIO 3 | Do not use. Routed to USB-UART bridge. |
| Boot Strapping | GPIO 0 | Avoid for ISR outputs. Must be HIGH on boot; toggling it during reset can cause boot failures. |
Complete Compilable Code: ESP32 Hardware Timer Interrupt
The following code replicates a classic TimerOne use case: firing an interrupt exactly every 1,000 microseconds (1 millisecond) to generate a precise 500Hz square wave (1kHz full cycle) on GPIO 2.
Crucial Embedded Detail: We do not use digitalWrite() inside the ISR. On the ESP32, digitalWrite() interacts with the RTOS pin mapping layer and can trigger a Watchdog Timer (WDT) panic if called from an interrupt context. Instead, we use direct GPIO register manipulation (GPIO.out_w1ts and GPIO.out_w1tc) for zero-latency, ISR-safe toggling.
/*
* ESP32 Native Hardware Timer Interrupt
* Target: ESP32-WROOM-32 DevKit V1 (30-pin)
* Core: ESP32 Arduino Core v2.0.x / v3.0.x
* Purpose: Replicates TimerOne.initialize(1000) functionality
*/
// Pin Definitions
#define LED_PIN 2 // Built-in LED on most ESP32 DevKits
// Hardware timer pointer
hw_timer_t * timer = NULL;
// ISR-safe state tracking
volatile bool pinState = false;
// ARDUINO_ISR_ATTR places this function in IRAM for fast execution
void ARDUINO_ISR_ATTR onTimer() {
// Direct register manipulation for ISR safety (GPIO 0-31 only)
if (pinState) {
GPIO.out_w1tc = (1 << LED_PIN); // Clear pin (LOW)
pinState = false;
} else {
GPIO.out_w1ts = (1 << LED_PIN); // Set pin (HIGH)
pinState = true;
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
// 1. Initialize Timer at 1 MHz resolution (1 tick = 1 microsecond)
timer = timerBegin(1000000);
// Error Handling: Check if timer allocation failed
if (timer == NULL) {
Serial.println("[ERROR] Failed to allocate hardware timer. Halting.");
while (true) { delay(1000); }
}
// 2. Attach the ISR callback function
timerAttachInterrupt(timer, &onTimer);
// 3. Set the alarm to trigger every 1000 ticks (1000us = 1ms)
// Parameters: timer, alarm_value, autoreload, reload_count
timerAlarm(timer, 1000, true, 0);
Serial.println("[INFO] Hardware timer started. 1ms ISR active on GPIO 2.");
}
void loop() {
// Main loop is free for Wi-Fi, BLE, or sensor polling.
// The timer runs entirely in hardware/ISR context.
delay(1000);
}
Troubleshooting: First Three Things to Check When It Fails
If your ESP32 compiles but crashes, reboots, or outputs a jittery signal, check these three ranked failure modes. These are the most common pitfalls when migrating from AVR TimerOne to the ESP32.
1. The ISR Watchdog Timeout (Guru Meditation Error)
Exact Error String: Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
Cause: You are doing too much work inside the onTimer() function. The ESP32 runs FreeRTOS. If your ISR takes longer than a few microseconds (e.g., you put Serial.println(), delay(), or digitalWrite() inside it), the RTOS Interrupt Watchdog assumes the CPU is locked and reboots the chip.
Fix: Keep the ISR strictly to flag-setting or direct register toggles. Move all heavy processing, I2C reads, or Serial prints to the main loop() and trigger them using a volatile bool flag.
2. Core Version API Mismatch
Exact Error String: no matching function for call to 'timerBegin(int, int, bool)'
Cause: You copied legacy code from an ESP32 Core v1.0.x tutorial. The old API required passing a prescaler and count-up boolean. The modern v2.x/v3.x API only takes the base frequency.
Fix: Update your ESP32 Board Manager package to v2.0.14 or v3.0.x, and use the timerBegin(1000000) syntax provided in our code block above.
3. Interrupt Starvation from Wi-Fi/BT
Symptom: The timer works perfectly until you connect to Wi-Fi, at which point the oscilloscope shows missed pulses or severe timing jitter.
Cause: The ESP32's Wi-Fi and Bluetooth stacks run on Core 0 and Core 1 with high-priority RTOS tasks. If your timer interrupt is routed to a core currently processing a Wi-Fi beacon, it may be delayed by 10-50 microseconds.
Fix: If you need absolute microsecond precision while using Wi-Fi, use the ESP32's LEDC (LED Control) peripheral for hardware PWM instead of a software ISR toggle. The LEDC peripheral operates entirely independent of the CPU cores.
Extending and Simplifying the Build
Depending on your project requirements, bare-metal hardware timers might be overkill or insufficient. Here is how to scale this architecture.
- To Simplify (Millisecond Precision Only): If you do not need microsecond accuracy and just want a function to run every 500ms, abandon
hw_timer_tentirely. Use the built-inTickerlibrary (#include <Ticker.h>). It runs on the RTOS timer task, requires no IRAM allocation, and allows you to call standard functions without ISR restrictions. - To Extend (Multiple Independent Timers): If you need to manage 3 or 4 different timers with varying intervals (e.g., 10us, 1ms, and 50ms), writing bare-metal
hw_timer_tinstances gets messy. Install the ESP32TimerInterrupt library by Khoih-Hoang via the Library Manager. It provides a wrapper that mimics the AVR Timer library structure while safely managing the ESP32's four hardware timer groups under the hood.
Frequently Asked Questions
Can I use the TimerOne library on ESP32-S3 or ESP32-C3?
No. The TimerOne library is strictly for 8-bit AVR microcontrollers (ATmega328P, ATmega2560). Neither the dual-core Xtensa LX7 (ESP32-S3) nor the RISC-V (ESP32-C3) architectures support AVR register calls. You must use the hw_timer_t API or the ESP32TimerInterrupt library for all Espressif SoCs.
How do I replicate Timer1 PWM mode on the ESP32?
If you used Timer1.pwm(pin, duty) on an Arduino Uno, do not use hardware timer interrupts to generate PWM on the ESP32. Toggling pins in software introduces jitter. Instead, use the ESP32's dedicated LEDC (LED Control) peripheral via the ledcSetup() and ledcAttachPin() functions. The LEDC peripheral generates hardware-perfect PWM signals without any CPU intervention.
What is the maximum timer frequency on the ESP32?
The ESP32's Timer Group is clocked by the APB clock, which typically runs at 80 MHz. Therefore, the theoretical maximum resolution is 12.5 nanoseconds per tick. However, the overhead of entering and exiting the ISR takes roughly 1 to 2 microseconds. Practically, the fastest reliable software ISR toggle rate you can achieve is around 200kHz to 300kHz before the CPU spends 100% of its time just servicing the interrupt context switching.
Why does my ESP32 reboot when the timer interrupt fires?
This is almost always caused by an ISR Watchdog panic. You likely included a yield-blocking function inside the interrupt callback. Ensure your onTimer() function contains only volatile variable assignments or direct GPIO register writes. Never use Serial.print(), delay(), Wire.requestFrom(), or digitalRead() inside an ESP32 hardware timer ISR.






