mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 18:02:29 +00:00
Use nRF52 profile clock only for dual-radio scanning
This commit is contained in:
1 parent
1e1ee2618a
commit
69c2e4855f
4 files changed
+128
-27
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user