| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- #include "fault_diag.h"
- #include <Arduino.h> // readResetReason(), NRF_POWER, NVIC_SystemReset, POWER_RESETREAS_*_Msk
- // GPREGRET2 markers. GPREGRET (id 0) is reserved by the Adafruit bootloader for the DFU magic; GPREGRET2 is
- // free for the application and is retained across soft/watchdog/pin reset, cleared only on power-on/brownout.
- // Arbitrary non-zero tags -- any value distinct from each other and from the power-on default (0) works.
- #define G2_INTENT 0xB1u
- #define G2_FAULT 0xFAu
- static uint8_t g_reason = RR_UNKNOWN;
- static uint32_t g_resetReas = 0;
- static uint8_t g_hangStage = 0xFF;
- // Hang breadcrumb: loop() stage written to GPREGRET2 as 0x80|stage. 0x80..0x9F can never collide with the
- // intentional-reboot (0xB1) / HardFault (0xFA) markers or the power-on default (0), so the same register
- // disambiguates all of them at boot.
- #define G2_STAGE_FLAG 0x80u
- static const char *const STAGE_STR[] = { "webusb", "ctrl.task", "serial",
- "rfdiag", "rflink", "haptic",
- "led", "usbmount", "usbtx" };
- #define STAGE_COUNT ((uint8_t)(sizeof STAGE_STR / sizeof STAGE_STR[0]))
- static const char *const REASON_STR[RR_COUNT] = {
- "unknown", "power-on", "pin/replug", "WATCHDOG (hang)", "CPU lockup",
- "HARDFAULT", "reboot", "soft reset", "wake-from-off",
- };
- // HardFault override. The Adafruit core's default handler (cores/.../debug.cpp) already does NVIC_SystemReset();
- // we replace it to first stamp the fault marker so the NEXT boot classifies this SREQ as RR_HARDFAULT rather
- // than an intentional reboot. The core's strong symbol is only linked to satisfy the vector table, so our own
- // strong definition takes its place. Keep this MINIMAL: we are in fault context on a possibly-corrupt stack --
- // no Serial, no allocations, just stamp and reset.
- extern "C" void HardFault_Handler(void)
- {
- NRF_POWER->GPREGRET2 = G2_FAULT;
- NVIC_SystemReset();
- while (1) {
- }
- }
- void faultDiagArmIntentionalReset()
- {
- NRF_POWER->GPREGRET2 = G2_INTENT;
- }
- // Live stage + loop heartbeat. The post-reset GPREGRET2 breadcrumb only works if the bootloader preserves
- // GPREGRET2 across a watchdog reset -- some clone boards clear it, so it reads blank. These LIVE values don't
- // depend on surviving a reset: the WebUSB blob send runs on the USB SOF interrupt (independent of loop()), so
- // when loop() wedges, the SOF drain can still report the current stage + how long loop() has been stuck,
- // straight to the panel during the ~8s before the watchdog fires.
- static volatile uint8_t g_curStage = 0xFF;
- static volatile uint32_t g_loopBeatMs = 0;
- void faultDiagSetStage(uint8_t stage)
- {
- g_curStage = stage;
- NRF_POWER->GPREGRET2 = (uint8_t)(G2_STAGE_FLAG | (stage & 0x1Fu));
- }
- void faultDiagBeat()
- {
- g_loopBeatMs = millis();
- }
- uint8_t faultDiagCurStage()
- {
- return g_curStage;
- }
- uint32_t faultDiagStallMs()
- {
- // ms since loop() last beat. ~0 when healthy (loop beats ~250x/s); grows while loop() is stuck.
- return (uint32_t)(millis() - g_loopBeatMs);
- }
- const char *faultDiagStageStr(uint8_t s)
- {
- return s < STAGE_COUNT ? STAGE_STR[s] : "?";
- }
- uint8_t faultDiagHangStage()
- {
- return g_hangStage;
- }
- const char *faultDiagHangStageStr()
- {
- return g_hangStage < STAGE_COUNT ? STAGE_STR[g_hangStage] : "n/a";
- }
- void faultDiagBoot()
- {
- uint32_t rr =
- readResetReason(); // latched + cleared by the core's init()
- uint8_t g2 = (uint8_t)NRF_POWER->GPREGRET2;
- NRF_POWER->GPREGRET2 = 0; // consume the marker for this boot cycle
- g_resetReas = rr;
- // If this boot follows a watchdog/lockup reset and GPREGRET2 holds a stage breadcrumb (0x80|stage), recover
- // which loop stage was stuck. Only meaningful for hangs -- an intentional reboot/HardFault stamps its own
- // marker over the breadcrumb, and a clean power-on zeroes it.
- bool isHang = (rr & POWER_RESETREAS_DOG_Msk) ||
- (rr & POWER_RESETREAS_LOCKUP_Msk);
- if (isHang && (g2 & G2_STAGE_FLAG) && g2 < G2_INTENT)
- g_hangStage = (uint8_t)(g2 & 0x1Fu);
- else
- g_hangStage = 0xFF;
- uint8_t reason;
- // Precedence: a physical pin reset / watchdog / lockup is unambiguous from RESETREAS; only a SREQ needs the
- // GPREGRET2 marker to split intentional reboot vs HardFault.
- if (rr & POWER_RESETREAS_RESETPIN_Msk)
- reason = RR_PIN;
- else if (rr & POWER_RESETREAS_DOG_Msk)
- reason = RR_WATCHDOG;
- else if (rr & POWER_RESETREAS_LOCKUP_Msk)
- reason = RR_LOCKUP;
- else if (rr & POWER_RESETREAS_SREQ_Msk)
- reason = (g2 == G2_FAULT) ? RR_HARDFAULT :
- (g2 == G2_INTENT) ? RR_REBOOT :
- RR_SOFT;
- else if (rr & POWER_RESETREAS_OFF_Msk)
- reason = RR_WAKE;
- else if (rr == 0)
- reason = RR_POWERON; // all bits clear == power-on / brownout
- else
- reason = RR_UNKNOWN;
- g_reason = reason;
- Serial.printf(
- "# reset cause: %s (RESETREAS=0x%08lX gpregret2=0x%02X)\n",
- REASON_STR[reason], (unsigned long)rr, g2);
- if (g_hangStage != 0xFF)
- Serial.printf("# hang stage: %s (%u)\n", faultDiagHangStageStr(),
- g_hangStage);
- }
- // ---- clock fingerprint -------------------------------------------------------------------------------------
- static uint8_t g_clkLf = 0, g_clkHf = 0;
- static uint16_t g_clkUsPerMs = 0;
- void clockDiagBoot()
- {
- // LFCLKSTAT: bit16 STATE(running), bits1:0 SRC (0=RC,1=Xtal,2=Synth). HFCLKSTAT: bit16 STATE, bit0 SRC
- // (0=RC,1=Xtal). The bare-metal radio needs HFXO; if HF shows RC here, RF runs on the 64 MHz internal RC.
- uint32_t lf = NRF_CLOCK->LFCLKSTAT;
- uint32_t hf = NRF_CLOCK->HFCLKSTAT;
- bool lfrun = lf & CLOCK_LFCLKSTAT_STATE_Msk;
- uint8_t lfsrc = (uint8_t)(lf & CLOCK_LFCLKSTAT_SRC_Msk);
- g_clkLf = lfrun ? (uint8_t)(lfsrc + 1) : 0; // 1=RC,2=Xtal,3=Synth, 0=stopped
- bool hfxtal = hf & CLOCK_HFCLKSTAT_SRC_Msk;
- g_clkHf = hfxtal ? 2 : 0; // 2=crystal, 0=RC
- Serial.printf("# clock: LFCLK=%s HFCLK=%s\n",
- g_clkLf == 2 ? "xtal" :
- g_clkLf == 1 ? "RC" :
- g_clkLf == 3 ? "synth" :
- "stopped",
- g_clkHf == 2 ? "xtal" : "RC");
- }
- void clockDiagTick()
- {
- // Cross-check the two time bases: count micros() (HFCLK-derived) elapsed over a ~1s millis() (LFCLK/RTC)
- // window. usPerMs = micros-delta / millis-delta; 1000 = the clocks agree. A clone whose HFCLK runs fast vs
- // its LFCLK reads >1000 here -- the exact signature behind "poll rate too high, delivered too low".
- static uint32_t u0 = 0;
- static unsigned long m0 = 0;
- static bool init = false;
- unsigned long m = millis();
- if (!init) {
- u0 = micros();
- m0 = m;
- init = true;
- return;
- }
- unsigned long dm = m - m0;
- if (dm >= 1000) {
- uint32_t du = (uint32_t)(micros() - u0);
- g_clkUsPerMs = (uint16_t)(du / dm);
- u0 = micros();
- m0 = m;
- }
- }
- uint8_t clockLfSrc()
- {
- return g_clkLf;
- }
- uint8_t clockHfSrc()
- {
- return g_clkHf;
- }
- uint16_t clockUsPerMs()
- {
- return g_clkUsPerMs;
- }
- uint8_t faultDiagReason()
- {
- return g_reason;
- }
- uint32_t faultDiagResetReas()
- {
- return g_resetReas;
- }
- const char *faultDiagReasonStr()
- {
- return REASON_STR[g_reason < RR_COUNT ? g_reason : 0];
- }
|