diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index 68d22c369..071a2c0ac 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -339,8 +339,9 @@ receiver's setting does not lengthen packets sent by other nodes. `get radio2.timing` (alias `get radio.timing`) reports the active pair in `radio,radio2` order: dwell in chirps, recommended preambles in symbols, and the switching/loop allowances. Before calibration it reports `timing self-test -pending` and the provisional preambles. On the T1000-E, the Full Companion now -uses RTC2 for both retune timing and scan visits (about 31 us resolution). +pending` and the provisional preambles. On nRF52 boards, enabling `radio2` +uses RTC2 for both retune timing and scan visits (about 31 us resolution); +turning `radio2` off stops RTC2 and returns to the usual `micros()` clock. Previously, the nRF52 `micros()` fallback counted 1,024 Hz RTOS ticks: a real sub-millisecond retune could be reported as exactly 977 us, and two ticks as 1,953 us. Those old numbers are timer artifacts, not measured RF costs. diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index b4858665e..1de223ce1 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -24,32 +24,44 @@ static volatile uint8_t state = STATE_IDLE; // 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. RTC2 is otherwise unused on the T1000-E and -// provides a continuous 32768 Hz clock without changing micros() globally or -// holding the high-frequency clock on for this measurement. -static uint32_t profileTimestamp() { -#if defined(T1000_E) && defined(NRF52_PLATFORM) - static bool started = false; - if (!started) { +// 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. +static void syncProfileClock(bool dual_profile) { +#if defined(NRF52_PLATFORM) + static bool active = false; + if (active == dual_profile) return; + if (dual_profile) { NRF_RTC2->TASKS_STOP = 1; NRF_RTC2->PRESCALER = 0; NRF_RTC2->TASKS_CLEAR = 1; NRF_RTC2->TASKS_START = 1; - started = true; + } else { + NRF_RTC2->TASKS_STOP = 1; } - return NRF_RTC2->COUNTER; + active = dual_profile; #else - return micros(); + (void) dual_profile; #endif } -static uint32_t profileElapsedUs(uint32_t since, uint32_t now) { -#if defined(T1000_E) && defined(NRF52_PLATFORM) +static uint32_t profileTimestamp(bool dual_profile) { + syncProfileClock(dual_profile); +#if defined(NRF52_PLATFORM) + if (dual_profile) return NRF_RTC2->COUNTER; +#endif + return micros(); +} + +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((uint64_t(ticks) * 15625 + 511) >> 9); #else + (void) dual_profile; return now - since; #endif } @@ -66,7 +78,7 @@ void setFlag(void) { } void RadioLibWrapper::begin() { - (void) profileTimestamp(); + syncProfileClock(_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. @@ -288,7 +300,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw, } } - _profile_visit_stamp = profileTimestamp(); + _profile_visit_stamp = profileTimestamp(_profiles.enabled()); return success ? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED; } @@ -321,7 +333,8 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) { if (!_profile_refresh_required && _active_profile == profile && _profile_generation == _profiles.generation[profile] && _physical_preamble == preamble) return mesh::RadioParamApplyResult::APPLIED; if (!validateProfile(p)) return mesh::RadioParamApplyResult::FAILED; - const uint32_t started = profileTimestamp(); + const bool dual_profile = _profiles.enabled(); + const uint32_t started = profileTimestamp(dual_profile); beginProfileRetune(isInRecvMode() && !_rx_ps_armed && !_rx_ps_enabled); const uint8_t resume = beginReconfigure(); if (resume > 1) { @@ -364,13 +377,13 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) { if (restored) endReconfigure(resume); if (!restored || (resume && !isInRecvMode())) _profile_refresh_required = true; } - _profile_visit_stamp = profileTimestamp(); + _profile_visit_stamp = profileTimestamp(dual_profile); // A profile retune re-enters RX on a different tuple. Do not read its // instantaneous RSSI until the chip family's settle interval has elapsed. if (applied && _nf_refresh_requested) { _nf_sample_from = millis() + NF_CALIB_SETTLE_MS; } - const uint32_t elapsed = profileElapsedUs(started, _profile_visit_stamp); + const uint32_t elapsed = profileElapsedUs(started, _profile_visit_stamp, dual_profile); if (elapsed > _profiles.longest_switch_us) _profiles.longest_switch_us = elapsed; // Require four good RX-to-RX hops in each direction before using the budget, // then continue raising the observed maximum as later good hops run. @@ -403,6 +416,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::tryRestoreCodingRate(uint8_t cr) { } void RadioLibWrapper::serviceProfileScan() { + syncProfileClock(_profiles.enabled()); if (_cw_active) return; if (!_params_valid || (_profile_retry_at && (int32_t)(millis() - _profile_retry_at) < 0)) return; _profile_retry_at = 0; @@ -477,13 +491,13 @@ void RadioLibWrapper::serviceProfileScan() { && visit_us < NF_CALIB_SETTLE_MS * 1000UL) { visit_us = NF_CALIB_SETTLE_MS * 1000UL; } - if (profileElapsedUs(_profile_visit_stamp, profileTimestamp()) >= visit_us) target ^= 1; + if (profileElapsedUs(_profile_visit_stamp, profileTimestamp(true), true) >= visit_us) target ^= 1; } const auto result = tuneProfile(target); if (restart_scan && result == mesh::RadioParamApplyResult::APPLIED) { _profile_scan_generation[0] = _profiles.generation[0]; _profile_scan_generation[1] = _profiles.generation[1]; - _profile_visit_stamp = profileTimestamp(); + _profile_visit_stamp = profileTimestamp(true); } if (result == mesh::RadioParamApplyResult::FAILED) { // Retry a rejected or failing profile at a bounded rate; retain primary RX. @@ -935,6 +949,7 @@ bool RadioLibWrapper::serviceCarrierWave() { } void RadioLibWrapper::loop() { + syncProfileClock(_profiles.enabled()); if (serviceCarrierWave()) return; if (_profiles.enabled()) { const unsigned long now = millis(); @@ -1081,7 +1096,7 @@ void RadioLibWrapper::startRecv() { int err = startReceiveMode(); if (err == RADIOLIB_ERR_NONE) { // A very short frame may complete while startReceiveMode() returns. - _profile_visit_stamp = profileTimestamp(); + _profile_visit_stamp = profileTimestamp(_profiles.enabled()); // Retain that RX interrupt instead of overwriting it with software state. noInterrupts(); state = (state & STATE_INT_READY) | STATE_RX; diff --git a/test/test_radio_profile_clock.py b/test/test_radio_profile_clock.py new file mode 100644 index 000000000..f0eed6101 --- /dev/null +++ b/test/test_radio_profile_clock.py @@ -0,0 +1,74 @@ +"""Exercise the production profile clock on and off both nRF52 and other boards.""" +from pathlib import Path +import os +import subprocess +import tempfile +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("// this function is called", 1)[0] + +HARNESS = r''' +#include +#include +struct FakeRtc { + uint32_t TASKS_STOP = 0, PRESCALER = 9, TASKS_CLEAR = 0, TASKS_START = 0; + uint32_t COUNTER = 0; +} rtc; +#define NRF_RTC2 (&rtc) +uint32_t micros() { return 1234; } +@CLOCK@ +int main() { + assert(profileTimestamp(false) == 1234); + assert(rtc.TASKS_START == 0 && rtc.TASKS_STOP == 0); + assert(profileElapsedUs(12, 42, false) == 30); +#if defined(NRF52_PLATFORM) + rtc.COUNTER = 100; + assert(profileTimestamp(true) == 100); + assert(rtc.TASKS_STOP == 1 && rtc.PRESCALER == 0); + assert(rtc.TASKS_CLEAR == 1 && rtc.TASKS_START == 1); + rtc.TASKS_CLEAR = rtc.TASKS_START = rtc.TASKS_STOP = 0; + assert(profileTimestamp(true) == 100); + 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); + 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); +#else + assert(profileTimestamp(true) == 1234); + assert(profileElapsedUs(12, 42, true) == 30); + assert(rtc.TASKS_START == 0 && rtc.TASKS_STOP == 0); +#endif +} +''' + + +class ProfileClockTest(unittest.TestCase): + def test_clock_selection_and_lifecycle(self): + with tempfile.TemporaryDirectory() as folder: + cpp = Path(folder) / "test.cpp" + exe = Path(folder) / "test.exe" + cpp.write_text(HARNESS.replace("@CLOCK@", CLOCK)) + for defines in ([], ["-DNRF52_PLATFORM=1"]): + result = subprocess.run( + [os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", + "-Werror", *defines, str(cpp), "-o", str(exe)], + capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + result = subprocess.run([str(exe)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_radio_profile_scan.py b/test/test_radio_profile_scan.py index 3aec970b1..116ff5811 100644 --- a/test/test_radio_profile_scan.py +++ b/test/test_radio_profile_scan.py @@ -24,8 +24,10 @@ namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; } static uint64_t elapsed_us; static uint8_t state; uint32_t micros() { return (uint32_t)elapsed_us; } -uint32_t profileTimestamp() { return micros(); } -uint32_t profileElapsedUs(uint32_t since, uint32_t now) { return now - since; } +static bool profile_clock_active=false; +void syncProfileClock(bool dual_profile) { profile_clock_active=dual_profile; } +uint32_t profileTimestamp(bool) { 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; } }; using CustomSX1262 = Chip; @@ -59,7 +61,7 @@ struct RadioLibWrapper { state=STATE_RX; elapsed_us=100000; auto& p=_profiles.primary; p.freq=_cur_freq; p.bw=_cur_bw; p.sf=_cur_sf; p.cr=_cur_cr; - _profile_visit_stamp=profileTimestamp(); + _profile_visit_stamp=profileTimestamp(_profiles.enabled()); } bool isChipBusy() { return busy; } bool isInRecvMode() const { return (state & ~STATE_INT_READY) == STATE_RX; } @@ -71,7 +73,7 @@ struct RadioLibWrapper { void startRecv() { if (failRxStarts) { --failRxStarts; state=STATE_IDLE; return; } if (_profile_rxps_suspended) assert(chip.standbyXOSC && !_rx_ps_enabled); - state=STATE_RX; _profile_visit_stamp=profileTimestamp(); _rx_ps_armed=_rx_ps_enabled; + state=STATE_RX; _profile_visit_stamp=profileTimestamp(_profiles.enabled()); _rx_ps_armed=_rx_ps_enabled; } void stopReceiveDutyCycle() { _rx_ps_armed=false; } bool applyParams(float f,float,uint8_t,uint8_t) { ++applies; elapsed_us+=switch_apply_us; return !fail || f==909.5f; } @@ -100,6 +102,15 @@ struct RadioLibWrapper { @METHODS@ int main() { using Result=mesh::RadioParamApplyResult; + { + RadioLibWrapper w; + w.enable(); + assert(profile_clock_active); + w._cw_active=true; + w._profiles.setSecondary({},false); + w.serviceProfileScan(); + assert(!profile_clock_active); // even a carrier-wave early return stops RTC2 + } { RadioLibWrapper w; w.enable(); assert(w._profiles.switchTestReady()); @@ -275,7 +286,7 @@ int main() { w.enable(); w._floor_estimator.add(-100, millis()); w._active_profile=0; - w._profile_visit_stamp=profileTimestamp(); + w._profile_visit_stamp=profileTimestamp(w._profiles.enabled()); const auto primary_visit=w._profiles.listenUs(0); elapsed_us += primary_visit - 1; w.serviceProfileScan(); assert(w._active_profile==0); elapsed_us++; w.serviceProfileScan(); assert(w._active_profile==1);