Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to keep time without delay()

millis() and elapsed time, a superloop of timed jobs, a state machine, a hardware timer, a FreeRTOS task delay, or a sleep timer. How precise, how many jobs, and how much power decide which.

delay() stops everything else, so the question is what should keep running while you wait. Ask how many things run at once, how exact the timing must be, and whether the chip should sleep in between.

  • One or a few periodic jobs, a few milliseconds of jitter is fine → check millis() in loop(): if (now - start >= interval), with every time in uint32_t. It survives the 49.7-day wrap.
  • Several jobs, each on its own period → a cooperative superloop: each job has its own last-run time and returns quickly. One slow or blocking job delays all the others.
  • Behaviour that depends on what already happened (a button that means different things in different modes) → a finite-state machine, stepped from the same non-blocking loop.
  • Timing that must be exact, or faster than a loop pass → a hardware timer: an alarm interrupt, a compare output (PWM), or a capture of an input edge.
  • Jobs that must preempt each other, with priorities → FreeRTOS tasks that wait with vTaskDelay, or vTaskDelayUntil for a fixed rhythm. On the ESP32-S3 Arduino core, loop() is already a task.
  • Long gaps on a battery → don't wait awake at all: deep-sleep with a timer wake source, which restarts the sketch at setup().

Why not delay()

During delay() nothing else in loop() runs, so a press can be missed; worst-case reaction time is the sum of the waits.

Code that makes the program wait, such as delay(), so nothing else can run.

Elapsed time in loop()

Compare elapsed time, never two moments, and keep both in uint32_t.

How much time has passed since a saved moment: now minus then.

Example

start = 4,294,960,000 and now = 1,200 after the wrap: now - start = 8,496 ms, the true elapsed time.
When a counter passes its maximum and starts again from zero.

The same elapsed-time test, used to ignore contact bounce.

Many jobs in one loop

A superloop of short jobs, each run when its own timer is due, and a tiny scheduler table.

States, events and transitions, coded with switch or a transition table.

The basic firmware structure: set up once, then repeat one loop forever.
A design where the program is always in one of a few named states and events move it between them.

Hardware timers

A counter clocked through a prescaler; compare for alarms and PWM, capture for periods, counting for pulses.

When an edge deserves an interrupt and when a loop that polls is enough.

A divider that slows a clock before it reaches a timer.

Example

An 80 MHz clock through a prescaler of 80 ticks once a microsecond, so a 500 ms alarm is 500,000 counts.
A peripheral that counts clock ticks and can trigger events or generate PWM.

An RTOS

A superloop waits for each job to return; a preemptive scheduler can stop one, at a cost in RAM and shared-data safety.

vTaskDelay(pdMS_TO_TICKS(ms)) blocks only this task; vTaskDelayUntil keeps a fixed rhythm.

Scheduling where the system can interrupt a running task to run a more urgent one.
The RTOS's regular timer interrupt, which counts time.

Sleep between events

A timer wake source in microseconds; deep sleep restarts at setup(), light sleep carries on.

An event that wakes the chip from sleep.