IdleSaver — Ultra‑Low‑Power Sleep Scheduler for Battery‑Powered MCUs
Why a tiny sleep scheduler?
Battery projects want two things: long life and predictable behavior. Full real‑time operating systems can do that but at a cost: flash, RAM, complexity. IdleSaver is a constrained, pragmatic approach: a tiny scheduler that handles timed wakeups, event coalescing, and safe short naps without an RTOS. It’s designed for MCUs with a hardware real‑time clock (RTC) or low‑power timer and a couple of GPIO interrupts.
What IdleSaver gives you
- Sub-256B flash state machine and about 32–128B RAM for event metadata (dependent on event count).
- Deterministic wake windows: you decide when the device wakes, no drifting or cascading timers.
- Interrupt-safe entry to deep sleep and graceful handling of missed wakeups after brownouts or long sleeps.
- Easy integration with a simple event list: periodic sampling, scheduled radio bursts, and user buttons.
Design constraints
Keep it simple and hardware‑agnostic. IdleSaver assumes:
- A tick source that can schedule a wake at millisecond to second granularity (RTC alarm, low‑power timer).
- GPIO interrupts that can wake the MCU from deep sleep.
- Nonvolatile storage only for configuration, not for scheduler state — runtime state is ephemeral.
Core idea
Instead of many independent timers, maintain a sorted list of upcoming events with absolute timestamps (seconds since epoch or uptime counter). Before sleeping, compute the interval until the first event and program the wake alarm. If an external interrupt arrives early, wake and re-evaluate the list. On wake, execute any due events; if multiple events are due, process them in order and reprogram the alarm for the next one.
Minimal state machine
- Build event list (volatile): each entry is {time, code, repeat_interval}.
- Compute next_due = earliest time > now.
- If next_due within callback_margin (a small nonzero time), handle now and loop.
- Else program RTC alarm for next_due and go to deep sleep (allow external IRQs to wake).
- On wake, determine wake reason: alarm or external IRQ. If IRQ, clear and handle event; if alarm, advance time and run due events.
- After handling, update repeating events (add repeat_interval) and go back to step 2.
Event representation
Keep entries compact. Example C struct layout that fits many tiny MCUs:
typedef struct {
uint32_t time; // 32-bit seconds, wrap handled by signed subtraction
uint8_t code; // small enum for action
uint8_t flags; // bit flags: repeat, persistent, reserved
uint16_t repeat_sec; // repeat interval in seconds (0=no repeat)
} event_t;
This is 8 bytes aligned to 8 or 12 bytes depending on compiler packing. You can shrink it further by storing time as 24 bits or reusing global state if needed.
Handling drift and missed wakes
Two practical problems crop up in low‑power systems: RTC drift and missed wakeups when the device loses power or sleeps for longer than expected. IdleSaver deals with both:
- Use absolute timestamps. On long sleeps, compute deltas using signed arithmetic so wraparound is robust for a limited window.
- If upon wake the system time is earlier than an event (RTC drift backward), treat the wake as spurious and reprogram the alarm slightly ahead to recheck — but limit retries to avoid an infinite loop.
- If wake finds many overdue events, process in a bounded loop and record a saturation flag to indicate backlog to the application (so you can avoid overwhelming radios or sensors).
Sample tiny loop (pseudo‑C)
while(1) {
now = rtc_now_seconds();
next = find_next_event(now);
if(next - now <= callback_margin) {
handle_due_events(now);
continue;
}
rtc_set_alarm(next);
enter_deep_sleep();
// wakes here either by alarm or IRQ
wake_reason = read_wake_reason();
clear_wake_sources();
// loop back; event loop will re-evaluate
}
Power measurement and tuning
Measure current draw in three places: active handling, shallow sleep, and deep sleep. Typical targets:
- Deep sleep: microamps (target 1–50 µA depending on MCU and peripherals).
- Shallow sleep: tens to hundreds of microamps if radios are powered.
- Active handling: milliamps during sensor reads and radio bursts.
Tuning tips:
- Increase callback_margin to avoid waking for marginal events if the handling cost is high.
- Coalesce events: if multiple events fall within a short window, cluster them and handle together.
- Prefer repeating intervals aligned to power‑friendly schedules (e.g., 60s, 300s) to reduce RTC alarm programming overhead.
Troubleshooting
- No wake from RTC: ensure alarm source is enabled in PMU and that the RTC continues in deep sleep. Check NVIC/interrupt masking.
- Missed external IRQs: configure wake on level/edge correctly; some MCUs only wake on edge and require pull config to avoid false wakes.
- Unexpected frequent wakes: enable logging of wake reason counters and check for noisy GPIOs or brownouts.
- Time jump after long battery removal: save a simple monotonic uptime counter in EEPROM periodically if absolute time matters, or re-synchronize on boot.
When to graduate to an RTOS
IdleSaver does not replace task scheduling, priorities, or complex concurrency. Use it when your app primarily sleeps, wakes to sample sensors or send telemetry, and does simple sequential processing. If you have many concurrent tasks, complex resource sharing, or need dynamic priorities, an RTOS will save you time despite the footprint.
Wrap up
IdleSaver is intentionally tiny and opinionated: absolute timestamps, a single prioritized event list, and a conservative wake/retry policy. It keeps flash and RAM small while avoiding the common pitfalls of low‑power embedded projects. Drop it into a sensor node, a battery‑operated controller, or any project where every microamp counts.