From 3ff3c7dd439926d356cfa18ebbfe00473be39b21 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Thu, 28 May 2026 19:39:18 +0100 Subject: [PATCH 1/2] RAK3401: powerOff() override + AIN1 SENSE LOW wake from SYSTEMOFF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two board-level pieces from dorfman2's PR #2414 that are useful for any RAK3401 firmware, not just the repeater variant the PR targets: 1. RAK3401Board::powerOff() override — routes _board->powerOff() through initiateShutdown(SHUTDOWN_REASON_USER) so the shutdown reason is properly tagged in GPREGRET2 instead of falling through to the base class default. 2. AIN1 GPIO SENSE LOW config in initiateShutdown() — when an env defines PIN_USER_BTN_ANA, configure that pin with pull-up + SENSE LOW before entering SYSTEMOFF, so pressing the AIN1 button wakes the board from the off state via the GPIO LATCH/SENSE mechanism. Waits for button release first (level-triggered SENSE would otherwise wake immediately if the user is still holding the button when we arm it). The repeater-specific UI redesign, status screen, and simple_repeater main.cpp button handler from the same PR are out of scope here; the companion radio UI tree already has its own equivalent button handling in examples/companion_radio/ui-new/UITask.cpp. Co-Authored-By: dorfman2 Co-Authored-By: Claude Opus 4.7 (1M context) --- variants/rak3401/RAK3401Board.cpp | 14 ++++++++++++++ variants/rak3401/RAK3401Board.h | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index cbf7c1087d..77f1174c27 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -24,6 +24,20 @@ void RAK3401Board::initiateShutdown(uint8_t reason) { configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel); } +#ifdef PIN_USER_BTN_ANA + // Configure AIN1 button as GPIO SENSE wake source (active LOW). + // Wait for button release first — SENSE is level-triggered, so if the user is + // still holding the button when we arm SENSE, the chip wakes immediately. + while (digitalRead(PIN_USER_BTN_ANA) == LOW) delay(10); + delay(50); // debounce + + uint32_t pin = (uint32_t)PIN_USER_BTN_ANA; + NRF_GPIO->PIN_CNF[pin] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) + | (GPIO_PIN_CNF_INPUT_Connect << GPIO_PIN_CNF_INPUT_Pos) + | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos) + | (GPIO_PIN_CNF_SENSE_Low << GPIO_PIN_CNF_SENSE_Pos); +#endif + enterSystemOff(reason); } #endif diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 3a080d5e2c..6b1d398735 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -20,6 +20,10 @@ class RAK3401Board : public NRF52BoardDCDC { RAK3401Board() : NRF52Board("RAK3401_OTA") {} void begin(); +#ifdef NRF52_POWER_MANAGEMENT + void powerOff() override { initiateShutdown(SHUTDOWN_REASON_USER); } +#endif + #define BATTERY_SAMPLES 8 uint16_t getBattMilliVolts() override { From 4b94311b8d13e9bcbd41dba1762b42825c608943 Mon Sep 17 00:00:00 2001 From: Kemal Hadimli Date: Mon, 1 Jun 2026 10:27:43 +0100 Subject: [PATCH 2/2] RAK3401: use LPCOMP (not GPIO SENSE) to wake from SYSTEMOFF on AIN button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AIN user button on the companion_radio build is an *analog* button (MomentaryButton reads it as analogRead() < threshold), not a digital switch. GPIO SENSE cannot wake from this pin: the digital input buffer reads the line as LOW even at the released idle level — verified on hardware, analogRead reports ~VDD while NRF_GPIO->IN reads 0 and the SENSE_Low condition latches immediately (LATCH bit re-asserts right after being cleared). So a GPIO SENSE_Low arm wakes the chip the instant it enters SYSTEMOFF and it can never stay off. This reproduced identically on USB and battery (RESETREAS = Wake from GPIO, 0x10000), ruling out VBUS. This also fixes an earlier hang: the original release-wait used digitalRead() on this pin, which always returns LOW (SAADC leaves the digital buffer disconnected), so shutdown never reached SYSTEMOFF — screen off, BLE/serial still alive. Fix: arm LPCOMP instead. It works in the analog domain, so it sees the idle level correctly. Configure a DOWN crossing at ~1/2 VDD (released idles near VDD = above, a press pulls to ~0V = below -> wake). LPCOMP is otherwise unused for a USER shutdown (voltage wake is only armed for the low-voltage / boot-protect reasons), so there is no conflict. - NRF52Board::configureVoltageWake() gains a detect_down flag (default false = existing upward voltage-recovery behavior, backward compatible) to also support downward-crossing wake. - RAK3401 initiateShutdown() waits for release via analogRead (bounded by a 5s timeout so a stuck reading can never wedge shutdown), then arms LPCOMP on AIN7. AIN channel / threshold are overridable build defines. Verified on hardware: device reaches SYSTEMOFF and stays off; a button press wakes it (RESETREAS = Wake from LPCOMP). Co-Authored-By: Claude Opus 4.8 --- src/helpers/NRF52Board.cpp | 10 ++++--- src/helpers/NRF52Board.h | 5 +++- variants/rak3401/RAK3401Board.cpp | 48 ++++++++++++++++++++++++------- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 2c8753d464..66823b0eb2 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -177,7 +177,7 @@ void NRF52Board::enterSystemOff(uint8_t reason) { NVIC_SystemReset(); } -void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { +void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel, bool detect_down) { // LPCOMP is not managed by SoftDevice - direct register access required // Halt and disable before reconfiguration NRF_LPCOMP->TASKS_STOP = 1; @@ -189,8 +189,10 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { // Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16) NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk; - // Detect UP events (voltage rises above threshold for battery recovery) - NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up; + // Crossing direction: UP for voltage recovery (rises above threshold), + // DOWN for an analog button press (pin pulled below threshold). + NRF_LPCOMP->ANADETECT = detect_down ? LPCOMP_ANADETECT_ANADETECT_Down + : LPCOMP_ANADETECT_ANADETECT_Up; // Enable 50mV hysteresis for noise immunity NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV; @@ -202,7 +204,7 @@ void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) { NRF_LPCOMP->EVENTS_CROSS = 0; NRF_LPCOMP->INTENCLR = 0xFFFFFFFF; - NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk; + NRF_LPCOMP->INTENSET = detect_down ? LPCOMP_INTENSET_DOWN_Msk : LPCOMP_INTENSET_UP_Msk; // Enable LPCOMP NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled; diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index c9f1e071b8..65fe897266 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -40,7 +40,10 @@ class NRF52Board : public mesh::MainBoard { bool checkBootVoltage(const PowerMgtConfig* config); void enterSystemOff(uint8_t reason); - void configureVoltageWake(uint8_t ain_channel, uint8_t refsel); + // Arm LPCOMP as a SYSTEMOFF wake source on the given analog channel. + // detect_down=false wakes on an upward crossing (voltage recovery); + // detect_down=true wakes on a downward crossing (e.g. an analog button press). + void configureVoltageWake(uint8_t ain_channel, uint8_t refsel, bool detect_down = false); virtual void initiateShutdown(uint8_t reason); #endif diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index 77f1174c27..07e28fcbe5 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -4,6 +4,17 @@ #include "RAK3401Board.h" #ifdef NRF52_POWER_MANAGEMENT +#ifdef PIN_USER_BTN_ANA +// LPCOMP wake config for the AIN user button. Defaults assume PIN_USER_BTN_ANA +// is pin 31 (P0.31 = AIN7); override via build flags if the button moves. +#ifndef PWRMGT_BTN_LPCOMP_AIN + #define PWRMGT_BTN_LPCOMP_AIN 7 +#endif +#ifndef PWRMGT_BTN_LPCOMP_REFSEL + #define PWRMGT_BTN_LPCOMP_REFSEL 3 // 4/8 VDD (~1.5V) threshold +#endif +#endif + // Static configuration for power management // Values set in variant.h defines const PowerMgtConfig power_config = { @@ -25,17 +36,34 @@ void RAK3401Board::initiateShutdown(uint8_t reason) { } #ifdef PIN_USER_BTN_ANA - // Configure AIN1 button as GPIO SENSE wake source (active LOW). - // Wait for button release first — SENSE is level-triggered, so if the user is - // still holding the button when we arm SENSE, the chip wakes immediately. - while (digitalRead(PIN_USER_BTN_ANA) == LOW) delay(10); - delay(50); // debounce + // Wake-from-SYSTEMOFF on the AIN user button (P0.31 = AIN7). + // + // This pin is wired as an *analog* button (see MomentaryButton in target.cpp: + // pressed == analogRead() < threshold). GPIO SENSE can't be used as the wake + // source: the digital input buffer reads this line as LOW even at the released + // idle level (verified on hardware — analogRead reports ~VDD while NRF_GPIO->IN + // reads 0 and SENSE_Low latches immediately), so a GPIO SENSE arm wakes the + // chip the instant we enter SYSTEMOFF and it can never stay off. + // + // LPCOMP works in the analog domain, so it sees the idle level correctly. Arm + // it for a DOWN crossing at ~1/2 VDD: released idles near VDD (above), a press + // pulls the pin toward 0V (below) -> downward crossing -> wake. The LPCOMP is + // otherwise unused for a USER shutdown (voltage wake is only armed for the + // low-voltage / boot-protect reasons handled above), so there is no conflict. + // + // Wait for release first so LPCOMP is armed while the level is above the + // threshold — otherwise the initial press generates no new downward crossing. + // Bounded by a timeout so a stuck/low reading can never wedge shutdown. + const int BTN_RELEASED_ADC = 1024; // well above the press threshold + uint32_t t0 = millis(); + int released_streak = 0; + while (released_streak < 5 && (millis() - t0) < 5000) { + if (analogRead(PIN_USER_BTN_ANA) > BTN_RELEASED_ADC) released_streak++; + else released_streak = 0; + delay(10); + } - uint32_t pin = (uint32_t)PIN_USER_BTN_ANA; - NRF_GPIO->PIN_CNF[pin] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) - | (GPIO_PIN_CNF_INPUT_Connect << GPIO_PIN_CNF_INPUT_Pos) - | (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos) - | (GPIO_PIN_CNF_SENSE_Low << GPIO_PIN_CNF_SENSE_Pos); + configureVoltageWake(PWRMGT_BTN_LPCOMP_AIN, PWRMGT_BTN_LPCOMP_REFSEL, /*detect_down=*/true); #endif enterSystemOff(reason);