fault_diag.h 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. // fault_diag.h -- "why did we reboot?" diagnostic for issue #72 (intermittent disconnects).
  2. //
  3. // Field reports conflate THREE different events that all look identical from the host (USB re-enumerates, the
  4. // WebUSB panel shows "RF Link down"):
  5. // 1. a genuine RF link drop -- NO reboot, the link self-recovers in seconds (handled in rf_link, not here);
  6. // 2. a loop() hang -- the ~8s hardware watchdog fires and resets the MCU (red LED);
  7. // 3. an MCU fault -- a HardFault triggers an immediate reset (the Adafruit core's default handler resets).
  8. // Without recording which one happened, (2) and (3) are indistinguishable in the field, so the thread keeps
  9. // guessing. This module classifies the cause of each boot and surfaces it (boot banner + WebUSB panel).
  10. //
  11. // How: the nRF52 RESETREAS register already distinguishes watchdog (DOG) / software (SREQ) / pin (RESETPIN) /
  12. // lockup resets; the Adafruit core latches it at boot (readResetReason()). A SREQ alone can't tell "we rebooted
  13. // on purpose" (mode-switch chord, config reboot, DFU) from "a HardFault reset us" -- so we stamp the GPREGRET2
  14. // retention register (retained across soft/watchdog/pin reset, cleared only on power-on; GPREGRET itself is
  15. // reserved by the bootloader for the DFU magic): faultDiagArmIntentionalReset() before every deliberate
  16. // NVIC_SystemReset, and the HardFault handler stamps a distinct fault marker.
  17. #pragma once
  18. #include <stdint.h>
  19. // Boot-cause classification (kept in sync with REASON_STR[] in fault_diag.cpp).
  20. enum {
  21. RR_UNKNOWN = 0,
  22. RR_POWERON, // cold power-on / brownout
  23. RR_PIN, // reset pin / cable replug
  24. RR_WATCHDOG, // WDT fired -> loop() stopped feeding it (a hang)
  25. RR_LOCKUP, // CPU lockup
  26. RR_HARDFAULT, // HardFault -> our handler reset us
  27. RR_REBOOT, // intentional NVIC_SystemReset (mode switch / config / DFU)
  28. RR_SOFT, // software reset, unattributed
  29. RR_WAKE, // wake from System OFF
  30. RR_COUNT,
  31. };
  32. // Classify + log the cause of THIS boot. Call once, early in setup() (after Serial.begin so the line lands).
  33. void faultDiagBoot();
  34. // Stamp "the reset about to happen is intentional" -- call immediately before a deliberate NVIC_SystemReset so
  35. // the next boot classifies it as RR_REBOOT, not RR_HARDFAULT.
  36. void faultDiagArmIntentionalReset();
  37. // ---- hang-stage breadcrumb -------------------------------------------------------------------------------
  38. // loop() writes the stage it is ENTERING to GPREGRET2 (encoded 0x80|stage, distinct from the intentional/fault
  39. // markers and from 0 during normal SREQ). If loop() wedges, the ~8s watchdog fires; GPREGRET2 survives that
  40. // reset, so the next boot can report WHICH loop stage was stuck -- the missing piece for the clone hangs.
  41. // Stage indices match the OPK_LOG timing order: 0 webusb,1 ctrl.task,2 serial,3 rfdiag,4 rflink,5 haptic,
  42. // 6 led,7 usbmount,8 usbtx.
  43. void faultDiagSetStage(uint8_t stage);
  44. // The stage loop() was in when the last WATCHDOG/LOCKUP reset fired (0xFF = last boot wasn't a hang, or the
  45. // breadcrumb didn't survive -- some clone bootloaders clear GPREGRET2). String form for the panel/console.
  46. uint8_t faultDiagHangStage();
  47. const char *faultDiagHangStageStr();
  48. // Live hang localization (does NOT need a reset to survive): faultDiagBeat() is called once per loop()
  49. // iteration; faultDiagStallMs() is ms since that last beat (~0 healthy, grows while loop() is wedged), and
  50. // faultDiagCurStage() is the stage loop() is currently in. The SOF-driven blob reports these so the panel can
  51. // show "STALLED in <stage>" live, before the watchdog even fires.
  52. void faultDiagBeat();
  53. uint8_t faultDiagCurStage();
  54. uint32_t faultDiagStallMs();
  55. const char *faultDiagStageStr(uint8_t s);
  56. // Watchdog pre-reset PC capture ("software SWD"): arm in setup() right after NRF_WDT->TASKS_START. After a
  57. // watchdog hang, faultDiagHangPC()/LR() return the PC/LR of the stuck code (0 if the hang hard-masked
  58. // interrupts so the capture ISR couldn't run). Map the PC with addr2line on the build .elf.
  59. void faultDiagArmHangCapture();
  60. // Flash black box: 4 Hz TIMER4 watchdog-of-the-watchdog; at ~6s of loop stall it dumps both tasks' stacked
  61. // PCs + vitals to a reserved flash page (survives ANY reset, incl. boards that wipe .noinit/GPREGRET2).
  62. // Reported + consumed by faultDiagBoot on the next boot. Arm once from setup(), after the WDT starts.
  63. void faultDiagBlackBoxArm();
  64. uint32_t faultDiagHangPC();
  65. uint32_t faultDiagHangLR();
  66. // Per-task stack headroom (words of stack never used = free). Call faultDiagStackTick() from loop() (self-gated
  67. // to ~1 Hz). faultDiagUsbdStackFree() trending toward 0 under haptic load confirms the usbd-task overflow.
  68. void faultDiagStackTick();
  69. uint16_t faultDiagUsbdStackFree();
  70. uint16_t faultDiagLoopStackFree();
  71. // Last-boot classification (RR_*) + the raw RESETREAS, for the WebUSB panel / console.
  72. uint8_t faultDiagReason();
  73. uint32_t faultDiagResetReas();
  74. const char *faultDiagReasonStr();
  75. // ---- flight recorder (catch a watchdog hang post-mortem) -------------------------------------------------
  76. // A .noinit ring of recent high-level events + a live vitals snapshot, BOTH surviving a watchdog reset (same
  77. // mechanism as the hang PC frame: .noinit RAM isn't zeroed by the C runtime, so it rides through the reset even
  78. // on clone boards whose bootloader wipes GPREGRET2). A hang drops USB the instant it resets, so live CDC
  79. // logging can never catch the moment itself -- instead we record the trail continuously and DUMP IT ON THE NEXT
  80. // BOOT when the last reset was a hang. Every push is a few stores under PRIMASK: safe to call from the fragile
  81. // 800B usbd task and from ISRs (NO printf, NO allocation). This is the "what was going on when it wedged" the
  82. // cheap boards (Teyleten hangs ~every 4 min) need. `arg` is event-specific, usually packed (high<<8)|low.
  83. enum {
  84. FR_NONE = 0,
  85. FR_BEAT, // periodic loop heartbeat (arg = stall ms) -- the gap between beats localizes the wedge in time
  86. FR_SET, // usbd handleSet feature write (arg = (rid<<8)|cmd)
  87. FR_GET, // usbd handleGet feature read (arg = (rid<<8)|cmd)
  88. FR_RELAY, // host->controller relay enqueued (arg = (slot<<8)|rid)
  89. FR_RFUP, // RF (re)connect (arg = slot)
  90. FR_RFDN, // RF link down (arg = slot)
  91. FR_HEAL, // RF-stall radio self-heal (power-cycle)
  92. FR_MOUNT, // USB HID mount/unmount changed (arg = mounted-slot count)
  93. FR_SUSP, // USB bus suspend edge
  94. FR_RESUME, // USB bus resume edge
  95. FR_OFF, // controller power-off (0x9F) relayed (arg = slot)
  96. FR_RINGF, // relay-ring fault caught (arg = running count)
  97. FR_SAVE, // bond flash write (saveBonds)
  98. };
  99. // Push one event into the flight recorder. Cheap + ISR/usbd-safe (a few stores under PRIMASK, no printf).
  100. void faultDiagTrace(uint8_t evt, uint16_t arg);
  101. // Per-loop upkeep: refresh the persistent vitals snapshot + emit a heartbeat (~4x/s), and stream the live
  102. // per-second vitals line over CDC when g_vitals is on. Call once per loop() iteration.
  103. void faultDiagFlightTick(void);
  104. // Print the pre-reset flight trail + vitals-at-wedge over CDC (from the boot-time copy of the surviving ring).
  105. // Called automatically from faultDiagBoot() after a hang; also re-dumpable on demand from the console ("FR").
  106. void faultDiagDumpFlight(void);
  107. // ---- WebUSB panel access to the saved (pre-reset) trail ---------------------------------------------------
  108. // The same boot-time copy the console "FR" dumps, exposed for the panel to stream as 0xA8 frames. Mirrors the
  109. // hapLog capture drain: reset the cursor to the oldest saved event, then pull entries until exhausted, so a
  110. // full 96-event trail streams across several panel polls without a single blocking write.
  111. bool faultDiagHaveFlight(
  112. void); // true if a pre-reset trail was captured at this boot
  113. uint16_t faultDiagFlightCount(void); // saved events available to stream (<= 96)
  114. uint16_t faultDiagFlightTotal(
  115. void); // total events pushed before the reset (for "showing N of M")
  116. void faultDiagFlightDrainReset(
  117. void); // rewind the pull cursor to the oldest saved event
  118. // Pull the next saved event; false when exhausted. *dtMs = ms the event fired BEFORE the last recorded crumb.
  119. bool faultDiagFlightPull(uint32_t *dtMs, uint8_t *evt, uint8_t *stage,
  120. uint16_t *arg);
  121. // Vitals as of the last ~4Hz refresh before the reset (the "@wedge" snapshot). false if no trail was captured.
  122. struct FaultFlightVitals {
  123. uint16_t loopPerSec;
  124. uint16_t usbdStkFree, loopStkFree;
  125. uint32_t heapUsed;
  126. uint16_t pollsps, relayps, crcps, norxps, rfHeal, ringFault;
  127. uint8_t curStage, stallMs;
  128. };
  129. bool faultDiagFlightVitals(struct FaultFlightVitals *out);
  130. // 1 = stream the live per-second vitals line over CDC (console "VIT" toggles). Watch usbd stack / heap / relay
  131. // rate trend on a flaky board in the seconds before it wedges.
  132. extern bool g_vitals;
  133. // ---- clock fingerprint (clone-board stability diagnostic) -------------------------------------------------
  134. // nice!nano clones vary in their crystals. The bare-metal radio needs HFXO (32 MHz xtal); millis()/RTC and the
  135. // watchdog run on LFCLK (32.768 kHz xtal or RC). If a clone runs HFCLK on the internal RC, or its LFCLK is
  136. // off-rate, the two time bases diverge -- which inflates the micros()-gated poll rate, shortens the RX window
  137. // (delivered drops), and skews the LFCLK-based watchdog. These let the panel show, per board: which source
  138. // each clock is actually on, and the measured micros()/millis() ratio (ideal 1000 us per ms).
  139. // LF code: 0=stopped, 1=RC, 2=crystal, 3=synth. HF code: 0=RC(running or not), 2=crystal.
  140. void clockDiagBoot(); // read+log the clock sources once at boot
  141. void clockDiagTick(); // call from loop(); recomputes usPerMs about once a second
  142. uint8_t clockLfSrc();
  143. uint8_t clockHfSrc();
  144. uint16_t
  145. clockUsPerMs(); // measured micros() advanced per millis() tick (ideal 1000; 0 until first sample)