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.
Where the interval comes from
Three ways to do something every N milliseconds
| Method | Exact? | Blocks? | Use when |
|---|---|---|---|
delay(n) | No | Yes | Never, past the first sketch |
millis() comparison | Roughly | No | Anything human-paced — a display, a poll |
| Hardware timer | Yes | No | Sampling, 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.
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.
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.
MicroPython wraps the same hardware. period is in milliseconds and the callback runs in interrupt context, so it obeys the same rules.
from machine import Timer, ADC, Pin
adc = ADC(Pin(34))
latest = 0
def sample(t):
global latest
latest = adc.read_u16()
Timer(0).init(period=10, mode=Timer.PERIODIC, callback=sample)
while True:
print(latest)
import time; time.sleep(0.5)Timer(-1) asks for a software timer, which is easier and less exact. A numbered timer is the hardware one - use it when the interval has to be right.
When it does not work
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 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.
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.
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.
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.