Optimize battery use for the Raspberry Pi Pico

The Raspberry Pi Pico’s high-performance chip is trimmed for I/​O and does not try to save power. However, a few tricks in battery mode can keep it running longer.

A large number of pins, variable voltage input, and good community support are what make the Raspberry Pi Pico a popular single-board computer (SBC) for newcomers and professionals alike. Just connect it to your PC over USB and get started, power supply included.

When you want the Pico to run autonomously, you will need to face the question of how long the battery supply will last. Like most microcontrollers, the Pi Pico requires comparatively little power, but it has limitations when it comes to operating continuously on battery operation, not least because it continues to consume power during sleep periods. A few tricks will help you reduce the power consumption in these phases to extend the battery runtime.

Whenever you read “Pico,” it also applies to other microcontrollers, either because they directly support CircuitPython (a software implementation of the Python 3 programming language used here and targeted toward beginners), or because everything also works in the same way with C/​C++.

There’s an old adage: If you work, you get to eat. But microcontrollers are more like non-workers in their normal state. A remote sensor or control lies around nearly 100 percent of the time, then has to respond very quickly when a button is pressed. Even a sensor that records the current temperature every 10 minutes has an uptime of less than one percent. Therefore, it’s a good idea for the Pico to save resources to the extent possible when it’s doing nothing.

Measure Again

The measurement setup for the program examples shown here is simple. The power supply is routed by way of a measuring circuit that continuously monitors voltage and current. The Pico simulates regular work with its built-in LED, which it switches on for one second every 30 seconds. It does nothing in between. The source code for the sample programs is available from my GitHub project.

If you are familiar with microcontroller programming, you will be aware that time can elapse in several ways (Listing 1). One possibility is an empty loop that keeps the program busy for a while (lines 26-29), which is the Pico treading virtual water. As you would expect, power consumption is high (Figure 1) and is why I will be using this approach as the benchmark when comparing alternatives.

Listing 1: Empty Test Loop

01 import time
02 import board
03 import alarm
04 from digitalio import DigitalInOut, Direction, Pull
05 
06 LED_TIME     = 1
07 INT_TIME     = 30 ‑ LED_TIME
08 SPIN         = 0x1
09 SLEEP        = 0x2
10 LIGHT_SLEEP  = 0x3
11 DEEP_SLEEP   = 0x4
12 MODE         = SPIN
13 
14 led           = DigitalInOut(board.LED)
15 led.direction = Direction.OUTPUT
16 
17 # ‑‑‑ Simulate work
18 def work():
19   led.value = 1
20   time.sleep(LED_TIME)
21   led.value = 0
22 
23 # ‑‑‑ Main loop
24 while True:
25   work()
26   if MODE == SPIN:
27     next = time.monotonic() + INT_TIME
28     while time.monotonic() < next:
29       continue
30   elif MODE == SLEEP:
31     time.sleep(INT_TIME)
32   elif MODE == LIGHT_SLEEP:
33     time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic()+INT_TIME)
34     alarm.light_sleep_until_alarms(time_alarm)
35   elif MODE == DEEP_SLEEP:
36     time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic()+INT_TIME) 40:
37     alarm.exit_and_deep_sleep_until_alarms(time_alarm)
Figure 1: Power consumption with the empty loop in Listing 1.

The basic consumption of the Pico in this active wait mode is 26mA. The LED pushes the consumption up by 5mA for a short time. Taken by itself, the basic requirement is still very low because even a Pi Zero uses 90mA in idle mode, which represents the lower limit of what headless operation can offer without digging deep into your bag of tricks.

Converting consumption into the expected battery life proves to be relatively easy. Popular lithium polymer (LiPo) batteries have a self-discharge rate of 10 percent within the first 24 hours, then about five percent per month. This value already includes the current draw by the integrated protection circuit. As soon as the voltage drops below 3V, the battery switches off. You can also assume the residual capacity to be 10 percent. Exact values for self-discharge and residual capacity need be determined experimentally.

Given these assumptions, the flashing Pico will run for 46 hours on a 1,500mAh battery, whereas the Pi Zero lasts a good 15 hours. Consequently, the use of a Raspberry Pi in combination with rechargeable batteries only makes sense in scenarios where long service lives are not important (e.g., robotic cars).

Sleep Modes

With the use of Python, you can send the Pico to sleep for X seconds with time.sleep(<X>) (Listing 1, lines 30 and 31). However, the technical implementation of time.sleep() on the Pico uses the Pico SDK’s busy_wait() functions. In other words, the sleep function has no effect on power consumption; it just makes the Python code a bit easier to understand. The power consumption graph looks exactly like Figure 1 when this function is used. For other microcontrollers, on the other hand, the implementation can offer good savings with the light-sleep mode.

Besides time.sleep(), CircuitPython offers two special sleep modes: light sleep and deep sleep. In both of these modes, the CPU not only stops computing but also shuts down various function blocks of the SBC, saving electricity. Both sleep modes require a timer to wake up.

In light sleep mode, the program continues normally at the point after the sleep command, but on waking up from deep sleep, when the Pico restarts after the specified time, the current program context, including the variable values, is lost. The Pico’s internal real-time clock (RTC) only retains its value during light sleep.

From the programming point of view, the two variants are not very complex, provided you import the alarm module. The timers created in lines 33 and 36 then use the alarm.light_sleep_until_alarms() (line 34) and alarm.exit_and_deep_sleep_until_alarms() (line 37) commands.

With these two modes, the idle power consumption drops to 17mA in light sleep mode and 6mA in deep sleep mode (Figure 2). The additional overhead when waking up from deep sleep can also be seen: It pushes the average consumption up to 10mA in the sample scen-ario. The battery will then last for five days. On the ESP32-S2, which also runs CircuitPython, the consumption in deep sleep drops to below 1mA and the average consumption to 2.5mA – with a correspondingly longer runtime.

Figure 2: Power consumption in light sleep mode (top) and deep sleep mode (bottom).

If you want to test the sleep modes yourself, please note that deep sleep does not work with an active USB connection (i.e., the serial console). This safety measure prevents buggy code from immediately sending the Pico into deep sleep without the user being able to intervene.

Also important is that the Pico does not turn off the 3.3V output. If a peripheral device is connected, it will continue to run. If you don’t want that, you also need to turn off the devices connected to the output, if possible, before you send the Pico to sleep, or use a simple circuit to deactivate them.

Awake at the Touch of a Button

Timed wake-up is a useful solution to many cyclic application problems but is of little value for the remote control example cited above. Fortunately, besides alarm.time.TimeAlarm, you also have alarm.pin.PinAlarm, which causes the Pico to wake up after a pin voltage change (Listing 2).

Listing 2: alarm.pin.PinAlarm

pin_alarm = alarm.pin.PinAlarm(WAKE_PIN,value=False,edge=True,pull=True)
if MODE == LIGHT_SLEEP:
  alarm.light_sleep_until_alarms(pin_alarm)
elif MODE == DEEP_SLEEP:
  alarm.exit_and_deep_sleep_until_alarms(pin_alarm)

Although light sleep doesn’t change anything in terms of power consumption, deep sleep is far more efficient with PinAlarm than with TimeAlarm; the current draw drops from 6mA to 1. You do not need to connect a pushbutton to the pin: Any component that pulls the pin to ground will work in the same way. One firm candidate for this is an external RTC like the DS3231 with its interrupt pin.

Another feature of the CircuitPython implementation proves to be very useful in this context: The sleep_until_ functions accept any number of alarms. If a device has several buttons, you don’t need to use a dedicated wake-up button. The alarms for the sleep functions can theoretically be a mix of timer and pin alarms, but not every chip supports all variants.

Some controllers additionally wake up on a touch event. The Pico is not one of them, but the ESP32-S2 supports this feature. As with the timer alarm, the potential savings in sleep modes for the pin and touch alarms also depends on the processor.

All Gone?

One disadvantage of deep sleep is the complete restart of the program, wherein (almost) the entire state of the Pico is lost. However, the Pico and other microcontroller units (MCUs) also have non-volatile memory or non-volatile RAM (NVRAM). This memory does not act like a disk drive; programs can only address it as a byte sequence. The Pico has 4KB for this purpose. The SAMD21 has just 256 bytes, and its big brother, the SAMD51, has 8KB. The size is not documented, but you can find the values in the CircuitPython source code.

Listing 3 shows how to access this NVRAM. The second and third lines convert a string into bytes and then write the value to memory. The last two lines read the memory again. The difference between the two alternatives is that the second variant can also handle Unicode characters that comprise more than one byte in UTF-8 encoding: The ä in the example occupies two bytes. You can convert numbers smaller than 255 directly. For larger numbers, use the standard to_bytes and from_bytes functions.

Listing 3: Non-Volatile Memory

import microcontroller
microcontroller.nvm[42:48] = b'Hello'
microcontroller.nvm[10:14] = 'Hä?'.encode('utf‑8')
print(microcontroller.nvm[42:48])
print(microcontroller.nvm[10:14].decode('utf‑8'))

Unfortunately, I could find no information online about the Pico’s potential number of memory write cycles. Microchip Technology Inc. specifies 100,000 (minimum) to 600,000 cycles (typical) for the SAMD21 processor, and the value should be in the same range for the Pico. Therefore, if you write to this memory every minute, you will run out of road within a year. You definitely need a program that is a little smarter.

Some boards have other memory areas besides the NVRAM whose contents survive the deep sleep but not a complete reset, including SAMD21/​51 boards and ESP microcontroller boards, but not the Pico. Unlike flash, this RAM area, sometimes known as backup memory, is not worn out by write operations. CircuitPython serves it up as a byte array in the alarm.sleep_memory variable. Access is like NVRAM access.

External Control

The Pico’s 3V3_EN pin (physical pin 37) offers another very efficient option for battery operation. When pulled to ground, it completely shuts down the Pico, including the 3.3V output. However, manufacturers typically omit this pin from smaller Pico boards.

The enable timer board (Figure 3) by Adafruit is perfect for this setup. You can set the wake-up interval with a slightly fiddly rotary potentiometer or optionally use a fixed resistor. Additionally, a button gives you a manual wake-up option.

Figure 3: The TPL5111 enable timer board is available from Adafruit for around $5.

The Pico takes care of shutting itself down as soon as you connect a GPIO pin to the Done pin of the timer and toggle it to high. The button’s own consumption is very low at 20µA, and the current curve starts looking pretty much ideal (Figure 4). The only disadvantage in this constellation is that the intervals are restricted to a range of between one second and two hours. Daily intervals are not supported, but the enable timer is perfect for many cyclic use cases.

Figure 4: The TPL5111 enable timer offers the best power-saving capabilities.`

C Alternative

If you rely on CircuitPython, you are relinquishing full control over the processor – not just the clock-synched signals, but also the memory and peripherals. If you want to use the Pico and other MCUs to the max, you have to use C/​C++.

Because ultimately the CircuitPython implementation uses the C/​C++ SDK, you cannot hope to extract too much more from the sleep modes. Pico SDK-speak does not refer to light sleep and deep sleep, but sleep mode and dormant mode. The SDK provides two sample programs – hello_sleep and hello_dormant – that demonstrate the use.

For your own projects, it’s worth taking a look at the SleepyPico GitHub repo, which contains a complete application, including a reusable Sleep class. You need to define a setup function for this (one-off) call, along with a loop function that calls the class once per work interval.

Conclusions

With the measures described here, you can reduce the Pico’s power consumption to somewhere between 0 and 1mA during idle time, which will significantly extend the battery life. Ultimately, power consumption at wake-up and during the work cycle will then determine the battery life, regardless of your choice of programming language. Again, CircuitPython plays to its strengths, because if you change the chip (e.g., because you need Bluetooth or WiFi), you don’t have to learn anything new as far as saving power is concerned.