Embedded Systems · Power Management
Tickless Idle and Timer Reprogramming
How a kernel suppresses the periodic tick while the system is idle, which timer it programs instead, and what the reprogramming costs in latency and in lost timekeeping.
A periodic system tick is the simplest way to build a scheduler: every tick interval the kernel increments a counter, checks for expired delays, and decides whether to reschedule. It is also a hard floor on idle energy, because a tick that fires every millisecond prevents the processor from sleeping for longer than a millisecond, regardless of whether anything needs attention. Tickless idle removes that floor by programming a different timer to fire when the next event is actually due.
The tick as an energy floor
At a 1 kHz tick, a device that could otherwise sleep for a second is woken 1000 times. If each wake costs even 20 µs of transition and handler time, the processor is awake for 2 percent of the time for no functional reason, and the sleep current it would have reached is replaced by an active current for that fraction. The energy penalty is not the handler’s work; it is the difference between the deepest reachable state and the one the tick allows.
The second cost of the tick is that it synchronises wakeups. Everything that derives its timing from the tick wakes at the same instants, so the device’s current profile has a large periodic spike rather than a spread of small events. On a system with a shared supply and a finite decoupling budget, the spike’s peak matters as much as its average.
Tickless operation is therefore not only about the number of wakeups. It converts a fixed periodic wake into a wake at the next scheduled event, which may be 200 µs away or 400 ms away, and the idle path then has the option to choose a sleep state proportional to the remaining interval.
The idle path and its decision
The idle loop, when nothing is runnable, must answer three questions in order: when the next event is due, how long it takes to enter and leave the candidate sleep state, and whether that state is compatible with the constraints the rest of the system has declared.
The first answer comes from the earliest of: the timer list, the next expiry of any armed delay, and any device with a scheduled completion. The kernel keeps these in a structure ordered by expiry, and reading the earliest is cheap if the structure is a heap or a sorted list. On systems with a single timer list, the earliest deadline is at the root and the read is constant time.
The second answer is a property of the hardware. Entering a state takes a known number of cycles and leaving it takes another, and both are documented per state. If the remaining interval is shorter than the round trip, entering the state is a net loss, and the idle loop should spin or enter a shallower state.
The third answer is the interesting one. A timer interrupt that can wake the core from a deep state is useful; a peripheral that needs the bus clock to complete a transfer is not. Devices declare latency constraints: a maximum time they can tolerate being unable to service an interrupt. The idle loop must choose a state whose exit latency is below the smallest declared constraint. This is the mechanism that makes wake-up latency a system property rather than a per-driver one, and the per-state numbers are the ones described in wake-up latency and its energy cost.
/* Idle selection: the deepest state whose exit latency fits the
earliest event and every registered device constraint. */
static enum sleep_state choose_state(uint64_t now, uint64_t next_event,
uint32_t max_latency_us)
{
uint64_t slack_us = (next_event - now) / NS_PER_US;
enum sleep_state best = SLEEP_NONE;
for (int s = SLEEP_DEEPEST; s >= SLEEP_LIGHT; s--) {
if (state_exit_latency_us[s] > slack_us)
continue;
if (state_exit_latency_us[s] > max_latency_us)
continue; /* a device forbids this state */
best = s;
break;
}
return best;
}
The loop descends from the deepest state, so the first candidate that satisfies both constraints wins. Ordering the states by depth and by latency is only valid if the two orders agree; on most parts they do, and where they do not, the table has to be built per part rather than assumed.
Reprogramming the wake timer
With the tick suppressed, the wake must be programmed explicitly. Two hardware arrangements exist.
In the first, the same counter that generated the tick is reprogrammed with a compare value at the next event. The counter keeps running, so timekeeping is preserved, and the interrupt is a compare match rather than a period match. The counter’s width sets the maximum sleep interval: a 16-bit counter at 32 kHz wraps every two seconds, and a 24-bit counter every 512 seconds. Wrapping while asleep is a correctness problem, not just a range problem, because the compare value is missed and the wake never happens.
In the second arrangement, the periodic tick source is stopped and a separate low-power timer takes over. That timer runs from a slow oscillator, often 32.768 kHz, chosen because it divides to one second exactly and because its current is a fraction of the main oscillator’s. The cost is resolution: at 32.768 kHz, one tick is about 30.5 µs, so the wake can be early or late by that much plus the oscillator’s error.
Both arrangements share a subtlety: the time between deciding to sleep and actually sleeping is not zero, and the event may arrive inside that window. The standard defence is to disable interrupts, re-check the next-event value, program the timer, and enter the state in a single critical section, so no event can slip between the check and the sleep. If the hardware supports it, a pending interrupt that arrives during the entry sequence aborts the sleep and the core continues without losing the event.
Timekeeping across suppressed ticks
A scheduler that counts ticks to maintain time cannot simply stop counting. The usual solution is to convert elapsed hardware time into ticks on wake: read the free-running counter, compute the difference from the value saved at sleep, and advance the tick count by that amount. The conversion needs the counter’s frequency and its width, and it needs to handle a wrap.
The wrap arithmetic is the part that is easy to get wrong. With a 32-bit counter at 32.768 kHz, a wrap takes about 36 hours, which sounds safe until a device is deployed and sleeps for a week. The correct difference is computed in unsigned arithmetic, where the subtraction is correct across one wrap, and the number of wraps must be tracked separately if more than one can occur. A firmware that stores the saved value in a signed integer and subtracts loses correctness at the wrap boundary.
The other consequence is that tick-based delays become coarser. A delay of one tick at 1 kHz is a millisecond; with the tick suppressed, a delay of one tick may be rounded to the next timer compare, which is accurate to the slow oscillator’s period. Code that assumed millisecond resolution in its timeouts has to be reviewed, and code that computed a timeout as a number of ticks and expected it to be exact has to be changed.
Where the accounting breaks
The largest source of error in tickless designs is the estimate of how long the sleep lasted. If the kernel advances time by the programmed interval rather than by the measured interval, every sleep introduces the oscillator’s error, and the errors accumulate. A 32.768 kHz watch crystal is accurate to tens of parts per million over temperature, which is a few seconds per day; an RC oscillator used as the low-power clock can be accurate to a few percent, which is minutes per day.
The standard remedy is to keep the accurate timebase running, or to calibrate the low-power oscillator against the accurate one whenever both are available. A device that wakes periodically can measure the low-power oscillator’s period against the main clock during each awake window and correct the conversion factor. Without that, the only defence is to avoid long sleeps on an uncalibrated oscillator.
A second break is the interaction with the scheduler’s own bookkeeping. If a task’s deadline passes during the sleep, the wake happens at the deadline and the task runs late by the transition time. That is expected. What is not expected is a task that becomes runnable for a reason other than time, such as a message or an interrupt, and the kernel has to distinguish a wake caused by the timer from one caused by an event, because the first advances time and the second does not.
A third is the interaction with debug and trace. Halting the core under a debugger while the low-power timer runs means the timer expires and the wake fires into a halted core, which either loses the event or produces an immediate wake on resume. Debug builds often disable tickless operation for that reason, and a bug that appears only in release is sometimes a bug in the idle path that the debug configuration never exercised.
What it buys and what it does not
Suppressing the tick converts a fixed wake rate into a rate set by the workload, and on a mostly idle system that is the difference between millisecond sleeps and second-long sleeps. The saving is bounded by the energy the tick was costing: if the tick handler was 1 percent of the active energy, removing it saves 1 percent, not a factor of ten. The larger effect is indirect, because the longer sleep allows a deeper state, and the deeper state’s current is where the real saving lies.
What it does not do is make the system responsive. A device that sleeps until the next event has a latency equal to the transition time, and any request that arrives during the sleep waits for the wake. The scheduling consequence is that the deadline of the fastest task sets the maximum sleep interval, which is the same constraint the tick imposed, expressed in the workload’s terms rather than the kernel’s. Systems that need both long sleeps and fast response resolve it by leaving a small always-on domain, or a peripheral, to handle the fast path while the core stays asleep.
The accounting for that arrangement is the subject of peripheral clock gating and autonomous transfer, where the work moves off the core entirely rather than being deferred to a wake.
References
- Linux kernel documentation: Timers — The Linux kernel project
- Linux kernel documentation: NO_HZ, Reducing Scheduling-Clock Ticks — The Linux kernel project
- FreeRTOS Reference Manual — Richard Barry; Real Time Engineers Ltd.