Skip to content

RAK3401: reliable AIN1 button β€” interrupt latch, debounce, digital read, instant screen-on πŸ€–πŸ€–#2652

Closed
disq wants to merge 2 commits into
meshcore-dev:devfrom
disq:rak3401-ain1-button-reliability
Closed

RAK3401: reliable AIN1 button β€” interrupt latch, debounce, digital read, instant screen-on πŸ€–πŸ€–#2652
disq wants to merge 2 commits into
meshcore-dev:devfrom
disq:rak3401-ain1-button-reliability

Conversation

@disq
Copy link
Copy Markdown

@disq disq commented May 31, 2026

Problem

The RAK3401 companion build has a single user button on AIN1 (P0.31), driven by analogRead() polled once per main-loop iteration β€” there is no interrupt serving it. Symptoms:

  • Presses dropped when the loop briefly stalls (LoRa RX/TX, BLE, flash writes).
  • Single clicks feel laggy: the screen only wakes after the multi-click window expires.
  • The button can't wake the device from hibernate (SYSTEMOFF) β€” and a naive GPIO SENSE wake makes it wake instantly and never stay off.

This PR makes the AIN1 button story self-contained: reliable input and a working hibernate wake. It folds in #2642's RAK3401 board pieces (so that PR can be closed in favor of this one).

Changes

MomentaryButton (generic)

  • Configurable multi-click window on the analog constructor (default unchanged).
  • reset() β€” abandon the in-flight gesture.
  • enableInterrupt() β€” permanent FALLING-edge GPIOTE interrupt that latches presses so they survive loop stalls. Debounced: one count per press, re-armed only after check() sees a clean release (rejects press/release contact bounce). Skipped for analog/SAADC-threshold buttons (GPIOTE vs analogRead conflict on the same pin).

companion_radio UITask

  • Wake the display instantly on the press edge when it's off, instead of after the multi-click window.
  • Arm the button interrupt at startup for PIN_USER_BTN_ANA boards.

variants/rak3401 β€” input

  • Read AIN1 digitally (active-low, internal pull-up) instead of via SAADC β€” digitalRead coexists with the GPIOTE edge interrupt where analogRead does not (analogRead otherwise reads stuck "always pressed" once the interrupt is attached). Multi-click retained: single=NEXT / double=PREV / triple=SELECT / long=ENTER.

Hibernate wake via LPCOMP (2nd commit; supersedes #2642's GPIO-SENSE approach)

  • NRF52Board::configureVoltageWake() gains a detect_down param (default false = UP crossing for voltage recovery; true = DOWN crossing for a button press), selecting ANADETECT Down/Up and INTENSET DOWN/UP accordingly.
  • RAK3401Board.h: powerOff() override routing through initiateShutdown(USER).
  • RAK3401Board::initiateShutdown(): when PIN_USER_BTN_ANA is defined, arm LPCOMP on AIN7 at REFSEL 3 (~Β½ VDD) for a DOWN crossing as the SYSTEMOFF wake source. Waits for a confirmed release (analogRead above threshold) before arming, bounded by a 5 s timeout. Channel/threshold overridable via PWRMGT_BTN_LPCOMP_AIN / PWRMGT_BTN_LPCOMP_REFSEL.

Why LPCOMP, not GPIO SENSE

AIN1 is wired as an analog button (pressed == analogRead() < threshold). Its digital input buffer reads the released idle level as LOW even though analogRead reports ~VDD, so arming SENSE_Low latches DETECT the instant we enter SYSTEMOFF β€” sd_power_system_off() returns immediately and the fall-through reset reboots ("instant wake"). LATCH/EVENTS_PORT clearing and release debounce don't help, because the released level itself reads LOW digitally. LPCOMP operates in the analog domain and sees the idle level correctly, so a DOWN-crossing arm works: released idles above threshold, a press pulls below β†’ wake.

Relationship to #2642

This supersedes #2642 β€” it ports #2642's powerOff() override and AIN SYSTEMOFF wake, but replaces the GPIO-SENSE wake (which instant-wakes on this pin) with the LPCOMP approach. #2642 can be closed once this lands.

Testing

  • Builds clean: RAK_3401_companion_radio_ble, RAK_3401_companion_radio_usb.
  • Button input bench-tested on RAK3401 companion_radio_ble hardware: digital read fixes the SAADC+GPIOTE "always pressed" conflict; presses reliable through loop stalls; single/double/triple/long all work, no dropped or double-counted clicks.
  • Hibernate: GPIO-SENSE instant-wakes on this pin (confirmed on hardware); the LPCOMP DOWN-crossing approach is the fix. Please re-flash/re-test the hibernate path before merge.

Known edge case

If the loop stalls through the middle of a fast double-click so check() never re-arms between the two presses, the second press is dropped (fails toward "one click", never a phantom extra). Normal clicking is unaffected.

πŸ€– Generated with Claude Code

…ad, instant screen-on

The RAK3401 companion build drives its only user button (AIN1) by polling
analogRead() once per main-loop iteration, with no interrupt. Presses are dropped
whenever the loop briefly stalls (LoRa RX/TX, BLE, flash writes), and a single click
waits out the full multi-click window before the screen wakes.

MomentaryButton (generic):
- Configurable multi-click window on the analog constructor (default unchanged).
- reset(): abandon the in-flight gesture (used to swallow a screen-wake press).
- enableInterrupt(): attach a permanent FALLING-edge GPIOTE interrupt that latches
  presses so they survive loop stalls (and can wake the MCU where the platform sleeps
  on interrupts). Debounced by counting one press per press and only re-arming after
  check() has seen a clean release β€” rejects press/release contact bounce. Skipped for
  analog/SAADC-threshold buttons (GPIOTE and analogRead conflict on one pin).

companion_radio UITask: wake the display instantly on the press edge when it is off
(instead of after the multi-click window), and arm the button interrupt at startup
for PIN_USER_BTN_ANA boards.

variants/rak3401: read the AIN1 button digitally (active-low, internal pull-up)
instead of via SAADC. digitalRead coexists with the GPIOTE edge interrupt where
analogRead does not (analogRead reads stuck-low once the interrupt is attached).
Multi-click retained: single=NEXT / double=PREV / triple=SELECT / long=ENTER.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@disq disq changed the title RAK3401: reliable AIN1 button β€” interrupt latch, debounce, digital read, instant screen-on RAK3401: reliable AIN1 button β€” interrupt latch, debounce, digital read, instant screen-on πŸ€–πŸ€– May 31, 2026
@disq disq marked this pull request as draft May 31, 2026 23:45
Brings meshcore-dev#2642's RAK3401 board pieces into this branch so the AIN1 button story is
self-contained, using LPCOMP (not GPIO SENSE) as the hibernate wake source.

AIN1 (P0.31) is wired as an analog button (pressed == analogRead() < threshold).
GPIO SENSE can't wake on it: the digital input buffer reads the *released* idle
level as LOW even though analogRead reports ~VDD, so arming SENSE_Low latches DETECT
the instant we enter SYSTEMOFF β€” sd_power_system_off() returns immediately and the
fall-through reset reboots ("instant wake"). LATCH/EVENTS_PORT clearing and release
debounce don't help, because the released level itself reads LOW digitally.

LPCOMP operates in the analog domain and sees the idle level correctly. Arm a DOWN
crossing at ~1/2 VDD: released idles above threshold, a press pulls the pin below ->
downward crossing -> wake. Wait for a confirmed release (analogRead above threshold)
before arming, bounded by a 5s timeout so a stuck/low reading can't wedge shutdown.

- NRF52Board::configureVoltageWake() gains a detect_down param (default false = UP
  crossing for voltage recovery; true = DOWN crossing for a button press), selecting
  ANADETECT Down/Up and INTENSET DOWN/UP accordingly.
- RAK3401Board.h: powerOff() override routing through initiateShutdown(USER).
- RAK3401Board::initiateShutdown() arms LPCOMP on AIN7 at REFSEL 3 (~1/2 VDD) for a
  USER shutdown when PIN_USER_BTN_ANA is defined. Channel/threshold overridable via
  PWRMGT_BTN_LPCOMP_AIN / PWRMGT_BTN_LPCOMP_REFSEL.

Ports the fix from meshcore-dev#2642. Built: RAK_3401_companion_radio_ble.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@disq
Copy link
Copy Markdown
Author

disq commented Jun 1, 2026

Probably not needed.

@disq disq closed this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant