From f20e5af9f48449b198b355afbc761cd78bc186f9 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 12 Mar 2026 14:41:20 +0100 Subject: [PATCH] Add 60s watchdog for nrf52 In order to restore after battery voltage sags too low --- examples/companion_radio/main.cpp | 1 + examples/kiss_modem/main.cpp | 1 + examples/simple_repeater/main.cpp | 2 + examples/simple_room_server/main.cpp | 2 + examples/simple_secure_chat/main.cpp | 1 + examples/simple_sensor/main.cpp | 2 + src/MeshCore.h | 1 + src/helpers/NRF52Board.cpp | 88 ++++++++++++++++++- src/helpers/NRF52Board.h | 19 +++- .../GAT562MeshTrackerProBoard.cpp | 4 +- variants/heltec_t114/T114Board.cpp | 4 +- variants/rak4631/RAK4631Board.cpp | 4 +- .../sensecap_solar/SenseCapSolarBoard.cpp | 4 +- variants/xiao_nrf52/XiaoNrf52Board.cpp | 4 +- 14 files changed, 128 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index eff9efca47..5105a47900 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -219,6 +219,7 @@ void setup() { } void loop() { + board.loop(); the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 3507959297..b005346532 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -119,6 +119,7 @@ void setup() { } void loop() { + board.loop(); modem->loop(); if (!modem->isActuallyTransmitting()) { diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5f..d9eb99d8c5 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -106,6 +106,8 @@ void setup() { } void loop() { + board.loop(); + int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 825fb007d5..cecab1c276 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -83,6 +83,8 @@ void setup() { } void loop() { + board.loop(); + int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index ab14d3933f..32fc843b9b 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -589,6 +589,7 @@ void setup() { } void loop() { + board.loop(); the_mesh.loop(); rtc_clock.tick(); } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 330adcc2e4..718ca324a4 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -117,6 +117,8 @@ void setup() { } void loop() { + board.loop(); + int len = strlen(command); while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); diff --git a/src/MeshCore.h b/src/MeshCore.h index 70cd0f0672..b23a3c6953 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -57,6 +57,7 @@ class MainBoard { virtual uint8_t getStartupReason() const = 0; virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + virtual void loop() { /* no op */ } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 2c8753d464..ad025a8258 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -112,15 +112,29 @@ const char* NRF52Board::getShutdownReasonString(uint8_t reason) { bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { initPowerMgr(); + // Store config for runtime use (voltage monitoring, WDT) + _power_config = config; + _last_voltage_check_ms = 0; + _low_voltage_count = 0; + // Read boot voltage boot_voltage_mv = getBattMilliVolts(); - - if (config->voltage_bootlock == 0) return true; // Protection disabled + + if (config->voltage_bootlock == 0) { + // Boot protection disabled, but still init WDT if configured + if (config->wdt_timeout_ms > 0) { + initWatchdog(config->wdt_timeout_ms); + } + return true; + } // Skip check if externally powered if (isExternalPowered()) { MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)"); boot_voltage_mv = getBattMilliVolts(); + if (config->wdt_timeout_ms > 0) { + initWatchdog(config->wdt_timeout_ms); + } return true; } @@ -136,6 +150,11 @@ bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) { return false; // Should never reach this } + // Boot voltage OK — start WDT if configured + if (config->wdt_timeout_ms > 0) { + initWatchdog(config->wdt_timeout_ms); + } + return true; } @@ -236,8 +255,67 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured"); } + +#define VOLTAGE_CHECK_INTERVAL_MS 30000 +#define LOW_VOLTAGE_COUNT_THRESHOLD 3 + +void NRF52Board::initWatchdog(uint32_t timeout_ms) { + // Configure WDT via direct register access (same pattern as LPCOMP/POWER registers) + NRF_WDT->CONFIG = WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos; // Keep running during WFE sleep + NRF_WDT->CRV = (uint32_t)((uint64_t)timeout_ms * 32768 / 1000); // Timeout in 32.768kHz ticks + NRF_WDT->RREN = WDT_RREN_RR0_Enabled << WDT_RREN_RR0_Pos; // Enable reload register 0 + NRF_WDT->TASKS_START = 1; // Start — cannot be stopped once started + + // Initial feed + NRF_WDT->RR[0] = WDT_RR_RR_Reload; + + MESH_DEBUG_PRINTLN("PWRMGT: WDT started (%lu ms)", (unsigned long)timeout_ms); +} + +void NRF52Board::feedWatchdog() { + if (NRF_WDT->RUNSTATUS) { + NRF_WDT->RR[0] = WDT_RR_RR_Reload; + } +} + +void NRF52Board::checkRuntimeVoltage() { + if (_power_config == nullptr || _power_config->voltage_runtime == 0) return; + + uint32_t now = millis(); + if (now - _last_voltage_check_ms < VOLTAGE_CHECK_INTERVAL_MS) return; + _last_voltage_check_ms = now; + + if (isExternalPowered()) { + _low_voltage_count = 0; + return; + } + + uint16_t mv = getBattMilliVolts(); + + // Ignore ADC glitch readings + if (mv < 1000) return; + + if (mv < _power_config->voltage_runtime) { + _low_voltage_count++; + MESH_DEBUG_PRINTLN("PWRMGT: Low voltage %u mV (%u/%u)", + mv, _low_voltage_count, LOW_VOLTAGE_COUNT_THRESHOLD); + if (_low_voltage_count >= LOW_VOLTAGE_COUNT_THRESHOLD) { + MESH_DEBUG_PRINTLN("PWRMGT: Runtime voltage too low - shutting down"); + initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE); + } + } else { + _low_voltage_count = 0; + } +} #endif +void NRF52Board::loop() { +#ifdef NRF52_POWER_MANAGEMENT + feedWatchdog(); + checkRuntimeVoltage(); +#endif +} + void NRF52BoardDCDC::begin() { NRF52Board::begin(); @@ -252,10 +330,14 @@ void NRF52BoardDCDC::begin() { } void NRF52Board::sleep(uint32_t secs) { +#ifdef NRF52_POWER_MANAGEMENT + feedWatchdog(); +#endif + // Clear FPU interrupt flags to avoid insomnia // see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html #if (__FPU_USED == 1) - __set_FPSCR(__get_FPSCR() & ~(0x0000009F)); + __set_FPSCR(__get_FPSCR() & ~(0x0000009F)); (void) __get_FPSCR(); NVIC_ClearPendingIRQ(FPU_IRQn); #endif diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 96f67dc950..534ff9d436 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -21,6 +21,11 @@ struct PowerMgtConfig { // Boot protection voltage threshold (millivolts) // Set to 0 to disable boot protection uint16_t voltage_bootlock; + + // Runtime low voltage shutdown threshold (millivolts), 0=disabled + uint16_t voltage_runtime; + // Watchdog timer timeout (ms), 0=disabled + uint32_t wdt_timeout_ms; }; #endif @@ -38,14 +43,25 @@ class NRF52Board : public mesh::MainBoard { uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF) uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts) + const PowerMgtConfig* _power_config; + uint32_t _last_voltage_check_ms; + uint8_t _low_voltage_count; + bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); virtual void initiateShutdown(uint8_t reason); + void initWatchdog(uint32_t timeout_ms); + void feedWatchdog(); + void checkRuntimeVoltage(); #endif public: - NRF52Board(char *otaname) : ota_name(otaname) {} + NRF52Board(char *otaname) : ota_name(otaname) +#ifdef NRF52_POWER_MANAGEMENT + , _power_config(nullptr), _last_voltage_check_ms(0), _low_voltage_count(0) +#endif + {} virtual void begin(); virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; @@ -53,6 +69,7 @@ class NRF52Board : public mesh::MainBoard { virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual void sleep(uint32_t secs) override; + virtual void loop() override; #ifdef NRF52_POWER_MANAGEMENT bool isExternalPowered() override; diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp index d1dc089629..36294cc68e 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.cpp @@ -10,7 +10,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, + .voltage_runtime = PWRMGT_VOLTAGE_BOOTLOCK - 200, + .wdt_timeout_ms = 60000 }; diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index c03d39afb6..176e5b46c3 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -9,7 +9,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, + .voltage_runtime = PWRMGT_VOLTAGE_BOOTLOCK - 200, + .wdt_timeout_ms = 60000 }; void T114Board::initiateShutdown(uint8_t reason) { diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 9fb47b432e..64977c7eea 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -9,7 +9,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, + .voltage_runtime = PWRMGT_VOLTAGE_BOOTLOCK - 200, + .wdt_timeout_ms = 60000 }; void RAK4631Board::initiateShutdown(uint8_t reason) { diff --git a/variants/sensecap_solar/SenseCapSolarBoard.cpp b/variants/sensecap_solar/SenseCapSolarBoard.cpp index b9dd2503d7..8b665bada3 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.cpp +++ b/variants/sensecap_solar/SenseCapSolarBoard.cpp @@ -7,7 +7,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, + .voltage_runtime = PWRMGT_VOLTAGE_BOOTLOCK - 200, + .wdt_timeout_ms = 60000 }; void SenseCapSolarBoard::initiateShutdown(uint8_t reason) { diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index 42ee6a87fe..5fd106013a 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, + .voltage_runtime = PWRMGT_VOLTAGE_BOOTLOCK - 200, + .wdt_timeout_ms = 60000 }; void XiaoNrf52Board::initiateShutdown(uint8_t reason) {