From 0065e2dee1b109d7fbabb2b3cc690455681435af Mon Sep 17 00:00:00 2001 From: Dave Koberstein Date: Wed, 28 Oct 2026 08:38:11 -0700 Subject: [PATCH] rak4631: implement button power-off and fix immediate wakeup (closes #1411) Add hardware button power management for RAK4631 and Muziworks R1 builds, and resolve immediate wakeup from SYSTEMOFF sleep (supersedes #2701). Key Changes: - Button Power-Down: Monitor BOARD_BUTTON_PIN (WB_A1 / PIN_USER_BTN_ANA) in RAK4631Board::loop(). A 3-second long press illuminates the green LED (PIN_LED1) as power-down confirmation (leaving the blue LED for BLE status) and initiates deep sleep upon button release. - Debounce & Release Settle: Wait for stable button release before power down, and enforce a 50ms settle window before arming SENSE LOW. - Radio Isolation: Detach RadioLib interrupts (DIO1, BUSY) and reset SX1262 pins to default prior to disabling SX126X_POWER_EN. This prevents the collapsing power rail from triggering a false GPIO DETECT interrupt. - BLE Graceful Disconnect: Disconnect active BLE connections and stop advertising before entering sleep so companion apps disconnect cleanly. - LATCH Clearing: Disable SENSE on non-button pins and clear NRF_P0/P1 LATCH registers right before entering SYSTEMOFF, preventing immediate auto-wake. Hardware Testing: - Verified on a physical Muziworks R1 unit (RAK4631 + WisBlock base with hardware button on WB_A1 / Pin 31). - Confirmed normal BLE operation, 3-second long-press detection with green LED indicator, graceful mobile app disconnect, and sustained SYSTEMOFF deep sleep with all LEDs off (no spurious or immediate wakeups). - Confirmed wake from sleep via reset button back to full operational state. Fixes https://github.com/meshcore-dev/MeshCore/issues/1411 Supersedes https://github.com/meshcore-dev/MeshCore/pull/2701 --- variants/rak4631/RAK4631Board.cpp | 187 ++++++++++++++++++++++++++++-- variants/rak4631/RAK4631Board.h | 4 + variants/rak4631/variant.h | 1 + 3 files changed, 184 insertions(+), 8 deletions(-) diff --git a/variants/rak4631/RAK4631Board.cpp b/variants/rak4631/RAK4631Board.cpp index 1b5698d0ebd..b3211918b13 100644 --- a/variants/rak4631/RAK4631Board.cpp +++ b/variants/rak4631/RAK4631Board.cpp @@ -4,6 +4,10 @@ #include "RAK4631Board.h" +#if __has_include() +#include +#endif + #ifdef ETHERNET_ENABLED // Drive WB_IO2 HIGH as early as possible using direct register access. // WB_IO2 (P1.02, Arduino pin 34) controls the WisBlock slot power switch. @@ -25,31 +29,182 @@ const PowerMgtConfig power_config = { .voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK }; +#if defined(PIN_USER_BTN_ANA) && (PIN_USER_BTN_ANA >= 0) + #define BOARD_BUTTON_PIN PIN_USER_BTN_ANA +#elif defined(WB_A1) + #define BOARD_BUTTON_PIN WB_A1 +#elif defined(PIN_USER_BTN) && (PIN_USER_BTN >= 0) + #define BOARD_BUTTON_PIN PIN_USER_BTN +#endif + +static bool isButtonPressed() { +#if defined(PIN_USER_BTN_ANA) && (PIN_USER_BTN_ANA >= 0) + return analogRead(PIN_USER_BTN_ANA) < 20; +#elif defined(BOARD_BUTTON_PIN) + return digitalRead(BOARD_BUTTON_PIN) == LOW; +#else + return false; +#endif +} + +void RAK4631Board::powerOff() { +#if defined(BOARD_BUTTON_PIN) + // Turn on LED to indicate long press was recognized and signal to release + #if defined(PIN_LED1) + digitalWrite(PIN_LED1, HIGH); + #elif defined(PIN_LED2) + digitalWrite(PIN_LED2, HIGH); + #endif + + // Wait until the user physically releases the button and it remains stably released + uint32_t stable_start = millis(); + while (millis() - stable_start < 100) { + if (isButtonPressed()) { + stable_start = millis(); + } + delay(5); + } + + // Turn off LEDs before shutdown + #if defined(PIN_LED1) + digitalWrite(PIN_LED1, LOW); + #endif + #if defined(PIN_LED2) + digitalWrite(PIN_LED2, LOW); + #endif +#endif + +#ifdef NRF52_POWER_MANAGEMENT + initiateShutdown(SHUTDOWN_REASON_USER); +#else + NRF52BoardDCDC::powerOff(); +#endif +} + void RAK4631Board::initiateShutdown(uint8_t reason) { +#if __has_include() + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + Bluefruit.autoConnLed(false); + uint16_t conn_hdl = Bluefruit.connHandle(); + if (conn_hdl != BLE_CONN_HANDLE_INVALID) { + Bluefruit.disconnect(conn_hdl); + delay(50); + } + Bluefruit.Advertising.stop(); + } +#endif + + // Disconnect LoRa pins and detach RadioLib interrupts before powering down + // Otherwise, the collapsing power rail on the SX1262 triggers a false DETECT wakeup + detachInterrupt(digitalPinToInterrupt(P_LORA_DIO_1)); +#ifdef P_LORA_BUSY + detachInterrupt(digitalPinToInterrupt(P_LORA_BUSY)); + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_BUSY]); +#endif +#ifdef P_LORA_RESET + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_RESET]); +#endif +#ifdef P_LORA_NSS + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_NSS]); +#endif + nrf_gpio_cfg_default(g_ADigitalPinMap[P_LORA_DIO_1]); + // Disable LoRa module power before shutdown digitalWrite(SX126X_POWER_EN, LOW); +#if defined(BOARD_BUTTON_PIN) + // Ensure pullup is engaged and line is stably HIGH before arming SENSE + pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP); + uint32_t settle_start = millis(); + while (millis() - settle_start < 50) { + if (digitalRead(BOARD_BUTTON_PIN) == LOW) { + settle_start = millis(); + } + delay(5); + } + + // Ensure no other pin has SENSE enabled + for (uint32_t p = 0; p < 32; p++) { + if (p != g_ADigitalPinMap[BOARD_BUTTON_PIN]) { + nrf_gpio_cfg_sense_set(p, NRF_GPIO_PIN_NOSENSE); + } + } +#if defined(NRF_P1) + for (uint32_t p = 32; p < 48; p++) { + if (p != g_ADigitalPinMap[BOARD_BUTTON_PIN]) { + nrf_gpio_cfg_sense_set(p, NRF_GPIO_PIN_NOSENSE); + } + } +#endif + + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[BOARD_BUTTON_PIN], NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); +#endif + if (reason == SHUTDOWN_REASON_LOW_VOLTAGE || reason == SHUTDOWN_REASON_BOOT_PROTECT) { configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); } - enterSystemOff(reason); + // Record shutdown reason in GPREGRET2 + uint8_t sd_on = 0; + sd_softdevice_is_enabled(&sd_on); + if (sd_on) { + sd_power_gpregret_clr(1, 0xFF); + sd_power_gpregret_set(1, reason); + } else { + NRF_POWER->GPREGRET2 = reason; + } + + Serial.flush(); + delay(50); + + // Ensure all LEDs are explicitly powered off and disconnected right before entering sleep +#if defined(PIN_LED1) + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED1, INPUT); + nrf_gpio_pin_clear(g_ADigitalPinMap[PIN_LED1]); + nrf_gpio_cfg_default(g_ADigitalPinMap[PIN_LED1]); +#endif +#if defined(PIN_LED2) + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, INPUT); + nrf_gpio_pin_clear(g_ADigitalPinMap[PIN_LED2]); + nrf_gpio_cfg_default(g_ADigitalPinMap[PIN_LED2]); +#endif + +#if defined(BOARD_BUTTON_PIN) + // Ensure the button is unpressed before clearing latch + while (digitalRead(BOARD_BUTTON_PIN) == LOW) { + delay(10); + } +#endif + + // Clear any latched GPIO DETECT events right before sleep + NRF_P0->LATCH = 0xFFFFFFFF; +#if defined(NRF_P1) + NRF_P1->LATCH = 0xFFFFFFFF; +#endif + + if (sd_on) { + sd_power_system_off(); + } + // Force hardware system off directly + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + while (1) { + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + } } #endif // NRF52_POWER_MANAGEMENT void RAK4631Board::begin() { NRF52BoardDCDC::begin(); pinMode(PIN_VBAT_READ, INPUT); -#ifdef PIN_USER_BTN - pinMode(PIN_USER_BTN, INPUT_PULLUP); -#endif - -#ifdef PIN_USER_BTN_ANA - pinMode(PIN_USER_BTN_ANA, INPUT_PULLUP); +#if defined(BOARD_BUTTON_PIN) + pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP); #endif - #if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) Wire.setPins(PIN_BOARD_SDA, PIN_BOARD_SCL); #endif @@ -64,4 +219,20 @@ void RAK4631Board::begin() { #endif digitalWrite(SX126X_POWER_EN, HIGH); delay(10); // give sx1262 some time to power up +} + +void RAK4631Board::loop() { + NRF52BoardDCDC::loop(); + +#if defined(BOARD_BUTTON_PIN) + if (isButtonPressed()) { + if (btn_press_start == 0) { + btn_press_start = millis(); + } else if (millis() - btn_press_start >= 3000) { + powerOff(); + } + } else { + btn_press_start = 0; + } +#endif } \ No newline at end of file diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index 7e67165b197..a8e54a65696 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -10,6 +10,7 @@ class RAK4631Board : public NRF52BoardDCDC { protected: + uint32_t btn_press_start = 0; #ifdef NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif @@ -17,6 +18,7 @@ class RAK4631Board : public NRF52BoardDCDC { public: RAK4631Board() : NRF52Board("RAK4631_OTA") {} void begin(); + void loop() override; #define BATTERY_SAMPLES 8 @@ -35,4 +37,6 @@ class RAK4631Board : public NRF52BoardDCDC { const char* getManufacturerName() const override { return "RAK 4631"; } + + void powerOff() override; }; diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index 38cc88685c0..ba9a6aaac52 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -104,6 +104,7 @@ extern "C" static const uint8_t A7 = PIN_A7; #define ADC_RESOLUTION 14 +#define NRF52_POWER_MANAGEMENT // Power management boot protection threshold (millivolts) // Set to 0 to disable boot protection #define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)