Extend nRF52 radio profile timing across RTC wraps

This commit is contained in:
mikecarper committed 2026-10-19 18:53:33 -07:00
1 parent ca67494f8c
commit 8038715ffe
5 files changed
+65 -27

No files matched your search

+1
View File
@@ -589,6 +589,7 @@ jobs:
python3 -B test/test_radio_profile_spiffs_missing.py -v
python3 -B test/test_radio_reply_mutation.py
python3 -B test/test_radio_profile_scan.py
python3 -B test/test_radio_profile_clock.py -v
python3 -B test/test_radio_profile_runtime_recommendation.py -v
python3 -B test/test_radio_profile_idle_dispatcher.py -v
python3 -B test/test_radio_rxps_rejection.py -v
+28 -16
View File
@@ -56,12 +56,13 @@ static RadioInterruptEvents radio_interrupt_events[RADIO_INTERRUPT_SLOT_COUNT];
static RadioLibWrapper* radio_interrupt_owners[RADIO_INTERRUPT_SLOT_COUNT] = {};
// The Adafruit nRF52 core's micros() falls back to 1024 Hz RTOS ticks when
// DWT is disabled. A sub-millisecond retune then appears to take exactly
// 977 us (or 1953 us across two ticks), making the learned switch allowance
// and scan dwell inaccurate. On nRF52, RTC2 provides a 32768 Hz profile clock
// without changing micros() globally or holding the high-frequency clock on.
// Keep it stopped in the ordinary single-profile mode.
// Extend the 24-bit RTC counter on each loop poll. A valid explicit preamble
// can require a visit longer than its 512-second hardware wrap period.
#if defined(NRF52_PLATFORM)
static uint32_t profile_clock_raw = 0;
static uint32_t profile_clock_ticks = 0;
#endif
static void syncProfileClock(bool dual_profile) {
#if defined(NRF52_PLATFORM)
static bool active = false;
@@ -69,7 +70,10 @@ static void syncProfileClock(bool dual_profile) {
if (dual_profile) {
NRF_RTC2->TASKS_STOP = 1;
NRF_RTC2->PRESCALER = 0;
NRF_RTC2->TASKS_CLEAR = 1;
// Keep the retained hardware count: an asynchronous clear could otherwise
// look like a wrap before its first poll.
profile_clock_raw = NRF_RTC2->COUNTER & 0x00ffffffUL;
profile_clock_ticks = 0;
NRF_RTC2->TASKS_START = 1;
} else {
NRF_RTC2->TASKS_STOP = 1;
@@ -83,7 +87,12 @@ static void syncProfileClock(bool dual_profile) {
static uint32_t profileTimestamp(bool dual_profile) {
syncProfileClock(dual_profile);
#if defined(NRF52_PLATFORM)
if (dual_profile) return NRF_RTC2->COUNTER;
if (dual_profile) {
const uint32_t raw = NRF_RTC2->COUNTER & 0x00ffffffUL;
profile_clock_ticks += (raw - profile_clock_raw) & 0x00ffffffUL;
profile_clock_raw = raw;
return profile_clock_ticks;
}
#endif
return micros();
}
@@ -91,10 +100,9 @@ static uint32_t profileTimestamp(bool dual_profile) {
static uint32_t profileElapsedUs(uint32_t since, uint32_t now, bool dual_profile) {
#if defined(NRF52_PLATFORM)
if (!dual_profile) return now - since;
// RTC2 is 24-bit and wraps every 512 seconds. Active scanning samples it
// many times per second; a freshly configured profile gets a new visit stamp.
const uint32_t ticks = (now - since) & 0x00ffffffUL;
return static_cast<uint32_t>((uint64_t(ticks) * 15625 + 511) >> 9);
const uint32_t ticks = now - since;
const uint64_t elapsed = (uint64_t(ticks) * 15625 + 511) >> 9;
return elapsed > UINT32_MAX ? UINT32_MAX : static_cast<uint32_t>(elapsed);
#else
(void) dual_profile;
return now - since;
@@ -177,7 +185,7 @@ void RadioLibWrapper::begin() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return;
#endif
syncProfileClock(_profiles.enabled());
(void)profileTimestamp(_profiles.enabled());
#ifdef USE_CC310_HW_CRYPTO
// Initialize CryptoCell once from normal task context. The session helper
// still initializes lazily if an earlier crypto operation runs first.
@@ -800,7 +808,7 @@ void RadioLibWrapper::serviceProfileScan() {
#if defined(NRF52_PLATFORM)
_profile_idle_visit_ticks = 0; // Every deferred/restarted scan fails closed.
#endif
syncProfileClock(_profiles.enabled());
(void)profileTimestamp(_profiles.enabled());
if (_cw_active) return;
if (!_params_valid || (_profile_retry_at && (int32_t)(millis() - _profile_retry_at) < 0)) return;
_profile_retry_at = 0;
@@ -942,7 +950,11 @@ bool RadioLibWrapper::getProfileIdleDelay(uint32_t& delay_ms) const {
#if MESH_ENABLE_MORSE_RX
if (_morse_rx_owned) return false;
#endif
const uint32_t elapsed = (NRF_RTC2->COUNTER - _profile_idle_stamp) & 0x00ffffffUL;
// Use the extended domain without mutating the clock in this final sleep
// check. The scanner already sampled it before publishing this idle proof.
const uint32_t stamp = profile_clock_ticks
+ ((NRF_RTC2->COUNTER - profile_clock_raw) & 0x00ffffffUL);
const uint32_t elapsed = stamp - _profile_idle_stamp;
// RTC1 wakes the ready sketch task at 1024 Hz (32 RTC2 ticks). Retain
// another 16 ticks (488 us) for return-to-loop work, validated on RAK3401.
// Short visits poll; neither a new timer nor RF duty cycling is needed.
@@ -1868,7 +1880,7 @@ void RadioLibWrapper::loop() {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return;
#endif
syncProfileClock(_profiles.enabled());
(void)profileTimestamp(_profiles.enabled());
#if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX
if (serviceCarrierWave()) return;
#endif
+9 -1
View File
@@ -18,6 +18,7 @@ HARNESS = r'''
#define MESH_ENABLE_MORSE_RX 1
struct Rtc { uint32_t COUNTER = 200; } rtc;
#define NRF_RTC2 (&rtc)
uint32_t profile_clock_raw=0, profile_clock_ticks=0;
struct NRF52Board {
static bool ready;
static bool canIdleRadioProfile() { return ready; }
@@ -54,6 +55,13 @@ int main() {
assert(!w.getProfileIdleDelay(delay) && delay==0);
#else
assert(w.getProfileIdleDelay(delay) && delay==26);
// Identical raw RTC values after a full hardware wrap must not resurrect
// an old idle proof. Both stamps use the software-extended tick domain.
profile_clock_ticks=0x01000000UL;
assert(!w.getProfileIdleDelay(delay));
w._profile_visit_stamp=w._profile_idle_stamp=0x01000064UL;
assert(w.getProfileIdleDelay(delay) && delay==26);
profile_clock_ticks=0;w=RadioLibWrapper();
// Exact early-wake boundary, rounding, already expired, and RTC wrap.
for (uint32_t remaining=0; remaining<=48; ++remaining) {
rtc.COUNTER=100+1000-remaining;
@@ -63,7 +71,7 @@ int main() {
assert(w.getProfileIdleDelay(delay) && delay==1);
rtc.COUNTER=1101;
assert(!w.getProfileIdleDelay(delay));
w._profile_visit_stamp=w._profile_idle_stamp=0x00fffff0;
w._profile_visit_stamp=w._profile_idle_stamp=0xfffffff0;
rtc.COUNTER=0x10;
assert(w.getProfileIdleDelay(delay) && delay==29);
w=RadioLibWrapper(); rtc.COUNTER=200;
+26 -9
View File
@@ -8,9 +8,8 @@ import unittest
ROOT = Path(__file__).resolve().parents[1]
SOURCE = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
CLOCK = "static void syncProfileClock(bool dual_profile) {" + SOURCE.split(
"static void syncProfileClock(bool dual_profile) {", 1
)[1].split("\nstatic\n#if defined(ESP8266)", 1)[0]
CLOCK = SOURCE[SOURCE.index("// Extend the 24-bit RTC counter"):].split(
"\nstatic\n#if defined(ESP8266)", 1)[0]
HARNESS = r'''
#include <cassert>
@@ -28,22 +27,40 @@ int main() {
assert(profileElapsedUs(12, 42, false) == 30);
#if defined(NRF52_PLATFORM)
rtc.COUNTER = 100;
assert(profileTimestamp(true) == 100);
assert(profileTimestamp(true) == 0);
assert(rtc.TASKS_STOP == 1 && rtc.PRESCALER == 0);
assert(rtc.TASKS_CLEAR == 1 && rtc.TASKS_START == 1);
assert(rtc.TASKS_CLEAR == 0 && rtc.TASKS_START == 1);
rtc.TASKS_CLEAR = rtc.TASKS_START = rtc.TASKS_STOP = 0;
assert(profileTimestamp(true) == 100);
assert(profileTimestamp(true) == 0);
assert(rtc.TASKS_CLEAR == 0 && rtc.TASKS_START == 0 && rtc.TASKS_STOP == 0);
assert(profileElapsedUs(0, 32768, true) == 1000000);
assert(profileElapsedUs(0x00fffff0, 0x10, true) == 977);
assert(profileElapsedUs(0xfffffff0, 0x10, true) == 977);
uint32_t stamp=0;
for(unsigned second=1;second<=532;++second) {
rtc.COUNTER=(rtc.COUNTER+32768)&0x00ffffffUL;
stamp=profileTimestamp(true);
assert(stamp==second*32768UL);
assert(profileElapsedUs(0,stamp,true)==second*1000000UL);
}
assert(stamp>0x00ffffffUL); // A valid visit can exceed the hardware period.
assert(profileElapsedUs(0,UINT32_MAX,true)==UINT32_MAX);
syncProfileClock(false);
assert(rtc.TASKS_STOP == 1 && profileTimestamp(false) == 1234);
rtc.TASKS_CLEAR = rtc.TASKS_START = rtc.TASKS_STOP = 0;
syncProfileClock(false);
assert(rtc.TASKS_CLEAR == 0 && rtc.TASKS_START == 0 && rtc.TASKS_STOP == 0);
rtc.COUNTER = 200;
assert(profileTimestamp(true) == 200);
assert(rtc.TASKS_CLEAR == 1 && rtc.TASKS_START == 1);
assert(profileTimestamp(true) == 0);
assert(rtc.TASKS_CLEAR == 0 && rtc.TASKS_START == 1);
for(unsigned chunk=0;chunk<511;++chunk) {
rtc.COUNTER=(rtc.COUNTER+256UL*32768)&0x00ffffffUL;
profileTimestamp(true);
}
rtc.COUNTER=(rtc.COUNTER+255UL*32768)&0x00ffffffUL;
const auto before=profileTimestamp(true);assert(before==0xffff8000UL);
rtc.COUNTER=(rtc.COUNTER+32768)&0x00ffffffUL;
const auto wrapped=profileTimestamp(true);assert(wrapped==0);
assert(profileElapsedUs(before,wrapped,true)==1000000UL);
#else
assert(profileTimestamp(true) == 1234);
assert(profileElapsedUs(12, 42, true) == 30);
+1 -1
View File
@@ -26,7 +26,7 @@ static uint64_t elapsed_us;
uint32_t micros() { return (uint32_t)elapsed_us; }
static bool profile_clock_active=false;
void syncProfileClock(bool dual_profile) { profile_clock_active=dual_profile; }
uint32_t profileTimestamp(bool) { return micros(); }
uint32_t profileTimestamp(bool dual) { syncProfileClock(dual); return micros(); }
uint32_t profileElapsedUs(uint32_t since, uint32_t now, bool) { return now - since; }
uint32_t millis() { return (uint32_t)(elapsed_us / 1000); }
struct Chip { bool standbyXOSC=false; int standby() { return 0; } };