feat(nrf): hardware watchdog with reset-reason and phase breadcrumbs - #139
Merged
jonasniesner merged 2 commits intoAug 3, 2026
Merged
Conversation
nRF had no watchdog, so any unbounded wait was permanent: nrfx_spim.c's
`while (!nrf_spim_event_check(END)){}` and the six Wire_nRF52 TWIM spins
have no timeout and no yield, and checkTransferTimeouts() cannot help
because it runs FROM loop(), which is what is stuck. This makes that
class recoverable -- the residual PLAN_PHASE2_BOUND_WAITS D-L accepted
and D-K assumed unrecoverable.
Portable module, not an nRF-only one: every feed site and breadcrumb
stamp lives in code that compiles for both targets, so an nRF-only API
would mean #ifdef TARGET_NRF around ~20 call sites in shared files.
Follows the ble_transport pattern -- one header with no vendor includes,
two whole-file-gated implementations, ESP32 stubbed. Its reset-reason
decode moves out of main.cpp, a net #ifdef reduction there.
Timeout is 300 s, and the number alone is not what makes it safe. The
longest span the firmware cannot instrument is a REFRESH_FULL on a
7-colour split-buffer panel: 4 BUSY_WAIT entries x 30 s, sent to BOTH
controllers, ~240 s inside one bbepRefresh(). What keeps that from
resetting a healthy device is the feed immediately before all 15
bb_epaper entry points, so the dog faces one call rather than that call
plus everything preceding it. Margin is ~1.25x -- re-check it when
adding a panel.
Three details that are easy to get wrong:
- RESETREAS must come from readResetReason(). The core reads AND
clears the register in init() before setup(), so reading the
peripheral (or sd_power_reset_reason_get) returns zero forever and
reports every watchdog reset as a power-on.
- GPREGRET2 needs two access paths. sd_power_gpregret_* are numbered
from SOC_SVC_BASE_NOT_AVAILABLE and cannot be used before
ble.begin(); direct register access is correct while the SoftDevice
is disabled, SVCs once it is enabled.
- A running WDT cannot be stopped or reconfigured, and which resets
clear it is NOT established by anything in-tree. So inherit-detection
via RUNSTATUS runs on every build INCLUDING the disabled one, and
feeds every enabled RREN channel of whatever it finds. A disabled
build that inherited a live dog it never fed would be a brick.
Boot-loop containment: armed before the boot panel path so boot wedges
are covered, with a 3-strike counter in GPREGRET2 bits 5:4 entering a
safe mode that refuses panel work at both epdSessionAcquire and pwrmgm.
Strikes clear after 10 min of uptime rather than on a successful
refresh -- refresh-based clearing would never accumulate (every boot
refreshes) and would make safe mode permanent (safe mode never
refreshes).
Not verified on hardware. The 240 s figure is read from bb_ep.inl, not
measured; T2 in the plan is the gate before this reaches devices.
Safe mode rejects transfers late and generically -- a clean NACK needs a
"device in safe mode" code, which must originate in opendisplay-protocol.
Plan, decisions and four rounds of review findings:
docs/PLAN_NRF_HARDWARE_WATCHDOG_2026-08-01.md
Timeout dropped from 300s to 120s, which is now below the ~240s worst-case REFRESH_FULL span on a 7-colour split-buffer panel -- a healthy refresh on that panel class will trip the watchdog mid-refresh. Known gap, documented in platformio.ini; re-check before shipping to such a panel. Adds two new breadcrumb phases (IDLE_OFF/IDLE_WARM replacing the shared IDLE) so a freeze while the panel session is idle can be told apart from one during keep-alive, plus four more (PWRMGM_AXP2101/RAIL/PINS/WIRE) instrumenting pwrmgm()'s previously-blind power-up path, added after a watchdog reset landed there with no breadcrumb to explain why. A phaseName() lookup makes the retained phase human-readable in the boot log instead of a bare integer. WDT-DEBUG-tagged od_log_debug lines pair with each EPD-session and pwrmgm() breadcrumb (stamped first, since the log call itself can hang on the same USB CDC mutex delay() depends on) -- grep "WDT-DEBUG" to remove the whole set later. Also adds a bounded (2s cap), debug-build-only wait for the USB CDC host to reconnect before the first log line: without it, the reset-reason and retained-breadcrumb lines -- the whole point of this feature -- reliably lose the race against USB re-enumeration after a reset and are silently discarded by od_log's dark-port check, which doesn't count them as drops.
davelee98
force-pushed
the
feat/nrf-hardware-watchdog
branch
from
August 3, 2026 19:15
780a41e to
4b54a88
Compare
4 tasks
davelee98
marked this pull request as ready for review
August 3, 2026 19:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — depends on #135. This branch is stacked on
feat/phase4-auth-abuse-disconnect(PR #135), which hasn't merged into
mainyet, so the diff currently includes all ofPhases 2-4 in addition to the watchdog work below. Once #135 merges, this diff will shrink
to just the watchdog commits automatically (no rebase needed) — mark ready for review then.
Fork-local stacked PR tracking the same branch, for incremental review in the meantime:
davelee98#10
Summary
watchdog.hAPI, ESP32 stubbed) to recover from unbounded waits belowloop()that no software timeout can catch (nrfx SPIM busy-spin, Wire_nRF52 TWIM spins) — seedocs/PLAN_NRF_HARDWARE_WATCHDOG_2026-08-01.md.OPENDISPLAY_NRF_WDT_S), below this plan's documented 240 s worst-caseREFRESH_FULLspan on a 7-colour split-buffer panel — a known, deliberate gap, called out in bothplatformio.iniand a status-update section at the top of the plan doc. Do not ship to that panel class without re-deriving the timeout.pwrmgm()(previously blind to the watchdog) now has 4 breadcrumb phases covering its power-up sub-steps, added after a watchdog reset landed there with no breadcrumb to explain why.WDT-DEBUG-tagged debug logging at every EPD-session andpwrmgm()stage transition (grep "WDT-DEBUG"to strip later).Test plan
pio run -e nrf52840customand-e nrf52840custom-debugbuild cleanpio run(all 11 default envs) builds clean