How to use ESP32 timers: stop using delay() in your firmware
- 1 ESP32 GPIO explained: inputs, outputs, pull-ups and interrupts
- 2 How to use ESP32 timers: stop using delay() in your firmware
- 3 ESP32 MQTT tutorial: publish and subscribe with PubSubClient
- 4 ESP32 FreeRTOS: tasks, queues and semaphores in plain English
- 5 ESP32 watchdog timer: build firmware that recovers from crashes
- 6 ESP32 OTA updates: push firmware wirelessly in production
Every beginner learns delay(). Every professional stops using it.
Here’s why — and what to do instead.
The problem with delay()
When you call delay(1000), the ESP32 does absolutely nothing for one second. No sensor reads. No button checks. No MQTT messages processed. If you’re blinking an LED, that’s fine. If you’re building real firmware, it’s a disaster.
// This firmware is broken for any real use case
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(1000); // nothing else runs for 1 second
digitalWrite(LED_PIN, LOW);
delay(1000); // nothing else runs for another second
}
The moment you add a second feature — reading a button, checking a sensor, handling MQTT — delay() makes the whole system sluggish or completely unresponsive.
Solution 1: millis()-based timing
The simplest fix. Track time with millis() instead of blocking.
#define LED_PIN 2
#define BLINK_MS 500
unsigned long lastBlink = 0;
bool ledState = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - lastBlink >= BLINK_MS) {
lastBlink = now;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
// Everything else runs here freely — no blocking
readSensors();
handleMQTT();
checkButtons();
}
loop() runs hundreds of times per second. Each feature checks if enough time has passed and acts accordingly. Nothing blocks anything else.
Solution 2: ESP32 hardware timers
For precision timing — pulse generation, encoder reading, exact intervals — use the ESP32’s hardware timers directly.
#include "esp_timer.h"
#define TIMER_INTERVAL_US 500000 // 500ms in microseconds
esp_timer_handle_t blinkTimer;
void IRAM_ATTR onTimer(void* arg) {
// Runs in ISR context — keep it short
static bool state = false;
state = !state;
digitalWrite(LED_PIN, state);
}
void setup() {
pinMode(LED_PIN, OUTPUT);
esp_timer_create_args_t args = {
.callback = onTimer,
.name = "blink"
};
esp_timer_create(&args, &blinkTimer);
esp_timer_start_periodic(blinkTimer, TIMER_INTERVAL_US);
}
void loop() {
// LED blinks independently of everything in loop()
}
Hardware timers fire with microsecond precision regardless of what loop() is doing.
Solution 3: Ticker library (easiest)
If you don’t want to touch the ESP-IDF timer API directly, the Ticker library wraps it cleanly:
#include <Ticker.h>
Ticker blinkTicker;
Ticker sensorTicker;
void blinkLED() {
digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}
void readSensor() {
float temp = readTemperature();
Serial.println(temp);
}
void setup() {
pinMode(LED_PIN, OUTPUT);
blinkTicker.attach(0.5, blinkLED); // every 500ms
sensorTicker.attach(5.0, readSensor); // every 5s
}
void loop() {
// Completely free for other work
}
Two independent timers, zero blocking, clean code.
Which approach to use
| Situation | Use |
|---|---|
| Simple multi-task firmware | millis() pattern |
| Precise intervals (< 1ms) | Hardware timer via esp_timer |
| Quick setup, readable code | Ticker library |
| Real-time OS needed | FreeRTOS tasks (see next post) |
The rule
If you find yourself writing delay() in any firmware that does more than one thing, stop and use one of these patterns instead. Your firmware will be more responsive, more reliable, and easier to extend.
In the next post we’ll look at FreeRTOS tasks — the right tool when you need true parallel execution on the ESP32’s dual cores.
Related posts
Comments
Enjoyed this tutorial?
Get new ESP32, Arduino, and industrial IoT tutorials straight to your inbox — no spam, unsubscribe anytime.