rf_timeslot.cpp 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. #include "rf_timeslot.h"
  2. #include <Arduino.h>
  3. #include <nrf_soc.h>
  4. #include <nrf_sdm.h>
  5. volatile bool g_sdEnabled = false;
  6. volatile uint16_t g_tsGrants = 0, g_tsExtends = 0, g_tsStarved = 0;
  7. // Slot geometry. 15ms base slots with 15ms chained extensions: long enough that the ~4.5ms worst-case ESB op
  8. // fits many times per slot, short enough that the first grant after BLE activity is quick. The margin is how
  9. // long before the true slot end rfRadioOwned() starts refusing new ops -- it must exceed the pessimistic gate
  10. // the callers pass (they gate on the WHOLE op, so only the last-op-start needs to clear the margin).
  11. #define TS_LEN_US 15000u
  12. #define TS_EXT_US 15000u
  13. #define TS_MARGIN_US 500u
  14. // EARLIEST-request timeout: how far in the future the SD may schedule us before giving up (then rfTsTick
  15. // re-arms). Generous -- heavy scanning can legitimately push us out several tens of ms.
  16. #define TS_TIMEOUT_US 100000u
  17. static volatile bool s_sessionOpen = false;
  18. // micros() deadline until which the radio is ours (margin already subtracted). 0 = no slot. Written by the
  19. // priority-0 signal handler, read from loop context -- single aligned 32-bit write, no lock needed.
  20. static volatile uint32_t s_ownedUntil = 0;
  21. // current slot length so far (base + granted extensions), for the TIMER0 end-guard compare
  22. static uint32_t s_slotLen = 0;
  23. static volatile uint32_t s_lastGrantUs =
  24. 0; // last slot-start micros() (starvation watchdog)
  25. static unsigned long s_lastKickMs = 0;
  26. static nrf_radio_request_t s_req;
  27. static nrf_radio_signal_callback_return_param_t s_ret;
  28. static void tsBuildRequest()
  29. {
  30. s_req.request_type = NRF_RADIO_REQ_TYPE_EARLIEST;
  31. s_req.params.earliest.hfclk = NRF_RADIO_HFCLK_CFG_XTAL_GUARANTEED;
  32. s_req.params.earliest.priority = NRF_RADIO_PRIORITY_NORMAL;
  33. s_req.params.earliest.length_us = TS_LEN_US;
  34. s_req.params.earliest.timeout_us = TS_TIMEOUT_US;
  35. }
  36. // Timeslot signal handler. PRIORITY 0 -- preempts everything, including the SoftDevice's own lower-priority
  37. // handlers. Keep it register-poke + volatile-stamp only: no SD calls except via the returned action, no RTOS,
  38. // no logging. micros() is safe (DWT cycle counter read).
  39. static nrf_radio_signal_callback_return_param_t *tsSignal(uint8_t sig)
  40. {
  41. s_ret.callback_action = NRF_RADIO_SIGNAL_CALLBACK_ACTION_NONE;
  42. switch (sig) {
  43. case NRF_RADIO_CALLBACK_SIGNAL_TYPE_START:
  44. // The SD hands us the radio with ITS interrupt config -- clear it, we poll (no RADIO signals).
  45. NRF_RADIO->INTENCLR = 0xFFFFFFFF;
  46. s_slotLen = TS_LEN_US;
  47. s_lastGrantUs = micros();
  48. s_ownedUntil = s_lastGrantUs + TS_LEN_US - TS_MARGIN_US;
  49. g_tsGrants++;
  50. // TIMER0 runs from 0 (1 MHz) inside the slot. CC0 = the end guard: hand the slot back BEFORE the
  51. // scheduled end (overstaying is an SD assert). Extensions push it out.
  52. NRF_TIMER0->CC[0] = s_slotLen - (TS_MARGIN_US / 2);
  53. NRF_TIMER0->EVENTS_COMPARE[0] = 0;
  54. NRF_TIMER0->INTENSET = TIMER_INTENSET_COMPARE0_Msk;
  55. // the SD forwards the TIMER0 IRQ to this handler as SIGNAL_TYPE_TIMER0 -- it must be
  56. // NVIC-enabled (canonical Nordic timeslot pattern)
  57. NVIC_EnableIRQ(TIMER0_IRQn);
  58. // chain an extension right away; keeps the radio ours while BLE is idle
  59. s_ret.callback_action = NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND;
  60. s_ret.params.extend.length_us = TS_EXT_US;
  61. break;
  62. case NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_SUCCEEDED:
  63. s_slotLen += TS_EXT_US;
  64. s_ownedUntil += TS_EXT_US;
  65. NRF_TIMER0->CC[0] = s_slotLen - (TS_MARGIN_US / 2);
  66. g_tsExtends++;
  67. // keep chaining -- the SD fails the extend when a BLE event needs the radio
  68. s_ret.callback_action = NRF_RADIO_SIGNAL_CALLBACK_ACTION_EXTEND;
  69. s_ret.params.extend.length_us = TS_EXT_US;
  70. break;
  71. case NRF_RADIO_CALLBACK_SIGNAL_TYPE_EXTEND_FAILED:
  72. // BLE needs the radio at the current end: ride out the remainder, the CC0 guard hands it back.
  73. break;
  74. case NRF_RADIO_CALLBACK_SIGNAL_TYPE_TIMER0:
  75. // End guard fired: stop claiming ownership and hand back, asking for the next slot in one action.
  76. NRF_TIMER0->INTENCLR = TIMER_INTENCLR_COMPARE0_Msk;
  77. NRF_TIMER0->EVENTS_COMPARE[0] = 0;
  78. s_ownedUntil = 0;
  79. s_ret.callback_action =
  80. NRF_RADIO_SIGNAL_CALLBACK_ACTION_REQUEST_AND_END;
  81. s_ret.params.request.p_next = &s_req;
  82. break;
  83. case NRF_RADIO_CALLBACK_SIGNAL_TYPE_RADIO:
  84. default:
  85. // RADIO signals only fire if something set INTENSET (we never do; START clears the SD's).
  86. break;
  87. }
  88. return &s_ret;
  89. }
  90. bool rfTsBegin()
  91. {
  92. if (s_sessionOpen)
  93. return true;
  94. tsBuildRequest();
  95. if (sd_radio_session_open(tsSignal) != NRF_SUCCESS)
  96. return false;
  97. if (sd_radio_request(&s_req) != NRF_SUCCESS) {
  98. sd_radio_session_close();
  99. return false;
  100. }
  101. s_sessionOpen = true;
  102. s_lastGrantUs = micros();
  103. return true;
  104. }
  105. void rfTsEnd()
  106. {
  107. if (!s_sessionOpen)
  108. return;
  109. s_ownedUntil = 0;
  110. s_sessionOpen = false;
  111. sd_radio_session_close(); // async; caller is about to disable the SD anyway
  112. }
  113. bool rfRadioOwned(uint32_t usNeeded)
  114. {
  115. if (!g_sdEnabled)
  116. return true; // bare-metal mode: the radio is unconditionally ours
  117. uint32_t d = s_ownedUntil;
  118. if (!d)
  119. return false;
  120. return (int32_t)(d - micros()) > (int32_t)usNeeded;
  121. }
  122. void rfTsTick()
  123. {
  124. if (!g_sdEnabled || !s_sessionOpen)
  125. return;
  126. // Starvation watchdog. A request that ends BLOCKED/CANCELED reports via an SOC event the Adafruit core's
  127. // SOC task swallows, so a dead session would never self-revive. If no slot has STARTED for a while and we
  128. // aren't inside one, fire a fresh request. Errors (one already pending) are expected and ignored.
  129. if (s_ownedUntil)
  130. return;
  131. uint32_t now = micros();
  132. if ((uint32_t)(now - s_lastGrantUs) < 20000u)
  133. return;
  134. unsigned long ms = millis();
  135. if (ms - s_lastKickMs < 10)
  136. return; // don't SVC-spam while starved
  137. s_lastKickMs = ms;
  138. g_tsStarved++;
  139. tsBuildRequest();
  140. (void)sd_radio_request(&s_req);
  141. }