From 308589afa3f58d3546cf49f5ddcfbe50a1dcd355 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 31 Jul 2026 16:01:02 -0700 Subject: [PATCH] Harden nRF52 shutdown and radio recovery --- docs/nrf52_power_management.md | 50 ++++++++++--------- examples/companion_radio/main.cpp | 11 +++- examples/kiss_modem/main.cpp | 11 +++- examples/simple_repeater/main.cpp | 12 +++-- examples/simple_room_server/main.cpp | 11 +++- examples/simple_secure_chat/main.cpp | 11 +++- examples/simple_sensor/main.cpp | 11 +++- src/helpers/NRF52Board.cpp | 10 ++++ src/helpers/radiolib/CustomSX1262Wrapper.h | 1 + src/helpers/radiolib/RadioLibWrappers.h | 2 +- variants/gat562_30s_mesh_kit/platformio.ini | 2 +- variants/gat562_mesh_evb_pro/platformio.ini | 2 +- .../gat562_mesh_tracker_pro/platformio.ini | 2 +- variants/gat562_mesh_watch13/platformio.ini | 2 +- variants/heltec_t096/platformio.ini | 2 +- variants/heltec_t114/platformio.ini | 2 +- variants/rak3401/platformio.ini | 2 +- variants/rak4631/platformio.ini | 2 +- variants/sensecap_solar/platformio.ini | 2 +- variants/xiao_nrf52/platformio.ini | 2 +- 20 files changed, 107 insertions(+), 43 deletions(-) diff --git a/docs/nrf52_power_management.md b/docs/nrf52_power_management.md index ce673537..7f4922b9 100644 --- a/docs/nrf52_power_management.md +++ b/docs/nrf52_power_management.md @@ -16,6 +16,9 @@ The nRF52 Power Management module provides battery protection features to preven - Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF - Enables USB VBUS detection so external power can wake the device - Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected +- Uses no LPCOMP hysteresis; on divided battery inputs the additional 50 mV + comparator hysteresis can move the effective battery wake point by several + hundred millivolts and strand an otherwise charged node ### Early Boot Register Capture - Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them @@ -34,31 +37,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 family | Implemented | LPCOMP wake | VBUS wake | +|---------------------------------------------------|-------------|-------------|-----------| +| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes | +| SenseCAP Solar | Yes | Yes | Yes | +| RAK4631 / RAK3401 | Yes | Yes | Yes | +| GAT562 30S / EVB Pro / Tracker Pro / Watch13 | Yes | Yes | Yes | +| Heltec T096 / T114 / T1 / Tower V2 | Yes | Yes | Yes | +| Muzi Works R1 Neo | Partial | Inactive | Inactive | +| Promicro nRF52840 | No | No | No | +| RAK WisMesh Tag | No | No | No | +| Heltec Mesh Solar | No | No | No | +| LilyGo T-Echo / T-Echo Lite / T-Impulse Plus | No | No | No | +| WIO Tracker L1 / L1 E-Ink / WM1110 | No | No | No | +| Mesh Pocket / Meshtiny / Nano G2 Ultra | No | No | No | +| ThinkNode M1 / M3 / M6 | No | No | No | +| T1000-E | No | No | No | +| Ikoka Nano / Stick / Handheld (nRF) | No | No | No | +| Keepteen LT1 / Minewsemi ME25LS01 | No | No | No | Notes: - "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture). -- User power-off on Heltec T114 does not enable LPCOMP wake. +- R1 Neo has the integration compiled, but its board configuration deliberately + sets `PWRMGT_VOLTAGE_BOOTLOCK` to `0`; automatic protective shutdown and its + voltage/VBUS recovery wake are therefore inactive. +- User power-off does not enable LPCOMP wake; voltage recovery is armed only + for boot protection and automated low-voltage shutdown. - VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52. ## Technical Details @@ -138,7 +142,7 @@ The LPCOMP (Low Power Comparator) is configured to: - Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31) - Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) - Detect UP events (voltage rising above threshold) -- Use 50mV hysteresis for noise immunity +- Use no hysteresis so the configured recovery threshold is not widened by the battery divider - Wake the device from SYSTEMOFF when triggered VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever `configureVoltageWake()` is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB). diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index c67b9f32..8fa3bfaa 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -254,7 +254,16 @@ void setup() { } #endif - if (!radio_init()) { halt(); } + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); + } fast_rng.begin(radio_driver.getRngSeed()); diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 3ce35cda..89764787 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -78,8 +78,15 @@ void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) { void setup() { board.begin(); - if (!radio_init()) { - halt(); + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); } radio_driver.begin(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 06b5eba8..68d079a3 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -80,9 +80,15 @@ void setup() { } #endif - if (!radio_init()) { - MESH_DEBUG_PRINTLN("Radio init failed!"); - halt(); + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); } fast_rng.begin(radio_driver.getRngSeed()); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index bbdc9992..05c2f8bc 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -48,7 +48,16 @@ void setup() { } #endif - if (!radio_init()) { halt(); } + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); + } fast_rng.begin(radio_driver.getRngSeed()); diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index fb33e00d..0b3eb4ac 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -568,7 +568,16 @@ void setup() { external_watchdog.begin(); #endif - if (!radio_init()) { halt(); } + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); + } fast_rng.begin(radio_driver.getRngSeed()); diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 6fbc8e6d..e041dd3b 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -70,7 +70,16 @@ void setup() { } #endif - if (!radio_init()) { halt(); } + int radioinit_attempts = 0; + while (!radio_init()) { + ++radioinit_attempts; + MESH_DEBUG_PRINTLN("Radio init failed! (attempt %d)", radioinit_attempts); + if (radioinit_attempts >= 3) { + MESH_DEBUG_PRINTLN("Radio init failed 3x - rebooting"); + board.reboot(); + } + delay(500); + } fast_rng.begin(radio_driver.getRngSeed()); diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 08addb83..6fe5af1e 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -467,6 +467,16 @@ void NRF52Board::shutdownPeripherals() { // Power off LoRa radio_driver.powerOff(); + // RadioLib's IRQ setup can leave DIO1 as a GPIO wake source. Once the radio + // is asleep that line may float or remain asserted, immediately waking an + // nRF52 that is trying to enter SYSTEMOFF. Release the interrupt channel and + // explicitly disable pin sensing; board-specific code can then arm only its + // intended wake source (for example, the SenseCAP user button or LPCOMP). + #ifdef P_LORA_DIO_1 + detachInterrupt(P_LORA_DIO_1); + pinMode(P_LORA_DIO_1, INPUT); + #endif + // Keep LoRa inactive during deepsleep #ifdef P_LORA_NSS digitalWrite(P_LORA_NSS, HIGH); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index ee5626c3..3e52bcce 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -50,6 +50,7 @@ public: } uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; } virtual void powerOff() override { + ((CustomSX1262 *)_radio)->standby(); ((CustomSX1262 *)_radio)->sleep(false); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index c65f8e70..c05f5256 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -120,7 +120,7 @@ public: } void begin() override; - virtual void powerOff() { _radio->sleep(); } + virtual void powerOff() { _radio->standby(); _radio->sleep(); } int recvRaw(uint8_t* bytes, int sz) override; uint32_t getEstAirtimeFor(int len_bytes) override; bool startSendRaw(const uint8_t* bytes, int len) override; diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index fb8a0674..a933e2c7 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${rak4631_hw.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index e9ceb908..20dedd84 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -5,7 +5,7 @@ board_check = true build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D RADIO_CLASS=CustomSX1262 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index 5cd83271..eb34c869 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -6,7 +6,7 @@ board_build.ldscript = boards/nrf52840_s140_v6.ld build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index a9c06861..59ec79d3 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -8,7 +8,7 @@ build_flags = ${rak4631_hw.build_flags} -I variants/gat562_mesh_watch13 -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 -D PIN_OLED_RESET=-1 diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 7cc29826..1fda9c35 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -14,7 +14,7 @@ build_flags = ${rak4631_hw.build_flags} -I variants/heltec_t096 -I src/helpers/ui -D HELTEC_T096 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=21 -D P_LORA_NSS=5 -D P_LORA_RESET=16 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 2c118bb6..e1929843 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -13,7 +13,7 @@ build_flags = ${rak4631_hw.build_flags} -I variants/heltec_t114 -I src/helpers/ui -D HELTEC_T114 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D P_LORA_DIO_1=20 -D P_LORA_NSS=24 -D P_LORA_RESET=25 diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index eb648ee0..cb27ba61 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -7,7 +7,7 @@ build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak3401 -D RAK_3401 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index db31fa5b..9c7e5b20 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -11,7 +11,7 @@ build_flags = ${nrf52_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D MOTA_HW_ID='"RAK4631"' ; OTA hardware tag (apply refuses a .mota for different hw) -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index 5a1f6c71..5f01eae5 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -10,7 +10,7 @@ build_flags = ${nrf52_base.build_flags} -I src/helpers/nrf52 -D NRF52_PLATFORM=1 -D USE_SX1262 -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D P_LORA_TX_LED=12 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 09309e94..ba6a953a 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -9,7 +9,7 @@ build_flags = ${rak4631_hw.build_flags} -I variants/xiao_nrf52 -UENV_INCLUDE_GPS -D NRF52_PLATFORM -; -D NRF52_POWER_MANAGEMENT + -D NRF52_POWER_MANAGEMENT -D XIAO_NRF52 -D USE_SX1262 -D RADIO_CLASS=CustomSX1262