Tickless Idle & Low Power¶
By default FreeRTOS wakes the CPU on every tick interrupt — typically every 1 ms — whether or not any task needs to run. On a battery-powered device that "wake up, check, go back to sleep" cycle can dominate power draw even when the system is 99% idle. Tickless idle mode lets the idle task tell the hardware "nothing is scheduled for the next N ticks — stop the tick interrupt and let the CPU sleep for real," waking exactly when the next timer or event actually needs attention.
How it works: suppressing the tick¶
Normally the idle task just spins (portSUPPRESS_TICKS_AND_SLEEP never
called). With tickless idle enabled, when the scheduler determines no task
is ready and finds the next unblock time is more than one tick away, it
calls into portSUPPRESS_TICKS_AND_SLEEP():
// FreeRTOSConfig.h
#define configUSE_TICKLESS_IDLE 1 // 1 = whenever possible, 2 = ESP-IDF's "always" style variant
The sequence, per the portable layer's default implementation:
- Idle task runs, scheduler computes
xExpectedIdleTime— how many ticks until the next task is due to wake (from a delay, timer, or blocked queue timeout). - If that's below
configEXPECTED_IDLE_TIME_BEFORE_SLEEP(default varies by port), it's not worth the sleep/wake overhead — busy-idle as normal. - Otherwise, the port stops the periodic tick source, reconfigures a
single-shot low-power timer for the expected idle duration, and puts the
core into a sleep mode (
WFI, or a deeper low-power state on ports that support it). - On wake (either the low-power timer firing, or any other interrupt), the
port corrects
xTickCountfor the number of ticks that were "missed" while asleep, based on how long the CPU actually slept, then resumes normal ticking.
That correction step is exactly why this needs port-specific support: the kernel must know how to read elapsed time from a hardware timer that kept running (or can report elapsed time) through the sleep, and it must guarantee no interrupt is lost in the process.
Application hooks around sleep¶
Two hooks let application code participate in the sleep decision and in power-domain management:
// Optional: veto or shorten a sleep, e.g. because a peripheral transaction
// is mid-flight and must not be interrupted by a clock change.
void vApplicationDeepSleepHook(void) {
if (uartTransactionInFlight()) {
// some ports check a global/flag here instead of a dedicated hook —
// consult your port's documentation for the exact veto mechanism
}
}
// Called by the port immediately before/after actually sleeping —
// typical use: switch peripheral clocks, disable unneeded domains.
void vApplicationSleepPriorToSleep(void) { disable_unneeded_peripherals(); }
void vApplicationSleepAfterSleep(void) { restore_peripheral_clocks(); }
Exact hook names and availability are port-specific (ESP-IDF, for example,
layers its own power-management API — esp_pm_configure() — on top of
this rather than exposing the raw hooks above). Always check your port's
port.c for the precise integration points before relying on a hook name.
The interrupt-latency tradeoff¶
Waking from a deep sleep state is not free — it costs cycles to restore clocks, PLLs, and peripheral state before the CPU can service an interrupt. That directly increases worst-case interrupt latency, which matters if the system has a hard real-time deadline on an external event:
// Trade: allow tickless idle, but never let it choose a sleep depth
// deeper than what your worst-case interrupt-latency budget allows.
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2 // ticks — tune, don't guess
A system with a 1 ms hard deadline on a GPIO interrupt cannot afford a sleep mode with a 5 ms wake latency, even if it saves substantial power — measure your specific low-power state's wake latency against your tightest ISR deadline before enabling anything deeper than the shallowest sleep mode your platform offers.
Traps¶
- Assuming tick-based delays stay tick-accurate across a sleep: the
kernel corrects
xTickCountfor elapsed sleep time, but any code reading a hardware timer/counter directly during that window (not going through the RTOS tick) may see a discontinuity if that timer itself was gated during sleep. Route all timing throughxTaskGetTickCount()or an always-running hardware timer, never a clock that pauses. configUSE_TICKLESS_IDLEalone does nothing without port support: not every FreeRTOS port implementsportSUPPRESS_TICKS_AND_SLEEP— on ports without it, the macro compiles but the idle task never actually sleeps deeper than aWFI/spin. Check the port layer, not just the config header.- Priority inversion via missed ticks: a task blocked with a short
relative timeout (
pdMS_TO_TICKS(2)) can wake later than expected if the system just entered a deep sleep and the wake-up correction has coarser granularity than the timeout itself — don't rely on sub-tick timing precision immediately after a tickless sleep window. - Peripheral clocks disabled mid-transaction: entering a deep sleep mode that gates a peripheral clock while a DMA transfer or UART byte is still in flight corrupts that transfer. Use the deep-sleep veto hook (or your port's equivalent) to hold off sleep during active I/O.
- Debugging is harder: many debuggers/JTAG probes lose the target (or show it as unresponsive) once the core clock stops in a deep sleep state. Keep a build variant with tickless idle disabled for active debugging sessions.
How It Actually Works¶
Normal FreeRTOS scheduling depends on a hardware timer interrupting every
single tick period — 1ms on ESP32 Arduino — purely to let the kernel check
"has anything's delay expired yet?" Tickless idle recognizes that this check
is wasted work when the idle task is the only thing Ready: instead of
letting the tick fire uselessly, vPortSuppressTicksAndSleep looks ahead in
the Delayed task list (which is already kept sorted by wake time) to find
the soonest wake-up, reprograms the timer hardware to fire exactly once at
that point instead of every 1ms, and then puts the core into a low-power
mode. When that single deferred interrupt fires, the kernel doesn't just
wake one task — it computes how many tick periods were "missed" while
sleeping and calls vTaskStepTick() to fast-forward the tick count
atomically, so every timer and delay in the system stays correctly
accounted for as if every individual tick had actually occurred. The
interrupt-latency tradeoff exists because waking from a deep sleep state
takes real microseconds-to-milliseconds for clocks/PLLs to stabilize — time
during which even a high-priority ISR cannot be serviced, which is why
tickless idle is disabled or bounded near any hard real-time deadline.
Cheat sheet¶
| Config / API | Purpose |
|---|---|
configUSE_TICKLESS_IDLE |
0 = classic ticking idle; 1 = sleep when idle time allows; 2 = ESP-IDF variant, more aggressive |
configEXPECTED_IDLE_TIME_BEFORE_SLEEP |
Minimum idle ticks before it's worth sleeping at all |
portSUPPRESS_TICKS_AND_SLEEP(ticks) |
Port-layer function that actually performs the sleep + tick correction |
xTaskGetTickCount() |
Always correct across sleeps — use for all app-level timing |
vApplicationSleepPriorToSleep/AfterSleep |
Hooks to gate/restore peripherals around a sleep |
esp_pm_configure() (ESP-IDF) |
Higher-level power-management config layered over tickless idle |
| Interrupt latency | Deeper sleep = higher wake latency — bound by your tightest ISR deadline |
Exercise¶
- Enable
configUSE_TICKLESS_IDLEon a port that supports it (ESP-IDF or a Cortex-M port with tickless support) on an otherwise-idle system with one task blocked onvTaskDelay(pdMS_TO_TICKS(2000)). Measure current draw (or, without hardware, log actual wake timestamps) with tickless idle on vs. off. - Add a hard real-time task that must respond to a GPIO interrupt within 1 ms. Measure observed interrupt latency with tickless idle enabled at your platform's deepest sleep mode, and again at its shallowest. State which sleep depth you'd ship for this deadline.
- Simulate an in-flight UART transaction that must not be interrupted by a clock-domain change: add the veto/hook logic and confirm the system holds off sleeping until the transaction completes, then sleeps immediately after.