ESP32/Interrupts and timing/19. Hardware timers
Your chip
Your language
Interrupts and timing · 19 of 81

Hardware timers

delay stops everything and is never exact. A hardware timer keeps time in silicon whether your code is busy or not, which is the difference between a sensor that samples evenly and one that samples whenever it gets round to it.

/esp32/hardware-timers · arduino · S3

Where the interval comes from

divider 80, alarm 1,000
every 1 ms
Clock divider80 → tick = 1.0 µs
Alarm value, in ticks1,000
Interval
1 ms
Rate
1.0 kHz
delay() drifts by
1.0 µs per tick × 1,000 ticks = 1 ms. A divider of 80 gives a 1 µs tick, which is why almost every example uses it: the alarm value is then simply microseconds. The counter is 64-bit, so even this tick will not run out for 72 minutes.

Three ways to do something every N milliseconds

MethodExact?Blocks?Use when
delay(n)NoYesNever, past the first sketch
millis() comparisonRoughlyNoAnything human-paced — a display, a poll
Hardware timerYesNoSampling, stepping, anything you will call a frequency

The millis() pattern is the workhorse and it is worth knowing by heart:

static unsigned long last = 0;
if (millis() - last >= 1000) { last += 1000; doTheThing(); }

Adding the interval rather than assigning millis() is what keeps it from drifting.

The other kind of timer

FreeRTOS also gives you vTaskDelay, which suspends your task and lets the rest of the system run. It is not exact either, but it is the right way for a task to wait — a task that busy-waits starves the idle task, and the idle task is what feeds the watchdog.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

Arduino core 3.x changed this API - you now give timerBegin a frequency directly instead of a divider. A 1 MHz timer means alarm values are simply microseconds.

timer_sample.ino
hw_timer_t *timer = nullptr;
volatile bool tick = false;

void IRAM_ATTR onTimer() { tick = true; }

void setup() {
  Serial.begin(115200);
  timer = timerBegin(1000000);              // 1 MHz, so 1 tick = 1 µs
  timerAttachInterrupt(timer, &onTimer);
  timerAlarm(timer, 10000, true, 0);        // every 10000 µs = 100 Hz
}

void loop() {
  if (tick) {
    tick = false;
    Serial.println(analogRead(34));
  }
}

The ISR sets a flag and returns. Sampling, printing and deciding all happen in loop. That separation is what stops a timer interrupt from becoming a watchdog reset.

When it does not work

The interval is right on average and wrong every time

You are using delay or millis, not a timer. Everything else on the chip - Wi-Fi housekeeping, a slow sensor - adds to a delay and never subtracts. A hardware timer does not care what the CPU is doing.

The board resets as soon as the timer starts

The ISR is doing too much. Printing, allocating and touching the flash from interrupt context all end this way. Set a flag, return, and do the work in loop.

Example code from a tutorial does not compile

The API changed in Arduino core 3.0. timerBegin now takes a frequency, and timerAlarmWrite became timerAlarm. Almost every tutorial online is still on the old one.

Two timers interfere

There are four on the classic ESP32 and two on the C3. Ask for a fifth and you get a null pointer, which crashes on first use rather than at setup.

Where this goes next

A timer counts the clock. The pulse counter counts a pin, in hardware, whether or not your code is looking.

Counting pulses

Edit this page — content/esp32/hardware-timers.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32 on the forum