Use nRF52 profile clock only for dual-radio scanning

This commit is contained in:
mikecarper committed 2026-09-23 15:42:12 -07:00
1 parent 1e1ee2618a
commit 69c2e4855f
4 files changed
+128 -27

No files matched your search

+3 -2
View File
@@ -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.
+35 -20
View File
@@ -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<uint32_t>((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;
+74
View File
@@ -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 <cassert>
#include <cstdint>
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()
+16 -5
View File
@@ -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);