Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 96 additions & 168 deletions docs/nrf52_power_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,41 @@

## 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.
The nRF52 power management module protects batteries from over-discharge, prevents brownout-related flash corruption, and enables automatic voltage-based recovery. When enabled and configured, it checks battery voltage at boot and enters protective shutdown (SYSTEMOFF) if voltage is too low, then automatically wakes when the battery recovers or external power is connected.

## Features
## CLI Commands

### 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

### 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 |
| Command | Description |
|---------|-------------|
| `get pwrmgt.support` | Returns "supported" or "unsupported" |
| `get pwrmgt.source` | Returns current power source: "battery" or "external" |
| `get pwrmgt.bootreason` | Returns reset reason and shutdown reason strings |
| `get pwrmgt.bootmv` | Returns battery voltage at boot in millivolts |
| `get pwrmgt.batt` | Returns configured battery chemistry: liion, lfp, or lto |
| `set pwrmgt.batt liion\|lfp\|lto` | Set battery chemistry (persisted, takes effect on next boot) |
| `get pwrmgt.bootlock` | Returns boot protection state: "enabled" or "disabled" |
| `set pwrmgt.bootlock on\|off` | Enable or disable boot protection (persisted, reboot required) |
| `poweroff` / `shutdown` | Immediate power off. Does not configure voltage wake — recovery requires USB power or a hardware wake source (e.g. button press). |

On boards without power management support, all commands except `get pwrmgt.support` return `ERROR: Power management not supported`.

## Configuration

### Set battery chemistry

The correct chemistry must be set so the firmware uses the right voltage thresholds. Default is Li-ion.

| Chemistry | Boot lockout threshold |
|-----------|----------------------|
| Li-ion/LiPo (`liion`) | 3000 mV |
| Lithium Iron Phosphate (`lfp`) | 2500 mV |
| Lithium Titanate Oxide (`lto`) | 1800 mV |

### Enable boot protection

Boot protection is disabled by default. Enable with `set pwrmgt.bootlock on`, then reboot.

Verify with `get pwrmgt.batt` and `get pwrmgt.bootlock`.

## Supported Boards

Expand All @@ -38,11 +45,11 @@ Shutdown reason codes (stored in GPREGRET2):
| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes |
| RAK4631 (`rak4631`) | Yes | Yes | Yes |
| Heltec T114 (`heltec_t114`) | Yes | Yes | Yes |
| SenseCAP Solar (`sensecap_solar`) | Yes | Yes | Yes |
| Promicro nRF52840 | No | No | No |
| RAK WisMesh Tag | No | No | No |
| Heltec Mesh Solar | No | No | No |
| LilyGo T-Echo / T-Echo Lite | No | No | No |
| SenseCAP Solar | Yes | Yes | Yes |
| WIO Tracker L1 / L1 E-Ink | No | No | No |
| WIO WM1110 | No | No | No |
| Mesh Pocket | No | No | No |
Expand All @@ -53,159 +60,80 @@ Shutdown reason codes (stored in GPREGRET2):
| Keepteen LT1 | No | No | No |
| Minewsemi ME25LS01 | No | No | No |

Notes:
- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
- User power-off on Heltec T114 does not enable LPCOMP wake.
- 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

### 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:

1. **Enable in platformio.ini**:
```ini
-D NRF52_POWER_MANAGEMENT
```

2. **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)
```

3. **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).

4. **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 50mV hysteresis for noise immunity
- 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.
## How It Works

## CLI Commands
### Boot Voltage Protection

Power management status can be queried via the CLI:
On every boot (when enabled), the firmware reads the battery voltage before starting mesh operations. If USB/external power is detected, the check is skipped. If the voltage is below the lockout threshold for the configured chemistry, the device configures LPCOMP and VBUS wake sources and enters SYSTEMOFF.

| 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 |
### Wake Sources

On boards without power management enabled, all commands except `get pwrmgt.support` return:
A device in protective shutdown can be woken by two sources:

- **LPCOMP (Low Power Comparator)**: The nRF52's hardware comparator monitors battery voltage on an analog input pin during SYSTEMOFF. When voltage rises above the configured recovery threshold, the comparator triggers a wake event. The recovery threshold is set above the boot lockout threshold so the device does not immediately shut down again. LPCOMP draws negligible current (~1 uA) during SYSTEMOFF.
- **VBUS (USB power detection)**: The nRF52's POWER peripheral detects USB voltage on the VBUS pin and triggers a wake event. This is configured alongside LPCOMP whenever the board routes VBUS to the nRF52 (standard on nRF52840 boards with native USB). Connecting any USB power source will wake the device immediately regardless of battery state.

### Shutdown Reasons

The firmware records why the device entered SYSTEMOFF. This value is stored in the GPREGRET2 register (persists across SYSTEMOFF) and can be queried after boot with `get pwrmgt.bootreason`.

| Code | Name | Description |
|------|------|-------------|
| 0x00 | NONE | Normal boot / no previous shutdown |
| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
| 0x55 | USER | Manual shutdown via `poweroff` or `shutdown` CLI command |
| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |

### Boot Reason Tracking

The firmware captures the nRF52 RESETREAS and GPREGRET2 registers at early boot before system initialisation clears them. This allows `get pwrmgt.bootreason` to report both the wake source (e.g. LPCOMP, VBUS, reset pin, watchdog) and the prior shutdown reason.

## LPCOMP Wake Voltage Reference

The LPCOMP wake voltage depends on the board's voltage divider ratio and the REFSEL value programmed before entering SYSTEMOFF.

**Wake voltage formula**:
```
ERROR: Power management not supported
VBAT_wake = REFSEL_fraction x VDD_sys x ADC_MULTIPLIER
```

Where VDD_sys is approximately 3.0-3.3V (regulator output during SYSTEMOFF) and ADC_MULTIPLIER is the board's voltage divider scale factor.

**REFSEL fraction reference**:

| REFSEL | Fraction | REFSEL | Fraction |
|--------|----------|--------|----------|
| 0 | 1/8 | 8 | 1/16 |
| 1 | 2/8 | 9 | 3/16 |
| 2 | 3/8 | 10 | 5/16 |
| 3 | 4/8 | 11 | 7/16 |
| 4 | 5/8 | 12 | 9/16 |
| 5 | 6/8 | 13 | 11/16 |
| 6 | 7/8 | 14 | 13/16 |
| 7 | ARef | 15 | 15/16 |

**Per-board per-chemistry wake voltages**:

| Board | ADC_MUL | Li-ion REFSEL | Wake range | LFP REFSEL | Wake range | LTO REFSEL | Wake range |
|-------|---------|---------------|------------|------------|------------|------------|------------|
| RAK4631 | ~1.73 | 4 (5/8) | 3.24-3.57V | 4 (5/8) | 3.24-3.57V | 11 (7/16) | 2.27-2.50V |
| T114 | 4.90 | 1 (2/8) | 3.68-4.04V | 9 (3/16) | 2.76-3.03V | 0 (1/8) | 1.84-2.02V* |
| XIAO | 3.0 | 2 (3/8) | 3.38-3.71V | 10 (5/16) | 2.81-3.09V | 1 (2/8) | 2.25-2.47V |

*T114 LTO uses a narrower margin (~50 mV plus LPCOMP 50 mV hysteresis) and assumes VDD_sys >= 3.0V in SYSTEMOFF.

## Debug Output

When `MESH_DEBUG=1` is enabled, the power management module outputs:
When the firmware is built with `MESH_DEBUG=1`, the power management module logs at boot:

```
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)
PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
PWRMGT: Boot protection enabled (Li-ion), threshold=3000 mV
PWRMGT: Boot voltage=3450 mV
PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
PWRMGT: VBUS wake configured
```

## Phase 2 (Planned)

- Runtime voltage monitoring
- Voltage state machine (Normal -> Warning -> Critical -> Shutdown)
- Configurable thresholds
- Load shedding callbacks for power reduction
- Deep sleep integration
- Scheduled wake-up
- Extended sleep with periodic monitoring

## References

- [nRF52840 Product Specification - POWER](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
Expand Down
4 changes: 3 additions & 1 deletion examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ void setup() {
fast_rng.begin(radio_get_rng_seed());

#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
#if defined(STM32_PLATFORM)
InternalFS.begin();
#endif
#if defined(QSPIFLASH)
if (!QSPIFlash.begin()) {
// debug output might not be available at this point, might be too early. maybe should fall back to InternalFS here?
Expand Down
1 change: 0 additions & 1 deletion examples/kiss_modem/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ void halt() {

void loadOrCreateIdentity() {
#if defined(NRF52_PLATFORM)
InternalFS.begin();
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
SPIFFS.begin(true);
Expand Down
4 changes: 3 additions & 1 deletion examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ void setup() {

FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
#if defined(STM32_PLATFORM)
InternalFS.begin();
#endif
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
Expand Down
1 change: 0 additions & 1 deletion examples/simple_room_server/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ void setup() {

FILESYSTEM* fs;
#if defined(NRF52_PLATFORM)
InternalFS.begin();
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(RP2040_PLATFORM)
Expand Down
1 change: 0 additions & 1 deletion examples/simple_secure_chat/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,6 @@ void setup() {
fast_rng.begin(radio_get_rng_seed());

#if defined(NRF52_PLATFORM)
InternalFS.begin();
the_mesh.begin(InternalFS);
#elif defined(RP2040_PLATFORM)
LittleFS.begin();
Expand Down
4 changes: 3 additions & 1 deletion examples/simple_sensor/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ void setup() {

FILESYSTEM* fs;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
InternalFS.begin();
#if defined(STM32_PLATFORM)
InternalFS.begin();
#endif
fs = &InternalFS;
IdentityStore store(InternalFS, "");
#elif defined(ESP32)
Expand Down
Loading
Loading