I’ve always kept an interest in low-power explorations, most of which I did many years ago. First there was the ATmega328, then the LPC824, and finally on the STM32L052. One reason, is that I find it fascinating to see a µC drop multiple orders of magnitude in power consumption, yet still have enough wits to wake up, periodically or via an I/O pin.

It’s hard to overstate the effect this can have on real-world devices: letting a circuit drop to µWatt power levels, i.e. low enough to remain operational for years on a small battery.

But let’s just start simple. Let’s look into the basics of taking a standard STM32F103 µC into low-power mode, with periodic wakeup to briefly turn on an LED (so we can see it really is still functioning), and then measure the power consumption. The F103 is the chip used in the Blue Pill, but for this low-power experiment, I’m going to use this board:

This is a Nucleo-F103, produced by STM, also the manufacturer of the F103 chips.

The reason to use this board, is that it’s all set up to measure the µC current consumption (and not the current draw of any supporting circuitry, such as the voltage regulator). There is a jumper on the board, marked “JP6”, right in the centre of the image above, which can be replaced by a current meter. Can’t get any easier than that, right?

As with every new board, the very first upload I do is always a blink demo, with some output to the serial port - see Getting started with the Blue Pill for a very similar demo.

I have again set up a project for PlatformIO, which can be found here. It’s just two files, platformio.ini, which contains:

[env:nucleo]
build_flags = -DSTM32F1
platform = ststm32
board = genericSTM32F103RB
framework = stm32cube
upload_protocol = stlink
lib_deps = jeeh

Note the upload protocol, which is now “stlink”, as the Nucleo has an ST-Link built-in.

The second file is src/main.cpp:

#include <jee.h>

PinA<5> led;

int main() {
    enableSysTick();
    led.mode(Pinmode::out);

    while (true) {
        led = 1;
        wait_ms(100);
        led = 0;
        wait_ms(400);
    }
}

This is a stripped-down version without the serial output code, since the goal here is to do as little as possible. A serial port needs a clock and draws extra current.

The other difference with the Blue Pill demo is that the LED is on another GPIO pin.

There is also a more substantial difference, in that the Nucleo board does not contain external clock crystals by default (it can run off the ST-Link’s 8 MHz clock, if needed). But in this context, that’s irrelevant: this is about achieving low-power, not top performance.

And sure enough, “pio run -t upload” is all it takes to make the on-board LED blink.

Current draw?

For a µC to drop into a serious low-power mode, many things have to be just right, and all of this is highly dependent on the actual µC chip and chip family used. And on the board.

Let’s start by just slowing down the blink to briefly once every 10 seconds, and measuring the current consumption via jumper JP6 of the Nucleo-F103, to establish a baseline:

        led = 1;
        wait_ms(10);
        led = 0;
        wait_ms(10000);

Current consumption = 1.19 mA, with the µC running on its internal 8 MHz RC clock. But that can’t be right: the datasheet specifies over 5 mA @ 25 °C. There is something fishy going on: without JP6, the µC should power down, right? Well… the LED keeps blinking!

This is an example of the puzzles you can run into when trying to get a µC into low-power mode and then measuring the effect to confirm that it really did.

Clearing it up

The first step is to simplify. I suspect that some connection on the Nucleo is back-feeding the µC through one of its I/O pins (perhaps the USART1 RX, tied to the ST-Link). So I switched to another Nucleo-F103 board, with the ST-Link physically removed, powering it through an (older) ST-Link v1 with just +5V, GND, SWCLK, and SWDIO attached:

And indeed, now the µC loses power when JP6 is removed: the blinking stops.

But then things get weirder: the current consumption is still 1.19 mA. The only thing I can say is that I now trust this reading, because the setup is simpler. Aha - it turns out that the wait_ms() code in JeeH issues a WFI instruction while waiting for the next clock tick. This puts the F103 in sleep mode while idling. The datasheet says: max 3 mA @ 85 °C.

I’ll let it pass. Let’s assume that it’s not a contradiction. High temp implies more leakage.

Back to low-power

The next step is to prepare for a total shutdown. This leads to a new puzzle: a halted µC will not respond to SWD, so re-flashing is no longer possible. The on-board ST-Link can work around this, but my 4-wire hookup can’t. Luckily, there is a simple solution: keep the RESET button pressed during uploads, and release it right after. That’s workable.

Here is an overview of the available STM32F103’s low power options:

Despite the name, “standby” is a lower-power mode than “stop”. Just about everything on the chip is turned off, it can only wake up via a signal on one specific I/O pin, via the Real Time Clock (RTC), or via the Independent Watchdog (IWDG). The RTC needs an external crystal. The IWDG does not, but is also quite inaccurate.

I’ll pick the IWDG for now. I don’t mind bad timing accuracy, even up to ± 50%.

So the next step is to put the F103 in standby mode, with the watchdog to periodically get us back out of this comatose state. This code will put the F103 into a coma:

static void powerDown () {
    Periph::bit(Periph::rcc+0x1C, 28) = 1; // PWREN
    Periph::bit(Periph::pwr, 1) = 1;  // set PDDS
     
    constexpr uint32_t scr = 0xE000ED10;
    MMIO32(scr) |= 1<<2;  // set SLEEPDEEP
     
    __asm("cpsid i");
    __asm("wfi");
}

But now there’s a problem: when waking up from standby mode, the µC will do a reset. Looping, with the program counter keeping track of where we are, is no longer an option. Instead, all the “real” work (a brief LED blink in this case) has to happen on power-up, and then we go into coma and let the watchdog issue a reset later:

Iwdg dog (5);  // approx 13s

int main() {
    enableSysTick();

    led.mode(Pinmode::out);
    led = 1;
    wait_ms(10);
    led = 0;

    powerDown();
}

The µC will be in standby mode most of the time. SWD will no longer work, so from now on, uploads are going to require the RESET trick. Low-power development can be messy!

But this step does lead to a huge power reduction: current consumption is now 3 µA.

Update - I’ve fixed powerDown() to set PWREN, for proper standby mode (was 45 µA).