Browse Source

nRF52840 Power Management - Phase 1 - Boot Low VBAT Voltage Lockout

Added NRF52840 power management core functionality:
- Boot‑voltage lockout
- Initial support for shutdown/reset reason storage and capture (via RESETREAS/GPREGRET2)
- LPCOMP wake (for voltage-driven shutdowns)
- VBUS wake (for voltage-driven shutdowns)
- Per-board shutdown handler for board-specific tasks
- Exposed CLI queries for power‑management status in CommonCLI.cpp
- Added documentation in docs/nrf52_power_management.md.
- Enabled power management support in Xiao nRF52840, RAK4631, Heltec T114 boards
entr0p1 6 months ago
parent
commit
1f59e52880

+ 213 - 0
docs/nrf52_power_management.md

@@ -0,0 +1,213 @@
+# 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
+
+### 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 | Implemented | LPCOMP wake | VBUS wake |
+|-------|-------------|-------------|-----------|
+| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes |
+| RAK4631 (`rak4631`) | Yes | Yes | Yes |
+| Heltec T114 (`heltec_t114`) | 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 | No | No | No |
+| WIO Tracker L1 / L1 E-Ink | No | No | No |
+| WIO WM1110 | No | No | No |
+| Mesh Pocket | No | No | No |
+| 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 | 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.
+
+## 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 |
+
+On boards without power management enabled, all commands except `get pwrmgt.support` return:
+```
+ERROR: Power management not supported
+```
+
+## 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)
+```
+
+## 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)
+- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
+- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group__nrf__sdm__api.html)

+ 8 - 0
src/MeshCore.h

@@ -56,6 +56,14 @@ public:
   virtual void setGpio(uint32_t values) {}
   virtual uint8_t getStartupReason() const = 0;
   virtual bool startOTAUpdate(const char* id, char reply[]) { return false; }   // not supported
+
+  // Power management interface (boards with power management override these)
+  virtual bool isExternalPowered() { return false; }
+  virtual uint16_t getBootVoltage() { return 0; }
+  virtual uint32_t getResetReason() const { return 0; }
+  virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
+  virtual uint8_t getShutdownReason() const { return 0; }
+  virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
 };
 
 /**

+ 27 - 0
src/helpers/CommonCLI.cpp

@@ -364,6 +364,33 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
         } else {
           sprintf(reply, "> %.3f", adc_mult);
         }
+      // Power management commands
+      } else if (memcmp(config, "pwrmgt.support", 14) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+        strcpy(reply, "> supported");
+#else
+        strcpy(reply, "> unsupported");
+#endif
+      } else if (memcmp(config, "pwrmgt.source", 13) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+        strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
+#else
+        strcpy(reply, "ERROR: Power management not supported");
+#endif
+      } else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+        sprintf(reply, "> Reset: %s; Shutdown: %s",
+          _board->getResetReasonString(_board->getResetReason()),
+          _board->getShutdownReasonString(_board->getShutdownReason()));
+#else
+        strcpy(reply, "ERROR: Power management not supported");
+#endif
+      } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
+#ifdef NRF52_POWER_MANAGEMENT
+        sprintf(reply, "> %u mV", _board->getBootVoltage());
+#else
+        strcpy(reply, "ERROR: Power management not supported");
+#endif
       } else {
         sprintf(reply, "??: %s", config);
       }

+ 217 - 0
src/helpers/NRF52Board.cpp

@@ -2,6 +2,7 @@
 #include "NRF52Board.h"
 
 #include <bluefruit.h>
+#include <nrf_soc.h>
 
 static BLEDfu bledfu;
 
@@ -21,6 +22,222 @@ void NRF52Board::begin() {
   startup_reason = BD_STARTUP_NORMAL;
 }
 
+#ifdef NRF52_POWER_MANAGEMENT
+#include "nrf.h"
+
+// Power Management global variables
+uint32_t g_nrf52_reset_reason = 0;     // Reset/Startup reason
+uint8_t g_nrf52_shutdown_reason = 0;   // Shutdown reason
+
+// Early constructor - runs before SystemInit() clears the registers
+// Priority 101 ensures this runs before SystemInit (102) and before
+// any C++ static constructors (default 65535)
+static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
+  g_nrf52_reset_reason = NRF_POWER->RESETREAS;
+  g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
+}
+
+void NRF52Board::initPowerMgr() {
+  // Copy early-captured register values
+  reset_reason = g_nrf52_reset_reason;
+  shutdown_reason = g_nrf52_shutdown_reason;
+  boot_voltage_mv = 0;  // Will be set by checkBootVoltage()
+
+  // Clear registers for next boot
+  // Note: At this point SoftDevice may or may not be enabled
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+  if (sd_enabled) {
+    sd_power_reset_reason_clr(0xFFFFFFFF);
+    sd_power_gpregret_clr(1, 0xFF);
+  } else {
+    NRF_POWER->RESETREAS = 0xFFFFFFFF;  // Write 1s to clear
+    NRF_POWER->GPREGRET2 = 0;
+  }
+
+  // Log reset/shutdown info
+  if (shutdown_reason != SHUTDOWN_REASON_NONE) {
+    MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX); Shutdown = %s (0x%02X)",
+      getResetReasonString(reset_reason), (unsigned long)reset_reason,
+      getShutdownReasonString(shutdown_reason), shutdown_reason);
+  } else {
+    MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
+      getResetReasonString(reset_reason), (unsigned long)reset_reason);
+  }
+}
+
+bool NRF52Board::isExternalPowered() {
+  // Check if SoftDevice is enabled before using its API
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+
+  if (sd_enabled) {
+    uint32_t usb_status;
+    sd_power_usbregstatus_get(&usb_status);
+    return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
+  } else {
+    return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
+  }
+}
+
+const char* NRF52Board::getResetReasonString(uint32_t reason) {
+  if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin";
+  if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog";
+  if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset";
+  if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup";
+  #ifdef POWER_RESETREAS_LPCOMP_Msk
+    if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP";
+  #endif
+  #ifdef POWER_RESETREAS_VBUS_Msk
+    if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS";
+  #endif
+  #ifdef POWER_RESETREAS_OFF_Msk
+    if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO";
+  #endif
+  #ifdef POWER_RESETREAS_DIF_Msk
+    if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface";
+  #endif
+  return "Cold Boot";
+}
+
+const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
+  switch (reason) {
+    case SHUTDOWN_REASON_LOW_VOLTAGE:  return "Low Voltage";
+    case SHUTDOWN_REASON_USER:         return "User Request";
+    case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
+  }
+  return "Unknown";
+}
+
+bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
+  initPowerMgr();
+
+  // Read boot voltage
+  boot_voltage_mv = getBattMilliVolts();
+  
+  if (config->voltage_bootlock == 0) return true;  // Protection disabled
+
+  // Skip check if externally powered
+  if (isExternalPowered()) {
+    MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)");
+    boot_voltage_mv = getBattMilliVolts();
+    return true;
+  }
+
+  MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
+      boot_voltage_mv, config->voltage_bootlock);
+
+  // Only trigger shutdown if reading is valid (>1000mV) AND below threshold
+  // This prevents spurious shutdowns on ADC glitches or uninitialized reads
+  if (boot_voltage_mv > 1000 && boot_voltage_mv < config->voltage_bootlock) {
+    MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown");
+
+    initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
+    return false;  // Should never reach this
+  }
+
+  return true;
+}
+
+void NRF52Board::initiateShutdown(uint8_t reason) {
+  enterSystemOff(reason);
+}
+
+void NRF52Board::enterSystemOff(uint8_t reason) {
+  MESH_DEBUG_PRINTLN("PWRMGT: Entering SYSTEMOFF (%s)", getShutdownReasonString(reason));
+
+  // Record shutdown reason in GPREGRET2
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+  if (sd_enabled) {
+    sd_power_gpregret_clr(1, 0xFF);
+    sd_power_gpregret_set(1, reason);
+  } else {
+    NRF_POWER->GPREGRET2 = reason;
+  }
+
+  // Flush serial buffers
+  Serial.flush();
+  delay(100);
+
+  // Enter SYSTEMOFF
+  if (sd_enabled) {
+    uint32_t err = sd_power_system_off();
+    if (err == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) {  //SoftDevice not enabled
+      sd_enabled = 0;
+    }
+  }
+
+  if (!sd_enabled) {
+    // SoftDevice not available; write directly to POWER->SYSTEMOFF
+    NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
+  }
+
+  // If we get here, something went wrong. Reset to recover.
+  NVIC_SystemReset();
+}
+
+void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
+  // LPCOMP is not managed by SoftDevice - direct register access required
+  // Halt and disable before reconfiguration
+  NRF_LPCOMP->TASKS_STOP = 1;
+  NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
+
+  // Select analog input (AIN0-7 maps to PSEL 0-7)
+  NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
+
+  // Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
+  NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
+
+  // Detect UP events (voltage rises above threshold for battery recovery)
+  NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
+
+  // Enable 50mV hysteresis for noise immunity
+  NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
+
+  // Clear stale events/interrupts before enabling wake
+  NRF_LPCOMP->EVENTS_READY = 0;
+  NRF_LPCOMP->EVENTS_DOWN = 0;
+  NRF_LPCOMP->EVENTS_UP = 0;
+  NRF_LPCOMP->EVENTS_CROSS = 0;
+
+  NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
+  NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
+
+  // Enable LPCOMP
+  NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
+  NRF_LPCOMP->TASKS_START = 1;
+
+  // Wait for comparator to settle before entering SYSTEMOFF
+  for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
+    delayMicroseconds(50);
+  }
+
+  if (refsel == 7) {
+    MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
+  } else if (refsel <= 6) {
+    MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
+      ain_channel, refsel + 1);
+  } else {
+    uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
+    MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
+      ain_channel, ref_num);
+  }
+
+  // Configure VBUS (USB power) wake alongside LPCOMP
+  uint8_t sd_enabled = 0;
+  sd_softdevice_is_enabled(&sd_enabled);
+  if (sd_enabled) {
+    sd_power_usbdetected_enable(1);
+  } else {
+    NRF_POWER->EVENTS_USBDETECTED = 0;
+    NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
+  }
+
+  MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
+}
+#endif
+
 void NRF52BoardDCDC::begin() {
   NRF52Board::begin();
 

+ 43 - 0
src/helpers/NRF52Board.h

@@ -5,15 +5,58 @@
 
 #if defined(NRF52_PLATFORM)
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Shutdown Reason Codes (stored in GPREGRET before SYSTEMOFF)
+#define SHUTDOWN_REASON_NONE          0x00
+#define SHUTDOWN_REASON_LOW_VOLTAGE   0x4C  // 'L' - Runtime low voltage threshold
+#define SHUTDOWN_REASON_USER          0x55  // 'U' - User requested powerOff()
+#define SHUTDOWN_REASON_BOOT_PROTECT  0x42  // 'B' - Boot voltage protection
+
+// Boards provide this struct with their hardware-specific settings and callbacks.
+struct PowerMgtConfig {
+  // LPCOMP wake configuration (for voltage recovery from SYSTEMOFF)
+  uint8_t lpcomp_ain_channel;       // AIN0-7 for voltage sensing pin
+  uint8_t lpcomp_refsel;            // REFSEL value: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16
+
+  // Boot protection voltage threshold (millivolts)
+  // Set to 0 to disable boot protection
+  uint16_t voltage_bootlock;
+};
+#endif
+
 class NRF52Board : public mesh::MainBoard {
+#ifdef NRF52_POWER_MANAGEMENT
+  void initPowerMgr();
+#endif
+
 protected:
   uint8_t startup_reason;
 
+#ifdef NRF52_POWER_MANAGEMENT
+  uint32_t reset_reason;              // RESETREAS register value
+  uint8_t shutdown_reason;            // GPREGRET value (why we entered last SYSTEMOFF)
+  uint16_t boot_voltage_mv;           // Battery voltage at boot (millivolts)
+
+  bool checkBootVoltage(const PowerMgtConfig* config);
+  void enterSystemOff(uint8_t reason);
+  void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
+  virtual void initiateShutdown(uint8_t reason);
+#endif
+
 public:
   virtual void begin();
   virtual uint8_t getStartupReason() const override { return startup_reason; }
   virtual float getMCUTemperature() override;
   virtual void reboot() override { NVIC_SystemReset(); }
+
+#ifdef NRF52_POWER_MANAGEMENT
+  bool isExternalPowered() override;
+  uint16_t getBootVoltage() override { return boot_voltage_mv; }
+  virtual uint32_t getResetReason() const override { return reset_reason; }
+  uint8_t getShutdownReason() const override { return shutdown_reason; }
+  const char* getResetReasonString(uint32_t reason) override;
+  const char* getShutdownReasonString(uint8_t reason) override;
+#endif
 };
 
 /*

+ 34 - 0
variants/heltec_t114/T114Board.cpp

@@ -3,6 +3,35 @@
 #include <Arduino.h>
 #include <Wire.h>
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Static configuration for power management
+// Values come from variant.h defines
+const PowerMgtConfig power_config = {
+  .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+  .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+  .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+};
+
+void T114Board::initiateShutdown(uint8_t reason) {
+#if ENV_INCLUDE_GPS == 1
+  pinMode(GPS_EN, OUTPUT);
+  digitalWrite(GPS_EN, LOW);
+#endif
+  digitalWrite(SX126X_POWER_EN, LOW);
+
+  bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+                        reason == SHUTDOWN_REASON_BOOT_PROTECT);
+  pinMode(PIN_BAT_CTL, OUTPUT);
+  digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW);
+
+  if (enable_lpcomp) {
+    configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+  }
+
+  enterSystemOff(reason);
+}
+#endif // NRF52_POWER_MANAGEMENT
+
 void T114Board::begin() {
   NRF52Board::begin();
   NRF_POWER->DCDCEN = 1;
@@ -21,6 +50,11 @@ void T114Board::begin() {
 #endif
 
   pinMode(SX126X_POWER_EN, OUTPUT);
+#ifdef NRF52_POWER_MANAGEMENT
+  // Boot voltage protection check (may not return if voltage too low)
+  // We need to call this after we configure SX126X_POWER_EN as output but before we pull high
+  checkBootVoltage(&power_config);
+#endif
   digitalWrite(SX126X_POWER_EN, HIGH);
   delay(10); // give sx1262 some time to power up
 }

+ 9 - 4
variants/heltec_t114/T114Board.h

@@ -10,6 +10,11 @@
 #define  MV_LSB   (3000.0F / 4096.0F) // 12-bit ADC with 3.0V input range
 
 class T114Board : public NRF52BoardOTA {
+protected:
+#ifdef NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
     T114Board() : NRF52BoardOTA("T114_OTA") {}
   void begin();
@@ -42,13 +47,13 @@ public:
   }
 
   void powerOff() override {
-    #ifdef LED_PIN
+#ifdef LED_PIN
     digitalWrite(LED_PIN, HIGH);
-    #endif
-    #if ENV_INCLUDE_GPS == 1
+#endif
+#if ENV_INCLUDE_GPS == 1
     pinMode(GPS_EN, OUTPUT);
     digitalWrite(GPS_EN, LOW);
-    #endif
+#endif
     sd_power_system_off();
   }
 };

+ 1 - 0
variants/heltec_t114/platformio.ini

@@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/heltec_t114
   -I src/helpers/ui
   -D HELTEC_T114
+  -D NRF52_POWER_MANAGEMENT
   -D P_LORA_DIO_1=20
   -D P_LORA_NSS=24
   -D P_LORA_RESET=25

+ 8 - 0
variants/heltec_t114/variant.h

@@ -30,6 +30,14 @@
 
 #define AREF_VOLTAGE            (3.0)
 
+// Power management boot protection threshold (millivolts)
+// Set to 0 to disable boot protection
+#define PWRMGT_VOLTAGE_BOOTLOCK 3300   // Won't boot below this voltage (mV)
+// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
+// AIN2 = P0.04 = BATTERY_PIN / PIN_VBAT_READ
+#define PWRMGT_LPCOMP_AIN 2
+#define PWRMGT_LPCOMP_REFSEL 1  // 2/8 VDD (~3.68-4.04V)
+
 ////////////////////////////////////////////////////////////////////////////////
 // Number of pins
 

+ 27 - 0
variants/rak4631/RAK4631Board.cpp

@@ -3,6 +3,28 @@
 
 #include "RAK4631Board.h"
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Static configuration for power management
+// Values set in variant.h defines
+const PowerMgtConfig power_config = {
+  .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+  .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+  .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+};
+
+void RAK4631Board::initiateShutdown(uint8_t reason) {
+  // Disable LoRa module power before shutdown
+  digitalWrite(SX126X_POWER_EN, LOW);
+
+  if (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+      reason == SHUTDOWN_REASON_BOOT_PROTECT) {
+    configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+  }
+
+  enterSystemOff(reason);
+}
+#endif // NRF52_POWER_MANAGEMENT
+
 void RAK4631Board::begin() {
   NRF52BoardDCDC::begin();
   pinMode(PIN_VBAT_READ, INPUT);
@@ -21,6 +43,11 @@ void RAK4631Board::begin() {
   Wire.begin();
 
   pinMode(SX126X_POWER_EN, OUTPUT);
+#ifdef NRF52_POWER_MANAGEMENT
+  // Boot voltage protection check (may not return if voltage too low)
+  // We need to call this after we configure SX126X_POWER_EN as output but before we pull high
+  checkBootVoltage(&power_config);
+#endif
   digitalWrite(SX126X_POWER_EN, HIGH);
   delay(10);   // give sx1262 some time to power up
 }

+ 5 - 0
variants/rak4631/RAK4631Board.h

@@ -30,6 +30,11 @@
 #define  ADC_MULTIPLIER   (3 * 1.73 * 1.187 * 1000)
 
 class RAK4631Board : public NRF52BoardDCDC, public NRF52BoardOTA {
+protected:
+#ifdef NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
   RAK4631Board() : NRF52BoardOTA("RAK4631_OTA") {}
   void begin();

+ 1 - 0
variants/rak4631/platformio.ini

@@ -7,6 +7,7 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/rak4631
   -D RAK_4631
   -D RAK_BOARD
+  -D NRF52_POWER_MANAGEMENT
   -D PIN_BOARD_SCL=14
   -D PIN_BOARD_SDA=13
   -D PIN_GPS_TX=PIN_SERIAL1_RX

+ 8 - 0
variants/rak4631/variant.h

@@ -104,6 +104,14 @@ extern "C"
 	static const uint8_t A7 = PIN_A7;
 #define ADC_RESOLUTION 14
 
+// Power management boot protection threshold (millivolts)
+// Set to 0 to disable boot protection
+#define PWRMGT_VOLTAGE_BOOTLOCK 3300   // Won't boot below this voltage (mV)
+// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
+// AIN3 = P0.05 = PIN_A0 / PIN_VBAT_READ
+#define PWRMGT_LPCOMP_AIN 3
+#define PWRMGT_LPCOMP_REFSEL 4  // 5/8 VDD (~3.13-3.44V)
+
 // Other pins
 #define PIN_AREF (2)
 #define PIN_NFC1 (9)

+ 43 - 4
variants/xiao_nrf52/XiaoNrf52Board.cpp

@@ -5,12 +5,40 @@
 
 #include "XiaoNrf52Board.h"
 
+#ifdef NRF52_POWER_MANAGEMENT
+// Static configuration for power management
+// Values set in variant.h defines
+const PowerMgtConfig power_config = {
+  .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
+  .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
+  .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
+};
+
+void XiaoNrf52Board::initiateShutdown(uint8_t reason) {
+  bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
+                        reason == SHUTDOWN_REASON_BOOT_PROTECT);
+
+  pinMode(VBAT_ENABLE, OUTPUT);
+  digitalWrite(VBAT_ENABLE, enable_lpcomp ? LOW : HIGH);
+
+  if (enable_lpcomp) {
+    configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
+  }
+
+  enterSystemOff(reason);
+}
+#endif // NRF52_POWER_MANAGEMENT
+
 void XiaoNrf52Board::begin() {
   NRF52BoardDCDC::begin();
 
+  // Configure battery voltage ADC
   pinMode(PIN_VBAT, INPUT);
   pinMode(VBAT_ENABLE, OUTPUT);
-  digitalWrite(VBAT_ENABLE, HIGH);
+  digitalWrite(VBAT_ENABLE, LOW);  // Enable VBAT divider for reading
+  analogReadResolution(12);
+  analogReference(AR_INTERNAL_3_0);
+  delay(50);  // Allow ADC to settle
 
 #ifdef PIN_USER_BTN
   pinMode(PIN_USER_BTN, INPUT_PULLUP);
@@ -27,9 +55,20 @@ void XiaoNrf52Board::begin() {
   digitalWrite(P_LORA_TX_LED, HIGH);
 #endif
 
-  //  pinMode(SX126X_POWER_EN, OUTPUT);
-  //  digitalWrite(SX126X_POWER_EN, HIGH);
-  delay(10); // give sx1262 some time to power up
+#ifdef NRF52_POWER_MANAGEMENT
+  // Boot voltage protection check (may not return if voltage too low)
+  checkBootVoltage(&power_config);
+#endif
+
+  delay(10);  // Give sx1262 some time to power up
+}
+
+uint16_t XiaoNrf52Board::getBattMilliVolts() {
+  // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
+  // VBAT_ENABLE must be LOW to read battery voltage
+  digitalWrite(VBAT_ENABLE, LOW);
+  int adcvalue = analogRead(PIN_VBAT);
+  return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
 }
 
 #endif

+ 7 - 16
variants/xiao_nrf52/XiaoNrf52Board.h

@@ -6,7 +6,12 @@
 
 #ifdef XIAO_NRF52
 
-class XiaoNrf52Board : public NRF52BoardDCDC, public NRF52BoardOTA {
+class XiaoNrf52Board : public NRF52BoardDCDC, public NRF52BoardOTA { 
+protected:
+#if NRF52_POWER_MANAGEMENT
+  void initiateShutdown(uint8_t reason) override;
+#endif
+
 public:
   XiaoNrf52Board() : NRF52BoardOTA("XIAO_NRF52_OTA") {}
   void begin();
@@ -20,21 +25,7 @@ public:
   }
 #endif
 
-  uint16_t getBattMilliVolts() override {
-    // Please read befor going further ;)
-    // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
-
-    // We can't drive VBAT_ENABLE to HIGH as long
-    // as we don't know wether we are charging or not ...
-    // this is a 3mA loss (4/1500)
-    digitalWrite(VBAT_ENABLE, LOW);
-    int adcvalue = 0;
-    analogReadResolution(12);
-    analogReference(AR_INTERNAL_3_0);
-    delay(10);
-    adcvalue = analogRead(PIN_VBAT);
-    return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
-  }
+  uint16_t getBattMilliVolts() override;
 
   const char* getManufacturerName() const override {
     return "Seeed Xiao-nrf52";

+ 1 - 0
variants/xiao_nrf52/platformio.ini

@@ -9,6 +9,7 @@ build_flags = ${nrf52_base.build_flags}
   -I variants/xiao_nrf52
   -UENV_INCLUDE_GPS
   -D NRF52_PLATFORM
+  -D NRF52_POWER_MANAGEMENT
   -D XIAO_NRF52
   -D RADIO_CLASS=CustomSX1262
   -D WRAPPER_CLASS=CustomSX1262Wrapper

+ 15 - 0
variants/xiao_nrf52/variant.h

@@ -75,6 +75,21 @@ static const uint8_t D10 = 10;
 #define AREF_VOLTAGE            (3.0)
 #define ADC_MULTIPLIER          (3.0F) // 1M, 512k divider bridge
 
+// Power management boot protection threshold (millivolts)
+// Set to 0 to disable boot protection
+#define PWRMGT_VOLTAGE_BOOTLOCK    3300   // Won't boot below this voltage
+
+// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
+#define PWRMGT_LPCOMP_AIN           7     // AIN7 = P0.31 = PIN_VBAT
+// IMPORTANT: The XIAO exposes battery via a resistor divider (ADC_MULTIPLIER = 3.0).
+// LPCOMP measures the divided voltage, not the battery voltage directly.
+// Vpin = VDD * (REFSEL fraction), and VBAT ≈ Vpin * ADC_MULTIPLIER.
+//
+// Using 3/8 VDD gives a wake threshold above the boot protection point:
+// - If VDD ≈ 3.0V:  VBAT ≈ (3.0 * 3/8) * 3 ≈ 3375mV
+// - If VDD ≈ 3.3V:  VBAT ≈ (3.3 * 3/8) * 3 ≈ 3712mV
+#define PWRMGT_LPCOMP_REFSEL   2     // 3/8 VDD (~3.38-3.71V)
+
 static const uint8_t A0  = PIN_A0;
 static const uint8_t A1  = PIN_A1;
 static const uint8_t A2  = PIN_A2;