Use precise T1000-E dual-profile timing

This commit is contained in:
mikecarper committed 2026-09-23 15:31:43 -07:00
1 parent 960936183c
commit d66b9336a9
11 files changed
+142 -74

No files matched your search

+4 -1
View File
@@ -2,12 +2,15 @@
Historical validation record: the nominal 600 us policy described below has
been replaced by the activation self-test and 10% guarded measured budget.
The later automatic policy also removed the pair-specific 88-symbol SF8/500
floor and the fixed 300 us loop allowance; the older calculations below remain
as test history.
See [radio profiles](radio_profiles.md#preamble) for
current behavior.
Implemented in the `keymindCascade` working tree, 2026-09-14. The initial
4.1/4000-us implementation record below is retained as history; the
[4.6/300-us policy update](#automatic-46-chirp--300-us-policy-update) is now current.
[4.6/300-us policy update](#automatic-46-chirp--300-us-policy-update) was a prior stage.
No radios were flashed or RF experiments started for either function-only change.
## Initial implementation scope
+3
View File
@@ -2,6 +2,9 @@
Historical validation record: the nominal 600 us switch allowance below has
been replaced by the activation self-test and a 10% guarded measured budget.
The current automatic policy also removed the fixed 300 us loop allowance and
the pair-specific SF8/500 88-symbol floor. Values below are retained as bench
history, not current automatic defaults.
See [radio profiles](radio_profiles.md#preamble) for current behavior.
The shared `RadioProfiles` function uses a 4.6-symbol minimum for both logical
+34 -20
View File
@@ -300,30 +300,31 @@ profile**, then spends the remaining budgeted time on the faster profile,
also with a **4.6-symbol floor**. Slower
means a longer LoRa symbol (`2^SF / bandwidth`), regardless of profile number.
The slower preamble sets the return deadline. The calculation allows two slow
visits within that preamble, reserving the self-tested switch time per hop and 0.3 ms of main-loop
margin. Fast-channel transmit preambles include sixteen acquisition symbols.
For a primary SF7 / 62.5 kHz profile with a *measured* 600 us guarded switch allowance:
visits within that preamble, reserving the self-tested, 10%-guarded switch time
per hop. There is no extra fixed main-loop allowance. Fast-channel transmit
preambles include sixteen acquisition symbols.
For a primary SF7 / 62.5 kHz profile with a 763 us measured maximum switch
(840 us after the 10% guard, from the BLE-on T1000-E trial):
| Secondary profile | Automatic secondary preamble | Previously tested explicit preamble | Primary preamble |
| --- | ---: | ---: | ---: |
| SF9 / 500 kHz | 32 | 32 | 32 |
| SF8 / 500 kHz | 88 | 64 | 32 |
| SF8 / 500 kHz | 40 | 64 | 32 |
| SF7 / 500 kHz | 64 | 80 | 32 |
With preamble 32 on that slower profile, its visit is 9.421 ms and the faster
visit is 21.847 ms, plus the actual switching time. Increasing the slow preamble
visit is 21.667 ms, plus the actual switching time. Increasing the slow preamble
also increases the time available on the fast channel. If the symbols are equal
in length, the primary profile goes first. Detected packets hold the current
channel until reception finishes.
The SF8 / 500 kHz value also retains the measured 88-symbol floor from the
production V4/XIAO tests, where 72 and 80 each missed a packet.
The explicit values come from the
[production validation](radio_profiles_validation.md): the final fast-channel
attempts each received 50/50 packets at 4.8 slow symbols. Automatic values use
the switching and loop-jitter allowances below.
The SF8 / 500 kHz value is calculated from the same formula as every other
profile; the older pair-specific 88-symbol floor has been removed. The explicit
values come from [historical production validation](radio_profiles_validation.md):
the final fast-channel attempts each received 50/50 packets at 4.8 slow symbols.
Automatic values use the measured switch allowance below.
Those explicit-preamble tests used the older 4.8-symbol policy; they do not
validate the new 4.6-symbol slow dwell / 0.3 ms reserve. The
validate the newer 4.6-symbol slow dwell. The
[4.6/0.3 ms two-profile bench](radio_dwell_policy_validation.md#hardware-context) received 197/200
at SF7/62.5 + SF8/500 with explicit 32-symbol preambles. The newer four-channel
5.1/6.1/7.7 tests also had misses, so this is a timing policy, not a claim of zero packet loss.
@@ -338,13 +339,27 @@ 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. For SF7/62.5 on `radio` and SF7/500 on
`radio2` after a 600 us guarded measurement:
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).
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.
For SF7/62.5 on `radio` and SF8/500 on `radio2`, one live BLE-on read returned:
```text
> chirps=4.60,85.34; need=32,64; switch=600us; loop=300us (estimate); test=4/4,4/4 ready; WARN recommended preamble: radio2=64
> chirps=4.60,42.84; need=32,40; switch=706us; loop=0us; test=4/4,4/4 ready; WARN recommended preamble: radio2=40
```
Later good hops in another BLE-on trial raised the measured maximum to 763 us
(840 us with the 10% guard); the recommendation stayed at 40 symbols.
The measured switch duration is wall time, so an interrupt during the hop is
included. Bluetooth's SoftDevice may still preempt application code; this
clock correction does not stop it. Ordinary Companion/USB work in the main
loop does not run inside the synchronous retune, but it can delay when the
next scan visit begins. The zero fixed loop allowance is not a guarantee of
lossless reception under arbitrary application load.
`WARN recommended preamble` identifies **each** profile whose recommendation
exceeds the standard 32 symbols or its explicitly selected value. For example,
with SF7/62.5 on the primary profile, `set radio2 910.5,500,7,5,rx,32`
@@ -383,14 +398,13 @@ T[i] = 2^SF[i] * 1000 / BW_kHz[i]
slow = profile with the longer T (radio wins ties)
L[slow] = max(requested_visit_us[slow], ceil(4.6 * T[slow]))
L[fast] = max(requested_visit_us[fast], ceil(4.6 * T[fast]))
O = 2 * switch_us + loop_margin_us
O = 2 * switch_us
C = L[radio] + L[radio2] + O
P[slow] = round_up_to_8(max(32, 2 * C / T[slow]))
P[fast] = round_up_to_8(max(32, (L[slow] + O) / T[fast] + 16))
```
The previous SF8/500 versus SF7/62.5 measured 88-symbol floor remains in force.
Automatic selection uses the minimum visits to choose a preamble, then allocates
the fast visit from half the slow preamble's duration. Diagnostics feed the
actual allocated visits back into the same function, including longer visits
@@ -406,11 +420,11 @@ fast_dwell = max(ceil(4.6 * T[fast]),
```
For the tested SF7/62.5 + SF8/500 pair with slow preamble 32, this yields
9,421 us slow and 21,847 us fast (42.67 fast chirps). Swapping `radio` and
`radio2` swaps the allocations automatically. The 0.3 ms reserve is not a sleep.
9,421 us slow and 21,197 us fast (41.40 fast chirps) with the 1,075 us guarded
switch allowance above. Swapping `radio` and `radio2` swaps the allocations automatically.
Production scheduling and automatic preambles use the **self-tested maximum
successful switch plus 10%**, together with 300 us per cycle. Subsequent
successful switch plus 10%**, without an additional fixed loop allowance. Subsequent
successful hops can only raise that allowance until the next profile change.
The measured recommendation is rounded up to eight symbols and capped at 128
for automatic settings. The calculator and warnings retain the *uncapped*
+10 -13
View File
@@ -42,8 +42,9 @@ class RadioProfiles {
static constexpr uint16_t MaxPreamble = 65528;
static constexpr uint16_t MaxAutomaticPreamble = 128;
// Successful RX-to-RX hops self-test the board-specific switch allowance.
// Packet receptions can extend a visit; these budgets describe an idle scan.
static constexpr uint32_t LoopBudgetUs = 300;
// Packet receptions can extend a visit. No fixed loop allowance is added to
// the self-tested, guarded switch time in the idle-scan timing model.
static constexpr uint32_t LoopBudgetUs = 0;
static constexpr uint8_t SwitchTestSamplesPerDirection = 4;
struct ChirpTiming {
@@ -148,21 +149,17 @@ class RadioProfiles {
return isfinite(symbol) && symbol > 0 && symbol <= UINT32_MAX / symbols
? (uint32_t)ceil(symbols * symbol) : 0;
}
static double preambleForVisits(const RadioProfileParams& p, const RadioProfileParams& other,
bool slow, double own_us, double other_us, double overhead_us) {
static double preambleForVisits(const RadioProfileParams& p, bool slow,
double own_us, double other_us, double overhead_us) {
const double symbol = symbolUs(p);
if (symbol <= 0) return 32;
double result = slow ? roundPreamble(2.0 * (own_us + other_us + overhead_us) / symbol)
return slow ? roundPreamble(2.0 * (own_us + other_us + overhead_us) / symbol)
: roundPreamble((other_us + overhead_us) / symbol + 2 * AcquisitionSymbols);
// Production V4/XIAO empirical floor; retain prior measured margin.
if (!slow && p.sf == 8 && p.bw == 500 && other.sf == 7 && other.bw == 62.5f && result < 88)
result = 88;
return result;
}
// A two-profile idle-scan timing policy, NOT a fitted zero-loss model.
// Use a 4.6-symbol minimum on both profiles. Reserve
// two return opportunities on the slow profile, and blind time + 16
// acquisition symbols on the fast one. The 4.6/300-us pair bench received
// acquisition symbols on the fast one. The 4.6-symbol pair bench received
// 197/200: a sweep fitting inside 32 is not a guarantee of acquisition.
// Explicit visit times let diagnostics account for the actual scheduler,
// including a longer fast visit selected by an explicit slow preamble.
@@ -180,8 +177,8 @@ class RadioProfiles {
t.switch_us = switch_us; t.loop_us = loop_us;
const double overhead = 2.0 * switch_us + loop_us;
t.cycle_us = double(t.listen_us[0]) + t.listen_us[1] + overhead;
t.preamble[0] = preambleForVisits(a, b, t.slow == 0, t.listen_us[0], t.listen_us[1], overhead);
t.preamble[1] = preambleForVisits(b, a, t.slow == 1, t.listen_us[1], t.listen_us[0], overhead);
t.preamble[0] = preambleForVisits(a, t.slow == 0, t.listen_us[0], t.listen_us[1], overhead);
t.preamble[1] = preambleForVisits(b, t.slow == 1, t.listen_us[1], t.listen_us[0], overhead);
t.valid = true;
return t;
}
@@ -190,7 +187,7 @@ class RadioProfiles {
// Keep the hot retune path to one profile's calculation; the full
// validated diagnostic structure is only needed by CLI/reporting.
const uint8_t slow = slowerProfile();
return preambleForVisits(params(profile), params(profile ^ 1), profile == slow,
return preambleForVisits(params(profile), profile == slow,
minimumListenUs(params(profile), profile == slow), minimumListenUs(params(profile ^ 1), (profile ^ 1) == slow),
2.0 * switchBudgetUs() + LoopBudgetUs);
}
+3 -2
View File
@@ -528,9 +528,10 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
radio_->profilePreamble(0), radio_->profilePreamble(1));
else if (!t.valid) snprintf(reply, capacity, "Error: timing unavailable");
else {
snprintf(reply, capacity, "> chirps=%.2f,%.2f; need=%.0f,%.0f; switch=%luus; loop=%luus (estimate)",
snprintf(reply, capacity, "> chirps=%.2f,%.2f; need=%.0f,%.0f; switch=%luus; loop=%luus%s",
t.listen_us[0] / t.symbol_us[0], t.listen_us[1] / t.symbol_us[1],
t.preamble[0], t.preamble[1], (unsigned long)t.switch_us, (unsigned long)t.loop_us);
t.preamble[0], t.preamble[1], (unsigned long)t.switch_us,
(unsigned long)t.loop_us, t.loop_us ? " (estimate)" : "");
size_t used = strlen(reply);
if (used < capacity) snprintf(reply + used, capacity - used, "; test=%u/%u,%u/%u%s",
p.switch_test_samples[0], RadioProfiles::SwitchTestSamplesPerDirection,
+44 -9
View File
@@ -21,6 +21,39 @@
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) {
NRF_RTC2->TASKS_STOP = 1;
NRF_RTC2->PRESCALER = 0;
NRF_RTC2->TASKS_CLEAR = 1;
NRF_RTC2->TASKS_START = 1;
started = true;
}
return NRF_RTC2->COUNTER;
#else
return micros();
#endif
}
static uint32_t profileElapsedUs(uint32_t since, uint32_t now) {
#if defined(T1000_E) && defined(NRF52_PLATFORM)
// 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
return now - since;
#endif
}
// this function is called when a complete packet
// is transmitted by the module
static
@@ -33,6 +66,7 @@ void setFlag(void) {
}
void RadioLibWrapper::begin() {
(void) profileTimestamp();
#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.
@@ -254,7 +288,7 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
}
}
_profile_visit_us = micros();
_profile_visit_stamp = profileTimestamp();
return success ? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED;
}
@@ -287,7 +321,7 @@ 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 = micros();
const uint32_t started = profileTimestamp();
beginProfileRetune(isInRecvMode() && !_rx_ps_armed && !_rx_ps_enabled);
const uint8_t resume = beginReconfigure();
if (resume > 1) {
@@ -330,16 +364,17 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
if (restored) endReconfigure(resume);
if (!restored || (resume && !isInRecvMode())) _profile_refresh_required = true;
}
_profile_visit_us = micros();
_profile_visit_stamp = profileTimestamp();
// 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 = _profile_visit_us - started;
const uint32_t elapsed = profileElapsedUs(started, _profile_visit_stamp);
if (elapsed > _profiles.longest_switch_us) _profiles.longest_switch_us = elapsed;
// Learn the actual board's RX-to-RX retune cost on the first four good hops
// in each direction. Deferred and rolled-back operations are not samples.
// 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.
// Deferred and rolled-back operations are not samples.
if (applied && resume && old_profile != profile) {
_profiles.sampleSwitch(old_profile, profile, elapsed);
}
@@ -442,13 +477,13 @@ void RadioLibWrapper::serviceProfileScan() {
&& visit_us < NF_CALIB_SETTLE_MS * 1000UL) {
visit_us = NF_CALIB_SETTLE_MS * 1000UL;
}
if ((uint32_t)(micros() - _profile_visit_us) >= visit_us) target ^= 1;
if (profileElapsedUs(_profile_visit_stamp, profileTimestamp()) >= 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_us = micros();
_profile_visit_stamp = profileTimestamp();
}
if (result == mesh::RadioParamApplyResult::FAILED) {
// Retry a rejected or failing profile at a bounded rate; retain primary RX.
@@ -1046,7 +1081,7 @@ void RadioLibWrapper::startRecv() {
int err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
// A very short frame may complete while startReceiveMode() returns.
_profile_visit_us = micros();
_profile_visit_stamp = profileTimestamp();
// Retain that RX interrupt instead of overwriting it with software state.
noInterrupts();
state = (state & STATE_INT_READY) | STATE_RX;
+1 -1
View File
@@ -81,7 +81,7 @@ protected:
uint16_t _physical_preamble = 0;
uint32_t _profile_generation = 0;
uint32_t _profile_scan_generation[2] = {};
uint32_t _profile_visit_us = 0;
uint32_t _profile_visit_stamp = 0;
uint32_t _profile_retry_at = 0;
bool _profile_rxps_suspended = false;
bool _profile_saved_rxps = false;
+25 -12
View File
@@ -11,7 +11,7 @@ int main() {
assert(Profiles::MinListenSymbols==4.6);
assert(Profiles::SlowListenSymbols==4.6);
assert(Profiles::MinFastListenSymbols==4.6);
assert(Profiles::LoopBudgetUs==300);
assert(Profiles::LoopBudgetUs==0);
auto a=params(10,125),b=params(10,125);
auto t=Profiles::calculateChirpTiming(a,b,1,0,1000,4000);
assert(t.valid && t.slow==0 && t.symbol_us[0]==8192);
@@ -31,9 +31,9 @@ int main() {
b=params(10,1e-30f);assert(Profiles::minimumListenUs(b)==0);
a=params(7,62.5);b=params(8,500);
t=Profiles::calculateChirpTiming(a,b);
assert(t.preamble[1]==88); // Existing measured acquisition floor retained.
assert(t.preamble[1]==40); // Derived from dwell and acquisition, not a pair-specific floor.
auto swapped=Profiles::calculateChirpTiming(b,a);
assert(swapped.slow==1 && swapped.preamble[0]==88);
assert(swapped.slow==1 && swapped.preamble[0]==40);
assert(swapped.preamble[1]==t.preamble[0]);
for (float bw0: {7.8f,62.5f,125.0f,250.0f,500.0f})
for (float bw1: {7.8f,62.5f,125.0f,250.0f,500.0f})
@@ -76,8 +76,8 @@ int main() {
for (unsigned n=0;n<Profiles::SwitchTestSamplesPerDirection;++n) {
p.sampleSwitch(0,1,545);p.sampleSwitch(1,0,545);
}
assert(p.listenUs(0)==9421 && p.listenUs(1)==21847);
assert(p.chirpTiming().switch_us==600 && p.chirpTiming().loop_us==300 && p.chirpTiming().preamble[1]==64);
assert(p.listenUs(0)==9421 && p.listenUs(1)==22147);
assert(p.chirpTiming().switch_us==600 && p.chirpTiming().loop_us==0 && p.chirpTiming().preamble[1]==64);
auto before=p.chirpTiming();const auto wire0=p.preamble(0,32),wire1=p.preamble(1,32);
p.sampleSwitch(0,1,8428);auto overrun=p.chirpTiming();
assert(overrun.switch_us==9271 && overrun.preamble[1]>before.preamble[1]);
@@ -97,23 +97,23 @@ int main() {
p.primary=params(7,62.5);p.primary.preamble=32;
p.secondary.params=params(8,500);p.secondary.params.preamble=32;
p.secondary.mode=mesh::RadioProfileMode::Rx;
assert(p.automaticPreambleFits() && p.listenUs(0)==9421 && p.listenUs(1)==21847);
assert(p.automaticPreambleFits() && p.listenUs(0)==9421 && p.listenUs(1)==22147);
assert(p.preamble(0,32)==32 && p.preamble(1,32)==32);
assert(p.chirpTiming().preamble[1]==88);
assert(p.chirpTiming().preamble[1]==40);
std::swap(p.primary,p.secondary.params);
assert(p.slowerProfile()==1 && p.listenUs(1)==9421 && p.listenUs(0)==21847);
assert(p.slowerProfile()==1 && p.listenUs(1)==9421 && p.listenUs(0)==22147);
p.secondary.params.preamble=8;
assert(!p.automaticPreambleFits());
// This formerly fit the 4.1 fast floor (1050 us), but not 4.6 (1178 us).
// With no fixed loop reserve, 27 symbols cannot cover both 4.6-symbol visits.
p.primary=params(7,500);p.secondary.params=params(7,500);
p.primary.preamble=30;
p.primary.preamble=27;
assert(p.listenUs(0)==1178 && p.listenUs(1)>=1178);
assert(!p.automaticPreambleFits());
p.primary.preamble=32;
assert(p.automaticPreambleFits() && p.listenUs(1)==1418);
assert(p.automaticPreambleFits() && p.listenUs(1)==1718);
// Long requested visits remain visible, not replaced by policy minima.
t=Profiles::calculateChirpTiming(params(7,62.5),params(8,500),20000,30000);
assert(t.listen_us[0]==20000 && t.listen_us[1]==30000 && t.cycle_us==50300);
assert(t.listen_us[0]==20000 && t.listen_us[1]==30000 && t.cycle_us==50000);
Profiles measured;
measured.primary=params(7,500);
measured.secondary.params=params(7,500);
@@ -138,4 +138,17 @@ int main() {
measured.setSecondary(replacement,true);
assert(!measured.switchTestReady() && measured.switchBudgetUs()==0);
assert(measured.preamble(0,32)==128 && measured.preamble(1,32)==88);
// A T1000-E-sized 977 us hop calibrates to 1075 us, yet SF8/500 no longer
// inherits the historical 88-symbol floor or a fixed 300 us loop pad.
Profiles t1000;
t1000.primary=params(7,62.5);
t1000.secondary.params=params(8,500);
t1000.secondary.mode=mesh::RadioProfileMode::RxTx;
for (unsigned n=0;n<Profiles::SwitchTestSamplesPerDirection;++n) {
t1000.sampleSwitch(0,1,977);t1000.sampleSwitch(1,0,977);
}
assert(t1000.switchBudgetUs()==1075);
assert(t1000.preamble(0,32)==32 && t1000.preamble(1,32)==40);
assert(t1000.chirpTiming().loop_us==0 && t1000.chirpTiming().preamble[1]==40);
}
+7 -7
View File
@@ -367,11 +367,11 @@ int main(int argc, char** argv) {
assert(strstr(f.reply,"switch=600us"));
assert(strstr(f.cmd("set radio2 910.5,500,7,5,rxtx"),"preamble=64;"));
assert(strstr(f.reply,"switch=600us"));
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,21847"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,85.34; need=32,64"));
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,22147"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,86.51; need=32,64"));
assert(strstr(f.reply,"switch=600us"));
assert(strstr(f.reply,"loop=300us"));
assert(strstr(f.reply,"estimate"));
assert(strstr(f.reply,"loop=0us"));
assert(!strstr(f.reply,"estimate"));
assert(strstr(f.reply,"WARN recommended preamble: radio2=64"));
char timing_reply[160]; strcpy(timing_reply, f.reply);
assert(!strcmp(f.cmd("get radio.timing"),timing_reply));
@@ -476,9 +476,9 @@ int main(int argc, char** argv) {
f.advance(2000);
f.finishTimingTest();
assert(f.radio.p.secondary.params.preamble==32);
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,21847"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,42.67; need=32,88"));
assert(strstr(f.reply,"loop=300us"));
assert(strstr(f.cmd("get radio2.scan"),"slow=radio; listen_us=9421,22147"));
assert(strstr(f.cmd("get radio2.timing"),"chirps=4.60,43.26; need=32,40"));
assert(strstr(f.reply,"loop=0us"));
}
{
Fixture f;
+5 -5
View File
@@ -699,15 +699,15 @@ TEST(RadioProfiles, AutomaticPreamblesUseSelfTestedSwitchingMargin) {
p.sampleSwitch(0, 1, 545); p.sampleSwitch(1, 0, 545);
}
EXPECT_EQ(600, p.switchBudgetUs());
EXPECT_EQ(300, p.LoopBudgetUs);
const uint16_t expected[] = {64, 88, 32};
EXPECT_EQ(0, p.LoopBudgetUs);
const uint16_t expected[] = {64, 40, 32};
for (int sf = 7; sf <= 9; ++sf) {
p.secondary.params.sf = sf;
EXPECT_EQ(expected[sf-7], p.preamble(1, 32));
EXPECT_EQ(32, p.preamble(0, 32));
EXPECT_EQ(0, p.preamble(1, 32) % 8);
EXPECT_EQ(9421, p.listenUs(0));
EXPECT_EQ(21847, p.listenUs(1));
EXPECT_EQ(22147, p.listenUs(1));
EXPECT_LE(2 * (p.listenUs(0) + p.listenUs(1) + 2 * p.switchBudgetUs() + p.LoopBudgetUs),
p.preamble(0, 32) * p.symbolUs(p.primary));
EXPECT_LE(p.listenUs(1) + 2 * p.switchBudgetUs() + p.LoopBudgetUs
@@ -729,9 +729,9 @@ TEST(RadioProfiles, SlowerProfileDeterminesOrderAndFasterReceiveWindow) {
std::swap(p.primary, p.secondary.params);
EXPECT_EQ(1, p.slowerProfile());
EXPECT_EQ(9421, p.listenUs(1));
EXPECT_EQ(21847, p.listenUs(0));
EXPECT_EQ(22147, p.listenUs(0));
p.secondary.params.preamble = 40;
EXPECT_EQ(30039, p.listenUs(0));
EXPECT_EQ(30339, p.listenUs(0));
p.secondary.params.preamble = 8;
EXPECT_FALSE(p.automaticPreambleFits()); // no safe time for the faster channel
}
+6 -4
View File
@@ -24,6 +24,8 @@ 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; }
uint32_t millis() { return (uint32_t)(elapsed_us / 1000); }
struct Chip { bool standbyXOSC=false; int standby() { return 0; } };
using CustomSX1262 = Chip;
@@ -45,7 +47,7 @@ struct RadioLibWrapper {
}
uint32_t _rx_ps_rx_us=50000, _rx_ps_sleep_us=50000;
uint8_t _active_profile=0, _cur_sf=7, _cur_cr=5;
uint32_t _profile_generation=1, _profile_visit_us=0, _profile_retry_at=0;
uint32_t _profile_generation=1, _profile_visit_stamp=0, _profile_retry_at=0;
uint32_t _profile_scan_generation[2]={};
uint16_t _physical_preamble=32;
float _cur_freq=909.5, _cur_bw=62.5;
@@ -57,7 +59,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_us=micros();
_profile_visit_stamp=profileTimestamp();
}
bool isChipBusy() { return busy; }
bool isInRecvMode() const { return (state & ~STATE_INT_READY) == STATE_RX; }
@@ -69,7 +71,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_us=micros(); _rx_ps_armed=_rx_ps_enabled;
state=STATE_RX; _profile_visit_stamp=profileTimestamp(); _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; }
@@ -273,7 +275,7 @@ int main() {
w.enable();
w._floor_estimator.add(-100, millis());
w._active_profile=0;
w._profile_visit_us=micros();
w._profile_visit_stamp=profileTimestamp();
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);