diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7d0179f3ab..23ef51a944 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,5 +1,8 @@ #include "MyMesh.h" #include +#if defined(NRF52_PLATFORM) +#include "helpers/NRF52Board.h" +#endif /* ------------------------------ Config -------------------------------- */ @@ -946,6 +949,15 @@ void MyMesh::begin(FILESYSTEM *fs) { // load persisted prefs _cli.loadPrefs(_fs); acl.load(_fs, self_id); +#if defined(NRF52_PLATFORM) + { + uint8_t wdt_to = 0; + if (_prefs.wdt_enabled) { + wdt_to = constrain(_prefs.wdt_timeout_secs, 1, 255); + } + static_cast(board).initWatchdog(wdt_to); + } +#endif // TODO: key_store.begin(); region_map.load(_fs); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index a714db68ec..14fd299924 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -187,6 +187,9 @@ void loop() { the_mesh.loop(); sensors.loop(); +#if defined(NRF52_PLATFORM) + board.loop(); +#endif #ifdef DISPLAY_CLASS ui_task.loop(); #endif diff --git a/src/MeshCore.h b/src/MeshCore.h index e67371ef17..3820eee2b9 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -64,6 +64,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 */ } virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } virtual bool canControlLoRaFemLna() const { return false; } virtual bool isLoRaFemLnaEnabled() const { return false; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e8..f3468ee700 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -5,6 +5,10 @@ #include "TxtDataHelpers.h" #include +#if defined(NRF52_PLATFORM) + #include "helpers/NRF52Board.h" +#endif + #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif @@ -126,6 +130,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14); _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1); + _prefs->wdt_enabled = constrain(_prefs->wdt_enabled, 0, 1); + if (_prefs->wdt_timeout_secs == 0) _prefs->wdt_timeout_secs = 30; + _prefs->wdt_timeout_secs = constrain(_prefs->wdt_timeout_secs, 1, 255); _prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1); _prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2); @@ -397,6 +404,89 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "Can't find GPS"); } +#endif + } else if (memcmp(command, "watchdog lockup", 15) == 0) { +#if defined(NRF52_PLATFORM) + NRF52Board* nb = static_cast(_board); + if (!nb->isWatchdogRunning()) { + strcpy(reply, "error - watchdog not running"); + } else { + strcpy(reply, "OK - locking loop"); + nb->requestLockup(); + } +#else + strcpy(reply, "not supported on this platform"); +#endif + } else if (memcmp(command, "watchdog timeout ", 17) == 0) { +#if defined(NRF52_PLATFORM) + uint32_t n = _atoi(&command[17]); + if (n < 1 || n > 255) { + strcpy(reply, "error - timeout must be 1..255 (use watchdog off to disable)"); + } else { + _prefs->wdt_timeout_secs = (uint8_t)n; + savePrefs(); + NRF52Board* nb = static_cast(_board); + if (nb->isWatchdogRunning() && nb->getWatchdogRunningTimeoutSecs() != n) { + strcpy(reply, "OK - reboot to apply"); + } else { + sprintf(reply, "OK - timeout %us", (unsigned)n); + } + } +#else + strcpy(reply, "not supported on this platform"); +#endif + } else if (memcmp(command, "watchdog timeout", 16) == 0 && (command[16] == 0 || command[16] == ' ')) { +#if defined(NRF52_PLATFORM) + NRF52Board* nb = static_cast(_board); + uint8_t run_to = nb->getWatchdogRunningTimeoutSecs(); + if (run_to) { + sprintf(reply, "pref %us, running %us", (unsigned)_prefs->wdt_timeout_secs, (unsigned)run_to); + } else { + sprintf(reply, "pref %us, not running", (unsigned)_prefs->wdt_timeout_secs); + } +#else + strcpy(reply, "not supported on this platform"); +#endif + } else if (memcmp(command, "watchdog on", 11) == 0) { +#if defined(NRF52_PLATFORM) + _prefs->wdt_enabled = 1; + savePrefs(); + NRF52Board* nb = static_cast(_board); + if (nb->isWatchdogRunning()) { + strcpy(reply, "OK - already running"); + } else { + strcpy(reply, "OK - reboot to start"); + } +#else + strcpy(reply, "not supported on this platform"); +#endif + } else if (memcmp(command, "watchdog off", 12) == 0) { +#if defined(NRF52_PLATFORM) + _prefs->wdt_enabled = 0; + savePrefs(); + strcpy(reply, "OK - reboot to stop"); +#else + strcpy(reply, "not supported on this platform"); +#endif + } else if (memcmp(command, "watchdog status", 15) == 0 || memcmp(command, "watchdog", 8) == 0) { +#if defined(NRF52_PLATFORM) + NRF52Board* nb = static_cast(_board); + const char* en = _prefs->wdt_enabled ? "enabled" : "disabled"; + const char* run = nb->isWatchdogRunning() ? "running" : "not running"; + const char* last = nb->getResetReasonString(nb->getResetReason()); + uint8_t run_to = nb->getWatchdogRunningTimeoutSecs(); + if (_prefs->wdt_enabled && !nb->isWatchdogRunning()) { + sprintf(reply, "%s, %s, timeout %us (reboot to start), last reset: %s", + en, run, (unsigned)_prefs->wdt_timeout_secs, last); + } else if (!_prefs->wdt_enabled && nb->isWatchdogRunning()) { + sprintf(reply, "%s, %s, timeout %us (reboot to stop), last reset: %s", + en, run, (unsigned)(run_to ? run_to : _prefs->wdt_timeout_secs), last); + } else { + sprintf(reply, "%s, %s, timeout %us, last reset: %s", + en, run, (unsigned)(run_to ? run_to : _prefs->wdt_timeout_secs), last); + } +#else + strcpy(reply, "not supported on this platform"); #endif } else if (memcmp(command, "powersaving on", 14) == 0) { #if defined(NRF52_PLATFORM) diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e9f..6378370e74 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -56,6 +56,8 @@ class NodePrefs : public ConfigSerializer { char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting uint8_t powersaving_enabled = 0; // boolean + uint8_t wdt_enabled = 1; // nRF52 hardware watchdog + uint8_t wdt_timeout_secs = 30; // seconds (1..255) // Gps settings uint8_t gps_enabled = 0; uint32_t gps_interval = 0; // in seconds @@ -134,6 +136,8 @@ class NodePrefs : public ConfigSerializer { void structure() override { def("adc_mult", _parent->adc_multiplier); def("pwr_sav_en", _parent->powersaving_enabled); + def("wdt_en", _parent->wdt_enabled); + def("wdt_timeout", _parent->wdt_timeout_secs); } public: PowerPrefs(NodePrefs* parent) : _parent(parent) { } diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index eb88c89744..1619b58000 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -23,30 +23,92 @@ static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { MESH_DEBUG_PRINTLN("BLE client disconnected"); } +#include "nrf.h" + +// Reset/shutdown capture — always compiled so watchdog status works without NRF52_POWER_MANAGEMENT +uint32_t g_nrf52_reset_reason = 0; +uint8_t g_nrf52_shutdown_reason = 0; + +static void __attribute__((constructor(101))) nrf52_early_reset_capture() { + g_nrf52_reset_reason = NRF_POWER->RESETREAS; + g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2; +} + void NRF52Board::begin() { startup_reason = BD_STARTUP_NORMAL; + reset_reason = g_nrf52_reset_reason; +#ifndef NRF52_POWER_MANAGEMENT + NRF_POWER->RESETREAS = 0xFFFFFFFF; +#endif #ifdef USE_CC310_HW_CRYPTO - // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. nRFCrypto.begin(); #endif } -#ifdef NRF52_POWER_MANAGEMENT -#include "nrf.h" +static const uint32_t WDT_FEED_MAGIC = 0x6E524635u; -// Power Management global variables -uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason -uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason +void NRF52Board::feedWatchdogIfRunning() { + if (NRF_WDT->RUNSTATUS) NRF_WDT->RR[0] = WDT_FEED_MAGIC; +} -// Early constructor - runs before SystemInit() clears the registers -// Priority 101 ensures this runs before SystemInit (102) and before -// any C++ static constructors (default 65535) -static void __attribute__((constructor(101))) nrf52_early_reset_capture() { - g_nrf52_reset_reason = NRF_POWER->RESETREAS; - g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2; +bool NRF52Board::isWatchdogRunning() const { + return NRF_WDT->RUNSTATUS != 0; +} + +uint8_t NRF52Board::getWatchdogRunningTimeoutSecs() const { + if (!isWatchdogRunning()) return 0; + uint32_t crv = NRF_WDT->CRV; + if (crv == 0) return 0; + uint32_t secs = (crv + 32767u) / 32768u; + return secs > 255u ? 255u : (uint8_t)secs; +} + +void NRF52Board::initWatchdog(uint8_t timeout_secs) { + if (timeout_secs == 0) return; + _wdt_timeout_secs = timeout_secs; + if (NRF_WDT->RUNSTATUS) return; // already counting (e.g. carried over from prior boot) + NRF_WDT->CRV = (uint32_t)timeout_secs * 32768u; + NRF_WDT->RREN = WDT_RREN_RR0_Enabled; + NRF_WDT->CONFIG = (WDT_CONFIG_SLEEP_Pause << WDT_CONFIG_SLEEP_Pos) + | (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos); + NRF_WDT->TASKS_START = 1; +} + +void NRF52Board::requestLockup() { + _lockup_at = millis() + 500; } +void NRF52Board::loop() { + feedWatchdogIfRunning(); + if (_lockup_at != 0 && (int32_t)(millis() - _lockup_at) >= 0) { + Serial.flush(); + while (1) {} + } +} + +const char* NRF52Board::getResetReasonString(uint32_t reason) { + if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin"; + if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog"; + if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset"; + if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup"; + #ifdef POWER_RESETREAS_LPCOMP_Msk + if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP"; + #endif + #ifdef POWER_RESETREAS_VBUS_Msk + if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS"; + #endif + #ifdef POWER_RESETREAS_OFF_Msk + if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO"; + #endif + #ifdef POWER_RESETREAS_DIF_Msk + if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface"; + #endif + return "Cold Boot"; +} + +#ifdef NRF52_POWER_MANAGEMENT + void NRF52Board::initPowerMgr() { // Copy early-captured register values reset_reason = g_nrf52_reset_reason; @@ -76,26 +138,6 @@ void NRF52Board::initPowerMgr() { } } -const char* NRF52Board::getResetReasonString(uint32_t reason) { - if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin"; - if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog"; - if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset"; - if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup"; - #ifdef POWER_RESETREAS_LPCOMP_Msk - if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP"; - #endif - #ifdef POWER_RESETREAS_VBUS_Msk - if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS"; - #endif - #ifdef POWER_RESETREAS_OFF_Msk - if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO"; - #endif - #ifdef POWER_RESETREAS_DIF_Msk - if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface"; - #endif - return "Cold Boot"; -} - const char* NRF52Board::getShutdownReasonString(uint8_t reason) { switch (reason) { case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage"; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index dba15f974e..4abe81e216 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -32,9 +32,11 @@ class NRF52Board : public mesh::MainBoard { protected: uint8_t startup_reason; char *ota_name; + uint32_t reset_reason; // RESETREAS captured at boot (before clear) + uint32_t _lockup_at = 0; // millis deadline for deferred watchdog lockup test + uint8_t _wdt_timeout_secs = 0; // configured/pref timeout (CRV frozen after start) #ifdef NRF52_POWER_MANAGEMENT - uint32_t reset_reason; // RESETREAS register value uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF) uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts) @@ -45,7 +47,7 @@ class NRF52Board : public mesh::MainBoard { #endif public: - NRF52Board(char *otaname) : ota_name(otaname) {} + NRF52Board(char *otaname) : ota_name(otaname), reset_reason(0) {} virtual void begin(); virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; @@ -55,13 +57,22 @@ 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; bool isExternalPowered() override; + void initWatchdog(uint8_t timeout_secs); + void requestLockup(); + bool isWatchdogRunning() const; + uint8_t getWatchdogTimeoutSecs() const { return _wdt_timeout_secs; } + uint8_t getWatchdogRunningTimeoutSecs() const; + static void feedWatchdogIfRunning(); + + virtual uint32_t getResetReason() const override { return reset_reason; } + const char* getResetReasonString(uint32_t reason) override; + #ifdef NRF52_POWER_MANAGEMENT uint16_t getBootVoltage() override { return boot_voltage_mv; } - virtual uint32_t getResetReason() const override { return reset_reason; } uint8_t getShutdownReason() const override { return shutdown_reason; } - const char* getResetReasonString(uint32_t reason) override; const char* getShutdownReasonString(uint8_t reason) override; #endif };