From c6ed5f19f68b9cf3fc6a46053eb3dccb31ba76f2 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Thu, 25 Jun 2026 14:11:15 +0100 Subject: [PATCH 01/12] Ignore local Pipfile --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a0ad5f6ea9..e65b723081 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ compile_commands.json .venv/ venv/ platformio.local.ini +Pipfile From 7316958c559f6ce854f76be4a2a47bd4c1ae7a38 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 22:59:00 +0100 Subject: [PATCH 02/12] Add XIAO nRF52 power-fail brownout shutdown --- docs/nrf52_power_management.md | 71 ++++++++++++----- src/helpers/NRF52Board.cpp | 103 ++++++++++++++++++++++--- src/helpers/NRF52Board.h | 16 +++- variants/xiao_nrf52/XiaoNrf52Board.cpp | 6 +- variants/xiao_nrf52/variant.h | 7 ++ 5 files changed, 167 insertions(+), 36 deletions(-) diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 6d97f2c306..35fcef2aad 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -17,6 +17,12 @@ The nRF52 Power Management module provides battery protection features to preven - 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 +### Runtime Power-Fail Shutdown +- Optionally arms the nRF52 power-fail warning comparator on regulated VDD +- Allows firmware to enter SYSTEMOFF before an uncontrolled brownout if VDD falls through the configured threshold +- Can arm VBUS detection as the recovery wake source for builds powered by a battery connected to VUSB +- Does not replace hardware brownout behaviour: if voltage collapses before firmware runs the handler, recovery is controlled by reset and regulator hardware + ### 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.) @@ -35,31 +41,32 @@ Shutdown reason codes (stored in GPREGRET2): ## 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 | -| GAT562 Mesh Watch13 | 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 | -| 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 | +| Board | Implemented | LPCOMP wake | VBUS wake | Runtime POF shutdown | +|-------------------------------------------|-------------|-------------|-----------|----------------------| +| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes | Yes | +| RAK4631 (`rak4631`) | Yes | Yes | Yes | No | +| Heltec T114 (`heltec_t114`) | Yes | Yes | Yes | No | +| GAT562 Mesh Watch13 | Yes | Yes | Yes | No | +| Promicro nRF52840 | No | No | No | No | +| RAK WisMesh Tag | No | No | No | No | +| Heltec Mesh Solar | No | No | No | No | +| LilyGo T-Echo / T-Echo Lite | No | No | No | No | +| SenseCAP Solar | Yes | Yes | Yes | No | +| WIO Tracker L1 / L1 E-Ink | No | No | No | No | +| WIO WM1110 | No | No | No | No | +| Mesh Pocket | No | No | No | No | +| Nano G2 Ultra | No | No | No | No | +| ThinkNode M1/M3/M6 | No | No | No | No | +| T1000-E | No | No | No | No | +| Ikoka Nano/Stick/Handheld (nRF) | No | No | No | No | +| Keepteen LT1 | No | No | No | No | +| Minewsemi ME25LS01 | No | 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. +- Runtime POF shutdown uses the nRF52 power-fail warning comparator. On XIAO it is configured for regulated VDD at 2.8 V and arms VBUS wake for SYSTEMOFF recovery. ## Technical Details @@ -89,6 +96,7 @@ To enable power management on a board variant: #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) + #define PWRMGT_POWER_FAIL_VDD_THRESHOLD POWER_POFCON_THRESHOLD_V28 // Optional; 2.8 V regulated VDD ``` 3. **Implement in board .cpp file**: @@ -97,7 +105,9 @@ To enable power management on a board variant: const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .power_fail_vdd_threshold = 0, // Optional; 0 disables runtime POF shutdown + .power_fail_vbus_wake = false // Optional; true arms VBUS wake after POF shutdown }; void MyBoard::initiateShutdown(uint8_t reason) { @@ -143,6 +153,25 @@ The LPCOMP (Low Power Comparator) is configured to: 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). +### Runtime Power-Fail Configuration + +Runtime power-fail shutdown is configured by optional `PowerMgtConfig` fields: + +| Field | Default | Description | +|-------|---------|-------------| +| `power_fail_vdd_threshold` | `0` | Disabled when `0`; otherwise an nRF52 `POWER_POFCON_THRESHOLD_*` value for regulated VDD | +| `power_fail_vbus_wake` | `false` | When true, the POF handler arms VBUS detect as the SYSTEMOFF wake source | + +For nRF52840 VDD, supported POF thresholds are 1.7 V through 2.8 V in 0.1 V steps. The XIAO nRF52840 default uses `POWER_POFCON_THRESHOLD_V28`, the highest available regulated-VDD threshold, so firmware gets the earliest available warning when the 3.3 V rail starts to collapse. + +For nRF52840 VDDH, hardware also supports 2.7 V through 4.2 V thresholds. MeshCore does not currently use the VDDH threshold for XIAO because a battery on the XIAO VUSB pin is not the same as direct nRF52840 VDDH measurement in the board abstraction. + +This path is deliberately separate from SYSTEMOFF wake source selection: +- If supply voltage simply collapses, firmware does not choose a wake source; reset and regulator hardware determine when execution resumes. +- POF shutdown only applies if the MCU is still executing when VDD crosses the configured threshold. +- When POF shutdown succeeds and `power_fail_vbus_wake` is true, recovery happens when the nRF52 VBUS detector sees VUSB again, not when BAT sense rises. +- SoftDevice builds need a SoftDevice SoC event hook for `NRF_EVT_POWER_FAILURE_WARNING`. The current Adafruit nRF52 framework consumes that event internally, so this branch only enables direct POF shutdown when SoftDevice is not active. + **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) | diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 17265f0455..3eed1c40ba 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -24,10 +24,12 @@ void NRF52Board::begin() { #ifdef NRF52_POWER_MANAGEMENT #include "nrf.h" +#include // Power Management global variables uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason +static bool nrf52_power_fail_vbus_wake = false; // Early constructor - runs before SystemInit() clears the registers // Priority 101 ensures this runs before SystemInit (102) and before @@ -37,6 +39,39 @@ static void __attribute__((constructor(101))) nrf52_early_reset_capture() { g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2; } +static void nrf52_record_shutdown_reason(uint8_t reason) { + 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; + } +} + +static void nrf52_configure_vbus_wake_direct() { +#ifdef POWER_INTENSET_USBDETECTED_Msk + NRF_POWER->EVENTS_USBDETECTED = 0; + NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk; +#endif +} + +static void nrf52_power_fail_warning_handler() { + // This callback runs in the POWER interrupt with the SoftDevice disabled. + // Keep the path register-only: Serial, delay(), and board callbacks are not + // safe when VDD is already falling. + nrfx_power_pof_disable(); + nrf52_record_shutdown_reason(SHUTDOWN_REASON_LOW_VOLTAGE); + + if (nrf52_power_fail_vbus_wake) { + nrf52_configure_vbus_wake_direct(); + } + + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + while (1) { } +} + void NRF52Board::initPowerMgr() { // Copy early-captured register values reset_reason = g_nrf52_reset_reason; @@ -100,6 +135,7 @@ bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { // Read boot voltage boot_voltage_mv = getBattMilliVolts(); + configurePowerFailShutdown(config); if (config->voltage_bootlock == 0) return true; // Protection disabled @@ -135,12 +171,7 @@ void NRF52Board::enterSystemOff(uint8_t 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; - } + nrf52_record_shutdown_reason(reason); // Flush serial buffers Serial.flush(); @@ -163,6 +194,18 @@ void NRF52Board::enterSystemOff(uint8_t reason) { NVIC_SystemReset(); } +void NRF52Board::configureVbusWake() { + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + sd_power_usbdetected_enable(1); + } else { + nrf52_configure_vbus_wake_direct(); + } + + MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); +} + 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 @@ -210,17 +253,53 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { ain_channel, ref_num); } - // Configure VBUS (USB power) wake alongside LPCOMP + // Configure VBUS (USB power) wake alongside LPCOMP. + configureVbusWake(); +} + +void NRF52Board::configurePowerFailShutdown(const PowerMgtConfig* config) { + if (config->power_fail_vdd_threshold == 0) return; + 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; + // SoftDevice delivers POFWARN through its SoC event queue. The current + // framework consumes that queue internally and does not expose a power-fail + // callback, so leave runtime power-fail shutdown disabled rather than + // installing a competing interrupt handler. + MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown skipped (SoftDevice active)"); + return; } - MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); + nrfx_power_config_t power_config = {}; + power_config.dcdcen = NRF_POWER->DCDCEN != 0; +#if NRFX_POWER_SUPPORTS_DCDCEN_VDDH + power_config.dcdcenhv = NRF_POWER->DCDCEN0 != 0; +#endif + + nrfx_err_t err = nrfx_power_init(&power_config); + if (err != NRFX_SUCCESS && err != NRFX_ERROR_ALREADY_INITIALIZED) { + MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown setup failed (nrfx init %lu)", (unsigned long)err); + return; + } + + nrf52_power_fail_vbus_wake = config->power_fail_vbus_wake; + + nrfx_power_pofwarn_config_t pof_config = {}; + pof_config.handler = nrf52_power_fail_warning_handler; +#if NRFX_POWER_SUPPORTS_POFCON + pof_config.thr = (nrf_power_pof_thr_t)config->power_fail_vdd_threshold; +#endif +#if NRFX_POWER_SUPPORTS_POFCON_VDDH + pof_config.thrvddh = NRF_POWER_POFTHRVDDH_V27; +#endif + + nrfx_power_pof_init(&pof_config); + nrfx_power_pof_enable(&pof_config); + + MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown configured (VDD threshold code = %u; VBUS wake = %s)", + config->power_fail_vdd_threshold, + config->power_fail_vbus_wake ? "yes" : "no"); } #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 17065cf443..6ee85d3581 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -21,6 +21,18 @@ struct PowerMgtConfig { // Boot protection voltage threshold (millivolts) // Set to 0 to disable boot protection uint16_t voltage_bootlock; + + // Optional nRF52 power-fail warning threshold for regulated VDD. + // Set to 0 to disable runtime power-fail shutdown. Threshold values are + // the nRF52 POWER_POFCON_THRESHOLD_* enum values, for example + // POWER_POFCON_THRESHOLD_V28 for 2.8 V. + uint8_t power_fail_vdd_threshold; + + // If true, runtime power-fail shutdown arms VBUS detect as the SYSTEMOFF + // wake source. This is useful for boards powered by a battery on VUSB where + // BAT sense is not available, but it only applies after a deliberate + // firmware shutdown before uncontrolled brownout. + bool power_fail_vbus_wake; }; #endif @@ -41,6 +53,8 @@ class NRF52Board : public mesh::MainBoard { bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + void configureVbusWake(); + void configurePowerFailShutdown(const PowerMgtConfig* config); virtual void initiateShutdown(uint8_t reason); #endif @@ -76,4 +90,4 @@ class NRF52BoardDCDC : virtual public NRF52Board { NRF52BoardDCDC() {} virtual void begin() override; }; -#endif \ No newline at end of file +#endif diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index 42ee6a87fe..80a64871c7 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -11,7 +11,9 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .power_fail_vdd_threshold = PWRMGT_POWER_FAIL_VDD_THRESHOLD, + .power_fail_vbus_wake = true }; void XiaoNrf52Board::initiateShutdown(uint8_t reason) { @@ -71,4 +73,4 @@ uint16_t XiaoNrf52Board::getBattMilliVolts() { return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; } -#endif \ No newline at end of file +#endif diff --git a/variants/xiao_nrf52/variant.h b/variants/xiao_nrf52/variant.h index 25619b9e59..398897fb14 100644 --- a/variants/xiao_nrf52/variant.h +++ b/variants/xiao_nrf52/variant.h @@ -79,6 +79,13 @@ static const uint8_t D10 = 10; // Set to 0 to disable boot protection #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage +// Runtime power-fail warning for VUSB-battery builds. +// The nRF52840 can warn on regulated VDD down to configurable thresholds, but +// it cannot directly measure VUSB without a board-level ADC path. Use the +// highest VDD threshold so firmware can deliberately enter SYSTEMOFF before an +// uncontrolled brownout when the regulator output starts to collapse. +#define PWRMGT_POWER_FAIL_VDD_THRESHOLD POWER_POFCON_THRESHOLD_V28 + // 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). From c6a9ac6cccfd00b16b3843a686147a86c32adc8e Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 19:54:41 +0100 Subject: [PATCH 03/12] Fix nRF52 power source validity handling --- docs/cli_commands.md | 3 + docs/nrf52_power_management.md | 27 +++++--- src/MeshCore.h | 1 + src/helpers/CommonCLI.cpp | 2 +- src/helpers/NRF52Board.cpp | 69 +++++++++++++++++-- src/helpers/NRF52Board.h | 15 ++++ .../GAT56230SMeshKitBoard.cpp | 11 ++- .../gat562_mesh_evb_pro/GAT562EVBProBoard.cpp | 11 ++- .../GAT562MeshTrackerProBoard.cpp | 11 ++- .../GAT56MeshWatch13Board.cpp | 11 ++- variants/heltec_t096/T096Board.cpp | 11 ++- variants/heltec_t1/T1Board.cpp | 9 ++- variants/heltec_t114/T114Board.cpp | 11 ++- variants/muziworks_r1_neo/R1NeoBoard.cpp | 9 ++- variants/rak3401/RAK3401Board.cpp | 9 ++- variants/rak4631/RAK4631Board.cpp | 11 ++- .../sensecap_solar/SenseCapSolarBoard.cpp | 9 ++- variants/xiao_nrf52/XiaoNrf52Board.cpp | 9 ++- 18 files changed, 195 insertions(+), 44 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 66a9b77afe..14eb7c9321 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1107,6 +1107,9 @@ region save #### View the current power source **Usage:** `get pwrmgt.source` +Returns a composite source and confidence state, for example `vusb+bat:valid`, +`vusb-only:invalid`, `bat-only:implausible`, or `none:unknown`. + **Note:** Returns an error on boards without power management support. --- diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 35fcef2aad..85e521d434 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -8,13 +8,13 @@ The nRF52 Power Management module provides battery protection features to preven ### 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) +- If voltage is below a configurable threshold (e.g., 3300mV), and the board declares the battery sense path valid, the device configures voltage wake and enters protective shutdown (SYSTEMOFF) - Prevents boot loops when battery is critically low -- Skipped when external power (USB VBUS) is detected +- Skipped when external power (USB VBUS) is detected, or when the battery voltage evidence is invalid or implausible ### 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 +- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF only when the board declares the LPCOMP sense node valid +- Enables USB VBUS detection independently where hardware supports VBUS wake - Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected ### Runtime Power-Fail Shutdown @@ -23,6 +23,11 @@ The nRF52 Power Management module provides battery protection features to preven - Can arm VBUS detection as the recovery wake source for builds powered by a battery connected to VUSB - Does not replace hardware brownout behaviour: if voltage collapses before firmware runs the handler, recovery is controlled by reset and regulator hardware +### Power Source State +- `get pwrmgt.source` returns a composite state with a confidence suffix: `vusb+bat`, `vusb-only`, `bat-only`, or `none`, followed by `:valid`, `:implausible`, `:invalid`, or `:unknown` +- `invalid` means the board cannot use the configured battery sense path for protective decisions +- `implausible` means the sensed voltage is outside the board's configured plausible range + ### 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.) @@ -43,7 +48,7 @@ Shutdown reason codes (stored in GPREGRET2): | Board | Implemented | LPCOMP wake | VBUS wake | Runtime POF shutdown | |-------------------------------------------|-------------|-------------|-----------|----------------------| -| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes | Yes | +| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | No | Yes | Yes | | RAK4631 (`rak4631`) | Yes | Yes | Yes | No | | Heltec T114 (`heltec_t114`) | Yes | Yes | Yes | No | | GAT562 Mesh Watch13 | Yes | Yes | Yes | No | @@ -65,7 +70,8 @@ Shutdown reason codes (stored in GPREGRET2): 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. +- VBUS detection is used to skip boot lockout on external power. VBUS wake is configured independently from LPCOMP where supported hardware exposes VBUS to the nRF52. +- XIAO nRF52 disables trusted BAT/LPCOMP protection by default because BAT+ may be disconnected while VUSB is the actual supply. - Runtime POF shutdown uses the nRF52 power-fail warning comparator. On XIAO it is configured for regulated VDD at 2.8 V and arms VBUS wake for SYSTEMOFF recovery. ## Technical Details @@ -106,6 +112,11 @@ To enable power management on a board variant: .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500, .power_fail_vdd_threshold = 0, // Optional; 0 disables runtime POF shutdown .power_fail_vbus_wake = false // Optional; true arms VBUS wake after POF shutdown }; @@ -116,7 +127,7 @@ To enable power management on a board variant: reason == SHUTDOWN_REASON_BOOT_PROTECT); if (enable_lpcomp) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -211,7 +222,7 @@ 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.source` | Returns composite source and confidence, e.g. `vusb+bat:valid` | | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts | diff --git a/src/MeshCore.h b/src/MeshCore.h index cfa33cf90b..dc4f528b51 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -67,6 +67,7 @@ class MainBoard { // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } + virtual const char* getPowerSourceState() { return isExternalPowered() ? "vusb-only:unknown" : "none:unknown"; } virtual uint16_t getBootVoltage() { return 0; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b78ad6ebd6..a9447e62f0 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -908,7 +908,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #endif } else if (memcmp(config, "pwrmgt.source", 13) == 0) { #ifdef NRF52_POWER_MANAGEMENT - strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery"); + sprintf(reply, "> %s", _board->getPowerSourceState()); #else strcpy(reply, "ERROR: Power management not supported"); #endif diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 3eed1c40ba..dd3fafbb21 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -131,6 +131,7 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) { } bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { + active_power_config = config; initPowerMgr(); // Read boot voltage @@ -149,9 +150,17 @@ bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { 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) { + if (!config->battery_voltage_sense_valid) { + MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (battery voltage sense invalid)"); + return true; + } + + if (!isBatteryVoltagePlausible(boot_voltage_mv, config)) { + MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (implausible battery voltage)"); + return true; + } + + if (boot_voltage_mv < config->voltage_bootlock) { MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown"); initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT); @@ -253,8 +262,32 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { ain_channel, ref_num); } - // Configure VBUS (USB power) wake alongside LPCOMP. - configureVbusWake(); +} + +void NRF52Board::configureVoltageWake(const PowerMgtConfig* config) { + if (config == nullptr) return; + + if (config->lpcomp_voltage_wake_valid) { + configureVoltageWake(config->lpcomp_ain_channel, config->lpcomp_refsel); + } else { + MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake skipped (voltage sense invalid)"); + } + + if (config->vbus_wake_valid) { + configureVbusWake(); + } +} + +void NRF52Board::configureVbusWake() { + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + sd_power_usbdetected_enable(1); + } else { + nrf52_configure_vbus_wake_direct(); + } + + MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); } void NRF52Board::configurePowerFailShutdown(const PowerMgtConfig* config) { @@ -301,6 +334,32 @@ void NRF52Board::configurePowerFailShutdown(const PowerMgtConfig* config) { config->power_fail_vdd_threshold, config->power_fail_vbus_wake ? "yes" : "no"); } + +bool NRF52Board::isBatteryVoltagePlausible(uint16_t millivolts, const PowerMgtConfig* config) const { + if (config == nullptr) return false; + return millivolts >= config->battery_min_plausible_mv && + millivolts <= config->battery_max_plausible_mv; +} + +const char* NRF52Board::getPowerSourceState() { + bool vusb_connected = isExternalPowered(); + + if (active_power_config == nullptr) { + return vusb_connected ? "vusb-only:unknown" : "none:unknown"; + } + + uint16_t battery_mv = getBattMilliVolts(); + + if (!active_power_config->battery_voltage_sense_valid) { + return vusb_connected ? "vusb-only:invalid" : "none:invalid"; + } + + if (!isBatteryVoltagePlausible(battery_mv, active_power_config)) { + return vusb_connected ? "vusb+bat:implausible" : "bat-only:implausible"; + } + + return vusb_connected ? "vusb+bat:valid" : "bat-only:valid"; +} #endif void NRF52BoardDCDC::begin() { diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 6ee85d3581..d862a31607 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -22,6 +22,17 @@ struct PowerMgtConfig { // Set to 0 to disable boot protection uint16_t voltage_bootlock; + // Capability flags describe what this board can prove from its sense wiring. + // They prevent VBUS loss from being treated as proof that a BAT sense node is valid. + bool battery_voltage_sense_valid; + bool lpcomp_voltage_wake_valid; + bool vbus_wake_valid; + + // Broad plausibility limits for a sensed battery voltage. Readings outside + // this range are treated as unsafe evidence for protective shutdown decisions. + uint16_t battery_min_plausible_mv; + uint16_t battery_max_plausible_mv; + // Optional nRF52 power-fail warning threshold for regulated VDD. // Set to 0 to disable runtime power-fail shutdown. Threshold values are // the nRF52 POWER_POFCON_THRESHOLD_* enum values, for example @@ -49,13 +60,16 @@ class NRF52Board : public mesh::MainBoard { 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) + const PowerMgtConfig* active_power_config = nullptr; bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + void configureVoltageWake(const PowerMgtConfig* config); void configureVbusWake(); void configurePowerFailShutdown(const PowerMgtConfig* config); virtual void initiateShutdown(uint8_t reason); + bool isBatteryVoltagePlausible(uint16_t millivolts, const PowerMgtConfig* config) const; #endif public: @@ -71,6 +85,7 @@ class NRF52Board : public mesh::MainBoard { #ifdef NRF52_POWER_MANAGEMENT uint16_t getBootVoltage() override { return boot_voltage_mv; } + const char* getPowerSourceState() override; virtual uint32_t getResetReason() const override { return reset_reason; } uint8_t getShutdownReason() const override { return shutdown_reason; } const char* getResetReasonString(uint32_t reason) override; diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp index 87fa1a785d..3803a6eb6a 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp @@ -10,7 +10,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; @@ -20,7 +25,7 @@ void GAT56230SMeshKitBoard::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -54,4 +59,4 @@ void GAT56230SMeshKitBoard::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file +} diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp index ef4af8126b..8c6126e2f2 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp @@ -10,7 +10,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; @@ -20,7 +25,7 @@ void GAT562EVBProBoard::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -49,4 +54,4 @@ void GAT562EVBProBoard::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1268 some time to power up -} \ No newline at end of file +} diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp index a4e087e718..f36e44312e 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp @@ -10,7 +10,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; @@ -20,7 +25,7 @@ void GAT562MeshTrackerProBoard::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -54,4 +59,4 @@ void GAT562MeshTrackerProBoard::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file +} diff --git a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp index 5a24b541de..3912f6e046 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp @@ -10,14 +10,19 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void GAT56MeshWatch13Board::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); } @@ -43,4 +48,4 @@ void GAT56MeshWatch13Board::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file +} diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 550131571f..6cd86a0a41 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -9,7 +9,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void T096Board::initiateShutdown(uint8_t reason) { @@ -25,7 +30,7 @@ void T096Board::initiateShutdown(uint8_t reason) { digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW); if (enable_lpcomp) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -123,4 +128,4 @@ void T096Board::powerOff() { const char* T096Board::getManufacturerName() const { return "Heltec T096"; -} \ No newline at end of file +} diff --git a/variants/heltec_t1/T1Board.cpp b/variants/heltec_t1/T1Board.cpp index 490f86787e..bb3678b232 100644 --- a/variants/heltec_t1/T1Board.cpp +++ b/variants/heltec_t1/T1Board.cpp @@ -7,7 +7,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void T1Board::initiateShutdown(uint8_t reason) { @@ -19,7 +24,7 @@ void T1Board::initiateShutdown(uint8_t reason) { digitalWrite(PIN_BAT_CTL, enable_lpcomp ? ADC_CTRL_ENABLED : !ADC_CTRL_ENABLED); if (enable_lpcomp) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index c03d39afb6..352dcb0bb1 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -9,7 +9,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void T114Board::initiateShutdown(uint8_t reason) { @@ -25,7 +30,7 @@ void T114Board::initiateShutdown(uint8_t reason) { digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW); if (enable_lpcomp) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -56,4 +61,4 @@ void T114Board::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file +} diff --git a/variants/muziworks_r1_neo/R1NeoBoard.cpp b/variants/muziworks_r1_neo/R1NeoBoard.cpp index 89cde02a53..f531e35102 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.cpp +++ b/variants/muziworks_r1_neo/R1NeoBoard.cpp @@ -9,7 +9,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void R1NeoBoard::initiateShutdown(uint8_t reason) { @@ -22,7 +27,7 @@ void R1NeoBoard::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index cbf7c1087d..61e87908fa 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -9,7 +9,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void RAK3401Board::initiateShutdown(uint8_t reason) { @@ -21,7 +26,7 @@ void RAK3401Board::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 9fb47b432e..3d55ab39f2 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -9,7 +9,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void RAK4631Board::initiateShutdown(uint8_t reason) { @@ -18,7 +23,7 @@ void RAK4631Board::initiateShutdown(uint8_t reason) { if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); @@ -50,4 +55,4 @@ void RAK4631Board::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up -} \ No newline at end of file +} diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp index da7964c9e6..2dba364fbb 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.cpp +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -7,7 +7,12 @@ const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, - .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK + .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = true, + .lpcomp_voltage_wake_valid = true, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500 }; void SenseCapSolarBoard::initiateShutdown(uint8_t reason) { @@ -18,7 +23,7 @@ void SenseCapSolarBoard::initiateShutdown(uint8_t reason) { digitalWrite(VBAT_ENABLE, enable_lpcomp ? LOW : HIGH); if (enable_lpcomp) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index 80a64871c7..e458e08643 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -8,10 +8,17 @@ #ifdef NRF52_POWER_MANAGEMENT // Static configuration for power management // Values set in variant.h defines +// XIAO BAT+ is optional. When VUSB is the actual supply and BAT+ is open, the +// BAT divider/LPCOMP node is not valid evidence for protective shutdown. const PowerMgtConfig power_config = { .lpcomp_ain_channel = PWRMGT_LPCOMP_AIN, .lpcomp_refsel = PWRMGT_LPCOMP_REFSEL, .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK, + .battery_voltage_sense_valid = false, + .lpcomp_voltage_wake_valid = false, + .vbus_wake_valid = true, + .battery_min_plausible_mv = 1000, + .battery_max_plausible_mv = 6500, .power_fail_vdd_threshold = PWRMGT_POWER_FAIL_VDD_THRESHOLD, .power_fail_vbus_wake = true }; @@ -24,7 +31,7 @@ void XiaoNrf52Board::initiateShutdown(uint8_t reason) { digitalWrite(VBAT_ENABLE, enable_lpcomp ? LOW : HIGH); if (enable_lpcomp) { - configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); + configureVoltageWake(&power_config); } enterSystemOff(reason); From 00e7c33404ba41913c5fbbc2decd0b8b1d71eb76 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 21:09:39 +0100 Subject: [PATCH 04/12] Tighten nRF52 power source confidence states --- docs/cli_commands.md | 13 ++++++++- docs/nrf52_power_management.md | 29 +++++++++++++++++-- src/helpers/NRF52Board.cpp | 2 +- .../GAT56230SMeshKitBoard.cpp | 4 +-- .../gat562_mesh_evb_pro/GAT562EVBProBoard.cpp | 4 +-- .../GAT562MeshTrackerProBoard.cpp | 4 +-- .../GAT56MeshWatch13Board.cpp | 4 +-- variants/heltec_t096/T096Board.cpp | 4 +-- variants/heltec_t1/T1Board.cpp | 4 +-- variants/heltec_t114/T114Board.cpp | 4 +-- variants/muziworks_r1_neo/R1NeoBoard.cpp | 4 +-- variants/rak3401/RAK3401Board.cpp | 4 +-- variants/rak4631/RAK4631Board.cpp | 4 +-- .../sensecap_solar/SenseCapSolarBoard.cpp | 4 +-- variants/xiao_nrf52/XiaoNrf52Board.cpp | 4 +-- 15 files changed, 63 insertions(+), 29 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 14eb7c9321..ac05c9f9ee 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1108,7 +1108,18 @@ region save **Usage:** `get pwrmgt.source` Returns a composite source and confidence state, for example `vusb+bat:valid`, -`vusb-only:invalid`, `bat-only:implausible`, or `none:unknown`. +`vusb-only:valid`, `bat-only:implausible`, or `undetected`. + +Confidence thresholds are board-configured in `PowerMgtConfig`. The current +nRF52 power-management board configs all set `battery_min_plausible_mv = 2500` +and `battery_max_plausible_mv = 4500`, so BAT voltage is `valid` from 2500mV +to 4500mV inclusive. Readings outside that range are `implausible`, boards with +unusable BAT sensing report `invalid`, and missing power-management +configuration reports `unknown`. VUSB has no configured millivolt thresholds; +it is detected through the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal. +If a battery is connected to VUSB and falls below the hardware VBUS-detect +point while still powering the MCU, the source is reported as +`undetected`. **Note:** Returns an error on boards without power management support. diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 85e521d434..716d0c8439 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -24,9 +24,32 @@ The nRF52 Power Management module provides battery protection features to preven - Does not replace hardware brownout behaviour: if voltage collapses before firmware runs the handler, recovery is controlled by reset and regulator hardware ### Power Source State -- `get pwrmgt.source` returns a composite state with a confidence suffix: `vusb+bat`, `vusb-only`, `bat-only`, or `none`, followed by `:valid`, `:implausible`, `:invalid`, or `:unknown` +- `get pwrmgt.source` returns a source state. Normal detected sources use a confidence suffix: `vusb+bat`, `vusb-only`, `bat-only`, or `none`, followed by `:valid`, `:implausible`, `:invalid`, or `:unknown` - `invalid` means the board cannot use the configured battery sense path for protective decisions - `implausible` means the sensed voltage is outside the board's configured plausible range +- `undetected` means the board is powered, but neither VBUS detect nor a valid BAT sense path can prove which input is supplying it + +Confidence suffixes are assigned as follows: + +| Suffix | Condition | Protective BAT decisions | +|-----------------|-------------------------------------------------------------------------------------------|--------------------------| +| `:valid` | Battery sense is enabled for the board and the reading is within the configured range | Allowed | +| `:implausible` | Battery sense is enabled for the board but the reading is below minimum or above maximum | Blocked | +| `:invalid` | Battery sense is not valid for the board's supported wiring or operating mode | Blocked | +| `:unknown` | Power management has no active board configuration when the source is queried | Blocked | + +The current nRF52 power-management board configs all use these plausibility thresholds: + +| `PowerMgtConfig` field | Configured value | Meaning | +|------------------------------|------------------|----------------------------------------------| +| `battery_min_plausible_mv` | `2500` | Readings below 2500mV are `:implausible` | +| `battery_max_plausible_mv` | `4500` | Readings above 4500mV are `:implausible` | + +The range is inclusive: `2500mV <= battery_mv <= 4500mV` is `:valid` when the board's battery sense path is enabled. Boards can override these fields in their own `PowerMgtConfig`. + +There are no configured VUSB millivolt confidence thresholds in the current implementation. VUSB state is based on the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal, not a firmware ADC voltage reading centred around 5V. `PowerMgtConfig::vbus_wake_valid` only records whether VBUS wake is supported for the board. + +This means a battery connected to VUSB is reported as `vusb-only:valid` while `VBUSDETECT` is asserted. If that VUSB battery falls below the hardware detection point but still powers the MCU, firmware cannot prove the input path and reports `undetected` rather than `none`. ### Early Boot Register Capture - Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them @@ -115,8 +138,8 @@ To enable power management on a board variant: .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500, + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500, .power_fail_vdd_threshold = 0, // Optional; 0 disables runtime POF shutdown .power_fail_vbus_wake = false // Optional; true arms VBUS wake after POF shutdown }; diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index dd3fafbb21..edb7052701 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -351,7 +351,7 @@ const char* NRF52Board::getPowerSourceState() { uint16_t battery_mv = getBattMilliVolts(); if (!active_power_config->battery_voltage_sense_valid) { - return vusb_connected ? "vusb-only:invalid" : "none:invalid"; + return vusb_connected ? "vusb-only:valid" : "undetected"; } if (!isBatteryVoltagePlausible(battery_mv, active_power_config)) { diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp index 3803a6eb6a..d66bb2f325 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp @@ -14,8 +14,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp index 8c6126e2f2..23dd399e02 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp @@ -14,8 +14,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp index f36e44312e..2e5274ae9b 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp @@ -14,8 +14,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; diff --git a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp index 3912f6e046..6bc30a64cc 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp @@ -14,8 +14,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 6cd86a0a41..9e1421c604 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -13,8 +13,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void T096Board::initiateShutdown(uint8_t reason) { diff --git a/variants/heltec_t1/T1Board.cpp b/variants/heltec_t1/T1Board.cpp index bb3678b232..024fd60a74 100644 --- a/variants/heltec_t1/T1Board.cpp +++ b/variants/heltec_t1/T1Board.cpp @@ -11,8 +11,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void T1Board::initiateShutdown(uint8_t reason) { diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index 352dcb0bb1..562ba8127c 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -13,8 +13,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void T114Board::initiateShutdown(uint8_t reason) { diff --git a/variants/muziworks_r1_neo/R1NeoBoard.cpp b/variants/muziworks_r1_neo/R1NeoBoard.cpp index f531e35102..c8d095d02c 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.cpp +++ b/variants/muziworks_r1_neo/R1NeoBoard.cpp @@ -13,8 +13,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void R1NeoBoard::initiateShutdown(uint8_t reason) { diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index 61e87908fa..aef3dd6ad3 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -13,8 +13,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void RAK3401Board::initiateShutdown(uint8_t reason) { diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 3d55ab39f2..97849c20cb 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -13,8 +13,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void RAK4631Board::initiateShutdown(uint8_t reason) { diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp index 2dba364fbb..c098819a16 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.cpp +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -11,8 +11,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500 + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500 }; void SenseCapSolarBoard::initiateShutdown(uint8_t reason) { diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index e458e08643..0c5ebe3548 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -17,8 +17,8 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = false, .lpcomp_voltage_wake_valid = false, .vbus_wake_valid = true, - .battery_min_plausible_mv = 1000, - .battery_max_plausible_mv = 6500, + .battery_min_plausible_mv = 2500, + .battery_max_plausible_mv = 4500, .power_fail_vdd_threshold = PWRMGT_POWER_FAIL_VDD_THRESHOLD, .power_fail_vbus_wake = true }; From 450a7513aaa924e840d4ebdcca93f7f86921b0f9 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 21:39:36 +0100 Subject: [PATCH 05/12] Report possible VUSB battery source state --- docs/cli_commands.md | 4 ++-- docs/nrf52_power_management.md | 5 +++-- src/helpers/NRF52Board.cpp | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index ac05c9f9ee..d4399d7a0c 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1108,7 +1108,7 @@ region save **Usage:** `get pwrmgt.source` Returns a composite source and confidence state, for example `vusb+bat:valid`, -`vusb-only:valid`, `bat-only:implausible`, or `undetected`. +`vusb-only:valid`, `bat-only:implausible`, or `vusb-only:possible-battery`. Confidence thresholds are board-configured in `PowerMgtConfig`. The current nRF52 power-management board configs all set `battery_min_plausible_mv = 2500` @@ -1119,7 +1119,7 @@ configuration reports `unknown`. VUSB has no configured millivolt thresholds; it is detected through the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal. If a battery is connected to VUSB and falls below the hardware VBUS-detect point while still powering the MCU, the source is reported as -`undetected`. +`vusb-only:possible-battery`. **Note:** Returns an error on boards without power management support. diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 716d0c8439..0dbac178ee 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -27,7 +27,7 @@ The nRF52 Power Management module provides battery protection features to preven - `get pwrmgt.source` returns a source state. Normal detected sources use a confidence suffix: `vusb+bat`, `vusb-only`, `bat-only`, or `none`, followed by `:valid`, `:implausible`, `:invalid`, or `:unknown` - `invalid` means the board cannot use the configured battery sense path for protective decisions - `implausible` means the sensed voltage is outside the board's configured plausible range -- `undetected` means the board is powered, but neither VBUS detect nor a valid BAT sense path can prove which input is supplying it +- `possible-battery` means the board is powered without VBUS detect, and the board cannot prove whether VUSB is being used as a battery input Confidence suffixes are assigned as follows: @@ -37,6 +37,7 @@ Confidence suffixes are assigned as follows: | `:implausible` | Battery sense is enabled for the board but the reading is below minimum or above maximum | Blocked | | `:invalid` | Battery sense is not valid for the board's supported wiring or operating mode | Blocked | | `:unknown` | Power management has no active board configuration when the source is queried | Blocked | +| `:possible-battery` | VBUS detect is low, BAT sense is invalid, and VUSB may be acting as a battery input | Blocked | The current nRF52 power-management board configs all use these plausibility thresholds: @@ -49,7 +50,7 @@ The range is inclusive: `2500mV <= battery_mv <= 4500mV` is `:valid` when the bo There are no configured VUSB millivolt confidence thresholds in the current implementation. VUSB state is based on the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal, not a firmware ADC voltage reading centred around 5V. `PowerMgtConfig::vbus_wake_valid` only records whether VBUS wake is supported for the board. -This means a battery connected to VUSB is reported as `vusb-only:valid` while `VBUSDETECT` is asserted. If that VUSB battery falls below the hardware detection point but still powers the MCU, firmware cannot prove the input path and reports `undetected` rather than `none`. +This means a battery connected to VUSB is reported as `vusb-only:valid` while `VBUSDETECT` is asserted. If that VUSB battery falls below the hardware detection point but still powers the MCU, firmware reports `vusb-only:possible-battery` rather than `none`. ### Early Boot Register Capture - Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index edb7052701..0a1aeefcf2 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -351,7 +351,7 @@ const char* NRF52Board::getPowerSourceState() { uint16_t battery_mv = getBattMilliVolts(); if (!active_power_config->battery_voltage_sense_valid) { - return vusb_connected ? "vusb-only:valid" : "undetected"; + return vusb_connected ? "vusb-only:valid" : "vusb-only:possible-battery"; } if (!isBatteryVoltagePlausible(battery_mv, active_power_config)) { From 72c18f4042b1587d4cac7641b31ef5a2c5d85802 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 22:08:41 +0100 Subject: [PATCH 06/12] Mark invalid boot voltage readings --- docs/cli_commands.md | 9 ++++++++- docs/nrf52_power_management.md | 14 ++++++++------ src/MeshCore.h | 1 + src/helpers/CommonCLI.cpp | 3 ++- src/helpers/NRF52Board.cpp | 14 ++++++++++++-- src/helpers/NRF52Board.h | 8 ++++++-- .../gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp | 1 + variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp | 1 + .../GAT562MeshTrackerProBoard.cpp | 1 + .../gat562_mesh_watch13/GAT56MeshWatch13Board.cpp | 1 + variants/heltec_t096/T096Board.cpp | 1 + variants/heltec_t1/T1Board.cpp | 1 + variants/heltec_t114/T114Board.cpp | 1 + variants/muziworks_r1_neo/R1NeoBoard.cpp | 1 + variants/rak3401/RAK3401Board.cpp | 1 + variants/rak4631/RAK4631Board.cpp | 1 + variants/sensecap_solar/SenseCapSolarBoard.cpp | 1 + variants/xiao_nrf52/XiaoNrf52Board.cpp | 1 + 18 files changed, 49 insertions(+), 12 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d4399d7a0c..475e0fa830 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1111,12 +1111,16 @@ Returns a composite source and confidence state, for example `vusb+bat:valid`, `vusb-only:valid`, `bat-only:implausible`, or `vusb-only:possible-battery`. Confidence thresholds are board-configured in `PowerMgtConfig`. The current -nRF52 power-management board configs all set `battery_min_plausible_mv = 2500` +nRF52 power-management board configs all set `battery_min_present_mv = 1000`, +`battery_min_plausible_mv = 2500` and `battery_max_plausible_mv = 4500`, so BAT voltage is `valid` from 2500mV to 4500mV inclusive. Readings outside that range are `implausible`, boards with unusable BAT sensing report `invalid`, and missing power-management configuration reports `unknown`. VUSB has no configured millivolt thresholds; it is detected through the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal. +BAT readings below 1000mV are treated as absent/floating for boot-lock +decisions. Readings from 1000mV up to the boot-lock threshold still trigger +protective shutdown; high readings above 4500mV are treated as unsafe evidence. If a battery is connected to VUSB and falls below the hardware VBUS-detect point while still powering the MCU, the source is reported as `vusb-only:possible-battery`. @@ -1135,6 +1139,9 @@ point while still powering the MCU, the source is reported as #### View the boot voltage **Usage:** `get pwrmgt.bootmv` +Adds `invalid` when the board configuration marks BAT voltage sensing as +untrusted, for example `> 426 mV invalid`. + **Note:** Returns an error on boards without power management support. --- diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 0dbac178ee..3b6a1a7285 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -10,7 +10,7 @@ The nRF52 Power Management module provides battery protection features to preven - Checks battery voltage immediately after boot and before mesh operations commence - If voltage is below a configurable threshold (e.g., 3300mV), and the board declares the battery sense path valid, the device configures voltage wake and enters protective shutdown (SYSTEMOFF) - Prevents boot loops when battery is critically low -- Skipped when external power (USB VBUS) is detected, or when the battery voltage evidence is invalid or implausible +- Skipped when external power (USB VBUS) is detected, when the battery voltage sense path is invalid, when the battery reading is below the configured present threshold, or when the battery voltage evidence is implausibly high ### Voltage Wake (LPCOMP + VBUS) - Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF only when the board declares the LPCOMP sense node valid @@ -34,7 +34,7 @@ Confidence suffixes are assigned as follows: | Suffix | Condition | Protective BAT decisions | |-----------------|-------------------------------------------------------------------------------------------|--------------------------| | `:valid` | Battery sense is enabled for the board and the reading is within the configured range | Allowed | -| `:implausible` | Battery sense is enabled for the board but the reading is below minimum or above maximum | Blocked | +| `:implausible` | Battery sense is enabled for the board but the reading is below minimum or above maximum | Present low readings still shut down; absent/floating and high readings are blocked | | `:invalid` | Battery sense is not valid for the board's supported wiring or operating mode | Blocked | | `:unknown` | Power management has no active board configuration when the source is queried | Blocked | | `:possible-battery` | VBUS detect is low, BAT sense is invalid, and VUSB may be acting as a battery input | Blocked | @@ -43,10 +43,11 @@ The current nRF52 power-management board configs all use these plausibility thre | `PowerMgtConfig` field | Configured value | Meaning | |------------------------------|------------------|----------------------------------------------| -| `battery_min_plausible_mv` | `2500` | Readings below 2500mV are `:implausible` | -| `battery_max_plausible_mv` | `4500` | Readings above 4500mV are `:implausible` | +| `battery_min_present_mv` | `1000` | Readings below 1000mV are treated as absent/floating and skipped for boot protection | +| `battery_min_plausible_mv` | `2500` | Readings below 2500mV are reported as `:implausible`; readings at or above 1000mV still trigger boot protection when below `voltage_bootlock` | +| `battery_max_plausible_mv` | `4500` | Readings above 4500mV are `:implausible` and skipped for boot protection | -The range is inclusive: `2500mV <= battery_mv <= 4500mV` is `:valid` when the board's battery sense path is enabled. Boards can override these fields in their own `PowerMgtConfig`. +The confidence range is inclusive: `2500mV <= battery_mv <= 4500mV` is `:valid` when the board's battery sense path is enabled. Boot protection uses the lower present threshold separately: `1000mV <= battery_mv < voltage_bootlock` enters protective shutdown. Boards can override these fields in their own `PowerMgtConfig`. There are no configured VUSB millivolt confidence thresholds in the current implementation. VUSB state is based on the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal, not a firmware ADC voltage reading centred around 5V. `PowerMgtConfig::vbus_wake_valid` only records whether VBUS wake is supported for the board. @@ -139,6 +140,7 @@ To enable power management on a board variant: .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500, .power_fail_vdd_threshold = 0, // Optional; 0 disables runtime POF shutdown @@ -248,7 +250,7 @@ Power management status can be queried via the CLI: | `get pwrmgt.support` | Returns "supported" or "unsupported" | | `get pwrmgt.source` | Returns composite source and confidence, e.g. `vusb+bat:valid` | | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | -| `get pwrmgt.bootmv` | Returns boot voltage in millivolts | +| `get pwrmgt.bootmv` | Returns boot voltage in millivolts, with `invalid` when BAT sense is not trustworthy | On boards without power management enabled, all commands except `get pwrmgt.support` return: ``` diff --git a/src/MeshCore.h b/src/MeshCore.h index dc4f528b51..82b359a33d 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -69,6 +69,7 @@ class MainBoard { virtual bool isExternalPowered() { return false; } virtual const char* getPowerSourceState() { return isExternalPowered() ? "vusb-only:unknown" : "none:unknown"; } virtual uint16_t getBootVoltage() { return 0; } + virtual bool isBootVoltageValid() { return false; } virtual uint32_t getResetReason() const { return 0; } virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index a9447e62f0..78702de27c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -922,7 +922,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #endif } else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) { #ifdef NRF52_POWER_MANAGEMENT - sprintf(reply, "> %u mV", _board->getBootVoltage()); + sprintf(reply, _board->isBootVoltageValid() ? "> %u mV" : "> %u mV invalid", + _board->getBootVoltage()); #else strcpy(reply, "ERROR: Power management not supported"); #endif diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 0a1aeefcf2..2d3c95243d 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -155,8 +155,13 @@ bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { return true; } - if (!isBatteryVoltagePlausible(boot_voltage_mv, config)) { - MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (implausible battery voltage)"); + if (boot_voltage_mv < config->battery_min_present_mv) { + MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (battery absent or floating)"); + return true; + } + + if (boot_voltage_mv > config->battery_max_plausible_mv) { + MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (implausibly high battery voltage)"); return true; } @@ -341,6 +346,11 @@ bool NRF52Board::isBatteryVoltagePlausible(uint16_t millivolts, const PowerMgtCo millivolts <= config->battery_max_plausible_mv; } +bool NRF52Board::isBootVoltageValid() { + return active_power_config != nullptr && + active_power_config->battery_voltage_sense_valid; +} + const char* NRF52Board::getPowerSourceState() { bool vusb_connected = isExternalPowered(); diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index d862a31607..2ce36d90bc 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -28,8 +28,11 @@ struct PowerMgtConfig { bool lpcomp_voltage_wake_valid; bool vbus_wake_valid; - // Broad plausibility limits for a sensed battery voltage. Readings outside - // this range are treated as unsafe evidence for protective shutdown decisions. + // Battery voltage interpretation thresholds. Readings below the present + // threshold are treated as absent/floating, not as a discharged battery. + uint16_t battery_min_present_mv; + + // Broad plausibility limits for source-state confidence reporting. uint16_t battery_min_plausible_mv; uint16_t battery_max_plausible_mv; @@ -85,6 +88,7 @@ class NRF52Board : public mesh::MainBoard { #ifdef NRF52_POWER_MANAGEMENT uint16_t getBootVoltage() override { return boot_voltage_mv; } + bool isBootVoltageValid() override; const char* getPowerSourceState() override; virtual uint32_t getResetReason() const override { return reset_reason; } uint8_t getShutdownReason() const override { return shutdown_reason; } diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp index d66bb2f325..4edeac9e5f 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.cpp @@ -14,6 +14,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp index 23dd399e02..665417719b 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.cpp @@ -14,6 +14,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp index 2e5274ae9b..4eeba82031 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp @@ -14,6 +14,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp index 6bc30a64cc..c32cc5a4b0 100644 --- a/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp +++ b/variants/gat562_mesh_watch13/GAT56MeshWatch13Board.cpp @@ -14,6 +14,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 9e1421c604..7d0a90bb5d 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -13,6 +13,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/heltec_t1/T1Board.cpp b/variants/heltec_t1/T1Board.cpp index 024fd60a74..f0f66f6fe4 100644 --- a/variants/heltec_t1/T1Board.cpp +++ b/variants/heltec_t1/T1Board.cpp @@ -11,6 +11,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index 562ba8127c..cb9cf8c71f 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -13,6 +13,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/muziworks_r1_neo/R1NeoBoard.cpp b/variants/muziworks_r1_neo/R1NeoBoard.cpp index c8d095d02c..7af7e63dc7 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.cpp +++ b/variants/muziworks_r1_neo/R1NeoBoard.cpp @@ -13,6 +13,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index aef3dd6ad3..c84a8f2068 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -13,6 +13,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 97849c20cb..162372ec09 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -13,6 +13,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp index c098819a16..6394c0962e 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.cpp +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -11,6 +11,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = true, .lpcomp_voltage_wake_valid = true, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500 }; diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index 0c5ebe3548..d53bdb8678 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -17,6 +17,7 @@ const PowerMgtConfig power_config = { .battery_voltage_sense_valid = false, .lpcomp_voltage_wake_valid = false, .vbus_wake_valid = true, + .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500, .power_fail_vdd_threshold = PWRMGT_POWER_FAIL_VDD_THRESHOLD, From 40de2ffd39229f118e3449371dab04bb0b16e1eb Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 23:12:14 +0100 Subject: [PATCH 07/12] Resolve nRF52 power wake merge overlap --- src/helpers/NRF52Board.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 2d3c95243d..e7f365d528 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -283,18 +283,6 @@ void NRF52Board::configureVoltageWake(const PowerMgtConfig* config) { } } -void NRF52Board::configureVbusWake() { - uint8_t sd_enabled = 0; - sd_softdevice_is_enabled(&sd_enabled); - if (sd_enabled) { - sd_power_usbdetected_enable(1); - } else { - nrf52_configure_vbus_wake_direct(); - } - - MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); -} - void NRF52Board::configurePowerFailShutdown(const PowerMgtConfig* config) { if (config->power_fail_vdd_threshold == 0) return; From c6e7eb98caf9a82f1e2c68b1b9d81a5ab3d4fe42 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 23:41:51 +0100 Subject: [PATCH 08/12] Fix nRF52 absent battery and BLE POF handling --- docs/cli_commands.md | 6 ++++-- docs/nrf52_power_management.md | 10 ++++++---- src/helpers/NRF52Board.cpp | 4 ++++ variants/xiao_nrf52/XiaoNrf52Board.cpp | 6 ++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 475e0fa830..064169563d 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1119,8 +1119,10 @@ unusable BAT sensing report `invalid`, and missing power-management configuration reports `unknown`. VUSB has no configured millivolt thresholds; it is detected through the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal. BAT readings below 1000mV are treated as absent/floating for boot-lock -decisions. Readings from 1000mV up to the boot-lock threshold still trigger -protective shutdown; high readings above 4500mV are treated as unsafe evidence. +decisions and are not reported as a BAT source. With VBUS detected this reports +`vusb-only:valid`; without VBUS detect this reports `vusb-only:possible-battery`. +Readings from 1000mV up to the boot-lock threshold still trigger protective +shutdown; high readings above 4500mV are treated as unsafe evidence. If a battery is connected to VUSB and falls below the hardware VBUS-detect point while still powering the MCU, the source is reported as `vusb-only:possible-battery`. diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 3b6a1a7285..fc63fd2a19 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -34,7 +34,7 @@ Confidence suffixes are assigned as follows: | Suffix | Condition | Protective BAT decisions | |-----------------|-------------------------------------------------------------------------------------------|--------------------------| | `:valid` | Battery sense is enabled for the board and the reading is within the configured range | Allowed | -| `:implausible` | Battery sense is enabled for the board but the reading is below minimum or above maximum | Present low readings still shut down; absent/floating and high readings are blocked | +| `:implausible` | Battery sense is enabled, the reading is present, and it is below the plausible range or above maximum | Present low readings still shut down; absent/floating and high readings are blocked | | `:invalid` | Battery sense is not valid for the board's supported wiring or operating mode | Blocked | | `:unknown` | Power management has no active board configuration when the source is queried | Blocked | | `:possible-battery` | VBUS detect is low, BAT sense is invalid, and VUSB may be acting as a battery input | Blocked | @@ -49,6 +49,8 @@ The current nRF52 power-management board configs all use these plausibility thre The confidence range is inclusive: `2500mV <= battery_mv <= 4500mV` is `:valid` when the board's battery sense path is enabled. Boot protection uses the lower present threshold separately: `1000mV <= battery_mv < voltage_bootlock` enters protective shutdown. Boards can override these fields in their own `PowerMgtConfig`. +Readings below `battery_min_present_mv` are treated as no BAT source. With VBUS detected, the source is reported as `vusb-only:valid`; without VBUS detect, the source is reported as `vusb-only:possible-battery` because the board is still powered and VUSB may be acting as the battery input. + There are no configured VUSB millivolt confidence thresholds in the current implementation. VUSB state is based on the nRF52 `USBREGSTATUS.VBUSDETECT` hardware signal, not a firmware ADC voltage reading centred around 5V. `PowerMgtConfig::vbus_wake_valid` only records whether VBUS wake is supported for the board. This means a battery connected to VUSB is reported as `vusb-only:valid` while `VBUSDETECT` is asserted. If that VUSB battery falls below the hardware detection point but still powers the MCU, firmware reports `vusb-only:possible-battery` rather than `none`. @@ -73,7 +75,7 @@ Shutdown reason codes (stored in GPREGRET2): | Board | Implemented | LPCOMP wake | VBUS wake | Runtime POF shutdown | |-------------------------------------------|-------------|-------------|-----------|----------------------| -| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | No | Yes | Yes | +| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | No | Yes | USB builds only | | RAK4631 (`rak4631`) | Yes | Yes | Yes | No | | Heltec T114 (`heltec_t114`) | Yes | Yes | Yes | No | | GAT562 Mesh Watch13 | Yes | Yes | Yes | No | @@ -97,7 +99,7 @@ Notes: - User power-off on Heltec T114 does not enable LPCOMP wake. - VBUS detection is used to skip boot lockout on external power. VBUS wake is configured independently from LPCOMP where supported hardware exposes VBUS to the nRF52. - XIAO nRF52 disables trusted BAT/LPCOMP protection by default because BAT+ may be disconnected while VUSB is the actual supply. -- Runtime POF shutdown uses the nRF52 power-fail warning comparator. On XIAO it is configured for regulated VDD at 2.8 V and arms VBUS wake for SYSTEMOFF recovery. +- Runtime POF shutdown uses the nRF52 power-fail warning comparator. On XIAO USB builds it is configured for regulated VDD at 2.8 V and arms VBUS wake for SYSTEMOFF recovery. XIAO BLE companion builds leave direct POF disabled because SoftDevice is enabled after `board.begin()` and owns POWER events once started. ## Technical Details @@ -199,7 +201,7 @@ Runtime power-fail shutdown is configured by optional `PowerMgtConfig` fields: | `power_fail_vdd_threshold` | `0` | Disabled when `0`; otherwise an nRF52 `POWER_POFCON_THRESHOLD_*` value for regulated VDD | | `power_fail_vbus_wake` | `false` | When true, the POF handler arms VBUS detect as the SYSTEMOFF wake source | -For nRF52840 VDD, supported POF thresholds are 1.7 V through 2.8 V in 0.1 V steps. The XIAO nRF52840 default uses `POWER_POFCON_THRESHOLD_V28`, the highest available regulated-VDD threshold, so firmware gets the earliest available warning when the 3.3 V rail starts to collapse. +For nRF52840 VDD, supported POF thresholds are 1.7 V through 2.8 V in 0.1 V steps. The XIAO nRF52840 USB-build default uses `POWER_POFCON_THRESHOLD_V28`, the highest available regulated-VDD threshold, so firmware gets the earliest available warning when the 3.3 V rail starts to collapse. For nRF52840 VDDH, hardware also supports 2.7 V through 4.2 V thresholds. MeshCore does not currently use the VDDH threshold for XIAO because a battery on the XIAO VUSB pin is not the same as direct nRF52840 VDDH measurement in the board abstraction. diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index e7f365d528..760420b93a 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -352,6 +352,10 @@ const char* NRF52Board::getPowerSourceState() { return vusb_connected ? "vusb-only:valid" : "vusb-only:possible-battery"; } + if (battery_mv < active_power_config->battery_min_present_mv) { + return vusb_connected ? "vusb-only:valid" : "vusb-only:possible-battery"; + } + if (!isBatteryVoltagePlausible(battery_mv, active_power_config)) { return vusb_connected ? "vusb+bat:implausible" : "bat-only:implausible"; } diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index d53bdb8678..1e35873a21 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -20,7 +20,13 @@ const PowerMgtConfig power_config = { .battery_min_present_mv = 1000, .battery_min_plausible_mv = 2500, .battery_max_plausible_mv = 4500, +#ifdef BLE_PIN_CODE + // BLE companion builds enable SoftDevice after board.begin(). Do not install + // a direct nrfx POWER/POF interrupt before SoftDevice takes ownership. + .power_fail_vdd_threshold = 0, +#else .power_fail_vdd_threshold = PWRMGT_POWER_FAIL_VDD_THRESHOLD, +#endif .power_fail_vbus_wake = true }; From 120c3a39b26a2a12e3e14b1a2224f2cb8b2864b9 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Wed, 1 Jul 2026 00:38:59 +0100 Subject: [PATCH 09/12] Disable nRF52 POF before OTA SoftDevice start --- docs/nrf52_power_management.md | 4 ++-- src/helpers/NRF52Board.cpp | 25 +++++++++++++++++++++++++ src/helpers/NRF52Board.h | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index fc63fd2a19..448aa41cf9 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -99,7 +99,7 @@ Notes: - User power-off on Heltec T114 does not enable LPCOMP wake. - VBUS detection is used to skip boot lockout on external power. VBUS wake is configured independently from LPCOMP where supported hardware exposes VBUS to the nRF52. - XIAO nRF52 disables trusted BAT/LPCOMP protection by default because BAT+ may be disconnected while VUSB is the actual supply. -- Runtime POF shutdown uses the nRF52 power-fail warning comparator. On XIAO USB builds it is configured for regulated VDD at 2.8 V and arms VBUS wake for SYSTEMOFF recovery. XIAO BLE companion builds leave direct POF disabled because SoftDevice is enabled after `board.begin()` and owns POWER events once started. +- Runtime POF shutdown uses the nRF52 power-fail warning comparator. On XIAO USB builds it is configured for regulated VDD at 2.8 V and arms VBUS wake for SYSTEMOFF recovery. XIAO BLE companion builds leave direct POF disabled because SoftDevice is enabled after `board.begin()` and owns POWER events once started. USB builds that later enter OTA disable the direct POF handler before starting SoftDevice. ## Technical Details @@ -209,7 +209,7 @@ This path is deliberately separate from SYSTEMOFF wake source selection: - If supply voltage simply collapses, firmware does not choose a wake source; reset and regulator hardware determine when execution resumes. - POF shutdown only applies if the MCU is still executing when VDD crosses the configured threshold. - When POF shutdown succeeds and `power_fail_vbus_wake` is true, recovery happens when the nRF52 VBUS detector sees VUSB again, not when BAT sense rises. -- SoftDevice builds need a SoftDevice SoC event hook for `NRF_EVT_POWER_FAILURE_WARNING`. The current Adafruit nRF52 framework consumes that event internally, so this branch only enables direct POF shutdown when SoftDevice is not active. +- SoftDevice builds need a SoftDevice SoC event hook for `NRF_EVT_POWER_FAILURE_WARNING`. The current Adafruit nRF52 framework consumes that event internally, so this branch only enables direct POF shutdown while SoftDevice is not active and disables it before OTA starts SoftDevice. **LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL)**: diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 760420b93a..73e3303623 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -30,6 +30,7 @@ void NRF52Board::begin() { uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason static bool nrf52_power_fail_vbus_wake = false; +static bool nrf52_power_fail_configured = false; // Early constructor - runs before SystemInit() clears the registers // Priority 101 ensures this runs before SystemInit (102) and before @@ -322,12 +323,30 @@ void NRF52Board::configurePowerFailShutdown(const PowerMgtConfig* config) { nrfx_power_pof_init(&pof_config); nrfx_power_pof_enable(&pof_config); + nrf52_power_fail_configured = true; MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown configured (VDD threshold code = %u; VBUS wake = %s)", config->power_fail_vdd_threshold, config->power_fail_vbus_wake ? "yes" : "no"); } +void NRF52Board::disablePowerFailShutdown() { + if (!nrf52_power_fail_configured) return; + + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown left unchanged (SoftDevice active)"); + return; + } + + nrfx_power_pof_disable(); + nrfx_power_pof_uninit(); + nrf52_power_fail_configured = false; + nrf52_power_fail_vbus_wake = false; + MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown disabled"); +} + bool NRF52Board::isBatteryVoltagePlausible(uint16_t millivolts, const PowerMgtConfig* config) const { if (config == nullptr) return false; return millivolts >= config->battery_min_plausible_mv && @@ -463,6 +482,12 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[]) { Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); +#ifdef NRF52_POWER_MANAGEMENT + // OTA enables SoftDevice after normal board startup. Release the direct + // nrfx POWER/POF handler first so SoftDevice owns POWER events cleanly. + disablePowerFailShutdown(); +#endif + Bluefruit.begin(1, 0); // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 Bluefruit.setTxPower(4); diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 2ce36d90bc..9f80a3aa19 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -71,6 +71,7 @@ class NRF52Board : public mesh::MainBoard { void configureVoltageWake(const PowerMgtConfig* config); void configureVbusWake(); void configurePowerFailShutdown(const PowerMgtConfig* config); + void disablePowerFailShutdown(); virtual void initiateShutdown(uint8_t reason); bool isBatteryVoltagePlausible(uint16_t millivolts, const PowerMgtConfig* config) const; #endif From f4e6b8548d03b6af91d550642fe414a4994696c8 Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Wed, 1 Jul 2026 00:45:27 +0100 Subject: [PATCH 10/12] Fix nRF52 power management docs typo --- docs/nrf52_power_management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index 448aa41cf9..cd453569f3 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -75,7 +75,7 @@ Shutdown reason codes (stored in GPREGRET2): | Board | Implemented | LPCOMP wake | VBUS wake | Runtime POF shutdown | |-------------------------------------------|-------------|-------------|-----------|----------------------| -| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | No | Yes | USB builds only | +| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | No | Yes | Yes | | RAK4631 (`rak4631`) | Yes | Yes | Yes | No | | Heltec T114 (`heltec_t114`) | Yes | Yes | Yes | No | | GAT562 Mesh Watch13 | Yes | Yes | Yes | No | From 2b01b2fa3f419127ad3a5c9582298559384eeddf Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Wed, 1 Jul 2026 01:04:06 +0100 Subject: [PATCH 11/12] Fix nRF52 POF interrupt and OTA release --- src/helpers/NRF52Board.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 73e3303623..a71b427095 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -63,7 +63,7 @@ static void nrf52_power_fail_warning_handler() { // Keep the path register-only: Serial, delay(), and board callbacks are not // safe when VDD is already falling. nrfx_power_pof_disable(); - nrf52_record_shutdown_reason(SHUTDOWN_REASON_LOW_VOLTAGE); + NRF_POWER->GPREGRET2 = SHUTDOWN_REASON_LOW_VOLTAGE; if (nrf52_power_fail_vbus_wake) { nrf52_configure_vbus_wake_direct(); @@ -342,9 +342,10 @@ void NRF52Board::disablePowerFailShutdown() { nrfx_power_pof_disable(); nrfx_power_pof_uninit(); + nrfx_power_uninit(); nrf52_power_fail_configured = false; nrf52_power_fail_vbus_wake = false; - MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown disabled"); + MESH_DEBUG_PRINTLN("PWRMGT: POF shutdown disabled and POWER released"); } bool NRF52Board::isBatteryVoltagePlausible(uint16_t millivolts, const PowerMgtConfig* config) const { From 565e6137b9e239f986ad1469721c498ae162899a Mon Sep 17 00:00:00 2001 From: YouGottaHackThat Date: Tue, 30 Jun 2026 18:34:13 +0100 Subject: [PATCH 12/12] Retain current and previous boot diagnostics --- boards/nrf52840_s140_v6.ld | 7 ++ boards/nrf52840_s140_v6_extrafs.ld | 7 ++ boards/nrf52840_s140_v7.ld | 7 ++ boards/nrf52840_s140_v7_extrafs.ld | 7 ++ docs/cli_commands.md | 17 +++ docs/nrf52_power_management.md | 3 + src/helpers/CommonCLI.cpp | 19 ++++ src/helpers/NRF52Board.cpp | 2 + src/helpers/RadioInitDiagnostics.cpp | 79 ++++++++++++++ src/helpers/RadioInitDiagnostics.h | 153 +++++++++++++++++++++++++++ 10 files changed, 301 insertions(+) create mode 100644 src/helpers/RadioInitDiagnostics.cpp create mode 100644 src/helpers/RadioInitDiagnostics.h diff --git a/boards/nrf52840_s140_v6.ld b/boards/nrf52840_s140_v6.ld index 6dad975b0d..e4150be8fd 100644 --- a/boards/nrf52840_s140_v6.ld +++ b/boards/nrf52840_s140_v6.ld @@ -33,6 +33,13 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM + + .noinit (NOLOAD) : + { + . = ALIGN(4); + KEEP(*(.noinit)) + . = ALIGN(4); + } > RAM } INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v6_extrafs.ld b/boards/nrf52840_s140_v6_extrafs.ld index 352610679e..b78118aca3 100644 --- a/boards/nrf52840_s140_v6_extrafs.ld +++ b/boards/nrf52840_s140_v6_extrafs.ld @@ -33,6 +33,13 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM + + .noinit (NOLOAD) : + { + . = ALIGN(4); + KEEP(*(.noinit)) + . = ALIGN(4); + } > RAM } INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7.ld b/boards/nrf52840_s140_v7.ld index 6aaeb4034f..16b04f523a 100644 --- a/boards/nrf52840_s140_v7.ld +++ b/boards/nrf52840_s140_v7.ld @@ -33,6 +33,13 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM + + .noinit (NOLOAD) : + { + . = ALIGN(4); + KEEP(*(.noinit)) + . = ALIGN(4); + } > RAM } INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/boards/nrf52840_s140_v7_extrafs.ld b/boards/nrf52840_s140_v7_extrafs.ld index 5956183aa3..c5755ed974 100644 --- a/boards/nrf52840_s140_v7_extrafs.ld +++ b/boards/nrf52840_s140_v7_extrafs.ld @@ -33,6 +33,13 @@ SECTIONS KEEP(*(.fs_data)) PROVIDE(__stop_fs_data = .); } > RAM + + .noinit (NOLOAD) : + { + . = ALIGN(4); + KEEP(*(.noinit)) + . = ALIGN(4); + } > RAM } INSERT AFTER .data; INCLUDE "nrf52_common.ld" diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 064169563d..c894748f66 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1147,3 +1147,20 @@ untrusted, for example `> 426 mV invalid`. **Note:** Returns an error on boards without power management support. --- + +#### View the last radio initialisation status +**Usage:** `get radio.init_status` + +--- + +#### View the radio initialisation attempt count +**Usage:** `get radio.init_attempts` + +--- + +#### View compact boot diagnostics +**Usage:** `get diag.boot` + +**Note:** Returns current and previous reset-retained boot diagnostics. `cur_sd` and `prev_sd` are raw shutdown/fault markers, including low voltage, boot protection, user shutdown, and radio initialisation failure. + +--- diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index cd453569f3..91e05a0d6b 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -253,12 +253,15 @@ Power management status can be queried via the CLI: | `get pwrmgt.source` | Returns composite source and confidence, e.g. `vusb+bat:valid` | | `get pwrmgt.bootreason` | Returns reset and shutdown reason strings | | `get pwrmgt.bootmv` | Returns boot voltage in millivolts, with `invalid` when BAT sense is not trustworthy | +| `get diag.boot` | Returns current and previous reset-retained boot/radio diagnostics | On boards without power management enabled, all commands except `get pwrmgt.support` return: ``` ERROR: Power management not supported ``` +`get diag.boot` includes raw current and previous boot records. The shutdown fields include all `GPREGRET2` markers, not only radio initialisation failure, so low-voltage and boot-protection context is preserved when diagnosing reset loops. The previous slot is retained across MCU resets where RAM is preserved; it is not a flash-backed history and may be lost after a complete power loss. + ## Debug Output When `MESH_DEBUG=1` is enabled, the power management module outputs: diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 78702de27c..27f4e14488 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3,6 +3,7 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" +#include "RadioInitDiagnostics.h" #include #ifndef BRIDGE_MAX_BAUD @@ -927,6 +928,24 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep #else strcpy(reply, "ERROR: Power management not supported"); #endif + } else if (memcmp(config, "radio.init_status", 17) == 0) { + sprintf(reply, "> %d", (int)g_last_radio_init_status); + } else if (memcmp(config, "radio.init_attempts", 19) == 0) { + sprintf(reply, "> %u", (unsigned)g_radio_init_attempts); + } else if (memcmp(config, "diag.boot", 9) == 0) { + RadioInitBootRecord cur = radioInitCurrentBootRecord(); + RadioInitBootRecord prev = radioInitPreviousBootRecord(); + sprintf(reply, "> cur_rr=0x%08lX cur_sd=0x%02X cur_st=0x%02X cur_radio=%d cur_att=%u prev_rr=0x%08lX prev_sd=0x%02X prev_st=0x%02X prev_radio=%d prev_att=%u", + (unsigned long)cur.reset_reason, + (unsigned)cur.shutdown_reason, + (unsigned)cur.boot_stage, + (int)cur.radio_status, + (unsigned)cur.attempts, + (unsigned long)prev.reset_reason, + (unsigned)prev.shutdown_reason, + (unsigned)prev.boot_stage, + (int)prev.radio_status, + (unsigned)prev.attempts); } else { sprintf(reply, "??: %s", config); } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index a71b427095..97f0b8a891 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -1,5 +1,6 @@ #if defined(NRF52_PLATFORM) #include "NRF52Board.h" +#include "RadioInitDiagnostics.h" #include #include @@ -77,6 +78,7 @@ void NRF52Board::initPowerMgr() { // Copy early-captured register values reset_reason = g_nrf52_reset_reason; shutdown_reason = g_nrf52_shutdown_reason; + radioInitCaptureBoot(reset_reason, shutdown_reason); boot_voltage_mv = 0; // Will be set by checkBootVoltage() // Clear registers for next boot diff --git a/src/helpers/RadioInitDiagnostics.cpp b/src/helpers/RadioInitDiagnostics.cpp new file mode 100644 index 0000000000..41351dc6ef --- /dev/null +++ b/src/helpers/RadioInitDiagnostics.cpp @@ -0,0 +1,79 @@ +#include "RadioInitDiagnostics.h" + +volatile int16_t g_last_radio_init_status = RADIOLIB_ERR_NONE; +volatile uint8_t g_radio_init_attempts = 0; +volatile uint8_t g_radio_init_boot_stage = RADIO_BOOT_STAGE_NONE; +volatile uint8_t g_radio_init_fault = RADIO_INIT_FAULT_NONE; + +#if defined(NRF52_PLATFORM) +RadioInitBootHistory g_radio_init_boot_history __attribute__((section(".noinit"))); +#else +RadioInitBootHistory g_radio_init_boot_history = {}; +#endif + +static void resetRecord(volatile RadioInitBootRecord& record) { + record.reset_reason = 0; + record.radio_status = RADIOLIB_ERR_NONE; + record.shutdown_reason = RADIO_INIT_FAULT_NONE; + record.fault = RADIO_INIT_FAULT_NONE; + record.boot_stage = RADIO_BOOT_STAGE_NONE; + record.attempts = 0; +} + +static void ensureBootHistory() { + if (g_radio_init_boot_history.magic == RADIO_INIT_BOOT_DIAG_MAGIC && + g_radio_init_boot_history.version == RADIO_INIT_BOOT_DIAG_VERSION) { + return; + } + + g_radio_init_boot_history.magic = RADIO_INIT_BOOT_DIAG_MAGIC; + g_radio_init_boot_history.version = RADIO_INIT_BOOT_DIAG_VERSION; + resetRecord(g_radio_init_boot_history.current); + resetRecord(g_radio_init_boot_history.previous); +} + +void radioInitCaptureBoot(uint32_t reset_reason, uint8_t shutdown_reason) { + ensureBootHistory(); + + g_radio_init_boot_history.previous = g_radio_init_boot_history.current; + resetRecord(g_radio_init_boot_history.current); + + g_radio_init_boot_history.current.reset_reason = reset_reason; + g_radio_init_boot_history.current.shutdown_reason = shutdown_reason; + g_radio_init_boot_history.current.fault = shutdown_reason; + + g_radio_init_fault = shutdown_reason; + g_radio_init_boot_stage = RADIO_BOOT_STAGE_NONE; + g_radio_init_attempts = 0; + g_last_radio_init_status = RADIOLIB_ERR_NONE; +} + +RadioInitBootRecord radioInitCurrentBootRecord() { + ensureBootHistory(); + return g_radio_init_boot_history.current; +} + +RadioInitBootRecord radioInitPreviousBootRecord() { + ensureBootHistory(); + return g_radio_init_boot_history.previous; +} + +void radioInitHistorySetStage(uint8_t stage) { + ensureBootHistory(); + g_radio_init_boot_history.current.boot_stage = stage; +} + +void radioInitHistorySetAttempt(uint8_t attempt) { + ensureBootHistory(); + g_radio_init_boot_history.current.attempts = attempt; +} + +void radioInitHistorySetStatus(int16_t status) { + ensureBootHistory(); + g_radio_init_boot_history.current.radio_status = status; +} + +void radioInitHistorySetFault(uint8_t fault) { + ensureBootHistory(); + g_radio_init_boot_history.current.fault = fault; +} diff --git a/src/helpers/RadioInitDiagnostics.h b/src/helpers/RadioInitDiagnostics.h new file mode 100644 index 0000000000..15c0a74b7a --- /dev/null +++ b/src/helpers/RadioInitDiagnostics.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include + +#if defined(NRF52_PLATFORM) +#include +#endif + +#ifndef RADIOLIB_ERR_SPI_CMD_TIMEOUT +#define RADIOLIB_ERR_SPI_CMD_TIMEOUT -705 +#endif + +#define RADIO_INIT_FAULT_NONE 0x00 +#define RADIO_INIT_FAULT_RADIO_INIT_FAIL 0x52 +#define RADIO_INIT_BOOT_DIAG_MAGIC 0x4D435244UL // "MCRD" +#define RADIO_INIT_BOOT_DIAG_VERSION 1 + +enum RadioInitBootStage : uint8_t { + RADIO_BOOT_STAGE_NONE = 0x00, + RADIO_BOOT_STAGE_RADIO_INIT_ENTERED = 0x30, + RADIO_BOOT_STAGE_RTC_BEGIN_ENTERED = 0x31, + RADIO_BOOT_STAGE_RTC_BEGIN_RETURNED = 0x32, + RADIO_BOOT_STAGE_RADIO_STD_INIT_ENTERED = 0x40, + RADIO_BOOT_STAGE_RADIO_STD_INIT_SUCCESS = 0x41, + RADIO_BOOT_STAGE_RADIO_STD_INIT_FAIL = 0x42, + RADIO_BOOT_STAGE_RADIO_BUSY_TIMEOUT = 0x43, +}; + +struct RadioInitBootRecord { + uint32_t reset_reason; + int16_t radio_status; + uint8_t shutdown_reason; + uint8_t fault; + uint8_t boot_stage; + uint8_t attempts; +}; + +struct RadioInitBootHistory { + uint32_t magic; + uint8_t version; + RadioInitBootRecord current; + RadioInitBootRecord previous; +}; + +extern volatile int16_t g_last_radio_init_status; +extern volatile uint8_t g_radio_init_attempts; +extern volatile uint8_t g_radio_init_boot_stage; +extern volatile uint8_t g_radio_init_fault; +extern RadioInitBootHistory g_radio_init_boot_history; + +void radioInitCaptureBoot(uint32_t reset_reason, uint8_t shutdown_reason); +RadioInitBootRecord radioInitCurrentBootRecord(); +RadioInitBootRecord radioInitPreviousBootRecord(); +void radioInitHistorySetStage(uint8_t stage); +void radioInitHistorySetAttempt(uint8_t attempt); +void radioInitHistorySetStatus(int16_t status); +void radioInitHistorySetFault(uint8_t fault); + +inline void radioInitSetBootStage(uint8_t stage) { + g_radio_init_boot_stage = stage; + radioInitHistorySetStage(stage); +} + +inline void radioInitRecordAttempt(uint8_t attempt) { + g_radio_init_attempts = attempt; + radioInitHistorySetAttempt(attempt); +} + +inline void radioInitRecordStatus(int16_t status) { + g_last_radio_init_status = status; + radioInitHistorySetStatus(status); +} + +inline void radioInitRecordFault(uint8_t fault) { + g_radio_init_fault = fault; + radioInitHistorySetFault(fault); +#if defined(NRF52_PLATFORM) + // GPREGRET2 is already used as a reset-persistent one-byte reason store on + // nRF52. Writing the radio fault before reset lets the next boot report that + // the previous boot failed before normal application startup. + NRF_POWER->GPREGRET2 = fault; +#endif +} + +inline const char* radioInitFaultString(uint8_t fault) { + switch (fault) { + case RADIO_INIT_FAULT_NONE: + return "None"; + case RADIO_INIT_FAULT_RADIO_INIT_FAIL: + return "Radio Init Fail"; + } + return "Unknown"; +} + +inline bool radioInitWaitBusyLow(uint32_t timeout_ms) { +#if defined(P_LORA_BUSY) + if (P_LORA_BUSY == RADIOLIB_NC) return true; + + pinMode(P_LORA_BUSY, INPUT); + uint32_t start = millis(); + while (digitalRead(P_LORA_BUSY) == HIGH) { + if (millis() - start > timeout_ms) { + radioInitSetBootStage(RADIO_BOOT_STAGE_RADIO_BUSY_TIMEOUT); + radioInitRecordStatus(RADIOLIB_ERR_SPI_CMD_TIMEOUT); + return false; + } + delay(1); + } +#endif + return true; +} + +inline void radioInitResetPulse() { +#if defined(P_LORA_RESET) + if (P_LORA_RESET == RADIOLIB_NC) return; + + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); + delay(10); + digitalWrite(P_LORA_RESET, HIGH); + delay(10); +#endif +} + +inline void radioInitPowerCycle() { +#if defined(SX126X_POWER_EN) + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, LOW); + delay(50); + digitalWrite(SX126X_POWER_EN, HIGH); + delay(100); +#endif +} + +inline void radioInitPrepareAttempt(uint8_t attempt) { + if (attempt == 2) { + radioInitResetPulse(); + delay(100); + } else if (attempt >= 3) { + radioInitPowerCycle(); + radioInitResetPulse(); + delay(150); + } +} + +inline void radioInitRebootAfterFault(mesh::MainBoard& board, uint8_t fault) { + radioInitRecordFault(fault); + Serial.flush(); + delay(250); + board.reboot(); +}