View this page on MeshCore Docs.
nRF52 Power Management
Overview
The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.
Features
Boot Voltage Protection
- Checks battery voltage immediately after boot and before mesh operations commence
- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
- Prevents boot loops when battery is critically low
- Skipped when external power (USB VBUS) is detected
Voltage Wake (LPCOMP + VBUS)
- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
- Enables USB VBUS detection so external power can wake the device
- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected
- Uses no LPCOMP hysteresis; on divided battery inputs the additional 50 mV comparator hysteresis can move the effective battery wake point by several hundred millivolts and strand an otherwise charged node
Early Boot Register Capture
- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)
Shutdown Reason Tracking
Shutdown reason codes (stored in GPREGRET2):
| Code | Name | Description |
|---|---|---|
| 0x00 | NONE | Normal boot / no previous shutdown |
| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
| 0x55 | USER | User requested powerOff() |
| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |
Supported Boards
| Board family | Implemented | LPCOMP wake | VBUS wake |
|---|---|---|---|
Seeed Studio XIAO nRF52840 (xiao_nrf52) |
Yes | Yes | Yes |
| SenseCAP Solar | Yes | Yes | Yes |
| RAK4631 / RAK3401 | Yes | Yes | Yes |
| GAT562 30S / EVB Pro / Tracker Pro / Watch13 | Yes | Yes | Yes |
| Heltec T096 / T114 / T1 / Tower V2 | Yes | Yes | Yes |
| Muzi Works R1 Neo | Partial | Inactive | Inactive |
| Promicro nRF52840 | No | No | No |
| RAK WisMesh Tag | No | No | No |
| Heltec Mesh Solar | No | No | No |
| LilyGo T-Echo / T-Echo Lite / T-Impulse Plus | No | No | No |
| WIO Tracker L1 / L1 E-Ink / WM1110 | No | No | No |
| Mesh Pocket / Meshtiny / Nano G2 Ultra | No | No | No |
| ThinkNode M1 / M3 / M6 | No | No | No |
| T1000-E | No | No | No |
| Ikoka Nano / Stick / Handheld (nRF) | No | No | No |
| Keepteen LT1 / Minewsemi ME25LS01 | No | No | No |
Notes:
- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
- R1 Neo has the integration compiled, but its board configuration deliberately
sets PWRMGT_VOLTAGE_BOOTLOCK to 0; automatic protective shutdown and its
voltage/VBUS recovery wake are therefore inactive.
- User power-off does not enable LPCOMP wake; voltage recovery is armed only
for boot protection and automated low-voltage shutdown.
- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.
Technical Details
RAK peripheral shutdown
Explicit power-off and timed deep sleep on RAK4631 and RAK3401 stop peripheral
owners before switching off the shared 3V3_S supply. On RAK4631, Ethernet
must quiesce first; if its worker cannot stop within the bounded shutdown
interval, keep its rail and bus intact instead of removing power during an SPI
operation. Detected external-storage holds also retain the supply, including
after an unresolved flash-operation timeout. Before storage discovery and
configuration finish, rail removal is refused. Shutdown does not probe again,
cancel a staged update, or float a storage owner's select pin.
This does not change ordinary interrupt-idle sleep, boot/low-voltage
protection, or the configured WisBlock GPS/shared-rail policy during normal
operation.
The RAK19007 power schematic
and RAK13800 connector schematic
show that IO2 switches 3V3_S, not the separate upstream PoE VCC_IN supply.
Merely compiling Ethernet support therefore does not require this rail to
stay on after terminal shutdown. The RAK4631 radio-enable GPIO controls the
RF-switch supply, not the SX1262 core supply.
RAK3401's external RAK13302 radio and RAK13800 Ethernet use the same IO slot, SPI signals, select pin and IO3 control. Their simultaneous operation is not supported; Ethernet owner registration on RAK3401 is rejected rather than allowing those electrically conflicting controls to run together.
Software shutdown cannot remove every hardware leakage path. The baseboard has I2C pull-ups to unswitched VDD. RAK13302 VD's boost enable follows IO2, but the legacy VC default resistor configuration connects it directly to VBAT; that boost cannot be switched off by firmware alone.
XIAO battery-divider safety
XIAO and XIAO-based Ikoka startup preload READ_BAT_ENABLE LOW before enabling
its output. XIAO protective and user shutdown also keep it LOW. Seeed warns
that HIGH can overvoltage the ADC input during charging, so this is not a
safe pin to switch HIGH as a power optimization. The schematic's 1 MOhm +
510 kOhm divider draws about 3 microamps at 4.2 V, not 3 milliamps. Charging
current settings are unchanged. See Seeed's battery guidance.
Architecture
The power management functionality is integrated into the NRF52Board base class in src/helpers/NRF52Board.cpp. Board variants provide hardware-specific configuration via a PowerMgtConfig struct and override initiateShutdown(uint8_t reason) to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).
Early Boot Capture
A static constructor with priority 101 in NRF52Board.cpp captures the RESETREAS and GPREGRET2 registers before:
- SystemInit() (priority 102) - which clears RESETREAS
- Static C++ constructors (default priority 65535)
This ensures we capture the true reset reason before any initialisation code runs.
Board Implementation
To enable power management on a board variant:
-
Enable in platformio.ini:
ini -D NRF52_POWER_MANAGEMENT -
Define configuration in variant.h:
c #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV) #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing #define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) -
Implement in board .cpp file: ```cpp #ifdef NRF52_POWER_MANAGEMENT const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK };
void MyBoard::initiateShutdown(uint8_t reason) { // Board-specific shutdown preparation (e.g., disable peripherals) bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT);
if (enable_lpcomp) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}
enterSystemOff(reason);
} #endif
void MyBoard::begin() { NRF52Board::begin(); // or NRF52BoardDCDC::begin() // ... board setup ...
#ifdef NRF52_POWER_MANAGEMENT checkBootVoltage(&power_config); #endif } ```
For user-initiated shutdowns, powerOff() remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
- Declare override in board .h file:
cpp #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif
Voltage Wake Configuration
The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) - Use no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever configureVoltageWake() is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL):
| REFSEL | Fraction | VBAT @ 1M/1M divider (VDD=3.0-3.3) | VBAT @ 1.5M/1M divider (VDD=3.0-3.3) |
|---|---|---|---|
| 0 | 1/8 | 0.75-0.82 V | 0.94-1.03 V |
| 1 | 2/8 | 1.50-1.65 V | 1.88-2.06 V |
| 2 | 3/8 | 2.25-2.47 V | 2.81-3.09 V |
| 3 | 4/8 | 3.00-3.30 V | 3.75-4.12 V |
| 4 | 5/8 | 3.75-4.12 V | 4.69-5.16 V |
| 5 | 6/8 | 4.50-4.95 V | 5.62-6.19 V |
| 6 | 7/8 | 5.25-5.77 V | 6.56-7.22 V |
| 7 | ARef | - | - |
| 8 | 1/16 | 0.38-0.41 V | 0.47-0.52 V |
| 9 | 3/16 | 1.12-1.24 V | 1.41-1.55 V |
| 10 | 5/16 | 1.88-2.06 V | 2.34-2.58 V |
| 11 | 7/16 | 2.62-2.89 V | 3.28-3.61 V |
| 12 | 9/16 | 3.38-3.71 V | 4.22-4.64 V |
| 13 | 11/16 | 4.12-4.54 V | 5.16-5.67 V |
| 14 | 13/16 | 4.88-5.36 V | 6.09-6.70 V |
| 15 | 15/16 | 5.62-6.19 V | 7.03-7.73 V |
Important: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:
VBAT_threshold ~ (VDD * fraction) * divider_scale, where divider_scale = (Rtop + Rbottom) / Rbottom (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
SoftDevice Compatibility
The power management code checks whether SoftDevice is enabled and uses the appropriate API:
- When SD enabled: sd_power_* functions
- When SD disabled: Direct register access (NRF_POWER->*)
This ensures compatibility regardless of BLE stack state.
CLI Commands
Power management status can be queried via the CLI:
| Command | Description |
|---|---|
get pwrmgt.support |
Returns "supported" or "unsupported" |
get pwrmgt.source |
Returns current power source - "battery" or "external" (5V/USB power) |
get pwrmgt.bootreason |
Returns reset and shutdown reason strings |
get pwrmgt.bootmv |
Returns boot voltage in millivolts |
Boards with nRF52 power management and a text CLI also support saved
pwrmgt.bootlock, pwrmgt.cutoff, battery.profile, battery.empty,
battery.full, and relative adc.multiplier settings. See the
CLI reference for their
ranges, board-specific defaults, and wake behavior. KISS builds honor a saved
policy if one exists, but have no text CLI for changing it.
On boards without power management enabled, get pwrmgt.source and
get pwrmgt.bootmv return:
ERROR: Power management not supported
get pwrmgt.support returns unsupported. get pwrmgt.bootreason is not
compiled out at all and answers on every board: getResetReason() and
getShutdownReason() are virtuals on the base board class, so a board that
does not override them reports Not available rather than an error. ESP32
boards override the reset half with esp_reset_reason(), so they return a real
reset reason and Not available for the shutdown reason.
Debug Output
When MESH_DEBUG=1 is enabled, the power management module outputs:
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
Further power-management work (Planned)
For the opt-in Sensor check-in workflow using System ON idle sleep and bounded GPS acquisition, see Sensor dog tracker. It is separate from the battery-protection SYSTEMOFF shutdown described here.
- A voltage state machine (Normal -> Warning -> Critical -> Shutdown)
- Configurable wake comparator thresholds
- Load shedding callbacks for power reduction
- Deep sleep integration
- Scheduled wake-up
- Extended sleep with periodic monitoring