fault_diag.cpp 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #include "fault_diag.h"
  2. #include <Arduino.h> // readResetReason(), NRF_POWER, NVIC_SystemReset, POWER_RESETREAS_*_Msk
  3. // GPREGRET2 markers. GPREGRET (id 0) is reserved by the Adafruit bootloader for the DFU magic; GPREGRET2 is
  4. // free for the application and is retained across soft/watchdog/pin reset, cleared only on power-on/brownout.
  5. // Arbitrary non-zero tags -- any value distinct from each other and from the power-on default (0) works.
  6. #define G2_INTENT 0xB1u
  7. #define G2_FAULT 0xFAu
  8. static uint8_t g_reason = RR_UNKNOWN;
  9. static uint32_t g_resetReas = 0;
  10. static uint8_t g_hangStage = 0xFF;
  11. // Hang breadcrumb: loop() stage written to GPREGRET2 as 0x80|stage. 0x80..0x9F can never collide with the
  12. // intentional-reboot (0xB1) / HardFault (0xFA) markers or the power-on default (0), so the same register
  13. // disambiguates all of them at boot.
  14. #define G2_STAGE_FLAG 0x80u
  15. static const char *const STAGE_STR[] = { "webusb", "ctrl.task", "serial",
  16. "rfdiag", "rflink", "haptic",
  17. "led", "usbmount", "usbtx" };
  18. #define STAGE_COUNT ((uint8_t)(sizeof STAGE_STR / sizeof STAGE_STR[0]))
  19. static const char *const REASON_STR[RR_COUNT] = {
  20. "unknown", "power-on", "pin/replug", "WATCHDOG (hang)", "CPU lockup",
  21. "HARDFAULT", "reboot", "soft reset", "wake-from-off",
  22. };
  23. // HardFault override. The Adafruit core's default handler (cores/.../debug.cpp) already does NVIC_SystemReset();
  24. // we replace it to first stamp the fault marker so the NEXT boot classifies this SREQ as RR_HARDFAULT rather
  25. // than an intentional reboot. The core's strong symbol is only linked to satisfy the vector table, so our own
  26. // strong definition takes its place. Keep this MINIMAL: we are in fault context on a possibly-corrupt stack --
  27. // no Serial, no allocations, just stamp and reset.
  28. extern "C" void HardFault_Handler(void)
  29. {
  30. NRF_POWER->GPREGRET2 = G2_FAULT;
  31. NVIC_SystemReset();
  32. while (1) {
  33. }
  34. }
  35. void faultDiagArmIntentionalReset()
  36. {
  37. NRF_POWER->GPREGRET2 = G2_INTENT;
  38. }
  39. // Live stage + loop heartbeat. The post-reset GPREGRET2 breadcrumb only works if the bootloader preserves
  40. // GPREGRET2 across a watchdog reset -- some clone boards clear it, so it reads blank. These LIVE values don't
  41. // depend on surviving a reset: the WebUSB blob send runs on the USB SOF interrupt (independent of loop()), so
  42. // when loop() wedges, the SOF drain can still report the current stage + how long loop() has been stuck,
  43. // straight to the panel during the ~8s before the watchdog fires.
  44. static volatile uint8_t g_curStage = 0xFF;
  45. static volatile uint32_t g_loopBeatMs = 0;
  46. void faultDiagSetStage(uint8_t stage)
  47. {
  48. g_curStage = stage;
  49. NRF_POWER->GPREGRET2 = (uint8_t)(G2_STAGE_FLAG | (stage & 0x1Fu));
  50. }
  51. void faultDiagBeat()
  52. {
  53. g_loopBeatMs = millis();
  54. }
  55. uint8_t faultDiagCurStage()
  56. {
  57. return g_curStage;
  58. }
  59. uint32_t faultDiagStallMs()
  60. {
  61. // ms since loop() last beat. ~0 when healthy (loop beats ~250x/s); grows while loop() is stuck.
  62. return (uint32_t)(millis() - g_loopBeatMs);
  63. }
  64. const char *faultDiagStageStr(uint8_t s)
  65. {
  66. return s < STAGE_COUNT ? STAGE_STR[s] : "?";
  67. }
  68. uint8_t faultDiagHangStage()
  69. {
  70. return g_hangStage;
  71. }
  72. const char *faultDiagHangStageStr()
  73. {
  74. return g_hangStage < STAGE_COUNT ? STAGE_STR[g_hangStage] : "n/a";
  75. }
  76. void faultDiagBoot()
  77. {
  78. uint32_t rr =
  79. readResetReason(); // latched + cleared by the core's init()
  80. uint8_t g2 = (uint8_t)NRF_POWER->GPREGRET2;
  81. NRF_POWER->GPREGRET2 = 0; // consume the marker for this boot cycle
  82. g_resetReas = rr;
  83. // If this boot follows a watchdog/lockup reset and GPREGRET2 holds a stage breadcrumb (0x80|stage), recover
  84. // which loop stage was stuck. Only meaningful for hangs -- an intentional reboot/HardFault stamps its own
  85. // marker over the breadcrumb, and a clean power-on zeroes it.
  86. bool isHang = (rr & POWER_RESETREAS_DOG_Msk) ||
  87. (rr & POWER_RESETREAS_LOCKUP_Msk);
  88. if (isHang && (g2 & G2_STAGE_FLAG) && g2 < G2_INTENT)
  89. g_hangStage = (uint8_t)(g2 & 0x1Fu);
  90. else
  91. g_hangStage = 0xFF;
  92. uint8_t reason;
  93. // Precedence: a physical pin reset / watchdog / lockup is unambiguous from RESETREAS; only a SREQ needs the
  94. // GPREGRET2 marker to split intentional reboot vs HardFault.
  95. if (rr & POWER_RESETREAS_RESETPIN_Msk)
  96. reason = RR_PIN;
  97. else if (rr & POWER_RESETREAS_DOG_Msk)
  98. reason = RR_WATCHDOG;
  99. else if (rr & POWER_RESETREAS_LOCKUP_Msk)
  100. reason = RR_LOCKUP;
  101. else if (rr & POWER_RESETREAS_SREQ_Msk)
  102. reason = (g2 == G2_FAULT) ? RR_HARDFAULT :
  103. (g2 == G2_INTENT) ? RR_REBOOT :
  104. RR_SOFT;
  105. else if (rr & POWER_RESETREAS_OFF_Msk)
  106. reason = RR_WAKE;
  107. else if (rr == 0)
  108. reason = RR_POWERON; // all bits clear == power-on / brownout
  109. else
  110. reason = RR_UNKNOWN;
  111. g_reason = reason;
  112. Serial.printf(
  113. "# reset cause: %s (RESETREAS=0x%08lX gpregret2=0x%02X)\n",
  114. REASON_STR[reason], (unsigned long)rr, g2);
  115. if (g_hangStage != 0xFF)
  116. Serial.printf("# hang stage: %s (%u)\n", faultDiagHangStageStr(),
  117. g_hangStage);
  118. }
  119. // ---- clock fingerprint -------------------------------------------------------------------------------------
  120. static uint8_t g_clkLf = 0, g_clkHf = 0;
  121. static uint16_t g_clkUsPerMs = 0;
  122. void clockDiagBoot()
  123. {
  124. // LFCLKSTAT: bit16 STATE(running), bits1:0 SRC (0=RC,1=Xtal,2=Synth). HFCLKSTAT: bit16 STATE, bit0 SRC
  125. // (0=RC,1=Xtal). The bare-metal radio needs HFXO; if HF shows RC here, RF runs on the 64 MHz internal RC.
  126. uint32_t lf = NRF_CLOCK->LFCLKSTAT;
  127. uint32_t hf = NRF_CLOCK->HFCLKSTAT;
  128. bool lfrun = lf & CLOCK_LFCLKSTAT_STATE_Msk;
  129. uint8_t lfsrc = (uint8_t)(lf & CLOCK_LFCLKSTAT_SRC_Msk);
  130. g_clkLf = lfrun ? (uint8_t)(lfsrc + 1) : 0; // 1=RC,2=Xtal,3=Synth, 0=stopped
  131. bool hfxtal = hf & CLOCK_HFCLKSTAT_SRC_Msk;
  132. g_clkHf = hfxtal ? 2 : 0; // 2=crystal, 0=RC
  133. Serial.printf("# clock: LFCLK=%s HFCLK=%s\n",
  134. g_clkLf == 2 ? "xtal" :
  135. g_clkLf == 1 ? "RC" :
  136. g_clkLf == 3 ? "synth" :
  137. "stopped",
  138. g_clkHf == 2 ? "xtal" : "RC");
  139. }
  140. void clockDiagTick()
  141. {
  142. // Cross-check the two time bases: count micros() (HFCLK-derived) elapsed over a ~1s millis() (LFCLK/RTC)
  143. // window. usPerMs = micros-delta / millis-delta; 1000 = the clocks agree. A clone whose HFCLK runs fast vs
  144. // its LFCLK reads >1000 here -- the exact signature behind "poll rate too high, delivered too low".
  145. static uint32_t u0 = 0;
  146. static unsigned long m0 = 0;
  147. static bool init = false;
  148. unsigned long m = millis();
  149. if (!init) {
  150. u0 = micros();
  151. m0 = m;
  152. init = true;
  153. return;
  154. }
  155. unsigned long dm = m - m0;
  156. if (dm >= 1000) {
  157. uint32_t du = (uint32_t)(micros() - u0);
  158. g_clkUsPerMs = (uint16_t)(du / dm);
  159. u0 = micros();
  160. m0 = m;
  161. }
  162. }
  163. uint8_t clockLfSrc()
  164. {
  165. return g_clkLf;
  166. }
  167. uint8_t clockHfSrc()
  168. {
  169. return g_clkHf;
  170. }
  171. uint16_t clockUsPerMs()
  172. {
  173. return g_clkUsPerMs;
  174. }
  175. uint8_t faultDiagReason()
  176. {
  177. return g_reason;
  178. }
  179. uint32_t faultDiagResetReas()
  180. {
  181. return g_resetReas;
  182. }
  183. const char *faultDiagReasonStr()
  184. {
  185. return REASON_STR[g_reason < RR_COUNT ? g_reason : 0];
  186. }