Low-power modes

Low-power modes #

A major goal for JeeH has always been to support low-power modes in µCs. In the ARM Cortex architecture, there are a number of ways in which power consumption can be reduced, some of which may lead to massive savings (from mA to sub-µA):

  • Run mode is when the CPU and all enabled h/w peripherals in the chip operate normally. Power consumption can also be varied (considerably!) by altering the clock speed.
  • Sleep mode is when the CPU has nothing to do but wait for some interrupt or event. The clock to the CPU is stopped, but the rest of the system continues to operate as before.
  • Low-power run mode operates at limited clock speeds, alowing the internal voltage regulators to be “less active” and running more efficiently.
  • Low-power sleep mode is again when the CPU has nothing to do. The CPU clock is switched off, while the rest continues to work in low-power mode.
  • Stop mode has the CPU as well as several peripherals in the system turned off, reducing power consumption further. Some chip variants have multiple stop modes, with progressively less hardware active. Only some hardware is able to get the system out of this mode in this case.
  • Standby mode turns everything off, essentially, but can keep more or less of the memory system going, allowing some RAM contents to be retained. When resumed, the system must go through a complete reset, registers and other CPU state is not preserved.
  • Shutdown mode is a maximally-off state: only very few triggers can bring the system up again (e.g. some GPIO pins, and the RTC if running on a 32 kHz crystal).

The less a µC needs to do, the more circuits it can shut down, obviously.

Resuming from a low-power mode does take time, especially when most parts have been switched off. It takes time to start up an oscillator. It takes even more time to enable the PLL and wait for it to lock again.

Some peripherals cannot easily be turned off: a UART which needs to receive data needs to stay active, even when nothing is coming in most of the time (although some UARTs can start up fast enough to resume by triggering on the high-to-low transition of the start bit).

Other peripherals are inherently low-power, such as GPIO pins configured to trigger on level changes. There is no clock involved here, it just needs to detect a logic level transition. GPIO pins can usually wake up a µC from very “deep” low-power states.

Then there is the notion of time: often, you want a µC to start doing something again “a little later” (or a minute/hour/day later, perhaps). There is no way around it: some clock needs to keep running to count off the passage of time. This can be a low-speed internal clock (LSI), or a low-speed external clock (LSE), usually driven by a 32 kHz quartz crystal. In many µCs, the Real-Time Clock (RTC) peripheral is optmised for rock-bottom power consumption, drawing well under 1 µA, i.e. lower than the self-discharge rate of most coin cells.

Low-power in practice #

So yes, today’s µC chips are definitely able to go into extremely low energy-consumption modes. The question is: how do you deal with it in software? How does the software choose a suitable clock rate, decide what to keep running when, and select the most effective low-power mode? Worse still, perhaps: how do you herd all these cats living in the different corners of your code?

The most important savings in most use cases, will come from “doing nothing as much as possible”. There is simply no better way to save energy than to shut down just about everything as much as possible: between sensor readouts, display updates, communcation activity, etc. Even a high spike of several mA in run mode becomes insignificant if the system can spend 99.9% of its time drawing only a few µA’s.

For context: a small 2032 coin cell battery has a capacity of ~200 mAh. With an (easily achieved) low-power current of 20 µA, it can last over a year. Even a 10 mA power “blip” would be insignificant if it happens once a minute and lasts only 10 ms, for example.

To drive this point home: the key is to get that baseline current draw as low as possible most of the time.

JeeH’s low-power modes #

What I’m about to describe is the way JeeH tries to help with this, even though most decisions will be very app-specific. Not all of this has been implemented at the time of this writing (mid-May 2024), but there is enough in place to confidently push further.

System wakeups are based on interrupts. In JeeH, these are all handled by device drivers, so it is clear that they determine what level of low-power sleep is possible at any point in time. To this end, each driver must keep a dPower variable up to date, specifying the lowest power level that driver is willing to tolerate.

The power levels are one of:

  • run modes at different clock rates
  • stop mode(s)
  • standby mode
  • shutdown mode

Taking a UART as example: if there are pending receive or transmit messages in its queues, then the UART and its clock must be kept running. Else - as far as the UART is concerned - the system can be shut down, since a UART driver is a service which only reacts to requests from other parts of the app.

There is also a “ticker” device in JeeH, which provides a timeout service: you send it a message, and it will reply in a specified number of milliseconds. Here again, the logic is similar: if there are timeouts pending, the µC must be kept in a state which can service these timeouts, else shutting down would be ok.

But this is where things get interesting: the real-time clock (RTC) is perfectly able to wake up its µC if nothing else is going on. So the ticker service can actually lean on the RTC to do its work, and even the most extreme stop mode would be acceptable.

The problem is power-switching overhead as well as the time (and power consuption!) needed to wake up from a low-power mode. A µC which is ramping up can actually draw a substantial amount of current. Going in and out of stop mode every 10 ms would be very inefficient.

The way JeeH solves this, is that it calls application code to decide which low-power mode (if any) to choose, given that a timeout is expected N milliseconds from now. The second argument to this function reports the lowest level allowed by all the installed device drivers. Based on this information, the app can pick a power level, and JeeH will switch to it, with a wakeup event scheduled in the RTC, just before the next timeout triggers.

While in low-power mode, interrupts from any enabled source can still wake up the system. Once handled, JeeH again queries each device driver and recalculates the next expected timeout, repeating the cycle.

In other words: low-power modes are automatic in JeeH. When there is nothing left to do, the system enters a low power mode. If all the app does is blink an LED once a second, it can easily enter a low-power mode drawing a few µA (apart from that LED current, obviously). If standby or shutdown are acceptable, i.e. a periodic system reset doing some work and then idling, it’s not hard to take the system down even lower, into the sub-µA range.

Long periods of inactivity #

The “Ticker” timeout system in JeeH only accepts values up to 60,000 milliseconds, i.e. one minute. This covers the most common timeouts, i.e. stepping in when something didn’t happen, or some device isn’t responding. These timeouts have 1 ms granularity, but their accuracy depends on the system clock source. This system clock is not active in all low-power modes.

The RTC clock has slightly different properties: its granularity is usually 256 Hz (≈ 4 ms), and its accuracy is either reasonably high (when based on a 32 kHz crystal) or moderately low (± 5% or so, for the LSI clock). Periodic RTC timeouts are limited to at most 16s, but JeeH automatically re-enters low-power modes as often as needed if the next timeout exceeds that time.

When based on this RTC’s “periodic wakeup” mechanism, the system will wake up every 16s, just to prepare the next wakeup. This is somewhat inefficient when all we want is a wake up once an hour or day.

For this, JeeH provides a sys::coma() function, which takes a time interval and a low-power mode specifier as arguments. The time interval can be up to 604,800,000 milliseconds, which is exactly 7 days. This uses the RTC’s “alarm clock” functionality to prepare a wakeup much further into the future, without any scheduled wakeups needed in between.

As with short-term auto-wakeups, the system may resume sooner than planned when some other interrupt wakes up the system. In this case, sys::coma will return the actual number of elapsed milliseconds. The granularity of these values is ≈ 4 ms and the accuracy is again determined by the RTC’s clock source.