diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md new file mode 100644 index 000000000..e99cadbc0 --- /dev/null +++ b/docs/nrf52_power_management.md @@ -0,0 +1,196 @@ +# 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., 3400mV), the device enables LPCOMP wake and enters protective shutdown (SYSTEMOFF) +- Prevents boot loops when battery is critically low +- Skipped when external power (USB VBUS) is detected + +### LPCOMP Wake +- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF +- Device automatically wakes when battery voltage rises above recovery threshold + +### 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 + +Currently enabled: +- **Seeed Studio XIAO nRF52840** (`xiao_nrf52`) +- **ProMicro nRF52840** (`promicro`) + +Compatible boards (can enable with configuration): +- RAK4631 +- RAK WisMesh Tag +- Heltec T114 +- Heltec Mesh Solar +- LilyGo T-Echo / T-Echo Lite +- SenseCAP Solar +- WIO Tracker L1 / L1 E-Ink +- WIO WM1110 +- Mesh Pocket +- Nano G2 Ultra +- ThinkNode M1/M3/M6 +- T1000-E +- Ikoka Nano/Stick/Handheld (nRF) +- Keepteen LT1 +- Minewsemi ME25LS01 + +## 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 can override `prepareForShutdown()` for board-specific shutdown preparation. + +### 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 3400 // Won't boot below this voltage (mV) + #define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing + #define PWRMGT_LPCOMP_REFSEL 2 // VDD fraction (0=1/8, 1=2/8, ..., 6=7/8) + ``` + +3. **Implement in board .cpp file**: + ```cpp + #ifdef NRF52_POWER_MANAGEMENT + const PowerMgtConfig power_config = { + .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, + .lpcomp_ref_eighths = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + }; + + void MyBoard::prepareForShutdown() { + // Board-specific shutdown preparation (e.g., disable peripherals) + } + #endif + + void MyBoard::begin() { + NRF52Board::begin(); // or NRF52BoardDCDC::begin() + // ... board setup ... + + #ifdef NRF52_POWER_MANAGEMENT + checkBootVoltage(&power_config); + #endif + } + + void MyBoard::powerOff() { + #ifdef NRF52_POWER_MANAGEMENT + prepareForShutdown(); + configureLpcompWake(power_config.lpcomp_ain_channel, power_config.lpcomp_ref_eighths); + enterSystemOff(SHUTDOWN_REASON_USER); + #else + sd_power_system_off(); + #endif + } + ``` + +4. **Declare override in board .h file**: + ```cpp + #ifdef NRF52_POWER_MANAGEMENT + void prepareForShutdown() override; + #endif + ``` + +### LPCOMP 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=1/8, 1=2/8, ..., 6=7/8) +- Detect UP events (voltage rising above threshold) +- Use 50mV hysteresis for noise immunity +- Wake the device from SYSTEMOFF when triggered + +**LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL)**: +| Value | Fraction | At VDD=3.0V | At VDD=3.3V | +|-------|----------|-------------|-------------| +| 0 | 1/8 | 375mV | 412mV | +| 1 | 2/8 | 750mV | 825mV | +| 2 | 3/8 | 1125mV | 1237mV | +| 3 | 4/8 | 1500mV | 1650mV | +| 4 | 5/8 | 1875mV | 2062mV | +| 5 | 6/8 | 2250mV | 2475mV | +| 6 | 7/8 | 2625mV | 2887mV | + +**Important**: For boards with a voltage divider on the battery sense pin (e.g., XIAO nRF52840 with 3:1 divider), the LPCOMP measures the divided voltage. The wake threshold in battery millivolts is: `(VDD * fraction) * divider_ratio`. + +### 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 = 3400 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) diff --git a/src/MeshCore.h b/src/MeshCore.h index 718660d3b..f194cdeb4 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -56,6 +56,14 @@ class MainBoard { 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"; } }; /** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2fc93006b..6dac9fff0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/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); } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index c0d58314e..96f59a3e2 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -2,6 +2,7 @@ #include "NRF52Board.h" #include +#include static BLEDfu bledfu; @@ -21,6 +22,204 @@ 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(0, 0xFF); + } else { + NRF_POWER->RESETREAS = 0xFFFFFFFF; // Write 1s to clear + NRF_POWER->GPREGRET = 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(); + + 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; + } + + // Read boot voltage + boot_voltage_mv = getBattMilliVolts(); + 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"); + + // Board-specific shutdown preparation + prepareForShutdown(); + + // Configure LPCOMP for voltage recovery wake + configureLpcompWake(config->lpcomp_ain_channel, config->lpcomp_ref_eighths); + + // Enter SYSTEMOFF (does not return) + enterSystemOff(SHUTDOWN_REASON_BOOT_PROTECT); + return false; // Should never reach this + } + + return true; +} + +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::configureLpcompWake(uint8_t ain_channel, uint8_t vdd_fraction_eighths) { + // 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: VDD fraction (0=1/8, 1=2/8, ..., 6=7/8) + NRF_LPCOMP->REFSEL = ((uint32_t)vdd_fraction_eighths << 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); + } + + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)", + ain_channel, vdd_fraction_eighths + 1); +} +#endif + void NRF52BoardDCDC::begin() { NRF52Board::begin(); diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 0d6c0a431..8f90cb70c 100644 --- a/src/helpers/NRF52Board.h +++ b/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_ref_eighths; // VDD fraction: 0=1/8, 1=2/8, ..., 6=7/8 + + // 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 configureLpcompWake(uint8_t ain_channel, uint8_t vdd_fraction_eighths); + virtual void prepareForShutdown() { } +#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 }; /* diff --git a/variants/promicro/PromicroBoard.cpp b/variants/promicro/PromicroBoard.cpp index 7011521b8..ee10d5c50 100644 --- a/variants/promicro/PromicroBoard.cpp +++ b/variants/promicro/PromicroBoard.cpp @@ -3,11 +3,28 @@ #include "PromicroBoard.h" -void PromicroBoard::begin() { +#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_ref_eighths = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +// Static callback: prepare board for SYSTEMOFF +void PromicroBoard::prepareForShutdown() { + // Disable LoRa module power before shutdown + digitalWrite(SX126X_POWER_EN, LOW); +} +#endif // NRF52_POWER_MANAGEMENT + +void PromicroBoard::begin() { NRF52Board::begin(); btn_prev_state = HIGH; - + pinMode(PIN_VBAT_READ, INPUT); + analogReadResolution(ADC_RESOLUTION); #ifdef BUTTON_PIN pinMode(BUTTON_PIN, INPUT_PULLUP); @@ -16,10 +33,32 @@ void PromicroBoard::begin() { #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); #endif - + Wire.begin(); pinMode(SX126X_POWER_EN, OUTPUT); digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file + +#ifdef NRF52_POWER_MANAGEMENT + // Boot voltage protection check (may not return if voltage too low) + checkBootVoltage(&power_config); +#endif +} + +void PromicroBoard::powerOff() { +#ifdef BUTTON_PIN + // Wait for button release before shutting down + while(digitalRead(BUTTON_PIN) == LOW); +#endif + +#ifdef NRF52_POWER_MANAGEMENT + // Use centralized shutdown with LPCOMP wake + prepareForShutdown(); + configureLpcompWake(power_config.lpcomp_ain_channel, power_config.lpcomp_ref_eighths); + enterSystemOff(SHUTDOWN_REASON_USER); +#else + // Fallback: direct SYSTEMOFF + sd_power_system_off(); +#endif +} diff --git a/variants/promicro/PromicroBoard.h b/variants/promicro/PromicroBoard.h index c23ed1c99..539d4495e 100644 --- a/variants/promicro/PromicroBoard.h +++ b/variants/promicro/PromicroBoard.h @@ -25,6 +25,10 @@ class PromicroBoard : public NRF52BoardOTA { uint8_t btn_prev_state; float adc_mult = ADC_MULTIPLIER; +#ifdef NRF52_POWER_MANAGEMENT + void prepareForShutdown() override; +#endif + public: PromicroBoard() : NRF52BoardOTA("ProMicro_OTA") {} void begin(); @@ -73,7 +77,5 @@ class PromicroBoard : public NRF52BoardOTA { return 0; } - void powerOff() override { - sd_power_system_off(); - } + void powerOff() override; }; diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 15bb5ce67..57959eccb 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -4,6 +4,7 @@ board = promicro_nrf52840 build_flags = ${nrf52_base.build_flags} -I variants/promicro -D PROMICRO + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/promicro/variant.h b/variants/promicro/variant.h index 98489da19..0daf199fe 100644 --- a/variants/promicro/variant.h +++ b/variants/promicro/variant.h @@ -25,6 +25,12 @@ #define BATTERY_PIN (17) #define ADC_RESOLUTION 12 +// Power management configuration +// Note: High-impedance divider (10M+10M) may affect LPCOMP accuracy +#define PWRMGT_VOLTAGE_BOOTLOCK 3400 // Won't boot below this voltage (mV) +#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = BATTERY_PIN (17) +#define PWRMGT_LPCOMP_REFSEL 3 // 4/8 VDD (~3.0V wake threshold) + //////////////////////////////////////////////////////////////////////////////// // Number of pins diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index b7b60dc63..c8bd97d57 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -5,12 +5,38 @@ #include "XiaoNrf52Board.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_ref_eighths = PWRMGT_LPCOMP_REFSEL, + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK +}; + +// Static callback: prepare board for SYSTEMOFF +void XiaoNrf52Board::prepareForShutdown() { + // Keep VBAT divider enabled for LPCOMP monitoring during SYSTEMOFF + pinMode(VBAT_ENABLE, OUTPUT); + digitalWrite(VBAT_ENABLE, LOW); +} +#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 NRF52_POWER_MANAGEMENT + // Boot voltage protection check (may not return if voltage too low) + checkBootVoltage(&power_config); +#endif #ifdef PIN_USER_BTN pinMode(PIN_USER_BTN, INPUT_PULLUP); @@ -27,9 +53,41 @@ 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 + 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; +} + +void XiaoNrf52Board::powerOff() { + // Visual feedback: LED on while waiting for button release + digitalWrite(PIN_LED, LOW); +#ifdef PIN_USER_BTN + while(digitalRead(PIN_USER_BTN) == LOW); +#endif + digitalWrite(LED_GREEN, HIGH); + digitalWrite(LED_BLUE, HIGH); + digitalWrite(PIN_LED, HIGH); + +#ifdef PIN_USER_BTN + // Configure button press to wake from SYSTEMOFF + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_USER_BTN], NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); +#endif + +#ifdef NRF52_POWER_MANAGEMENT + // Use centralized shutdown with LPCOMP wake + prepareForShutdown(); + configureLpcompWake(power_config.lpcomp_ain_channel, power_config.lpcomp_ref_eighths); + enterSystemOff(SHUTDOWN_REASON_USER); +#else + // Fallback: direct SYSTEMOFF + sd_power_system_off(); +#endif } -#endif \ No newline at end of file +#endif // XIAO_NRF52 diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index 1c46dfeee..73f57f7cc 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/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 prepareForShutdown() override; +#endif + public: XiaoNrf52Board() : NRF52BoardOTA("XIAO_NRF52_OTA") {} void begin(); @@ -20,43 +25,13 @@ class XiaoNrf52Board : public NRF52BoardDCDC, public NRF52BoardOTA { } #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"; } - void powerOff() override { - // set led on and wait for button release before poweroff - digitalWrite(PIN_LED, LOW); -#ifdef PIN_USER_BTN - while(digitalRead(PIN_USER_BTN) == LOW); -#endif - digitalWrite(LED_GREEN, HIGH); - digitalWrite(LED_BLUE, HIGH); - digitalWrite(PIN_LED, HIGH); - -#ifdef PIN_USER_BTN - // configure button press to wake up when in powered off state - nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); -#endif - - sd_power_system_off(); - } + void powerOff() override; }; -#endif \ No newline at end of file +#endif // XIAO_NRF52 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index edbf6275e..6e96018bc 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/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 diff --git a/variants/xiao_nrf52/variant.h b/variants/xiao_nrf52/variant.h index 3f4d7afeb..3c9473440 100644 --- a/variants/xiao_nrf52/variant.h +++ b/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 3400 // 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 (value 2 = 3/8) + static const uint8_t A0 = PIN_A0; static const uint8_t A1 = PIN_A1; static const uint8_t A2 = PIN_A2;