Chasing microamps: getting the nRF9151 Feather to 4 µA in active sleep
The last three commits on the v3.2.x branch of the nRF9160 Feather Examples and Drivers (NFED) repo took the active_sleep sample from “good enough” to ~4 µA at room temperature on the nRF9151 Feather. Two of them are workarounds for real bugs — one in Zephyr’s regulator framework, one in the board’s default pin configuration. The third is the cleanup that ties it all together.
Here’s what each one does and why.
1. c3e7055 — bypassing the regulator framework to actually disable BUCK2
The biggest single contributor to the old current draw was BUCK2 on the nPM1300 PMIC. On the Feather, BUCK2 is the user-configurable rail — and in active_sleep we have no external load on it, so it should be completely off during deep modem sleep.
The natural way to disable it is through Zephyr’s regulator framework:
const struct device *buck2 = DEVICE_DT_GET(DT_NODELABEL(npm1300_buck2));
regulator_disable(buck2);
That call returned success. The rail stayed up. Current draw stayed high.
After a lot of poking at PMIC registers over I²C, the cause turned out to be the regulator framework’s internal refcount. The count is bumped during init and again by certain runtime events — and on the Feather, a Power-Of-Failure (POF) event at low VBAT during boot will silently re-enable the rail and inflate the refcount past what user code can pull back down. regulator_disable() happily reports success, decrements the count to a still-non-zero value, and the rail never actually drops. There’s an ongoing bug report with Nordic tracking the root cause.
The workaround sidesteps the refcount entirely and talks to the PMIC’s MFD interface directly:
/* Enable BUCK2 pulldown so the rail discharges cleanly */
mfd_npm13xx_reg_update(pmic, NPM1300_BUCK_BASE, NPM1300_BUCK1_MODE,
NPM1300_BUCK2_PULLDOWN_EN, NPM1300_BUCK2_PULLDOWN_EN);
/* Clear the enable bit directly */
mfd_npm13xx_reg_write(pmic, NPM1300_BUCK_BASE,
NPM1300_BUCK2_OFFSET_EN_CLR, 0x01);
Two non-obvious details:
- Write to
BUCK2ENCLR, notBUCK2ENSET. The nPM1300 uses split set/clear registers; writing a 1 to the clear register is the only way to actually drop the enable bit. - Enable the pulldown first. Without it, the rail’s output cap holds charge after the converter shuts down. That stored charge keeps anything still hanging off BUCK2 powered longer than you’d expect, and it confuses current-draw measurements. The internal pulldown discharges the rail in microseconds.
The result was a clean rail-down event and a meaningful drop in sleep current — but not yet at 4 µA.
2. d2be255 — clamping floating pins with bias-pull-down
Once BUCK2 was off, the next problem was leakage on a couple of pins. CMOS input buffers draw shoot-through current whenever the input sits near Vdd/2 instead of being clamped to a rail. Two pins on the Feather were doing exactly that during sleep:
- UART0 RX (P0.10). With the modem in sleep and the console suspended, the line floats. Nothing on the other side is driving it.
- SPI3 MISO (P0.5). The W25Q128 NOR flash goes into Deep Power-Down (DPD) before sleep — its outputs go high-Z. The nRF9151’s input buffer sees a floating line.
The fix lives in the board’s pinctrl DTSI rather than as a per-sample overlay. The SPI3 group needed a small restructure because pinctrl groups share the same bias-* properties across all pins in the group, and the SCK/MOSI pins don’t want pull-downs while MISO does:
spi3_default: spi3_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 6)>,
<NRF_PSEL(SPIM_MOSI, 0, 7)>;
};
group2 {
psels = <NRF_PSEL(SPIM_MISO, 0, 5)>;
bias-pull-down;
};
};
The pull is weak enough that the W25Q128 actively driving MISO during normal flash I/O easily overdrives it, so this doesn’t affect transaction integrity. UART0 got the same treatment. Because the change is at the board level in circuitdojo_feather_nrf9151_common-pinctrl.dtsi, every sample on the branch gets the fix for free — no overlay-copy-paste between projects.
This is the commit that closed the gap to ~4 µA.
3. cb63031 — the cleanup that ties it together
The third commit is the one that makes the previous two reproducible. It restructures active_sleep/src/main.c around a clear teardown sequence and moves the previously sample-local pin overlay out of the sample (since d2be255 now handles it at the board level).
The order matters and is easy to get wrong:
int main(void) {
setup_gpio(); /* Disconnect unused IOs */
setup_accel(); /* ODR -> 0 on the on-board accel */
nor_storage_init(); /* W25Q128 -> DPD via spi_nor PM */
nrf_modem_lib_init();/* Modem will idle on its own */
setup_uart(); /* uart_rx_disable + PM_DEVICE_ACTION_SUSPEND */
setup_pmic(); /* BUCK2 pulldown + clear enable */
return 0;
}
A couple of things in there are worth noting if you’re adapting this to your own project:
uart_rx_disable()beforepm_device_action_run(..., SUSPEND). If async RX is still armed, the suspend won’t actually take the UART peripheral down.pm_device_action_runon the SPI NOR alias. That’s what triggers the W25Q128’s DPD entry through the upstreamspi_nordriver — you don’t need a vendor-specific command sequence anymore.- PMIC last. Touching the I²C bus to talk to the nPM1300 keeps the I²C controller awake; you want all higher-level shutdowns to have happened first so the only thing left for the system to do after
setup_pmic()returns is enter idle.
While not necessary (it depends on your application), the sample also picked up the right Kconfig knobs for the regulator-disable path:
CONFIG_REGULATOR=y
CONFIG_REGULATOR_NPM13XX=y
CONFIG_NPM13XX_CHARGER=n
NPM13XX_CHARGER=n is the easy-to-miss one — leaving the charger driver enabled keeps a chunk of the PMIC’s housekeeping logic active even when nothing is plugged in.
Try it
If you have an existing checkout:
cd <your nfed workspace>
git -C nfed checkout v3.2.x
west update
The updated active_sleep sample is what was used to measure the 4 µA number — run it as-is on an nRF9151 Feather with a current meter inline and you should see the same.
If your application is still pulling more than that, the culprit is almost always one of three things: a peripheral you forgot to suspend, an LED/regulator on your carrier board, or a floating IO that needs the same bias-pull-down treatment SPI3 MISO got. Post on the forum thread if you find one and I’ll help dig in.