From 9fa23eabd2c0fc7b31733460898e3abe177b701c Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:49:54 +1100 Subject: [PATCH] Watchdog Reset Timer for nRF52840 - NRF52Board: Add initWatchdog() to initialise WDT, set timer to 30 seconds and pause WDT if board enters sleep state - NRF52Board: Add feedWatchdog() to bump the watchdog timer during normal operations - NRF52Board: Add isWatchdogRunning() to query current run state of WDT - NRF52Board: add tick() to be called from each example firmware to bump timer and allow a place for future tasks to be added centrally - CommonCLI: Add "set wdt (on/off)" to enable or disable WDT (requires reboot) - CommonCLI: Add "get wdt" to query enabled status and if wdt is running - Repeater firmware: Add init and tick calls for WDT - Added Heltec T114, RAK4631, Xiao nRF52840 build flag to include WDT --- docs/nrf52_watchdog.md | 83 +++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.cpp | 7 +++ examples/simple_repeater/main.cpp | 1 + src/MeshCore.h | 3 ++ src/helpers/CommonCLI.cpp | 21 +++++++- src/helpers/CommonCLI.h | 1 + src/helpers/NRF52Board.cpp | 47 ++++++++++++++++ src/helpers/NRF52Board.h | 10 ++++ variants/heltec_t114/platformio.ini | 4 ++ variants/rak4631/platformio.ini | 3 ++ variants/xiao_nrf52/platformio.ini | 1 + 11 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 docs/nrf52_watchdog.md diff --git a/docs/nrf52_watchdog.md b/docs/nrf52_watchdog.md new file mode 100644 index 000000000..6c8812145 --- /dev/null +++ b/docs/nrf52_watchdog.md @@ -0,0 +1,83 @@ +# Overview + +The nRF52 hardware watchdog timer (WDT) provides automatic recovery from firmware hangs or crashes. When enabled, if the firmware fails to "feed" the watchdog within the timeout period, the device will automatically reset. + +## Parameters + +| Parameter | Value | +|-----------|-------| +| Timeout | 30 seconds | +| Sleep behavior | Pauses during sleep mode | +| Halt behavior | Pauses during halt state | +| Control | Compile-time via `NRF52_WATCHDOG` build flag + runtime pref `wdt_enabled` (CLI `set wdt on/off`) | + +The implementation uses the nRF52840 WDT peripheral with the following configuration: + +| Register | Value | Description | +|----------|-------|-------------| +| `CONFIG.SLEEP` | Pause | WDT pauses when CPU enters sleep | +| `CONFIG.HALT` | Pause | WDT pauses during debug halt | +| `CRV` | 983040 | Counter reload value (30s × 32768 Hz) | +| `RREN` | RR0 enabled | Uses reload request register 0 | +| `RR[0]` | 0x6E524635 | Magic value to feed watchdog | + +## Usage + +1. Watchdog is **disabled by default** - enable via `set wdt on` (reboot required to turn on/off) +2. Application code checks `prefs.wdt_enabled` after loading prefs and calls `board.initWatchdog()` if enabled +3. The main loop must call `board.tick()` regularly to feed the watchdog +4. If `board.tick()` is not called within 30 seconds, the device resets +5. The watchdog pauses during low-power sleep modes (won't reset during intended sleep) + +**Important**: Once the watchdog is started, it cannot be stopped during runtime. A power cycle is required to apply changes made via `set wdt on/off`. + +## Enabling Watchdog for a Board Variant + +Watchdog is not compiled by default. To enable it for a board variant, add the `NRF52_WATCHDOG` flag to the variant's `platformio.ini`. The `wdt_enabled` preference (set via `set wdt on/off`) controls whether it starts on boot (disabled by default): + +```ini + [env:your_variant] +extends = nrf52_base +build_flags = ${nrf52_base.build_flags} + -D NRF52_WATCHDOG + # ... other flags +``` + +## Firmware Integration + +Currently, Watchdog is only implemented in the Repeater firmware. To add to other firmware types, perform the below steps. + +After loading prefs in the `begin()` method, start the watchdog if enabled: + +```cpp +_cli.loadPrefs(_fs); + +#ifdef NRF52_WATCHDOG + if (_prefs.wdt_enabled) { + board.initWatchdog(); + } +#endif +``` + +Ensure the main `loop()` function calls `board.tick()` to feed the watchdog: + +```cpp +void loop() { + the_mesh.loop(); + sensors.loop(); +#ifdef DISPLAY_CLASS + ui_task.loop(); +#endif + rtc_clock.tick(); + board.tick(); // Feed the watchdog +} +``` + +## CLI Commands + +When `NRF52_WATCHDOG` is defined, the following CLI commands are available: + +| Command | Description | +|---------|-------------| +| `get wdt` | Returns `Enabled/Disabled, running/not running` | +| `set wdt on/off` | Enable/disable watchdog on next reboot | diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d926148d6..fd656cde2 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -808,6 +808,13 @@ void MyMesh::begin(FILESYSTEM *fs) { _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); + +#ifdef NRF52_WATCHDOG + if (_prefs.wdt_enabled) { + board.initWatchdog(); + } +#endif + acl.load(_fs); // TODO: key_store.begin(); region_map.load(_fs); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 8c745613e..3dc030d12 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -124,6 +124,7 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); + board.tick(); if (the_mesh.getNodePrefs()->powersaving_enabled && // To check if power saving is enabled the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) { // To check if it is time to sleep diff --git a/src/MeshCore.h b/src/MeshCore.h index f194cdeb4..d064f033d 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,6 +64,9 @@ class MainBoard { virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; } virtual uint8_t getShutdownReason() const { return 0; } virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; } + + // Watchdog interface (boards with watchdog support override these) + virtual bool isWatchdogRunning() { return false; } }; /** diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 6dac9fff0..08d299a2c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -74,7 +74,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.read((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.read((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.read(pad, 3); // 153 + file.read((uint8_t *)&_prefs->wdt_enabled, sizeof(_prefs->wdt_enabled)); // 153 + file.read(pad, 2); // 154 file.read((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.read((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.read((uint8_t *)&_prefs->advert_loc_policy, sizeof (_prefs->advert_loc_policy)); // 161 @@ -95,6 +96,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); + _prefs->wdt_enabled = constrain(_prefs->wdt_enabled, 0, 1); // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -158,7 +160,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.write(pad, 3); // 153 + file.write((uint8_t *)&_prefs->wdt_enabled, sizeof(_prefs->wdt_enabled)); // 153 + file.write(pad, 2); // 154 file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 @@ -390,6 +393,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %u mV", _board->getBootVoltage()); #else strcpy(reply, "ERROR: Power management not supported"); +#endif +#ifdef NRF52_WATCHDOG + } else if (memcmp(config, "wdt", 3) == 0 && config[3] == 0) { + sprintf(reply, "> %s, %s", + _prefs->wdt_enabled ? "Enabled" : "Disabled", + _board->isWatchdogRunning() ? "running" : "not running"); #endif } else { sprintf(reply, "??: %s", config); @@ -610,6 +619,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->adc_multiplier = 0.0f; strcpy(reply, "Error: unsupported by this board"); }; +#ifdef NRF52_WATCHDOG + } else if (memcmp(config, "wdt ", 4) == 0) { + const char* value = &config[4]; + bool enable = memcmp(value, "on", 2) == 0 || memcmp(value, "1", 1) == 0; + _prefs->wdt_enabled = enable ? 1 : 0; + savePrefs(); + strcpy(reply, enable ? "OK - reboot to enable watchdog" : "OK - reboot to disable watchdog"); +#endif } else { sprintf(reply, "unknown config: %s", config); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3b1d05f96..a05ddf090 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -51,6 +51,7 @@ struct NodePrefs { // persisted to file uint32_t discovery_mod_timestamp; float adc_multiplier; char owner_info[120]; + uint8_t wdt_enabled; // nRF52 watchdog enabled preference (0/1) }; class CommonCLICallbacks { diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 1303d5be6..f81e13d6e 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -4,6 +4,47 @@ #include #include +#ifdef NRF52_WATCHDOG +#include + +// nRF52 WDT reload register magic value ("nRF5" in ASCII, per Nordic SDK) +#define WDT_RR_VALUE 0x6E524635UL + +// 30 second timeout at 32.768kHz clock +#define WDT_TIMEOUT_SECONDS 30 +#define WDT_CRV_VALUE (WDT_TIMEOUT_SECONDS * 32768UL) + +bool NRF52Board::initWatchdog() { + // Check if already running - WDT cannot be reconfigured once started + if (NRF_WDT->RUNSTATUS) { + return false; + } + + // Configure WDT to pause during sleep and halt modes + NRF_WDT->CONFIG = (WDT_CONFIG_SLEEP_Pause << WDT_CONFIG_SLEEP_Pos) | + (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos); + + // Set timeout value (30 seconds) + NRF_WDT->CRV = WDT_CRV_VALUE; + + // Enable reload request register 0 + NRF_WDT->RREN = WDT_RREN_RR0_Enabled << WDT_RREN_RR0_Pos; + + // Start the watchdog + NRF_WDT->TASKS_START = 1; + + return true; +} + +void NRF52Board::feedWatchdog() { + NRF_WDT->RR[0] = WDT_RR_VALUE; +} + +bool NRF52Board::isWatchdogRunning() { + return NRF_WDT->RUNSTATUS != 0; +} +#endif + static BLEDfu bledfu; static void connect_callback(uint16_t conn_handle) { @@ -22,6 +63,12 @@ void NRF52Board::begin() { startup_reason = BD_STARTUP_NORMAL; } +void NRF52Board::tick() { +#ifdef NRF52_WATCHDOG + feedWatchdog(); +#endif +} + #ifdef NRF52_POWER_MANAGEMENT #include "nrf.h" diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 1f02bace7..3ceabf08b 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -43,8 +43,13 @@ class NRF52Board : public mesh::MainBoard { virtual void initiateShutdown(uint8_t reason); #endif +#ifdef NRF52_WATCHDOG + void feedWatchdog(); +#endif + public: virtual void begin(); + virtual void tick(); virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } @@ -57,6 +62,11 @@ class NRF52Board : public mesh::MainBoard { const char* getResetReasonString(uint32_t reason) override; const char* getShutdownReasonString(uint8_t reason) override; #endif + +#ifdef NRF52_WATCHDOG + bool initWatchdog(); + bool isWatchdogRunning() override; +#endif }; /* diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 20f5e8fec..a991b751c 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -57,6 +57,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D NRF52_WATCHDOG ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -72,6 +73,7 @@ build_flags = -D WITH_RS232_BRIDGE=Serial2 -D WITH_RS232_BRIDGE_RX=9 -D WITH_RS232_BRIDGE_TX=10 + -D NRF52_WATCHDOG ; -D BRIDGE_DEBUG=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -168,6 +170,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D NRF52_WATCHDOG ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 @@ -183,6 +186,7 @@ build_flags = -D WITH_RS232_BRIDGE=Serial2 -D WITH_RS232_BRIDGE_RX=9 -D WITH_RS232_BRIDGE_TX=10 + -D NRF52_WATCHDOG ; -D BRIDGE_DEBUG=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 9a9ab2dd3..5bdb03822 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -40,6 +40,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D NRF52_WATCHDOG ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} @@ -59,6 +60,7 @@ build_flags = -D WITH_RS232_BRIDGE=Serial1 -D WITH_RS232_BRIDGE_RX=PIN_SERIAL1_RX -D WITH_RS232_BRIDGE_TX=PIN_SERIAL1_TX + -D NRF52_WATCHDOG -UENV_INCLUDE_GPS ; -D BRIDGE_DEBUG=1 ; -D MESH_PACKET_LOGGING=1 @@ -82,6 +84,7 @@ build_flags = -D WITH_RS232_BRIDGE=Serial2 -D WITH_RS232_BRIDGE_RX=PIN_SERIAL2_RX -D WITH_RS232_BRIDGE_TX=PIN_SERIAL2_TX + -D NRF52_WATCHDOG -UENV_INCLUDE_GPS ; -D BRIDGE_DEBUG=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 6e96018bc..cbb52336b 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -91,6 +91,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D NRF52_WATCHDOG ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter}