From 7afa437c03debb2612c61fd477a78dcb98c8cedd Mon Sep 17 00:00:00 2001 From: phaseloop Date: Fri, 28 Nov 2025 23:03:48 +0000 Subject: [PATCH 01/12] Fix NRF52 memory corruption on low battery levels --- src/platform/nrf52/main-nrf52.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index c03cc4454a6..62cd25aa7ef 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -74,12 +74,26 @@ void getMacAddr(uint8_t *dmac) static void initBrownout() { - auto vccthresh = POWER_POFCON_THRESHOLD_V24; + // POF protection prevents flash memory writes when VDD voltage is 2.7V or less to avoid memory corruption + // In this setting voltage is checked both against VDD and VDDH so particular board + // wiring does not matter. + // It must be set to value greater than 2.5V because 2.5V is minimum voltage that can be supplied at VDDH + // and it borders at cutoff voltage for li-ion battery protectors. + // Originally it was set at 2.4V and it did cause a lot of flash memory corruptions when battery was around 2.5-2.6V + + // NiceNano!2 board have decent LDO which goes down to 2V + // In the future - boards with crappy LDO can be set to prevent memory corruption at higher voltage - like 3V + // using custom variant definition like #define LDO_3V_CUTOFF but this voltage detection must be done + // using sd_power_pof_thresholdvddh_set function and POWER_POFCON_THRESHOLDVDDH_V30 flag. + // You also need to be sure those boards supply voltage at VDDH (and not VDD and VDDH together) for it to work. + + + auto vddthresh = POWER_POFCON_THRESHOLD_V27; auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); assert(err_code == NRF_SUCCESS); - err_code = sd_power_pof_threshold_set(vccthresh); + err_code = sd_power_pof_threshold_set(vddthresh); assert(err_code == NRF_SUCCESS); // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice From 5b2d144fdedd743a90093de557466236342596bf Mon Sep 17 00:00:00 2001 From: phaseloop Date: Mon, 1 Dec 2025 00:16:37 +0100 Subject: [PATCH 02/12] detect USB power input on ProMicro boards --- src/Power.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Power.cpp b/src/Power.cpp index 7bb8896ce4f..fb661c47de6 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -460,7 +460,7 @@ class AnalogBatteryLevel : public HasBatteryLevel } // if it's not HIGH - check the battery #endif -#elif defined(MUZI_BASE) +#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; #endif return getBattVoltage() > chargingVolt; From 9714be49d08d571303d30bbccde05e60bdac4496 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Mon, 1 Dec 2025 02:46:45 +0100 Subject: [PATCH 03/12] prevent booting on power failure detection --- src/main.cpp | 51 +++++++++++++++++++++++++++++++ src/platform/nrf52/main-nrf52.cpp | 19 ++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index da2e396044e..4d172d5135f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -288,6 +288,51 @@ __attribute__((weak, noinline)) bool loopCanSleep() void lateInitVariant() __attribute__((weak)); void lateInitVariant() {} + +// NRF52 (and probably other platforms) can report when system is in power failure mode +// (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc). +// For example NRF52 will prevent any flash writes in that case automatically +// (but it causes issues we need to handle). +// This detection is independent from whatever ADC or dividers used in Meshtastic +// boards and is internal to chip. + +// Other platforms or variants can define it too - by knowing board's LDO cutoff voltage and measuring +// battery pin using ADC. + +__attribute__((weak, noinline)) bool isPowerLevelSafe() { return true;} + +// wait until isPowerLevelSafe() reports true +// blink user led in 3 flashes sequence to indicate what is happening +void waitUntilPowerLevelSafe(){ + + // pinMode(POWER_LED, OUTPUT); + // digitalWrite(POWER_LED, HIGH ^ LED_STATE_ON); + + pinMode(LED_PIN, OUTPUT); + + while(isPowerLevelSafe() == false){ + + + #ifdef LED_PIN + + // 3x: blink for 500 ms, pause for 500 ms + + for(int i=0;i<3;i++){ + digitalWrite(LED_PIN, LED_STATE_ON); + delay(500); + digitalWrite(LED_PIN, LED_STATE_OFF); + delay(500); + } + #endif + + // sleep for 2s + delay(2000); + + } + +} + + /** * Print info as a structured log message (for automated log processing) */ @@ -298,6 +343,12 @@ void printInfo() #ifndef PIO_UNIT_TESTING void setup() { + + + // prevent booting if device is in power failure mode + // boot sequence will follow when battery level raises to safe mode + waitUntilPowerLevelSafe(); + #if defined(R1_NEO) pinMode(DCDC_EN_HOLD, OUTPUT); digitalWrite(DCDC_EN_HOLD, HIGH); diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 62cd25aa7ef..9ca6aaf197a 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -38,6 +38,8 @@ void variant_shutdown() {} static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; +bool pofcon_configured = false; + static inline void debugger_break(void) { __asm volatile("bkpt #0x01\n\t" @@ -96,9 +98,26 @@ static void initBrownout() err_code = sd_power_pof_threshold_set(vddthresh); assert(err_code == NRF_SUCCESS); + pofcon_configured = true; + // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice } + +bool isPowerLevelSafe(){ + + return false; + + if(!pofcon_configured){ + initBrownout(); + } + + if(NRF_POWER->EVENTS_POFWARN) + return false; + return true; +} + + // This is a public global so that the debugger can set it to false automatically from our gdbinit bool useSoftDevice = true; // Set to false for easier debugging From 9647692448dd268c56ee078fef3ee93ba7a2c186 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Tue, 2 Dec 2025 16:03:54 +0000 Subject: [PATCH 04/12] introduce PowerHAL layer --- src/main.cpp | 23 +++++++++++------------ src/power/PowerHAL.cpp | 12 ++++++++++++ src/power/PowerHAL.h | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 src/power/PowerHAL.cpp create mode 100644 src/power/PowerHAL.h diff --git a/src/main.cpp b/src/main.cpp index 4d172d5135f..b948425c97e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" +#include "power/PowerHAL.h" #include "PowerFSM.h" #include "PowerMon.h" #include "ReliableRouter.h" @@ -296,22 +297,18 @@ void lateInitVariant() {} // This detection is independent from whatever ADC or dividers used in Meshtastic // boards and is internal to chip. -// Other platforms or variants can define it too - by knowing board's LDO cutoff voltage and measuring -// battery pin using ADC. +// we use powerHAL layer to get this info and delay booting until power level is safe -__attribute__((weak, noinline)) bool isPowerLevelSafe() { return true;} - -// wait until isPowerLevelSafe() reports true +// wait until power level is safe to continue booting (to avoid bootloops) // blink user led in 3 flashes sequence to indicate what is happening void waitUntilPowerLevelSafe(){ - // pinMode(POWER_LED, OUTPUT); - // digitalWrite(POWER_LED, HIGH ^ LED_STATE_ON); - - pinMode(LED_PIN, OUTPUT); - while(isPowerLevelSafe() == false){ + #ifdef LED_PIN + pinMode(LED_PIN, OUTPUT); + #endif + while(powerHAL_isPowerLevelSafe() == false){ #ifdef LED_PIN @@ -319,9 +316,9 @@ void waitUntilPowerLevelSafe(){ for(int i=0;i<3;i++){ digitalWrite(LED_PIN, LED_STATE_ON); - delay(500); + delay(300); digitalWrite(LED_PIN, LED_STATE_OFF); - delay(500); + delay(300); } #endif @@ -344,6 +341,8 @@ void printInfo() void setup() { + // initialize power HAL layer as early as possible + powerHAL_init(); // prevent booting if device is in power failure mode // boot sequence will follow when battery level raises to safe mode diff --git a/src/power/PowerHAL.cpp b/src/power/PowerHAL.cpp new file mode 100644 index 00000000000..f06dba3499f --- /dev/null +++ b/src/power/PowerHAL.cpp @@ -0,0 +1,12 @@ + +#include "PowerHAL.h" + +void powerHAL_init(){ + return powerHAL_platformInit(); +} + +__attribute__((weak, noinline)) void powerHAL_platformInit() {} + +__attribute__((weak, noinline)) bool powerHAL_isPowerLevelSafe() { return true; } + +__attribute__((weak, noinline)) bool powerHAL_isVBUSConnected() { return false; } diff --git a/src/power/PowerHAL.h b/src/power/PowerHAL.h new file mode 100644 index 00000000000..581a68eef3d --- /dev/null +++ b/src/power/PowerHAL.h @@ -0,0 +1,28 @@ + +/* + +Power Hardware Abstraction Layer. Set of API calls to offload power management, measurements, reboots, etc +to the platform and variant code to avoid #ifdef spaghetti hell and limitless device-based edge cases +in the main firmware code + +Functions declared here (with exception of powerHAL_init) should be defined in platform specific codebase. +Default function body does usually nothing. + +*/ + + +// Initialize HAL layer. Call it as early as possible during device boot +// do not overwrite it as it's not declared with "weak" attribute. +void powerHAL_init(); + +// platform specific init code if needed to be run early on boot +void powerHAL_platformInit(); + +// Return true is current battery level is safe for device operation (for example flash writes). +// This should be reported by power failure comparator (NRF52) or similar circuits on other platforms. +// Do not use battery ADC as improper ADC configuration may prevent device from booting. +bool powerHAL_isPowerLevelSafe(); + +// return if USB voltage is connected +bool powerHAL_isVBUSConnected(); + From 46c02be36868b54ea1b88abf9f5fd0b1513c8ea7 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Wed, 3 Dec 2025 16:55:11 +0000 Subject: [PATCH 05/12] powerHAL basic implementation for NRF52 --- src/Power.cpp | 7 ++++++- src/main.cpp | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index fb661c47de6..062bc3505c3 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -13,6 +13,7 @@ #include "power.h" #include "NodeDB.h" #include "PowerFSM.h" +#include "power/PowerHAL.h" #include "Throttle.h" #include "buzz/buzz.h" #include "configuration.h" @@ -460,8 +461,12 @@ class AnalogBatteryLevel : public HasBatteryLevel } // if it's not HIGH - check the battery #endif + +// technically speaking this should work for all(?) NRF52 boards +// but needs testing across multiple devices. NRF52 USB would not even work if +// VBUS was not properly connected and detected by the CPU #elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) - return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; + return powerHAL_isVBUSConnected(); #endif return getBattVoltage() > chargingVolt; } diff --git a/src/main.cpp b/src/main.cpp index b948425c97e..52f7eee9c30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -303,7 +303,6 @@ void lateInitVariant() {} // blink user led in 3 flashes sequence to indicate what is happening void waitUntilPowerLevelSafe(){ - #ifdef LED_PIN pinMode(LED_PIN, OUTPUT); #endif @@ -342,6 +341,7 @@ void setup() { // initialize power HAL layer as early as possible + // for NRF52 this also initializes SoftDevice framework powerHAL_init(); // prevent booting if device is in power failure mode From bcf56527ed9263c13e12d671a15ce1c9306efd54 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Wed, 3 Dec 2025 16:55:18 +0000 Subject: [PATCH 06/12] add missing change --- src/platform/nrf52/main-nrf52.cpp | 103 +++++++++++++++--------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 9ca6aaf197a..4894f34aaec 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -38,7 +38,11 @@ void variant_shutdown() {} static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; -bool pofcon_configured = false; +// This is a public global so that the debugger can set it to false automatically from our gdbinit +// @phaseloop comment: most part of codebase, including filesystem flash driver depend on softdevice +// methods so disabling it may actually crash thing. Proceed with caution. + +bool useSoftDevice = true; // Set to false for easier debugging static inline void debugger_break(void) { @@ -46,6 +50,44 @@ static inline void debugger_break(void) "mov pc, lr\n\t"); } +// PowerHAL NRF52 specific function implementations +bool powerHAL_isVBUSConnected() { + return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; +} + +bool powerHAL_isPowerLevelSafe() { + + // TODO: DEBUGGING ONLY, DO NOT MERGE TO PROD + return false; + + if(NRF_POWER->EVENTS_POFWARN) + return false; + return true; +} + + void powerHAL_platformInit(){ + + + // POF protection prevents flash memory writes when VDD voltage is 2.7V or less to avoid memory corruption + // In this setting voltage is checked both against VDD and VDDH so particular board + // wiring does not matter. + // It must be set to value greater than 2.5V because 2.5V is minimum voltage that can be supplied at VDDH + // and it borders at cutoff voltage for li-ion battery protectors. + // Originally it was set at 2.4V and it did cause a lot of flash memory corruptions when battery was around 2.5-2.6V + + // Many NRF52 boards have decent LDO which goes down to 2V + // In the future - boards with crappy LDO can be set to prevent memory corruption at higher voltage - like 3V + // using custom variant definition. Remember that above 2.8V you need to monitor VDDH voltage threshold using different + // registers + + // SoftDevice is only enabled by Adafruit Bluetooth library (Bluefruit) and there is no good way to change it or integrate with it. + // This is started at boot before bluetooth so we use raw registers instead of sd_power* + + NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V27 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); + + } + + bool loopCanSleep() { // turn off sleep only while connected via USB @@ -74,52 +116,6 @@ void getMacAddr(uint8_t *dmac) dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack } -static void initBrownout() -{ - // POF protection prevents flash memory writes when VDD voltage is 2.7V or less to avoid memory corruption - // In this setting voltage is checked both against VDD and VDDH so particular board - // wiring does not matter. - // It must be set to value greater than 2.5V because 2.5V is minimum voltage that can be supplied at VDDH - // and it borders at cutoff voltage for li-ion battery protectors. - // Originally it was set at 2.4V and it did cause a lot of flash memory corruptions when battery was around 2.5-2.6V - - // NiceNano!2 board have decent LDO which goes down to 2V - // In the future - boards with crappy LDO can be set to prevent memory corruption at higher voltage - like 3V - // using custom variant definition like #define LDO_3V_CUTOFF but this voltage detection must be done - // using sd_power_pof_thresholdvddh_set function and POWER_POFCON_THRESHOLDVDDH_V30 flag. - // You also need to be sure those boards supply voltage at VDDH (and not VDD and VDDH together) for it to work. - - - auto vddthresh = POWER_POFCON_THRESHOLD_V27; - - auto err_code = sd_power_pof_enable(POWER_POFCON_POF_Enabled); - assert(err_code == NRF_SUCCESS); - - err_code = sd_power_pof_threshold_set(vddthresh); - assert(err_code == NRF_SUCCESS); - - pofcon_configured = true; - - // We don't bother with setting up brownout if soft device is disabled - because during production we always use softdevice -} - - -bool isPowerLevelSafe(){ - - return false; - - if(!pofcon_configured){ - initBrownout(); - } - - if(NRF_POWER->EVENTS_POFWARN) - return false; - return true; -} - - -// This is a public global so that the debugger can set it to false automatically from our gdbinit -bool useSoftDevice = true; // Set to false for easier debugging #if !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) @@ -139,7 +135,6 @@ void setBluetoothEnable(bool enable) if (!initialized) { nrf52Bluetooth = new NRF52Bluetooth(); nrf52Bluetooth->startDisabled(); - initBrownout(); initialized = true; } return; @@ -153,9 +148,6 @@ void setBluetoothEnable(bool enable) LOG_DEBUG("Init NRF52 Bluetooth"); nrf52Bluetooth = new NRF52Bluetooth(); nrf52Bluetooth->setup(); - - // We delay brownout init until after BLE because BLE starts soft device - initBrownout(); } // Already setup, apparently else @@ -225,9 +217,18 @@ extern "C" void lfs_assert(const char *reason) delay(500); // Give the serial port a bit of time to output that last message. // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set // NRF_POWER->GPREGRET directly. + + + // TODO: this will/can crash CPU if bluetooth stack is not compiled in or bluetooth is not initialized + // (regardless if enabled or disabled) - as there is no live SoftDevice stack + // implement "safe" functions detecting softdevice stack state and using proper method to set registers if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; } + + // TODO: this should not be done when SoftDevice is enabled as device will not boot back on soft reset + // as some data is retained in RAM which will prevent re-enabling bluetooth stack + // Google what Nordic has to say about NVIC_* + SoftDevice NVIC_SystemReset(); } From 46aea8dda8bf83b3dfe2a01da6351bc8ea9324e3 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Thu, 4 Dec 2025 09:57:57 +0000 Subject: [PATCH 07/12] todo comments --- src/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 52f7eee9c30..aa446a2108a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -303,6 +303,9 @@ void lateInitVariant() {} // blink user led in 3 flashes sequence to indicate what is happening void waitUntilPowerLevelSafe(){ + // TODO: do not use delay but RTC/IRQ whatever so we don't burn + // energy which is already scarce + #ifdef LED_PIN pinMode(LED_PIN, OUTPUT); #endif From 4eb95d4f9360de59f251cb50586bcf0fe8e3a5f6 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Tue, 9 Dec 2025 12:33:58 +0000 Subject: [PATCH 08/12] prevent data saves on low power --- src/mesh/NodeDB.cpp | 51 +++++++++++++++++++++++++++++++ src/platform/nrf52/main-nrf52.cpp | 14 ++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d3000c500af..4faa2b3de5f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -12,6 +12,7 @@ #include "NodeDB.h" #include "PacketHistory.h" #include "PowerFSM.h" +#include #include "RTC.h" #include "Router.h" #include "SPILock.h" @@ -1378,6 +1379,14 @@ void NodeDB::loadFromDisk() bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct, bool fullAtomic) { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if(!powerHAL_isPowerLevelSafe()){ + LOG_ERROR("Error: trying to saveProto() on unsafe device power level."); + return false; + } + bool okay = false; #ifdef FSCom auto f = SafeFile(filename, fullAtomic); @@ -1404,6 +1413,14 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_ bool NodeDB::saveChannelsToDisk() { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. + if(!powerHAL_isPowerLevelSafe()){ + LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level."); + return false; + } + #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1414,6 +1431,15 @@ bool NodeDB::saveChannelsToDisk() bool NodeDB::saveDeviceStateToDisk() { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if(!powerHAL_isPowerLevelSafe()){ + LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level."); + return false; + } + + #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1426,6 +1452,15 @@ bool NodeDB::saveDeviceStateToDisk() bool NodeDB::saveNodeDatabaseToDisk() { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if(!powerHAL_isPowerLevelSafe()){ + LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level."); + return false; + } + + #ifdef FSCom spiLock->lock(); FSCom.mkdir("/prefs"); @@ -1438,6 +1473,14 @@ bool NodeDB::saveNodeDatabaseToDisk() bool NodeDB::saveToDiskNoRetry(int saveWhat) { + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if(!powerHAL_isPowerLevelSafe()){ + LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level."); + return false; + } + bool success = true; #ifdef FSCom spiLock->lock(); @@ -1493,6 +1536,14 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat) bool NodeDB::saveToDisk(int saveWhat) { LOG_DEBUG("Save to disk %d", saveWhat); + + // do not try to save anything if power level is not safe. In many cases flash will be lock-protected + // and all writes will fail anyway. Device should be sleeping at this point anyway. + if(!powerHAL_isPowerLevelSafe()){ + LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level."); + return false; + } + bool success = saveToDiskNoRetry(saveWhat); if (!success) { diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 4894f34aaec..fa0a68704eb 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -17,6 +17,7 @@ #include #include // #include +#include #include "NodeDB.h" #include "PowerMon.h" #include "error.h" @@ -57,9 +58,6 @@ bool powerHAL_isVBUSConnected() { bool powerHAL_isPowerLevelSafe() { - // TODO: DEBUGGING ONLY, DO NOT MERGE TO PROD - return false; - if(NRF_POWER->EVENTS_POFWARN) return false; return true; @@ -222,8 +220,14 @@ extern "C" void lfs_assert(const char *reason) // TODO: this will/can crash CPU if bluetooth stack is not compiled in or bluetooth is not initialized // (regardless if enabled or disabled) - as there is no live SoftDevice stack // implement "safe" functions detecting softdevice stack state and using proper method to set registers - if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { - NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + + // do not set GPREGRET if POFWARN is triggered because it means lfs_assert reports flash undervoltage protection + // and not data corruption. Reboot is fine as boot procedure will wait until power level is safe again + + if(powerHAL_isPowerLevelSafe()){ + if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { + NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + } } // TODO: this should not be done when SoftDevice is enabled as device will not boot back on soft reset From 276ce91a0ae2e13ca37d70588315b6c33ece3658 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Wed, 17 Dec 2025 17:16:26 +0000 Subject: [PATCH 09/12] abc --- src/Power.cpp | 18 ++++------------ src/platform/nrf52/architecture.h | 15 +++++++++++++ src/platform/nrf52/main-nrf52.cpp | 35 +++++++++++++++++-------------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 062bc3505c3..ea2ace7da26 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -171,22 +171,12 @@ Power *power; using namespace meshtastic; -#ifndef AREF_VOLTAGE -#if defined(ARCH_NRF52) -/* - * Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4, - * 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels. - * - * External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning - * VDD/4, VDD/2 or VDD for the ADC levels. - * - * Default settings are internal reference with 1/6 gain (GND..3.6V ADC range) - */ -#define AREF_VOLTAGE 3.6 -#else + +// NRF52 has AREF_VOLTAGE defined in architecture.h but +// make sure it's included +#if !defined(AREF_VOLTAGE) && defined(ARCH_NRF52) #define AREF_VOLTAGE 3.3 #endif -#endif /** * If this board has a battery level sensor, set this to a valid implementation diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 1568e179044..aa5128c1b70 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -5,6 +5,21 @@ // // defaults for NRF52 architecture // + +/* + * Internal Reference is +/-0.6V, with an adjustable gain of 1/6, 1/5, 1/4, + * 1/3, 1/2 or 1, meaning 3.6, 3.0, 2.4, 1.8, 1.2 or 0.6V for the ADC levels. + * + * External Reference is VDD/4, with an adjustable gain of 1, 2 or 4, meaning + * VDD/4, VDD/2 or VDD for the ADC levels. + * + * Default settings are internal reference with 1/6 gain (GND..3.6V ADC range) + * Some variants overwrite it. + */ +#ifndef AREF_VOLTAGE +#define AREF_VOLTAGE 3.6 +#endif + #ifndef HAS_BLUETOOTH #define HAS_BLUETOOTH 1 #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index fa0a68704eb..8537d1dcc8d 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -31,6 +31,10 @@ #include "BQ25713.h" #endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD + #define SAFE_VDD_VOLTAGE_THRESHOLD 2.7 +#endif + // Weak empty variant initialization function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); @@ -58,30 +62,29 @@ bool powerHAL_isVBUSConnected() { bool powerHAL_isPowerLevelSafe() { - if(NRF_POWER->EVENTS_POFWARN) - return false; + uint16_t vddVoltage = analogReadVDD(); + + // some variants use AREF_VOLTAGE as 3.0 + // but this is fine as we hunt for VDD values less than 3V + // otherwise either set here the reference for each measure and make sure + // same is done for battery reading (which long term should be implemented here as HAL function) + return true; } void powerHAL_platformInit(){ + // enable POF power failure comparator. It will prevent writing to NVMC flash when supply voltage is too low. + // Set to 2.4V as last resort - powerHAL_isPowerLevelSafe uses different method and should manage proper node behaviour on its own. - // POF protection prevents flash memory writes when VDD voltage is 2.7V or less to avoid memory corruption - // In this setting voltage is checked both against VDD and VDDH so particular board - // wiring does not matter. - // It must be set to value greater than 2.5V because 2.5V is minimum voltage that can be supplied at VDDH - // and it borders at cutoff voltage for li-ion battery protectors. - // Originally it was set at 2.4V and it did cause a lot of flash memory corruptions when battery was around 2.5-2.6V - - // Many NRF52 boards have decent LDO which goes down to 2V - // In the future - boards with crappy LDO can be set to prevent memory corruption at higher voltage - like 3V - // using custom variant definition. Remember that above 2.8V you need to monitor VDDH voltage threshold using different - // registers + // @phaseloop note: during my tests - setting threshold to 2.7V would still trigger POFWARN only at 2.5V fed to VDDH (??). According to datasheet, + // below 2.8V setting both VDD and VDDH level are covered by this register. So feeding 2.5V to VDDH would result at 2.2V at VDD (because LDO voltage drop) + // which is even weirder. Anyway we don't rely much on POFWARN. - // SoftDevice is only enabled by Adafruit Bluetooth library (Bluefruit) and there is no good way to change it or integrate with it. - // This is started at boot before bluetooth so we use raw registers instead of sd_power* + // POFWARN is pretty useless for node power management because it triggers only once and clearing this event will not re-trigger it again + // until voltage rises to safe level and drops again. So we will use SAADC routed to VDD to read safely voltage. - NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V27 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); + NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V24 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); } From 699cf5dc6ac74cc31f3f9911c65e5d2a9ea4efe3 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Thu, 18 Dec 2025 15:39:06 +0000 Subject: [PATCH 10/12] aab --- src/Power.cpp | 167 ++++++++++++++++++------------ src/main.cpp | 32 +++--- src/platform/nrf52/architecture.h | 4 + src/platform/nrf52/main-nrf52.cpp | 55 +++++----- 4 files changed, 148 insertions(+), 110 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index ea2ace7da26..7f0bd39cb98 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1,11 +1,14 @@ /** * @file Power.cpp - * @brief This file contains the implementation of the Power class, which is responsible for managing power-related functionality - * of the device. It includes battery level sensing, power management unit (PMU) control, and power state machine management. The - * Power class is used by the main device class to manage power-related functionality. + * @brief This file contains the implementation of the Power class, which is + * responsible for managing power-related functionality of the device. It + * includes battery level sensing, power management unit (PMU) control, and + * power state machine management. The Power class is used by the main device + * class to manage power-related functionality. * - * The file also includes implementations of various battery level sensors, such as the AnalogBatteryLevel class, which assumes - * the battery voltage is attached via a voltage-divider to an analog input. + * The file also includes implementations of various battery level sensors, such + * as the AnalogBatteryLevel class, which assumes the battery voltage is + * attached via a voltage-divider to an analog input. * * This file is part of the Meshtastic project. * For more information, see: https://meshtastic.org/ @@ -13,12 +16,12 @@ #include "power.h" #include "NodeDB.h" #include "PowerFSM.h" -#include "power/PowerHAL.h" #include "Throttle.h" #include "buzz/buzz.h" #include "configuration.h" #include "main.h" #include "meshUtils.h" +#include "power/PowerHAL.h" #include "sleep.h" #if defined(ARCH_PORTDUINO) @@ -171,10 +174,10 @@ Power *power; using namespace meshtastic; - // NRF52 has AREF_VOLTAGE defined in architecture.h but -// make sure it's included -#if !defined(AREF_VOLTAGE) && defined(ARCH_NRF52) +// make sure it's included. If something is wrong with NRF52 +// definition - compilation will fail on missing definition +#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52) #define AREF_VOLTAGE 3.3 #endif @@ -223,7 +226,8 @@ static void battery_adcDisable() #endif /** - * A simple battery level sensor that assumes the battery voltage is attached via a voltage-divider to an analog input + * A simple battery level sensor that assumes the battery voltage is attached + * via a voltage-divider to an analog input */ class AnalogBatteryLevel : public HasBatteryLevel { @@ -301,7 +305,8 @@ class AnalogBatteryLevel : public HasBatteryLevel #ifndef BATTERY_SENSE_SAMPLES #define BATTERY_SENSE_SAMPLES \ - 15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment. + 15 // Set the number of samples, it has an effect of increasing sensitivity in + // complex electromagnetic environment. #endif #ifdef BATTERY_PIN @@ -331,7 +336,8 @@ class AnalogBatteryLevel : public HasBatteryLevel battery_adcDisable(); if (!initial_read_done) { - // Flush the smoothing filter with an ADC reading, if the reading is plausibly correct + // Flush the smoothing filter with an ADC reading, if the reading is + // plausibly correct if (scaled > last_read_value) last_read_value = scaled; initial_read_done = true; @@ -340,8 +346,8 @@ class AnalogBatteryLevel : public HasBatteryLevel last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF } - // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) - // (last_read_value)); + // LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u", + // BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value)); } return last_read_value; #endif // BATTERY_PIN @@ -410,7 +416,8 @@ class AnalogBatteryLevel : public HasBatteryLevel /** * return true if there is a battery installed in this unit */ - // if we have a integrated device with a battery, we can assume that the battery is always connected + // if we have a integrated device with a battery, we can assume that the + // battery is always connected #ifdef BATTERY_IMMUTABLE virtual bool isBatteryConnect() override { return true; } #elif defined(ADC_V) @@ -431,10 +438,10 @@ class AnalogBatteryLevel : public HasBatteryLevel virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; } #endif - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power - /// On some boards we don't have the power management chip (like AXPxxxx) - /// so we use EXT_PWR_DETECT GPIO pin to detect external power source + /// If we see a battery voltage higher than physics allows - assume charger is + /// pumping in power On some boards we don't have the power management chip + /// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power + /// source virtual bool isVbusIn() override { #ifdef EXT_PWR_DETECT @@ -477,8 +484,9 @@ class AnalogBatteryLevel : public HasBatteryLevel #else #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION) if (hasINA()) { - // get current flow from INA sensor - negative value means power flowing into the battery - // default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT RESISTOR <--> INA_VIN- <--> LOAD + // get current flow from INA sensor - negative value means power flowing + // into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT + // RESISTOR <--> INA_VIN- <--> LOAD LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address); #if defined(INA_CHARGING_DETECTION_INVERT) return getINACurrent() > 0; @@ -494,8 +502,8 @@ class AnalogBatteryLevel : public HasBatteryLevel } private: - /// If we see a battery voltage higher than physics allows - assume charger is pumping - /// in power + /// If we see a battery voltage higher than physics allows - assume charger is + /// pumping in power /// For heltecs with no battery connected, the measured voltage is 2204, so // need to be higher than that, in this case is 2500mV (3000-500) @@ -504,7 +512,8 @@ class AnalogBatteryLevel : public HasBatteryLevel const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS; // Start value from minimum voltage for the filter to not start from 0 // that could trigger some events. - // This value is over-written by the first ADC reading, it the voltage seems reasonable. + // This value is over-written by the first ADC reading, it the voltage seems + // reasonable. bool initial_read_done = false; float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS); uint32_t last_read_time_ms = 0; @@ -646,7 +655,8 @@ bool Power::analogInit() #ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3 else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) { - LOG_INFO("ADC config based on Two Point values and fitting curve coefficients stored in eFuse"); + LOG_INFO("ADC config based on Two Point values and fitting curve " + "coefficients stored in eFuse"); } #endif else { @@ -759,7 +769,8 @@ void Power::reboot() HAL_NVIC_SystemReset(); #else rebootAtMsec = -1; - LOG_WARN("FIXME implement reboot for this platform. Note that some settings require a restart to be applied"); + LOG_WARN("FIXME implement reboot for this platform. Note that some settings " + "require a restart to be applied"); #endif } @@ -769,9 +780,12 @@ void Power::shutdown() #if HAS_SCREEN if (screen) { #ifdef T_DECK_PRO - screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", + 0); // T-Deck Pro has no power button #elif defined(USE_EINK) - screen->showSimpleBanner("Shutting Down...", 2250); // dismiss after 3 seconds to avoid the banner on the sleep screen + screen->showSimpleBanner("Shutting Down...", + 2250); // dismiss after 3 seconds to avoid the + // banner on the sleep screen #else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen #endif @@ -808,7 +822,8 @@ void Power::readPowerStatus() int32_t batteryVoltageMv = -1; // Assume unknown int8_t batteryChargePercent = -1; OptionalBool usbPowered = OptUnknown; - OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM code doesn't run every time + OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM + // code doesn't run every time OptionalBool isChargingNow = OptUnknown; if (batteryLevel) { @@ -821,9 +836,10 @@ void Power::readPowerStatus() if (batteryLevel->getBatteryPercent() >= 0) { batteryChargePercent = batteryLevel->getBatteryPercent(); } else { - // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error - // In that case, we compute an estimate of the charge percent based on open circuit voltage table defined - // in power.h + // If the AXP192 returns a percentage less than 0, the feature is either + // not supported or there is an error In that case, we compute an + // estimate of the charge percent based on open circuit voltage table + // defined in power.h batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) / ((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))), 0, 100); @@ -831,12 +847,12 @@ void Power::readPowerStatus() } } -// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way better instead to make a Nrf52IsUsbPowered subclass -// (which shares a superclass with the BatteryLevel stuff) -// that just provides a few methods. But in the interest of fixing this bug I'm going to follow current -// practice. -#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates the power states. Takes 20 seconds or so to detect - // changes. +// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way +// better instead to make a Nrf52IsUsbPowered subclass (which shares a +// superclass with the BatteryLevel stuff) that just provides a few methods. But +// in the interest of fixing this bug I'm going to follow current practice. +#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates + // the power states. Takes 20 seconds or so to detect changes. nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get(); // LOG_DEBUG("NRF Power %d", nrf_usb_state); @@ -910,8 +926,9 @@ void Power::readPowerStatus() #endif - // If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in - // a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. + // If we have a battery at all and it is less than 0%, force deep sleep if we + // have more than 10 low readings in a row. NOTE: min LiIon/LiPo voltage + // is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough. // if (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) { @@ -933,8 +950,8 @@ int32_t Power::runOnce() readPowerStatus(); #ifdef HAS_PMU - // WE no longer use the IRQ line to wake the CPU (due to false wakes from sleep), but we do poll - // the IRQ status by reading the registers over I2C + // WE no longer use the IRQ line to wake the CPU (due to false wakes from + // sleep), but we do poll the IRQ status by reading the registers over I2C if (PMU) { PMU->getIrqStatus(); @@ -976,7 +993,8 @@ int32_t Power::runOnce() PMU->clearIrqStatus(); } #endif - // Only read once every 20 seconds once the power status for the app has been initialized + // Only read once every 20 seconds once the power status for the app has been + // initialized return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME; } @@ -984,10 +1002,12 @@ int32_t Power::runOnce() * Init the power manager chip * * axp192 power - DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the - axp192 share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this - on!) LDO1 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of - days), can not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS + DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose + comms to the axp192 because the OLED and the axp192 share the same i2c bus, + instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> + ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the + tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can + not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS * */ bool Power::axpChipInit() @@ -1032,9 +1052,10 @@ bool Power::axpChipInit() if (!PMU) { /* - * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will be called at the same time. - * In order not to affect other devices, if the initialization of the PMU fails, Wire needs to be re-initialized once, - * if there are multiple devices sharing the bus. + * In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will + * be called at the same time. In order not to affect other devices, if the + * initialization of the PMU fails, Wire needs to be re-initialized once, if + * there are multiple devices sharing the bus. * * */ #ifndef PMU_USE_WIRE1 w->begin(I2C_SDA, I2C_SCL); @@ -1051,8 +1072,8 @@ bool Power::axpChipInit() PMU->enablePowerOutput(XPOWERS_LDO2); // oled module power channel, - // disable it will cause abnormal communication between boot and AXP power supply, - // do not turn it off + // disable it will cause abnormal communication between boot and AXP power + // supply, do not turn it off PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); // enable oled power PMU->enablePowerOutput(XPOWERS_DCDC1); @@ -1079,7 +1100,8 @@ bool Power::axpChipInit() PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); } else if (PMU->getChipModel() == XPOWERS_AXP2101) { - /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it uses an AXP2101 power chip*/ + /*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it + * uses an AXP2101 power chip*/ if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { // Unuse power channel PMU->disablePowerOutput(XPOWERS_DCDC2); @@ -1114,8 +1136,8 @@ bool Power::axpChipInit() // t-beam s3 core /** * gnss module power channel - * The default ALDO4 is off, you need to turn on the GNSS power first, otherwise it will be invalid during - * initialization + * The default ALDO4 is off, you need to turn on the GNSS power first, + * otherwise it will be invalid during initialization */ PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300); PMU->enablePowerOutput(XPOWERS_ALDO4); @@ -1165,7 +1187,8 @@ bool Power::axpChipInit() // disable all axp chip interrupt PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); - // Set the constant current charging current of AXP2101, temporarily use 500mA by default + // Set the constant current charging current of AXP2101, temporarily use + // 500mA by default PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); // Set up the charging voltage @@ -1231,11 +1254,12 @@ bool Power::axpChipInit() PMU->getPowerChannelVoltage(XPOWERS_BLDO2)); } -// We can safely ignore this approach for most (or all) boards because MCU turned off -// earlier than battery discharged to 2.6V. +// We can safely ignore this approach for most (or all) boards because MCU +// turned off earlier than battery discharged to 2.6V. // -// Unfortanly for now we can't use this killswitch for RAK4630-based boards because they have a bug with -// battery voltage measurement. Probably it sometimes drops to low values. +// Unfortanly for now we can't use this killswitch for RAK4630-based boards +// because they have a bug with battery voltage measurement. Probably it +// sometimes drops to low values. #ifndef RAK4630 // Set PMU shutdown voltage at 2.6V to maximize battery utilization PMU->setSysPowerDownVoltage(2600); @@ -1254,10 +1278,12 @@ bool Power::axpChipInit() attachInterrupt( PMU_IRQ, [] { pmu_irq = true; }, FALLING); - // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ because it occurs repeatedly while there is - // no battery also it could cause inadvertent waking from light sleep just because the battery filled - // we don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while no battery installed - // we don't look at AXPXXX_VBUS_REMOVED_IRQ because we don't have anything hooked to vbus + // we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ + // because it occurs repeatedly while there is no battery also it could cause + // inadvertent waking from light sleep just because the battery filled we + // don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while + // no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we + // don't have anything hooked to vbus PMU->enableIRQ(pmuIrqMask); PMU->clearIrqStatus(); @@ -1373,8 +1399,8 @@ class LipoCharger : public HasBatteryLevel bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); - // Set the minimum operating voltage. Below this voltage, the PPM will protect - // PPM->setSysPowerDownVoltage(3100); + // Set the minimum operating voltage. Below this voltage, the PPM will + // protect PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA // PPM->setInputCurrentLimit(800); @@ -1397,7 +1423,8 @@ class LipoCharger : public HasBatteryLevel PPM->enableMeasure(); // Turn on charging function - // If there is no battery connected, do not turn on the charging function + // If there is no battery connected, do not turn on the charging + // function PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); @@ -1432,7 +1459,8 @@ class LipoCharger : public HasBatteryLevel virtual int getBatteryPercent() override { return -1; - // return bq->getChargePercent(); // don't use BQ27220 for battery percent, it is not calibrated + // return bq->getChargePercent(); // don't use BQ27220 for battery percent, + // it is not calibrated } /** @@ -1554,7 +1582,8 @@ bool Power::meshSolarInit() #else /** - * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel + * The meshSolar battery level sensor is unavailable - default to + * AnalogBatteryLevel */ bool Power::meshSolarInit() { diff --git a/src/main.cpp b/src/main.cpp index a67d06bd1ef..090827421b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,12 +5,12 @@ #include "MeshRadio.h" #include "MeshService.h" #include "NodeDB.h" -#include "power/PowerHAL.h" #include "PowerFSM.h" #include "PowerMon.h" #include "ReliableRouter.h" #include "airtime.h" #include "buzz.h" +#include "power/PowerHAL.h" #include "FSCommon.h" #include "Led.h" @@ -289,7 +289,6 @@ __attribute__((weak, noinline)) bool loopCanSleep() void lateInitVariant() __attribute__((weak)); void lateInitVariant() {} - // NRF52 (and probably other platforms) can report when system is in power failure mode // (eg. too low battery voltage) and operating it is unsafe (data corruption, bootloops, etc). // For example NRF52 will prevent any flash writes in that case automatically @@ -301,37 +300,35 @@ void lateInitVariant() {} // wait until power level is safe to continue booting (to avoid bootloops) // blink user led in 3 flashes sequence to indicate what is happening -void waitUntilPowerLevelSafe(){ +void waitUntilPowerLevelSafe() +{ // TODO: do not use delay but RTC/IRQ whatever so we don't burn // energy which is already scarce - #ifdef LED_PIN - pinMode(LED_PIN, OUTPUT); - #endif +#ifdef LED_PIN + pinMode(LED_PIN, OUTPUT); +#endif - while(powerHAL_isPowerLevelSafe() == false){ + while (powerHAL_isPowerLevelSafe() == false) { - #ifdef LED_PIN +#ifdef LED_PIN // 3x: blink for 500 ms, pause for 500 ms - for(int i=0;i<3;i++){ - digitalWrite(LED_PIN, LED_STATE_ON); - delay(300); - digitalWrite(LED_PIN, LED_STATE_OFF); - delay(300); + for (int i = 0; i < 3; i++) { + digitalWrite(LED_PIN, LED_STATE_ON); + delay(300); + digitalWrite(LED_PIN, LED_STATE_OFF); + delay(300); } - #endif +#endif // sleep for 2s delay(2000); - } - } - /** * Print info as a structured log message (for automated log processing) */ @@ -491,6 +488,7 @@ void setup() serialSinceMsec = millis(); LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); + powerHAL_isPowerLevelSafe(); #if defined(DEBUG_MUTE) && defined(DEBUG_PORT) DEBUG_PORT.printf("\r\n\r\n//\\ E S H T /\\ S T / C\r\n"); diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index aa5128c1b70..5298393f0ec 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -20,6 +20,10 @@ #define AREF_VOLTAGE 3.6 #endif +#ifndef BATTERY_SENSE_RESOLUTION_BITS +#define BATTERY_SENSE_RESOLUTION_BITS 10 +#endif + #ifndef HAS_BLUETOOTH #define HAS_BLUETOOTH 1 #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 8537d1dcc8d..1d91c444b27 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -17,13 +17,13 @@ #include #include // #include -#include #include "NodeDB.h" #include "PowerMon.h" #include "error.h" #include "main.h" #include "meshUtils.h" #include "power.h" +#include #include @@ -32,7 +32,7 @@ #endif #ifndef SAFE_VDD_VOLTAGE_THRESHOLD - #define SAFE_VDD_VOLTAGE_THRESHOLD 2.7 +#define SAFE_VDD_VOLTAGE_THRESHOLD 2.7 #endif // Weak empty variant initialization function. @@ -56,38 +56,46 @@ static inline void debugger_break(void) } // PowerHAL NRF52 specific function implementations -bool powerHAL_isVBUSConnected() { - return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; +bool powerHAL_isVBUSConnected() +{ + return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; } -bool powerHAL_isPowerLevelSafe() { - - uint16_t vddVoltage = analogReadVDD(); - +bool powerHAL_isPowerLevelSafe() +{ // some variants use AREF_VOLTAGE as 3.0 // but this is fine as we hunt for VDD values less than 3V // otherwise either set here the reference for each measure and make sure // same is done for battery reading (which long term should be implemented here as HAL function) - return true; -} + // we use the same values as regular battery read so there is no conflict on SAADC + analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); - void powerHAL_platformInit(){ + uint16_t vddADCRead = analogReadVDD(); + voltage = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; + LOG_INFO("VDD VOLTAGE: %f", voltage); - // enable POF power failure comparator. It will prevent writing to NVMC flash when supply voltage is too low. - // Set to 2.4V as last resort - powerHAL_isPowerLevelSafe uses different method and should manage proper node behaviour on its own. + return true; +} - // @phaseloop note: during my tests - setting threshold to 2.7V would still trigger POFWARN only at 2.5V fed to VDDH (??). According to datasheet, - // below 2.8V setting both VDD and VDDH level are covered by this register. So feeding 2.5V to VDDH would result at 2.2V at VDD (because LDO voltage drop) - // which is even weirder. Anyway we don't rely much on POFWARN. +void powerHAL_platformInit() +{ - // POFWARN is pretty useless for node power management because it triggers only once and clearing this event will not re-trigger it again - // until voltage rises to safe level and drops again. So we will use SAADC routed to VDD to read safely voltage. + // Enable POF power failure comparator. It will prevent writing to NVMC flash when supply voltage is too low. + // Set to 2.4V as last resort - powerHAL_isPowerLevelSafe uses different method and should manage proper node behaviour on its + // own. - NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V24 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); + // @phaseloop note: during my tests - setting threshold to 2.7V would still trigger POFWARN only at 2.5V fed to VDDH (??). + // According to datasheet, below 2.8V setting both VDD and VDDH level are covered by this register. So feeding 2.5V to VDDH + // would result at 2.2V at VDD (because LDO voltage drop) which is even weirder. Anyway we don't rely much on POFWARN. - } + // POFWARN is pretty useless for node power management because it triggers only once and clearing this event will not + // re-trigger it again until voltage rises to safe level and drops again. So we will use SAADC routed to VDD to read safely + // voltage. + NRF_POWER->POFCON = + ((POWER_POFCON_THRESHOLD_V24 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); +} bool loopCanSleep() { @@ -117,7 +125,6 @@ void getMacAddr(uint8_t *dmac) dmac[0] = src[5] | 0xc0; // MSB high two bits get set elsewhere in the bluetooth stack } - #if !MESHTASTIC_EXCLUDE_BLUETOOTH void setBluetoothEnable(bool enable) { @@ -219,7 +226,6 @@ extern "C" void lfs_assert(const char *reason) // Try setting GPREGRET with the SoftDevice first. If that fails (perhaps because the SD hasn't been initialize yet) then set // NRF_POWER->GPREGRET directly. - // TODO: this will/can crash CPU if bluetooth stack is not compiled in or bluetooth is not initialized // (regardless if enabled or disabled) - as there is no live SoftDevice stack // implement "safe" functions detecting softdevice stack state and using proper method to set registers @@ -227,8 +233,9 @@ extern "C" void lfs_assert(const char *reason) // do not set GPREGRET if POFWARN is triggered because it means lfs_assert reports flash undervoltage protection // and not data corruption. Reboot is fine as boot procedure will wait until power level is safe again - if(powerHAL_isPowerLevelSafe()){ - if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { + if (powerHAL_isPowerLevelSafe()) { + if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && + sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; } } From 3a2149d49608aae5000bee390d5d0c81829959cf Mon Sep 17 00:00:00 2001 From: phaseloop Date: Fri, 19 Dec 2025 09:23:45 +0000 Subject: [PATCH 11/12] move analog reference --- src/Power.cpp | 8 +------- src/platform/nrf52/main-nrf52.cpp | 9 ++++++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 7f0bd39cb98..47a47d784e4 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -664,13 +664,7 @@ bool Power::analogInit() } #endif // ARCH_ESP32 -#ifdef ARCH_NRF52 -#ifdef VBAT_AR_INTERNAL - analogReference(VBAT_AR_INTERNAL); -#else - analogReference(AR_INTERNAL); // 3.6V -#endif -#endif // ARCH_NRF52 + // NRF52 ADC init moved to powerHAL_init in nrf52 platform #ifndef ARCH_ESP32 analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 1d91c444b27..30416a813f9 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -72,7 +72,7 @@ bool powerHAL_isPowerLevelSafe() analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); uint16_t vddADCRead = analogReadVDD(); - voltage = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; + float voltage = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; LOG_INFO("VDD VOLTAGE: %f", voltage); return true; @@ -95,6 +95,13 @@ void powerHAL_platformInit() NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V24 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); + + // remember to always match VBAT_AR_INTERNAL with AREF_VALUE in varian definition file +#ifdef VBAT_AR_INTERNAL + analogReference(VBAT_AR_INTERNAL); +#else + analogReference(AR_INTERNAL); // 3.6V +#endif } bool loopCanSleep() From 46d7d6293c2eb0f6abc25228b8000df819090e07 Mon Sep 17 00:00:00 2001 From: phaseloop Date: Mon, 22 Dec 2025 20:28:53 +0100 Subject: [PATCH 12/12] aaa --- src/main.cpp | 3 +-- src/platform/nrf52/main-nrf52.cpp | 33 ++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index ae787e41a63..03bf6039c2f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -495,8 +495,7 @@ void setup() serialSinceMsec = millis(); LOG_INFO("\n\n//\\ E S H T /\\ S T / C\n"); - powerHAL_isPowerLevelSafe(); - + #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #ifndef SENSECAP_INDICATOR // use PSRAM for malloc calls > 256 bytes diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 46bb7270a81..f3b16ff44c0 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -35,6 +35,10 @@ #define SAFE_VDD_VOLTAGE_THRESHOLD 2.7 #endif +#ifndef SAFE_VDD_VOLTAGE_THRESHOLD_HIST +#define SAFE_VDD_VOLTAGE_THRESHOLD_HOST 0.2 +#endif + // Weak empty variant initialization function. // May be redefined by variant files. void variant_shutdown() __attribute__((weak)); @@ -63,17 +67,10 @@ bool powerHAL_isVBUSConnected() bool powerHAL_isPowerLevelSafe() { - // some variants use AREF_VOLTAGE as 3.0 - // but this is fine as we hunt for VDD values less than 3V - // otherwise either set here the reference for each measure and make sure - // same is done for battery reading (which long term should be implemented here as HAL function) - // we use the same values as regular battery read so there is no conflict on SAADC - analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); + uint16_t threshhold = SAFE_VDD_VOLTAGE_THRESHOLD * 1000; // convert V to mV + - uint16_t vddADCRead = analogReadVDD(); - float voltage = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; - LOG_INFO("VDD VOLTAGE: %f", voltage); return true; } @@ -104,6 +101,22 @@ void powerHAL_platformInit() #endif } +// get VDD voltage (in millivolts) +uint16_t getVDDVoltage() +{ + // some variants use AREF_VOLTAGE as 3.0 + // but this is fine as we usually hunt for VDD values less than 3V + // otherwise either set here the reference for each measure and make sure + // same is done for battery reading (which long term should be implemented here as HAL function) + + // we use the same values as regular battery read so there is no conflict on SAADC + analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS); + + uint16_t vddADCRead = analogReadVDD(); + float voltage = ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * vddADCRead; + return voltage; +} + bool loopCanSleep() { // turn off sleep only while connected via USB @@ -287,6 +300,8 @@ void nrf52Loop() checkSDEvents(); reportLittleFSCorruptionOnce(); + LOG_INFO("VDD_VOLTAGE: %f", getVDDVoltage()); + } #ifdef USE_SEMIHOSTING