feat(radio): enable fast profile switching across direct SX1262 boards
Add acknowledged SetFs and packet-cache shortcuts for SX1262, extend the LR1110 FS-mode fast path with buffered SPI, and keep the expander-backed Indicator on its previous path. Include HIL fixtures, validation notes, and regression tests.
This commit is contained in:
1 parent
082466f71b
commit
8c7f4df1cd
28 files changed
+1954
-33
No files matched your search
@@ -458,3 +458,67 @@ against the pinned RadioLib. No firmware was uploaded, no radio command was
|
||||
issued, and no new RF result was collected. The temporary build overlay was
|
||||
removed. The earlier ~0.549 ms XIAO and ~8.263 ms Indicator timings do **not**
|
||||
measure this added optimization; its time saving and RF behavior await testing.
|
||||
|
||||
## MercerMesh Heltec T096 SX1262 experiment (2026-09-21)
|
||||
|
||||
The Pi has **one** Heltec T096 (USB serial `651F8E496197F882`); the other
|
||||
Heltec-named nRF52 is a MeshTower V2, not another T096. An application-only
|
||||
T096 HIL image tested two further RX-to-RX shortcuts with a buffered 8 MHz SPI
|
||||
bus: use SX1262 `SetFs` instead of warm XOSC standby, and skip
|
||||
`SetPacketParams` only when the LoRa RX packet tuple is unchanged. Both switches
|
||||
defaulted off in this HIL image. The image explicitly enabled the existing
|
||||
production fast-RX path. Normal T096 firmware does not currently enable that
|
||||
path or use the buffered SPI HAL, so these numbers are **not** stock T096
|
||||
switch timings.
|
||||
|
||||
The same image ran 256 measured hops per configuration, after eight warm-up
|
||||
hops, on 909.5/909.75 MHz. `freq` held SF7/BW125/CR5; `both` alternated
|
||||
SF7/SF8 at BW125/CR5. Preamble was 32 symbols. The DWT cycle counter measured
|
||||
through RX re-entry and BUSY release. Every 8 MHz configuration had zero
|
||||
switch failures, mode/cache/RSSI errors, or BUSY timeouts.
|
||||
|
||||
| 8 MHz HIL configuration | Frequency-only mean | Frequency + SF mean |
|
||||
| --- | ---: | ---: |
|
||||
| Fast RX baseline | 467.3 us | 553.6 us |
|
||||
| `SetFs` only | 422.5 us | 517.4 us |
|
||||
| Unchanged packet tuple skip only | 357.7 us | 444.0 us |
|
||||
| Both | **313.0 us** | **407.8 us** |
|
||||
|
||||
Combined savings were 154.3 us (33.0%) for frequency-only and 145.8 us
|
||||
(26.3%) for frequency-plus-SF relative to the same-image fast-RX baseline.
|
||||
Command counts confirmed one frequency command, IRQ clear and RX command per
|
||||
hop; the optimized cases eliminated the corresponding standby and packet
|
||||
commands. These are controlled short-range, same-band results, not a field
|
||||
packet-error rate or a qualification of large frequency jumps, sleep/wake,
|
||||
TX/CAD interruption, BLE scheduling, or all SX1262 boards.
|
||||
|
||||
A separate RAK4631 fixed transmitter sent 16-byte synthetic packets at
|
||||
909.5 MHz/SF10/BW125/CR5/preamble32, -9 dBm chip power. The T096 hopped to
|
||||
909.75 MHz and back before each transmission. Baseline and combined shortcuts
|
||||
each delivered **60/60** packets after **120** fast hops, with payload sequence
|
||||
and content checked. The RAK bootloader on this exact fixture enumerated as
|
||||
`239A:002A` (the fixture's earlier `0029` expectation was corrected).
|
||||
|
||||
Doubling buffered SPI to 16 MHz gave only 12–21 us (about 3–4%) savings in
|
||||
comparable successful cases, while five of eight setup attempts failed. The
|
||||
combined shortcut had no clean 16 MHz result. Keep this T096 at 8 MHz pending
|
||||
a separately diagnosed and repeatable 16 MHz bring-up; no 16 MHz over-air
|
||||
qualification was attempted.
|
||||
|
||||
Both radios were restored via app-only DFU after the experiment: the T096 to
|
||||
`Heltec_t096_repeater` and the RAK to `RAK_4631_companion_radio_full`, each
|
||||
built at `v1.17.1.7-hil-restore-e7160dfd`. The T096 answered normal `version`
|
||||
and `board` commands; the RAK answered the Full Companion terminal's version
|
||||
command. The V4 observer's memory-soak service remained active. Reproducible
|
||||
bench code is in `tools/hil/profile_switch_t096_sx1262.cpp` and
|
||||
`tools/hil/t096_sx1262_packet_probe.py`.
|
||||
|
||||
Follow-up: owned fast RX, `SetFs`, and acknowledged packet-tuple reuse are
|
||||
now defaults for `CustomSX1262` targets, including the original XIAO S3 WIO,
|
||||
Station G3 KISS, and Heltec V4 KISS paths. The Seeed Indicator retains its
|
||||
original owned-RX path but explicitly opts out of the two new shortcuts because
|
||||
its radio control and BUSY/IRQ pins run through an I2C expander. The owned-RX
|
||||
state now remembers only an acknowledged packet tuple and revokes it on
|
||||
ordinary packet setters, TX/CAD staging, standby outside the hop, sleep,
|
||||
reset, RX duty cycling, or command failure. The T096 numbers remain lab
|
||||
measurements, not on-board qualification of every `CustomSX1262` target.
|
||||
+3
-2
@@ -33,8 +33,9 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
|
||||
; after begin(). RadioLib defaults to 5000 and nothing used to override it, so
|
||||
; every board paid 5 ms on every duty-cycle wake. Lowest working value measured
|
||||
; on five modules: T096 400, Tracker V2 300, Waveshare 150, ThinkNode M3 200,
|
||||
; T1000-E 200 us - 1600 keeps 4x margin over the worst of them. It also feeds
|
||||
; the RXPS sleep floor and timer guard (transition = this + 1000), so lowering
|
||||
; T1000-E 200 us (single-unit RX scan test). The 1600 us base keeps 4x margin
|
||||
; over the worst of them. It also feeds the RXPS sleep floor and timer guard
|
||||
; (transition = this + 1000), so lowering
|
||||
; it makes low-SF profiles armable that were not. A board needing its own value
|
||||
; puts -D MC_TCXO_DELAY_US=... in its own build_flags; the later -D wins.
|
||||
-D MC_TCXO_DELAY_US=1600
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
#include "MeshCore.h"
|
||||
#include "RXPowerSaving.h"
|
||||
#include "LR1110RxRecovery.h"
|
||||
#include "LR1110ProfileSwitchState.h"
|
||||
|
||||
#ifndef MC_LR1110_FAST_PROFILE_SWITCH
|
||||
// All MeshCore LR1110 targets use the checked FS-mode continuous-RX retune.
|
||||
// Boards can opt out if hardware validation finds an incompatibility.
|
||||
#define MC_LR1110_FAST_PROFILE_SWITCH 1
|
||||
#endif
|
||||
|
||||
#ifndef LR11X0_TX_BUSY_TIMEOUT_MS
|
||||
#define LR11X0_TX_BUSY_TIMEOUT_MS 1000UL
|
||||
@@ -15,14 +22,173 @@ class CustomLR1110 : public LR1110 {
|
||||
uint32_t _activityAt = 0;
|
||||
bool _headerSeen = false;
|
||||
bool _rx_boosted = false;
|
||||
LR1110ProfileSwitchState _profileSwitch{MC_LR1110_FAST_PROFILE_SWITCH != 0};
|
||||
bool _profileStandbyWarm = false;
|
||||
bool _profilePacketDirty = false;
|
||||
uint32_t _profileFastResumes = 0;
|
||||
|
||||
public:
|
||||
CustomLR1110(Module *mod) : LR1110(mod) { }
|
||||
|
||||
void setProfileSwitchOptimization(bool enabled) {
|
||||
_profileSwitch.enabled = enabled;
|
||||
_profileSwitch.invalidate();
|
||||
_profilePacketDirty = false;
|
||||
}
|
||||
void setProfileStandbyWarm(bool enabled) {
|
||||
_profileStandbyWarm = enabled;
|
||||
if (!enabled) _profileSwitch.invalidate();
|
||||
}
|
||||
void beginProfileSwitch(bool continuousRx) {
|
||||
_profileSwitch.begin(continuousRx, _profileStandbyWarm);
|
||||
}
|
||||
void endProfileSwitch(bool success) { _profileSwitch.end(success); }
|
||||
bool profileSwitchFailed() const { return _profileSwitch.failed(); }
|
||||
uint32_t getOptimizedProfileSwitches() const { return _profileFastResumes; }
|
||||
#ifdef MC_LR1110_HIL
|
||||
uint64_t fastClearCycles = 0, fastPacketCycles = 0, fastSetRxCycles = 0;
|
||||
uint8_t hilFastClearMode = 2; // 0=always, 1=never, 2=checked (production)
|
||||
bool hilInjectRxTimeout = false;
|
||||
bool hilUseFsOnRetune = true;
|
||||
uint32_t hilPendingIrqSnapshots = 0;
|
||||
#endif
|
||||
|
||||
// Outside RX, no new packet IRQ can arise. Check the pending bits after
|
||||
// leaving RX and clear them only when present. RadioLib's plain
|
||||
// getIrqStatus() discards SPI errors, so use a checked transfer here.
|
||||
int16_t snapshotIrqAfterRxExit(uint32_t* irq) {
|
||||
uint8_t reply[6] = {};
|
||||
auto& statusWidth = mod->spiConfig.widths[RADIOLIB_MODULE_SPI_WIDTH_STATUS];
|
||||
const auto savedWidth = statusWidth;
|
||||
statusWidth = Module::BITS_0;
|
||||
const int16_t rc = mod->SPItransferStream(nullptr, 0, false, nullptr,
|
||||
reply, sizeof(reply), true);
|
||||
statusWidth = savedWidth;
|
||||
if (rc == RADIOLIB_ERR_NONE) {
|
||||
*irq = (uint32_t(reply[2]) << 24) | (uint32_t(reply[3]) << 16)
|
||||
| (uint32_t(reply[4]) << 8) | uint32_t(reply[5]);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int16_t standby() override {
|
||||
const bool warm = _profileSwitch.active && _profileSwitch.rxValid;
|
||||
if (!warm) _profileSwitch.invalidate();
|
||||
// RadioLib 7.7.1 defines STANDBY_XOSC as 0, the same as STANDBY_RC.
|
||||
// The LR1110 command's XOSC selector is 1 (Semtech UM §2.1.1).
|
||||
bool useFs = warm;
|
||||
#ifdef MC_LR1110_HIL
|
||||
useFs = useFs && hilUseFsOnRetune;
|
||||
#endif
|
||||
int16_t rc;
|
||||
if (useFs) {
|
||||
mod->setRfSwitchState(Module::MODE_IDLE);
|
||||
rc = setFs();
|
||||
} else if (warm) {
|
||||
rc = LR1110::standby(0x01, false);
|
||||
} else {
|
||||
rc = LR1110::standby();
|
||||
}
|
||||
_profileSwitch.standbyResult(rc);
|
||||
return rc;
|
||||
}
|
||||
int16_t standby(uint8_t mode) override {
|
||||
_profileSwitch.invalidate();
|
||||
return LR1110::standby(mode);
|
||||
}
|
||||
int16_t sleep() override {
|
||||
_profileSwitch.invalidate();
|
||||
return LR1110::sleep();
|
||||
}
|
||||
int16_t reset() {
|
||||
_profileSwitch.invalidate();
|
||||
return LR1110::reset();
|
||||
}
|
||||
int16_t stageMode(RadioModeType_t mode, RadioModeConfig_t* cfg) override {
|
||||
_profileSwitch.invalidate();
|
||||
return LR1110::stageMode(mode, cfg);
|
||||
}
|
||||
int16_t setPreambleLength(size_t symbols) override {
|
||||
if (_profileSwitch.canResumeFast()) {
|
||||
// Keep the acknowledged RX packet tuple until its preamble changes.
|
||||
if (preambleLengthLoRa != symbols) _profilePacketDirty = true;
|
||||
preambleLengthLoRa = symbols;
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
_profileSwitch.invalidate();
|
||||
_profilePacketDirty = false;
|
||||
return LR1110::setPreambleLength(symbols);
|
||||
}
|
||||
int16_t setFrequency(float freq) {
|
||||
if (_profileSwitch.canResumeFast() && freq == freqMHz) return RADIOLIB_ERR_NONE;
|
||||
if (!_profileSwitch.canResumeFast()) _profileSwitch.invalidate();
|
||||
const int16_t rc = LR1110::setFrequency(freq);
|
||||
if (rc != RADIOLIB_ERR_NONE) _profileSwitch.invalidate();
|
||||
return rc;
|
||||
}
|
||||
int16_t setBandwidth(float bw, bool high = false) {
|
||||
_profileSwitch.modulationValid = false;
|
||||
return LR1110::setBandwidth(bw, high);
|
||||
}
|
||||
int16_t setSpreadingFactor(uint8_t sf, bool legacy = false) {
|
||||
_profileSwitch.modulationValid = false;
|
||||
return LR1110::setSpreadingFactor(sf, legacy);
|
||||
}
|
||||
int16_t setCodingRate(uint8_t cr, bool longInterleave = false) {
|
||||
_profileSwitch.modulationValid = false;
|
||||
return LR1110::setCodingRate(cr, longInterleave);
|
||||
}
|
||||
|
||||
// A single acknowledged SetModulationParams replaces three RadioLib
|
||||
// setters. Keep RadioLib's cached tuple and automatic LDRO calculation.
|
||||
int16_t setLoRaModulationParams(float bw, uint8_t sf, uint8_t cr) {
|
||||
if (sf < 5 || sf > 12) return RADIOLIB_ERR_INVALID_SPREADING_FACTOR;
|
||||
if (cr < 4 || cr > 8) return RADIOLIB_ERR_INVALID_CODING_RATE;
|
||||
uint8_t bwCode;
|
||||
if (fabsf(bw - 62.5f) < 0.01f) bwCode = RADIOLIB_LR11X0_LORA_BW_62_5;
|
||||
else if (fabsf(bw - 125.0f) < 0.01f) bwCode = RADIOLIB_LR11X0_LORA_BW_125_0;
|
||||
else if (fabsf(bw - 250.0f) < 0.01f) bwCode = RADIOLIB_LR11X0_LORA_BW_250_0;
|
||||
else if (fabsf(bw - 500.0f) < 0.01f) bwCode = RADIOLIB_LR11X0_LORA_BW_500_0;
|
||||
else return RADIOLIB_ERR_INVALID_BANDWIDTH;
|
||||
if (!_profileSwitch.canResumeFast()) {
|
||||
uint8_t type = RADIOLIB_LR11X0_PACKET_TYPE_NONE;
|
||||
const int16_t rc = getPacketType(&type);
|
||||
if (rc != RADIOLIB_ERR_NONE) return rc;
|
||||
if (type != RADIOLIB_LR11X0_PACKET_TYPE_LORA) return RADIOLIB_ERR_WRONG_MODEM;
|
||||
}
|
||||
const uint8_t effectiveLdro = ldroAuto
|
||||
? (float(uint32_t(1) << sf) / bw >= 16.0f
|
||||
? RADIOLIB_LR11X0_LORA_LDRO_ENABLED : RADIOLIB_LR11X0_LORA_LDRO_DISABLED)
|
||||
: ldrOptimize;
|
||||
const bool unchanged = _profileSwitch.matchesModulation(sf, bwCode, cr - 4, effectiveLdro);
|
||||
const auto oldSf = spreadingFactor, oldBw = bandwidth, oldCr = codingRate, oldLdro = ldrOptimize;
|
||||
const auto oldBwKhz = bandwidthKhz;
|
||||
spreadingFactor = sf;
|
||||
bandwidth = bwCode;
|
||||
bandwidthKhz = bw;
|
||||
codingRate = cr - 4;
|
||||
if (unchanged) {
|
||||
ldrOptimize = effectiveLdro;
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
const int16_t rc = setModulationParamsLoRa(sf, bwCode, codingRate, effectiveLdro);
|
||||
_profileSwitch.modulationResult(rc, sf, bwCode, codingRate, ldrOptimize);
|
||||
if (rc != RADIOLIB_ERR_NONE) {
|
||||
spreadingFactor = oldSf;
|
||||
bandwidth = oldBw;
|
||||
bandwidthKhz = oldBwKhz;
|
||||
codingRate = oldCr;
|
||||
ldrOptimize = oldLdro;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Apply the measured TCXO delay on every initialization, including recovery.
|
||||
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
|
||||
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
|
||||
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
|
||||
_profileSwitch.invalidate();
|
||||
_profilePacketDirty = false;
|
||||
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
|
||||
tcxoVoltage);
|
||||
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
|
||||
@@ -186,12 +352,68 @@ class CustomLR1110 : public LR1110 {
|
||||
bool getRxBoostedGainMode() const { return _rx_boosted; }
|
||||
|
||||
int16_t startReceive() override {
|
||||
if (_profileSwitch.canResumeFast()) {
|
||||
_profileSwitch.rxValid = false; // fail closed until the RX command ACKs
|
||||
#ifdef MC_LR1110_HIL
|
||||
uint32_t segment = DWT->CYCCNT;
|
||||
#endif
|
||||
int16_t rc = RADIOLIB_ERR_NONE;
|
||||
#ifdef MC_LR1110_HIL
|
||||
if (hilInjectRxTimeout) {
|
||||
hilInjectRxTimeout = false;
|
||||
rc = setRx(1);
|
||||
if (rc == RADIOLIB_ERR_NONE) delayMicroseconds(150);
|
||||
}
|
||||
if (rc == RADIOLIB_ERR_NONE && hilFastClearMode == 0)
|
||||
rc = clearIrqState(RADIOLIB_LR11X0_IRQ_ALL);
|
||||
else if (rc == RADIOLIB_ERR_NONE && hilFastClearMode == 2)
|
||||
#else
|
||||
if (rc == RADIOLIB_ERR_NONE)
|
||||
#endif
|
||||
{
|
||||
uint32_t pending = 0;
|
||||
rc = snapshotIrqAfterRxExit(&pending);
|
||||
#ifdef MC_LR1110_HIL
|
||||
if (pending) ++hilPendingIrqSnapshots;
|
||||
#endif
|
||||
if (rc == RADIOLIB_ERR_NONE && pending)
|
||||
rc = clearIrqState(RADIOLIB_LR11X0_IRQ_ALL);
|
||||
}
|
||||
#ifdef MC_LR1110_HIL
|
||||
fastClearCycles += DWT->CYCCNT - segment;
|
||||
segment = DWT->CYCCNT;
|
||||
#endif
|
||||
if (rc == RADIOLIB_ERR_NONE && _profilePacketDirty) {
|
||||
rc = setPacketParamsLoRa(preambleLengthLoRa, headerType,
|
||||
RADIOLIB_LR11X0_MAX_PACKET_LENGTH, crcTypeLoRa, invertIQEnabled);
|
||||
}
|
||||
#ifdef MC_LR1110_HIL
|
||||
fastPacketCycles += DWT->CYCCNT - segment;
|
||||
segment = DWT->CYCCNT;
|
||||
#endif
|
||||
if (rc == RADIOLIB_ERR_NONE) {
|
||||
rxTimeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF;
|
||||
mod->setRfSwitchState(Module::MODE_RX);
|
||||
rc = setRx(rxTimeout);
|
||||
}
|
||||
#ifdef MC_LR1110_HIL
|
||||
fastSetRxCycles += DWT->CYCCNT - segment;
|
||||
#endif
|
||||
stagedMode = RADIOLIB_RADIO_MODE_NONE;
|
||||
if (rc == RADIOLIB_ERR_NONE) ++_profileFastResumes;
|
||||
if (rc == RADIOLIB_ERR_NONE) _profilePacketDirty = false;
|
||||
_profileSwitch.rxResult(rc, true);
|
||||
return rc;
|
||||
}
|
||||
// Keep preamble detection visible to CAD and route header errors to DIO1
|
||||
// so the wrapper can reset the RX path before another packet is read.
|
||||
return LR1110::startReceive(
|
||||
const int16_t rc = LR1110::startReceive(
|
||||
RADIOLIB_LR11X0_RX_TIMEOUT_INF,
|
||||
RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED),
|
||||
RADIOLIB_IRQ_RX_DEFAULT_MASK | (1UL << RADIOLIB_IRQ_HEADER_ERR), 0);
|
||||
_profileSwitch.rxResult(rc, true);
|
||||
if (rc == RADIOLIB_ERR_NONE) _profilePacketDirty = false;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid
|
||||
|
||||
@@ -19,6 +19,19 @@ public:
|
||||
|
||||
void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; }
|
||||
|
||||
protected:
|
||||
void beginProfileRetune(bool continuousRx) override {
|
||||
static_cast<CustomLR1110*>(_radio)->beginProfileSwitch(continuousRx);
|
||||
}
|
||||
void endProfileRetune(bool success) override {
|
||||
static_cast<CustomLR1110*>(_radio)->endProfileSwitch(success);
|
||||
}
|
||||
void setProfileStandbyWarm(bool enabled) override {
|
||||
static_cast<CustomLR1110*>(_radio)->setProfileStandbyWarm(enabled);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void powerOff() {
|
||||
if (isCarrierWaveActive()) setCarrierWave(0, 0);
|
||||
_radio->standby(); _radio->sleep();
|
||||
@@ -26,11 +39,18 @@ public:
|
||||
|
||||
protected:
|
||||
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
if (static_cast<CustomLR1110*>(_radio)->profileSwitchFailed()) return false;
|
||||
#if MC_LR1110_FAST_PROFILE_SWITCH
|
||||
bool success = ((CustomLR1110 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
|
||||
&& ((CustomLR1110 *)_radio)->setLoRaModulationParams(bw, sf, cr) == RADIOLIB_ERR_NONE
|
||||
&& updatePreamble(sf, bw);
|
||||
#else
|
||||
bool success = ((CustomLR1110 *)_radio)->setFrequency(freq) == RADIOLIB_ERR_NONE
|
||||
&& ((CustomLR1110 *)_radio)->setSpreadingFactor(sf) == RADIOLIB_ERR_NONE
|
||||
&& ((CustomLR1110 *)_radio)->setBandwidth(bw) == RADIOLIB_ERR_NONE
|
||||
&& ((CustomLR1110 *)_radio)->setCodingRate(cr) == RADIOLIB_ERR_NONE
|
||||
&& updatePreamble(sf, bw);
|
||||
#endif
|
||||
if (!success) return false;
|
||||
|
||||
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForParams(sf, bw));
|
||||
|
||||
@@ -6,7 +6,22 @@
|
||||
#include "SX1262ProfileSwitchState.h"
|
||||
|
||||
#ifndef MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#define MC_SX1262_FAST_PROFILE_SWITCH 0
|
||||
// CustomSX1262 targets use direct radio I/O unless a board opts out. Keep
|
||||
// the owned RX-to-RX path available across those boards, including roles that
|
||||
// did not previously need a local profile scanner.
|
||||
#define MC_SX1262_FAST_PROFILE_SWITCH 1
|
||||
#endif
|
||||
|
||||
#ifndef MC_SX1262_FAST_FS
|
||||
#define MC_SX1262_FAST_FS MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#endif
|
||||
|
||||
#ifndef MC_SX1262_FAST_PACKET_CACHE
|
||||
#define MC_SX1262_FAST_PACKET_CACHE MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#endif
|
||||
|
||||
#if (MC_SX1262_FAST_FS || MC_SX1262_FAST_PACKET_CACHE) && !MC_SX1262_FAST_PROFILE_SWITCH
|
||||
#error "SX1262 SetFs/packet reuse requires the owned fast profile-switch path"
|
||||
#endif
|
||||
|
||||
#ifndef SX126X_TX_BUSY_TIMEOUT_MS
|
||||
@@ -39,6 +54,11 @@ class CustomSX1262 : public SX1262 {
|
||||
#ifdef MC_SX1262_HIL
|
||||
// Same-image lab baseline: force only 0x8B, preserving fast RX ownership.
|
||||
void hilInvalidateModulation() { _profileSwitch.invalidateModulation(); }
|
||||
// Same-image A/B overrides; these default off on every HIL reboot.
|
||||
bool hilUseFsOnRetune = false;
|
||||
bool hilSkipUnchangedPacket = false;
|
||||
float hilFrequencyMHz() const { return freqMHz; }
|
||||
uint32_t hilFsEntries = 0, hilPacketSkips = 0;
|
||||
#endif
|
||||
|
||||
int16_t restoreTcxoAfterSleep() {
|
||||
@@ -56,9 +76,22 @@ class CustomSX1262 : public SX1262 {
|
||||
int16_t standby() override {
|
||||
const bool alreadyAwake = !_coldStandby && _profileSwitch.active && _profileSwitch.rxValid;
|
||||
if (!alreadyAwake) _profileSwitch.invalidate();
|
||||
int16_t rc = alreadyAwake
|
||||
? SX1262::standby(RADIOLIB_SX126X_STANDBY_XOSC, false)
|
||||
: _coldStandby ? SX1262::standby(RADIOLIB_SX126X_STANDBY_RC) : SX1262::standby();
|
||||
int16_t rc;
|
||||
bool useFs = MC_SX1262_FAST_FS != 0;
|
||||
#ifdef MC_SX1262_HIL
|
||||
useFs = hilUseFsOnRetune;
|
||||
#endif
|
||||
if (alreadyAwake && useFs) {
|
||||
getMod()->setRfSwitchState(Module::MODE_IDLE);
|
||||
rc = setFs();
|
||||
#ifdef MC_SX1262_HIL
|
||||
if (rc == RADIOLIB_ERR_NONE) ++hilFsEntries;
|
||||
#endif
|
||||
} else {
|
||||
rc = alreadyAwake
|
||||
? SX1262::standby(RADIOLIB_SX126X_STANDBY_XOSC, false)
|
||||
: _coldStandby ? SX1262::standby(RADIOLIB_SX126X_STANDBY_RC) : SX1262::standby();
|
||||
}
|
||||
if (rc == RADIOLIB_ERR_NONE) rc = restoreTcxoAfterSleep();
|
||||
if (rc != RADIOLIB_ERR_NONE) _coldStandby = true;
|
||||
_profileSwitch.standbyResult(rc);
|
||||
@@ -97,9 +130,31 @@ class CustomSX1262 : public SX1262 {
|
||||
preambleLengthLoRa = symbols;
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::setPreambleLength(symbols);
|
||||
}
|
||||
|
||||
// Ordinary packet setters may rewrite the hardware even when the LoRa
|
||||
// software fields later return to their old values. Never reuse an RX
|
||||
// acknowledgement across them, including when a setter reports failure.
|
||||
int16_t setCRC(uint8_t len, uint16_t initial = 0x1D0F,
|
||||
uint16_t polynomial = 0x1021, bool inverted = true) {
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::setCRC(len, initial, polynomial, inverted);
|
||||
}
|
||||
int16_t implicitHeader(size_t len) {
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::implicitHeader(len);
|
||||
}
|
||||
int16_t explicitHeader() {
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::explicitHeader();
|
||||
}
|
||||
int16_t invertIQ(bool enable) override {
|
||||
_profileSwitch.invalidatePacket();
|
||||
return SX1262::invertIQ(enable);
|
||||
}
|
||||
|
||||
// Ordinary setters can change hardware or leave RadioLib's fields updated
|
||||
// after a failed write. They cannot preserve our acknowledged hop tuple.
|
||||
int16_t setBandwidth(float bw) override {
|
||||
@@ -449,11 +504,23 @@ class CustomSX1262 : public SX1262 {
|
||||
// Existing continuous RX already established buffer bases and the
|
||||
// MeshCore IRQ map (including preamble detection). No TX/CAD/sleep/
|
||||
// reset/staging may intervene without revoking that context.
|
||||
bool skipPacket = MC_SX1262_FAST_PACKET_CACHE != 0;
|
||||
#ifdef MC_SX1262_HIL
|
||||
skipPacket = hilSkipUnchangedPacket;
|
||||
#endif
|
||||
skipPacket = skipPacket && _profileSwitch.matchesPacket(
|
||||
preambleLengthLoRa, crcTypeLoRa, implicitLen,
|
||||
headerType, invertIQEnabled);
|
||||
_profileSwitch.rxValid = false; // fail closed on any command error
|
||||
int16_t rc = clearIrqStatus();
|
||||
if (rc == RADIOLIB_ERR_NONE) {
|
||||
#ifdef MC_SX1262_HIL
|
||||
if (rc == RADIOLIB_ERR_NONE && skipPacket) ++hilPacketSkips;
|
||||
#endif
|
||||
if (rc == RADIOLIB_ERR_NONE && !skipPacket) {
|
||||
rc = setPacketParams(preambleLengthLoRa, crcTypeLoRa, implicitLen,
|
||||
headerType, invertIQEnabled);
|
||||
_profileSwitch.packetResult(rc, preambleLengthLoRa, crcTypeLoRa,
|
||||
implicitLen, headerType, invertIQEnabled);
|
||||
}
|
||||
if (rc == RADIOLIB_ERR_NONE) {
|
||||
rxTimeout = RADIOLIB_SX126X_RX_TIMEOUT_INF;
|
||||
@@ -476,6 +543,10 @@ class CustomSX1262 : public SX1262 {
|
||||
&& getPacketType() == RADIOLIB_SX126X_PACKET_TYPE_LORA;
|
||||
_coldStandby = rc != RADIOLIB_ERR_NONE;
|
||||
_profileSwitch.rxResult(rc, reusable);
|
||||
if (rc == RADIOLIB_ERR_NONE && reusable) {
|
||||
_profileSwitch.packetResult(rc, preambleLengthLoRa, crcTypeLoRa,
|
||||
implicitLen, headerType, invertIQEnabled);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <hal/Arduino/ArduinoHal.h>
|
||||
|
||||
// RadioLib's Arduino HAL transfers one byte at a time. ESP32 SPIClass can
|
||||
// transfer each LR1110 command in one transaction while retaining RadioLib's
|
||||
// existing CS, BUSY, status and transaction handling. LR1110 permits 16 MHz.
|
||||
class Esp32BufferedRadioHal : public ArduinoHal {
|
||||
public:
|
||||
explicit Esp32BufferedRadioHal(SPIClass& bus, uint32_t hz = 16000000)
|
||||
: ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {}
|
||||
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
if (len) spi->transferBytes(out, in, len);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// A short-lived, acknowledged continuous-RX context. It is never valid after
|
||||
// sleep, TX/CAD staging, an unrelated standby, reset, or a failed command.
|
||||
struct LR1110ProfileSwitchState {
|
||||
bool enabled;
|
||||
bool rxValid = false;
|
||||
bool open = false;
|
||||
bool active = false;
|
||||
bool standbyReady = false;
|
||||
int16_t error = 0;
|
||||
bool modulationValid = false;
|
||||
uint8_t modulationSf = 0, modulationBw = 0, modulationCr = 0, modulationLdro = 0;
|
||||
|
||||
explicit LR1110ProfileSwitchState(bool enable) : enabled(enable) {}
|
||||
void invalidate() {
|
||||
rxValid = active = standbyReady = modulationValid = false;
|
||||
}
|
||||
void begin(bool continuousRx, bool warm) {
|
||||
open = true;
|
||||
error = 0;
|
||||
standbyReady = false;
|
||||
active = enabled && rxValid && continuousRx && warm;
|
||||
}
|
||||
void standbyResult(int16_t result) {
|
||||
if (result != 0) {
|
||||
if (open) error = result;
|
||||
invalidate();
|
||||
} else if (active) {
|
||||
standbyReady = true;
|
||||
}
|
||||
}
|
||||
bool canResumeFast() const {
|
||||
return open && active && rxValid && standbyReady && error == 0;
|
||||
}
|
||||
bool matchesModulation(uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) const {
|
||||
return canResumeFast() && modulationValid && modulationSf == sf
|
||||
&& modulationBw == bw && modulationCr == cr && modulationLdro == ldro;
|
||||
}
|
||||
void modulationResult(int16_t result, uint8_t sf, uint8_t bw, uint8_t cr, uint8_t ldro) {
|
||||
if (result != 0) {
|
||||
if (open) error = result;
|
||||
invalidate();
|
||||
} else {
|
||||
modulationSf = sf;
|
||||
modulationBw = bw;
|
||||
modulationCr = cr;
|
||||
modulationLdro = ldro;
|
||||
modulationValid = true;
|
||||
}
|
||||
}
|
||||
bool failed() const { return open && error != 0; }
|
||||
void rxResult(int16_t result, bool reusable) {
|
||||
if (result != 0) {
|
||||
if (open) error = result;
|
||||
invalidate();
|
||||
} else {
|
||||
rxValid = enabled && reusable;
|
||||
standbyReady = false;
|
||||
}
|
||||
}
|
||||
void end(bool success) {
|
||||
if (!success) invalidate();
|
||||
open = active = standbyReady = false;
|
||||
error = 0;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <hal/Arduino/ArduinoHal.h>
|
||||
|
||||
// nRF52840 SPIClass::transfer(uint8_t) starts one SPIM transaction per byte.
|
||||
// RadioLib sends whole command buffers; keep its CS, BUSY, and status handling
|
||||
// but transfer each buffer in one SPIM transaction. LR1110 supports 16 MHz SPI.
|
||||
class Nrf52BufferedRadioHal : public ArduinoHal {
|
||||
public:
|
||||
explicit Nrf52BufferedRadioHal(SPIClass& bus, uint32_t hz = 16000000)
|
||||
: ArduinoHal(bus, SPISettings(hz, MSBFIRST, SPI_MODE0)) {}
|
||||
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
if (len) spi->transfer(out, in, len);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// A short-lived capability for reusing a fully configured continuous-RX
|
||||
@@ -12,12 +13,17 @@ struct SX1262ProfileSwitchState {
|
||||
int16_t error = 0;
|
||||
bool modulationValid = false;
|
||||
uint8_t modulationSf = 0, modulationBw = 0, modulationCr = 0, modulationLdro = 0;
|
||||
bool packetValid = false;
|
||||
size_t packetPreamble = 0;
|
||||
uint8_t packetCrc = 0, packetImplicitLen = 0, packetHeader = 0, packetIq = 0;
|
||||
|
||||
explicit SX1262ProfileSwitchState(bool enable) : enabled(enable) {}
|
||||
void invalidateModulation() { modulationValid = false; }
|
||||
void invalidatePacket() { packetValid = false; }
|
||||
void invalidate() {
|
||||
rxValid = active = standbyReady = false;
|
||||
invalidateModulation();
|
||||
invalidatePacket();
|
||||
}
|
||||
void begin(bool continuousRx, bool warm) {
|
||||
open = true;
|
||||
@@ -52,6 +58,26 @@ struct SX1262ProfileSwitchState {
|
||||
modulationLdro = ldro;
|
||||
modulationValid = true;
|
||||
}
|
||||
bool matchesPacket(size_t preamble, uint8_t crc, uint8_t implicitLen,
|
||||
uint8_t header, uint8_t iq) const {
|
||||
return canResumeFast() && packetValid && packetPreamble == preamble
|
||||
&& packetCrc == crc && packetImplicitLen == implicitLen
|
||||
&& packetHeader == header && packetIq == iq;
|
||||
}
|
||||
void packetResult(int16_t result, size_t preamble, uint8_t crc,
|
||||
uint8_t implicitLen, uint8_t header, uint8_t iq) {
|
||||
if (result != 0) {
|
||||
if (open) error = result;
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
packetPreamble = preamble;
|
||||
packetCrc = crc;
|
||||
packetImplicitLen = implicitLen;
|
||||
packetHeader = header;
|
||||
packetIq = iq;
|
||||
packetValid = true;
|
||||
}
|
||||
bool failed() const { return open && error != 0; }
|
||||
void rxResult(int16_t result, bool reusable) {
|
||||
if (result != 0) {
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
"""Host-side contracts for LR1110 profile ownership and batched modulation."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from test_radio_receive_contract import method
|
||||
import test_sx1262_batched_modulation as compiler
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class LR1110ProfileSwitchTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_fast_profile_switch_is_enabled_for_every_lr1110_target(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text()
|
||||
self.assertIn("#define MC_LR1110_FAST_PROFILE_SWITCH 1", source)
|
||||
targets = {
|
||||
"t1000-e": "Nrf52BufferedRadioHal",
|
||||
"wio_wm1110": "Nrf52BufferedRadioHal",
|
||||
"thinknode_m3": "Nrf52BufferedRadioHal",
|
||||
"minewsemi_me25ls01": "Nrf52BufferedRadioHal",
|
||||
"thinknode_m7": "Esp32BufferedRadioHal",
|
||||
"thinknode_m9": "Esp32BufferedRadioHal",
|
||||
}
|
||||
for target, hal in targets.items():
|
||||
with self.subTest(target=target):
|
||||
variant = ROOT / "variants" / target
|
||||
config = (variant / "platformio.ini").read_text()
|
||||
target_source = (variant / "target.cpp").read_text()
|
||||
self.assertIn("-D RADIO_CLASS=CustomLR1110", config)
|
||||
self.assertIn("-D WRAPPER_CLASS=CustomLR1110Wrapper", config)
|
||||
self.assertIn(f"{hal} radioHal(", target_source)
|
||||
self.assertIn("new Module(&radioHal,", target_source)
|
||||
|
||||
esp32_hal = (ROOT / "src/helpers/radiolib/Esp32BufferedRadioHal.h").read_text()
|
||||
nrf52_hal = (ROOT / "src/helpers/radiolib/Nrf52BufferedRadioHal.h").read_text()
|
||||
self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", esp32_hal)
|
||||
self.assertIn("spi->transferBytes(out, in, len)", esp32_hal)
|
||||
self.assertIn("SPISettings(hz, MSBFIRST, SPI_MODE0)", nrf52_hal)
|
||||
self.assertIn("spi->transfer(out, in, len)", nrf52_hal)
|
||||
|
||||
def test_checked_irq_snapshot_restores_spi_width_and_propagates_error(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text()
|
||||
implementation = method(source, "int16_t snapshotIrqAfterRxExit(")
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_MODULE_SPI_WIDTH_STATUS 2
|
||||
struct Module {
|
||||
enum { BITS_0=0 };
|
||||
struct { uint8_t widths[3]={0,0,8}; } spiConfig;
|
||||
uint8_t response[6]={0,0,0x12,0x34,0x56,0x78};
|
||||
int16_t result=0;
|
||||
int16_t SPItransferStream(const uint8_t*,uint8_t,bool,const uint8_t*,
|
||||
uint8_t* out,size_t length,bool wait) {
|
||||
assert(spiConfig.widths[2]==0 && length==6 && wait);
|
||||
std::memcpy(out,response,length);return result;
|
||||
}
|
||||
};
|
||||
struct Radio {
|
||||
Module* mod;
|
||||
@METHOD@
|
||||
};
|
||||
int main() {
|
||||
Module m; Radio r{&m}; uint32_t irq=0;
|
||||
assert(r.snapshotIrqAfterRxExit(&irq)==0 && irq==0x12345678);
|
||||
assert(m.spiConfig.widths[2]==8);
|
||||
m.result=-7;irq=0xAABBCCDD;
|
||||
assert(r.snapshotIrqAfterRxExit(&irq)==-7 && irq==0xAABBCCDD);
|
||||
assert(m.spiConfig.widths[2]==8);
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@METHOD@", implementation))
|
||||
|
||||
def test_fast_rx_skips_only_unchanged_packet_tuple_and_fails_closed(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text()
|
||||
signatures = (
|
||||
"void setProfileSwitchOptimization(", "void setProfileStandbyWarm(",
|
||||
"void beginProfileSwitch(", "void endProfileSwitch(",
|
||||
"bool profileSwitchFailed(", "int16_t standby()", "int16_t standby(uint8_t",
|
||||
"int16_t sleep()", "int16_t reset()", "int16_t stageMode(",
|
||||
"int16_t setPreambleLength(", "int16_t startReceive()",
|
||||
)
|
||||
methods = "\n".join(method(source, signature).replace(" override", "")
|
||||
for signature in signatures)
|
||||
state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text().replace(
|
||||
"#pragma once", ""
|
||||
)
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
@STATE@
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_LR11X0_STANDBY_XOSC 0
|
||||
#define RADIOLIB_LR11X0_RX_TIMEOUT_INF 0xffffff
|
||||
#define RADIOLIB_LR11X0_IRQ_ALL 0xffffffff
|
||||
#define RADIOLIB_LR11X0_MAX_PACKET_LENGTH 255
|
||||
#define RADIOLIB_RADIO_MODE_NONE 0
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS 1
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_MASK 2
|
||||
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 3
|
||||
#define RADIOLIB_IRQ_HEADER_ERR 6
|
||||
using RadioModeType_t=int;
|
||||
struct RadioModeConfig_t {};
|
||||
struct Module {
|
||||
enum { MODE_IDLE=0, MODE_RX=1 };
|
||||
std::string* trace;
|
||||
void setRfSwitchState(int mode) {
|
||||
assert(mode==MODE_RX || mode==MODE_IDLE);
|
||||
*trace+=mode==MODE_RX ? 'W' : 'I';
|
||||
}
|
||||
};
|
||||
struct LR1110 {
|
||||
std::string trace;
|
||||
char fail=0;
|
||||
size_t preambleLengthLoRa=32;
|
||||
uint8_t headerType=0,crcTypeLoRa=1,invertIQEnabled=0;
|
||||
uint32_t rxTimeout=0;
|
||||
int stagedMode=7;
|
||||
Module module{&trace};
|
||||
Module* mod=&module;
|
||||
int16_t call(char c) { trace+=c;return fail==c ? -7:0; }
|
||||
int16_t standby() { return call('S'); }
|
||||
int16_t standby(uint8_t) { return call('U'); }
|
||||
int16_t standby(uint8_t mode, bool wake) { assert(mode==1 && !wake);return call('s'); }
|
||||
int16_t setFs() { return call('F'); }
|
||||
int16_t sleep() { return call('L'); }
|
||||
int16_t reset() { return call('Z'); }
|
||||
int16_t stageMode(RadioModeType_t,RadioModeConfig_t*) { return call('G'); }
|
||||
int16_t setPreambleLength(size_t n) { preambleLengthLoRa=n;return call('P'); }
|
||||
int16_t startReceive(uint32_t,uint32_t,uint32_t,size_t) { return call('N'); }
|
||||
int16_t clearIrqState(uint32_t flags) { assert(flags==0xffffffff);return call('C'); }
|
||||
int16_t setPacketParamsLoRa(size_t p,uint8_t,uint8_t len,uint8_t,uint8_t) {
|
||||
assert(p==preambleLengthLoRa && len==255);return call('P');
|
||||
}
|
||||
int16_t setRx(uint32_t timeout) { assert(timeout==0xffffff);return call('R'); }
|
||||
};
|
||||
struct Radio:LR1110 {
|
||||
LR1110ProfileSwitchState _profileSwitch{true};
|
||||
bool _profileStandbyWarm=false,_profilePacketDirty=false;
|
||||
uint32_t _profileFastResumes=0;
|
||||
uint32_t pendingIrq=0;
|
||||
int16_t snapshotIrqAfterRxExit(uint32_t* irq) {
|
||||
const int16_t rc=call('Q');
|
||||
if (rc==0) *irq=pendingIrq;
|
||||
return rc;
|
||||
}
|
||||
@METHODS@
|
||||
};
|
||||
void prime(Radio& r) {
|
||||
r.trace.clear();assert(r.startReceive()==0 && r.trace=="N");
|
||||
assert(r._profileSwitch.rxValid);r.trace.clear();
|
||||
r.setProfileStandbyWarm(true);
|
||||
}
|
||||
void hop(Radio& r,size_t preamble,const std::string& expected) {
|
||||
r.beginProfileSwitch(true);assert(r.standby()==0 && r.trace=="IF");r.trace.clear();
|
||||
assert(r.setPreambleLength(preamble)==0 && r.trace.empty());
|
||||
assert(r.startReceive()==0 && r.trace==expected);
|
||||
assert(r._profileSwitch.rxValid && !r._profilePacketDirty);
|
||||
r.endProfileSwitch(true);r.trace.clear();
|
||||
}
|
||||
int main() {
|
||||
Radio r;prime(r);hop(r,32,"QWR");hop(r,48,"QPWR");hop(r,48,"QWR");
|
||||
r.pendingIrq=1;hop(r,32,"QCPWR");r.pendingIrq=0;
|
||||
assert(r._profileFastResumes==4);
|
||||
r.beginProfileSwitch(true);r.fail='F';
|
||||
assert(r.standby()==-7 && r.trace=="IF");
|
||||
assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid);
|
||||
r.endProfileSwitch(false);r.fail=0;prime(r);
|
||||
r.setProfileStandbyWarm(false);
|
||||
assert(!r._profileSwitch.rxValid);r.beginProfileSwitch(true);
|
||||
assert(r.standby()==0 && r.trace=="S");r.endProfileSwitch(true);
|
||||
prime(r);
|
||||
r.beginProfileSwitch(true);r.standby();r.trace.clear();
|
||||
r.pendingIrq=1;r.fail='C';assert(r.startReceive()==-7 && r.trace=="QC");
|
||||
assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid);
|
||||
r.endProfileSwitch(false);r.fail=0;r.pendingIrq=0;prime(r);hop(r,32,"QWR");
|
||||
r.beginProfileSwitch(true);r.standby();r.trace.clear();
|
||||
r.fail='Q';assert(r.startReceive()==-7 && r.trace=="Q");
|
||||
assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid);
|
||||
r.endProfileSwitch(false);r.fail=0;prime(r);
|
||||
r.beginProfileSwitch(true);r.standby();r.trace.clear();
|
||||
r.setPreambleLength(64);r.fail='P';
|
||||
assert(r.startReceive()==-7 && r.trace=="QP");
|
||||
assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid);
|
||||
r.endProfileSwitch(false);r.fail=0;prime(r);hop(r,32,"QPWR");
|
||||
r.beginProfileSwitch(true);r.standby();r.trace.clear();
|
||||
r.fail='R';assert(r.startReceive()==-7 && r.trace=="QWR");
|
||||
assert(r.profileSwitchFailed() && !r._profileSwitch.rxValid);
|
||||
r.endProfileSwitch(false);r.fail=0;prime(r);
|
||||
r.standby(uint8_t(0));assert(!r._profileSwitch.rxValid);
|
||||
prime(r);r.stageMode(2,nullptr);assert(!r._profileSwitch.rxValid);
|
||||
prime(r);r.sleep();assert(!r._profileSwitch.rxValid);
|
||||
prime(r);r.reset();assert(!r._profileSwitch.rxValid);
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@STATE@", state).replace("@METHODS@", methods))
|
||||
|
||||
def test_owned_rx_context_invalidates_on_errors_and_unowned_operations(self):
|
||||
state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text().replace(
|
||||
"#pragma once", ""
|
||||
)
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
@STATE@
|
||||
int main() {
|
||||
LR1110ProfileSwitchState s(true);
|
||||
s.begin(true, true);
|
||||
assert(!s.canResumeFast());
|
||||
s.end(true);
|
||||
s.rxResult(0, true);
|
||||
for (int continuous = 0; continuous < 2; ++continuous)
|
||||
for (int warm = 0; warm < 2; ++warm) {
|
||||
s.begin(continuous, warm);
|
||||
s.standbyResult(0);
|
||||
assert(s.canResumeFast() == bool(continuous && warm));
|
||||
s.end(true);
|
||||
}
|
||||
s.begin(true, true);
|
||||
assert(!s.canResumeFast());
|
||||
s.standbyResult(0);
|
||||
assert(s.canResumeFast());
|
||||
s.modulationResult(0, 8, 4, 1, 0);
|
||||
assert(s.matchesModulation(8, 4, 1, 0));
|
||||
assert(!s.matchesModulation(9, 4, 1, 0));
|
||||
s.rxResult(0, true);
|
||||
assert(!s.canResumeFast());
|
||||
s.end(true);
|
||||
s.begin(true, true);
|
||||
s.standbyResult(-7);
|
||||
assert(s.failed() && !s.rxValid && !s.modulationValid);
|
||||
s.end(false);
|
||||
assert(!s.failed());
|
||||
s.rxResult(0, true);
|
||||
s.begin(true, true);
|
||||
s.standbyResult(0);
|
||||
s.modulationResult(-8, 8, 4, 1, 0);
|
||||
assert(s.failed() && !s.rxValid && !s.modulationValid);
|
||||
s.end(false);
|
||||
s.rxResult(0, true);
|
||||
s.begin(true, true);
|
||||
s.standbyResult(0);
|
||||
s.invalidate();
|
||||
assert(!s.canResumeFast() && !s.rxValid);
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@STATE@", state))
|
||||
|
||||
def test_batched_modulation_validation_cache_and_failed_write_rollback(self):
|
||||
source = (ROOT / "src/helpers/radiolib/CustomLR1110.h").read_text()
|
||||
production = method(source, "int16_t setLoRaModulationParams(")
|
||||
state = (ROOT / "src/helpers/radiolib/LR1110ProfileSwitchState.h").read_text().replace(
|
||||
"#pragma once", ""
|
||||
)
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
@STATE@
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_INVALID_SPREADING_FACTOR -1
|
||||
#define RADIOLIB_ERR_INVALID_CODING_RATE -2
|
||||
#define RADIOLIB_ERR_INVALID_BANDWIDTH -3
|
||||
#define RADIOLIB_ERR_WRONG_MODEM -4
|
||||
#define RADIOLIB_LR11X0_PACKET_TYPE_NONE 0
|
||||
#define RADIOLIB_LR11X0_PACKET_TYPE_LORA 1
|
||||
#define RADIOLIB_LR11X0_LORA_BW_62_5 3
|
||||
#define RADIOLIB_LR11X0_LORA_BW_125_0 4
|
||||
#define RADIOLIB_LR11X0_LORA_BW_250_0 5
|
||||
#define RADIOLIB_LR11X0_LORA_BW_500_0 6
|
||||
#define RADIOLIB_LR11X0_LORA_LDRO_ENABLED 1
|
||||
#define RADIOLIB_LR11X0_LORA_LDRO_DISABLED 0
|
||||
struct Radio {
|
||||
LR1110ProfileSwitchState _profileSwitch{true};
|
||||
uint8_t spreadingFactor=7, bandwidth=4, codingRate=1, ldrOptimize=0;
|
||||
float bandwidthKhz=125;
|
||||
bool ldroAuto=true;
|
||||
uint8_t packetType=1;
|
||||
int16_t result=0;
|
||||
unsigned queries=0, writes=0;
|
||||
uint8_t payload[4]={};
|
||||
int16_t getPacketType(uint8_t* type) { ++queries;*type=packetType;return 0; }
|
||||
int16_t setModulationParamsLoRa(uint8_t sf,uint8_t bw,uint8_t cr,uint8_t ldro) {
|
||||
assert(spreadingFactor==sf && bandwidth==bw && codingRate==cr);
|
||||
ldrOptimize=ldroAuto ? ((float(1U<<sf)/bandwidthKhz)>=16.0f) : ldro;
|
||||
payload[0]=sf;payload[1]=bw;payload[2]=cr;payload[3]=ldrOptimize;
|
||||
++writes;return result;
|
||||
}
|
||||
@PRODUCTION@
|
||||
};
|
||||
int main() {
|
||||
Radio r;
|
||||
assert(r.setLoRaModulationParams(125,4,5)==RADIOLIB_ERR_INVALID_SPREADING_FACTOR);
|
||||
assert(r.setLoRaModulationParams(125,7,9)==RADIOLIB_ERR_INVALID_CODING_RATE);
|
||||
assert(r.setLoRaModulationParams(63,7,5)==RADIOLIB_ERR_INVALID_BANDWIDTH);
|
||||
assert(r.writes==0);
|
||||
r.packetType=0;
|
||||
assert(r.setLoRaModulationParams(125,7,5)==RADIOLIB_ERR_WRONG_MODEM);
|
||||
r.packetType=1;
|
||||
for(float bw: {62.5f,125.0f,250.0f,500.0f}) {
|
||||
for(uint8_t sf=5;sf<=12;++sf) {
|
||||
for(uint8_t cr=4;cr<=8;++cr) {
|
||||
assert(r.setLoRaModulationParams(bw,sf,cr)==0);
|
||||
assert(r.spreadingFactor==sf && r.bandwidthKhz==bw && r.codingRate==cr-4);
|
||||
assert(r.payload[0]==sf && r.payload[2]==cr-4);
|
||||
assert(r.payload[3]==((float(1U<<sf)/bw)>=16.0f));
|
||||
r._profileSwitch.rxResult(0,true);
|
||||
r._profileSwitch.begin(true,true);
|
||||
r._profileSwitch.standbyResult(0);
|
||||
const unsigned writes=r.writes;
|
||||
assert(r.setLoRaModulationParams(bw,sf,cr)==0 && r.writes==writes);
|
||||
r._profileSwitch.end(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
r._profileSwitch.rxResult(0,true);
|
||||
r._profileSwitch.begin(true,true);
|
||||
r._profileSwitch.standbyResult(0);
|
||||
const auto oldSf=r.spreadingFactor, oldBw=r.bandwidth, oldCr=r.codingRate;
|
||||
const auto oldKhz=r.bandwidthKhz;
|
||||
const auto oldLdro=r.ldrOptimize;
|
||||
r.result=-99;
|
||||
assert(r.setLoRaModulationParams(125,8,5)==-99);
|
||||
assert(r.spreadingFactor==oldSf && r.bandwidth==oldBw && r.codingRate==oldCr);
|
||||
assert(r.bandwidthKhz==oldKhz && r.ldrOptimize==oldLdro);
|
||||
assert(r._profileSwitch.failed() && !r._profileSwitch.rxValid);
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@STATE@", state).replace("@PRODUCTION@", production))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,6 +10,59 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
class FastProfileSwitchTests(unittest.TestCase):
|
||||
compile_run = compiler.BatchedModulationTests.compile_run
|
||||
|
||||
def test_fast_switch_defaults_cover_sx1262_variants_except_indicator(self):
|
||||
radio = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
self.assertIn("#define MC_SX1262_FAST_PROFILE_SWITCH 1", radio)
|
||||
self.assertIn("#define MC_SX1262_FAST_FS MC_SX1262_FAST_PROFILE_SWITCH", radio)
|
||||
self.assertIn("#define MC_SX1262_FAST_PACKET_CACHE MC_SX1262_FAST_PROFILE_SWITCH", radio)
|
||||
found = set()
|
||||
for path in (ROOT / "variants").glob("*/platformio.ini"):
|
||||
config = path.read_text()
|
||||
if "-D RADIO_CLASS=CustomSX1262" not in config:
|
||||
continue
|
||||
found.add(path.parent.name)
|
||||
if path.parent.name == "sensecap_indicator-espnow":
|
||||
self.assertIn("-D MC_SX1262_FAST_PROFILE_SWITCH=1", config)
|
||||
self.assertIn("-D MC_SX1262_FAST_FS=0", config)
|
||||
self.assertIn("-D MC_SX1262_FAST_PACKET_CACHE=0", config)
|
||||
self.assertIn("IndicatorRadioHal", (path.parent / "target.cpp").read_text())
|
||||
else:
|
||||
self.assertNotIn("-D MC_SX1262_FAST_PROFILE_SWITCH=0", config)
|
||||
self.assertNotIn("-D MC_SX1262_FAST_FS=0", config)
|
||||
self.assertNotIn("-D MC_SX1262_FAST_PACKET_CACHE=0", config)
|
||||
self.assertIn("sensecap_indicator-espnow", found)
|
||||
self.assertGreater(len(found), 70)
|
||||
|
||||
def test_packet_tuple_requires_matching_acknowledged_owned_rx(self):
|
||||
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text()
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@STATE@
|
||||
int main() {
|
||||
SX1262ProfileSwitchState s(true);
|
||||
s.packetResult(0, 32, 1, 255, 0, 0);
|
||||
assert(!s.matchesPacket(32, 1, 255, 0, 0));
|
||||
s.rxResult(0, true);
|
||||
s.begin(true, true);
|
||||
s.standbyResult(0);
|
||||
assert(s.matchesPacket(32, 1, 255, 0, 0));
|
||||
assert(!s.matchesPacket(33, 1, 255, 0, 0));
|
||||
assert(!s.matchesPacket(32, 0, 255, 0, 0));
|
||||
assert(!s.matchesPacket(32, 1, 16, 0, 0));
|
||||
assert(!s.matchesPacket(32, 1, 255, 1, 0));
|
||||
assert(!s.matchesPacket(32, 1, 255, 0, 1));
|
||||
s.invalidatePacket();
|
||||
assert(!s.matchesPacket(32, 1, 255, 0, 0));
|
||||
s.packetResult(0, 32, 1, 255, 0, 0);
|
||||
assert(s.matchesPacket(32, 1, 255, 0, 0));
|
||||
s.packetResult(-100, 32, 1, 255, 0, 0);
|
||||
assert(s.failed() && !s.packetValid && !s.rxValid);
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@STATE@", state))
|
||||
|
||||
def test_owned_rx_window_commands_errors_and_lifecycle_invalidation(self):
|
||||
src = (ROOT / "src/helpers/radiolib/CustomSX1262.h").read_text()
|
||||
state = (ROOT / "src/helpers/radiolib/SX1262ProfileSwitchState.h").read_text().replace("#pragma once", "")
|
||||
@@ -17,7 +70,9 @@ class FastProfileSwitchTests(unittest.TestCase):
|
||||
"void endProfileSwitch(", "bool profileSwitchFailed(", "int16_t restoreTcxoAfterSleep(",
|
||||
"int16_t standby()", "int16_t standby(uint8_t", "int16_t sleep()",
|
||||
"int16_t sleep(bool", "int16_t reset(bool", "int16_t stageMode(",
|
||||
"int16_t setPreambleLength(", "int16_t startReceive()",
|
||||
"int16_t setPreambleLength(", "int16_t setCRC(",
|
||||
"int16_t implicitHeader(", "int16_t explicitHeader()",
|
||||
"int16_t invertIQ(", "int16_t startReceive()",
|
||||
"int16_t startReceiveDutyCycle("]
|
||||
methods = "\n".join(method(src, signature).replace(" override", "") for signature in signatures)
|
||||
harness = r'''
|
||||
@@ -26,6 +81,8 @@ class FastProfileSwitchTests(unittest.TestCase):
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
@STATE@
|
||||
#define MC_SX1262_FAST_FS @FS@
|
||||
#define MC_SX1262_FAST_PACKET_CACHE @PACKET@
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_SX126X_STANDBY_XOSC 1
|
||||
#define RADIOLIB_SX126X_STANDBY_RC 0
|
||||
@@ -41,8 +98,8 @@ using RadioModeType_t=int;
|
||||
using RadioLibIrqFlags_t=uint32_t;
|
||||
struct RadioModeConfig_t {};
|
||||
struct Module {
|
||||
enum { MODE_RX=1 };std::string* trace;
|
||||
void setRfSwitchState(int mode) { assert(mode==MODE_RX);*trace+='W'; }
|
||||
enum { MODE_IDLE=0, MODE_RX=1 };std::string* trace;
|
||||
void setRfSwitchState(int mode) { assert(mode==MODE_IDLE || mode==MODE_RX);*trace+=mode==MODE_RX?'W':'I'; }
|
||||
};
|
||||
struct SX1262 {
|
||||
std::string trace;char fail=0;int modem=1;bool standbyXOSC=true;
|
||||
@@ -54,6 +111,7 @@ struct SX1262 {
|
||||
virtual ~SX1262()=default;
|
||||
int16_t call(char c) { trace+=c;return fail==c ? -100:0; }
|
||||
Module* getMod() { return mod; }
|
||||
int16_t setFs() { return call('f'); }
|
||||
virtual int16_t standby() { return call('S'); }
|
||||
virtual int16_t standby(uint8_t) { return call('U'); }
|
||||
int16_t standby(uint8_t mode,bool wake) { assert(mode==1);return call(wake?'S':'s'); }
|
||||
@@ -66,6 +124,10 @@ struct SX1262 {
|
||||
}
|
||||
virtual int16_t stageMode(RadioModeType_t mode,RadioModeConfig_t*) { return call(mode==1?'G':'T'); }
|
||||
virtual int16_t setPreambleLength(size_t n) { preambleLengthLoRa=n;return call('M'); }
|
||||
int16_t setCRC(uint8_t,uint16_t,uint16_t,bool) { return call('c'); }
|
||||
int16_t implicitHeader(size_t) { return call('h'); }
|
||||
int16_t explicitHeader() { return call('e'); }
|
||||
virtual int16_t invertIQ(bool) { return call('i'); }
|
||||
int16_t startReceive(uint32_t timeout,uint32_t flags,uint32_t mask,size_t len) {
|
||||
assert(timeout==0xffffff && flags==9 && mask==2 && len==0);
|
||||
int16_t rc=stageMode(1,nullptr);if(rc==0) rc=standby();if(rc==0) rc=call('F');return rc;
|
||||
@@ -90,14 +152,16 @@ void prime(Radio& r) {
|
||||
r.trace.clear();assert(r.startReceive()==0);assert(r.trace==(wake ? "GUEVFQ":cold ? "GUFQ":"GSFQ"));
|
||||
assert(!r._coldStandby);
|
||||
assert(!r._profileSwitch.modulationValid); // full RX setup does not prime a tuple
|
||||
assert(r._profileSwitch.rxValid);r.trace.clear();
|
||||
assert(r._profileSwitch.rxValid && r._profileSwitch.packetValid);r.trace.clear();
|
||||
}
|
||||
void fast(Radio& r) {
|
||||
r.beginProfileSwitch(true);assert(r.standby()==0);assert(r.trace=="s");r.trace.clear();
|
||||
r.beginProfileSwitch(true);assert(r.standby()==0);
|
||||
assert(r.trace==(MC_SX1262_FAST_FS ? "If" : "s"));r.trace.clear();
|
||||
r._profileSwitch.modulationResult(0,8,4,1,0);
|
||||
assert(r._profileSwitch.matchesModulation(8,4,1,0));
|
||||
assert(r.setPreambleLength(120)==0 && r.trace.empty());
|
||||
assert(r.startReceive()==0 && r.trace=="CPWR");
|
||||
const bool unchanged = r._profileSwitch.packetValid && r._profileSwitch.packetPreamble==120;
|
||||
assert(r.startReceive()==0 && r.trace==(MC_SX1262_FAST_PACKET_CACHE && unchanged ? "CWR" : "CPWR"));
|
||||
assert(r.stagedMode==0 && r._profileSwitch.rxValid);
|
||||
assert(r._profileSwitch.modulationValid); // successful fast resume preserves it
|
||||
r.endProfileSwitch(true);assert(!r._profileSwitch.open);r.trace.clear();
|
||||
@@ -122,7 +186,8 @@ int main() {
|
||||
case 5:x.reset();break;
|
||||
case 6:x.startReceiveDutyCycle(1000,1000);break;
|
||||
}
|
||||
assert(!x._profileSwitch.rxValid && !x._profileSwitch.modulationValid);
|
||||
assert(!x._profileSwitch.rxValid && !x._profileSwitch.modulationValid
|
||||
&& !x._profileSwitch.packetValid);
|
||||
x.trace.clear();x.beginProfileSwitch(true);
|
||||
const bool cold=x._coldStandby;
|
||||
const bool wake=x._tcxoWakePending;
|
||||
@@ -135,10 +200,22 @@ int main() {
|
||||
x.beginProfileSwitch(gate!=2);x.trace.clear();
|
||||
assert(x.standby()==0 && x.trace=="S");x.endProfileSwitch(true);
|
||||
}
|
||||
for(char fail:std::string("sCPR")) {
|
||||
for(int setter=0;setter<5;++setter) {
|
||||
Radio x;prime(x);
|
||||
switch(setter) {
|
||||
case 0:x.setPreambleLength(120);break;
|
||||
case 1:x.setCRC(1);break;
|
||||
case 2:x.implicitHeader(16);break;
|
||||
case 3:x.explicitHeader();break;
|
||||
case 4:x.invertIQ(true);break;
|
||||
}
|
||||
assert(!x._profileSwitch.packetValid && x._profileSwitch.rxValid);
|
||||
}
|
||||
const char standbyFail = MC_SX1262_FAST_FS ? 'f' : 's';
|
||||
for(char fail:std::string(1,standbyFail)+"CPR") {
|
||||
Radio x;prime(x);x.beginProfileSwitch(true);x.fail=fail;
|
||||
if(fail=='s') assert(x.standby()==-100);
|
||||
else { assert(x.standby()==0);assert(x.startReceive()==-100); }
|
||||
if(fail==standbyFail) assert(x.standby()==-100);
|
||||
else { assert(x.standby()==0);if(fail=='P') x.setPreambleLength(120);assert(x.startReceive()==-100); }
|
||||
assert(x.profileSwitchFailed() && !x._profileSwitch.rxValid);
|
||||
assert(x.trace.back()==fail);x.endProfileSwitch(false);x.fail=0;prime(x);fast(x);
|
||||
}
|
||||
@@ -158,7 +235,12 @@ int main() {
|
||||
assert(off.trace=="GUF" && !off._profileSwitch.rxValid);
|
||||
}
|
||||
'''
|
||||
self.compile_run(harness.replace("@STATE@", state).replace("@METHODS@", methods))
|
||||
for fs, packet in ((0, 0), (1, 1)):
|
||||
with self.subTest(fs=fs, packet=packet):
|
||||
self.compile_run(harness.replace("@STATE@", state)
|
||||
.replace("@METHODS@", methods)
|
||||
.replace("@FS@", str(fs))
|
||||
.replace("@PACKET@", str(packet)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Exact five-radio fixture. Never enumerate duplicate CDC interfaces as boards."""
|
||||
TXS = (
|
||||
dict(channel=0, serial='9AB3B64C641BA927', topology='1-1.3.1', board='RAK4631',
|
||||
env='profile_fixed_rak_tx', fwid=0xB6, app_base=0x26000, boot_vid=0x239A, boot_pid=0x0029),
|
||||
env='profile_fixed_rak_tx', fwid=0xB6, app_base=0x26000, boot_vid=0x239A, boot_pid=0x002A),
|
||||
dict(channel=1, serial='651F8E496197F882', topology='1-1.2.3', board='Heltec T096',
|
||||
env='profile_fixed_t096_tx', fwid=0xB6, app_base=0x26000, boot_vid=0x239A, boot_pid=0x0071),
|
||||
dict(channel=2, serial='9352162A72082314', topology='1-1.2.2', board='Heltec MeshTower V2',
|
||||
|
||||
@@ -154,3 +154,89 @@ build_flags =
|
||||
build_src_filter =
|
||||
+<helpers/UsbLogging.cpp>
|
||||
+<../tools/hil/profile_switch_nrf52_tx.cpp>
|
||||
|
||||
; Application-only, receive-only production-wrapper timing test for the
|
||||
; Seeed T1000-E LR1110. RX-only profiles are 909.5 and 909.75 MHz.
|
||||
[env:profile_switch_t1000_lr1110]
|
||||
extends = arduino_base
|
||||
platform = nordicnrf52
|
||||
platform_packages = ${nrf52_base.platform_packages}
|
||||
board = tracker-t1000-e
|
||||
board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
|
||||
board_build.variants_dir = tools/hil
|
||||
board_build.variant = fixed_t1000_variant
|
||||
board_upload.maximum_size = 708608
|
||||
upload_protocol = nrfutil
|
||||
extra_scripts =
|
||||
create-uf2.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
lib_deps = ${env:profile_switch_xiao.lib_deps}
|
||||
lib_ignore = PsychicMqttClient
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
-I lib/nrf52/s140_nrf52_7.3.0_API/include
|
||||
-I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
|
||||
-D USE_LR1110
|
||||
-D MC_LR1110_FAST_PROFILE_SWITCH=1
|
||||
-D MC_LR1110_HIL=1
|
||||
-D MC_TCXO_DELAY_US=200
|
||||
-D P_LORA_DIO_1=33
|
||||
-D P_LORA_NSS=12
|
||||
-D P_LORA_RESET=42
|
||||
-D P_LORA_BUSY=7
|
||||
-D P_LORA_SCLK=11
|
||||
-D P_LORA_MISO=40
|
||||
-D P_LORA_MOSI=41
|
||||
-D LORA_FREQ=909.5
|
||||
-D LORA_BW=125
|
||||
-D LORA_SF=7
|
||||
-D LORA_CR=5
|
||||
-D LORA_TX_POWER=0
|
||||
build_src_filter =
|
||||
+<helpers/radiolib/RadioLibWrappers.cpp>
|
||||
+<helpers/UsbLogging.cpp>
|
||||
+<../tools/hil/profile_switch_t1000_lr1110.cpp>
|
||||
|
||||
; Application-only SX1262 A/B timing and requested-packet receiver on the
|
||||
; single Pi-attached Heltec T096. No identity, flash settings, or autonomous TX.
|
||||
[env:profile_switch_t096_sx1262]
|
||||
extends = arduino_base
|
||||
platform = nordicnrf52
|
||||
platform_packages = ${nrf52_base.platform_packages}
|
||||
board = heltec_t096
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
board_build.variants_dir = tools/hil
|
||||
board_build.variant = fixed_t096_variant
|
||||
upload_protocol = nrfutil
|
||||
extra_scripts =
|
||||
create-uf2.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
lib_deps = ${env:profile_switch_xiao.lib_deps}
|
||||
lib_ignore = PsychicMqttClient
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
-D NRF52_PLATFORM
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||
-D USE_SX1262
|
||||
-D MC_SX1262_HIL=1
|
||||
-D MC_TCXO_DELAY_US=1600
|
||||
-D P_LORA_DIO_1=21
|
||||
-D P_LORA_NSS=5
|
||||
-D P_LORA_RESET=16
|
||||
-D P_LORA_BUSY=19
|
||||
-D P_LORA_SCLK=40
|
||||
-D P_LORA_MISO=14
|
||||
-D P_LORA_MOSI=11
|
||||
-D LORA_FREQ=909.5
|
||||
-D LORA_BW=125
|
||||
-D LORA_SF=7
|
||||
-D LORA_CR=5
|
||||
-D LORA_TX_POWER=0
|
||||
build_src_filter =
|
||||
+<helpers/radiolib/RadioLibWrappers.cpp>
|
||||
+<helpers/UsbLogging.cpp>
|
||||
+<../tools/hil/profile_switch_t096_sx1262.cpp>
|
||||
@@ -100,12 +100,19 @@ The default `production` group compares batching/2 MHz, fast RX/2 MHz,
|
||||
buffered SPI/8 MHz and their combination. Bit 512 selects the actual production
|
||||
fast state machine; low mask bits remain zero. A hardware success counter must
|
||||
advance on fast trials and packet readiness, and remain unchanged on controls.
|
||||
XIAO S3 WIO and Indicator LoRa opt in to fast RX and buffered 8 MHz SPI in their
|
||||
normal variants. Other variants do not opt in to the fast path or faster SPI.
|
||||
Owned fast RX, `SetFs`, and skipping unchanged, acknowledged RX packet tuples
|
||||
are defaults for `CustomSX1262` boards with direct radio I/O. The Seeed
|
||||
Indicator deliberately retains its earlier fast path without the two new
|
||||
shortcuts: its radio control and BUSY/IRQ pins run through an I2C expander.
|
||||
XIAO S3 WIO, Station G3 KISS, Heltec V4 KISS, and the Indicator retain their
|
||||
existing buffered 8 MHz SPI choices; this change does not raise SPI speed on
|
||||
other boards. The new operations were checked over the air on a T096 SX1262
|
||||
fixture, not separately qualified for every board now using them.
|
||||
|
||||
Fast RX reuses IRQ mapping and buffer bases only after successful continuous
|
||||
LoRa RX, within an owned warm retune. It retains packet-parameter/IQ programming,
|
||||
stale-IRQ clearing, RF-switch control, SetRx and BUSY waits. TX, CAD, ordinary
|
||||
LoRa RX, within an owned warm retune. It retains stale-IRQ clearing, RF-switch
|
||||
control, SetRx and BUSY waits. Packet-parameter/IQ programming remains on the
|
||||
Indicator and on any changed or untrusted packet tuple. TX, CAD, ordinary
|
||||
standby, sleep, reset, duty cycling and failed commands invalidate reuse. A
|
||||
failed RX resume rolls back the old tuple/profile through full setup.
|
||||
|
||||
@@ -783,3 +790,62 @@ Random arrival delays retain the existing distribution over approximately two
|
||||
nominal scan cycles, so their absolute time range scales with dwell.
|
||||
The [longer-dwell report](../../docs/four_fixed_tx_dwell_validation.md) records
|
||||
the user-stopped 6.1-symbol sample and completed 7.7-symbol repeat.
|
||||
|
||||
## T1000-E LR1110 RX-only timing
|
||||
|
||||
The checked FS-mode fast profile switch is enabled by default in
|
||||
`CustomLR1110` for all MeshCore LR1110 targets: T1000-E, Wio WM1110,
|
||||
ThinkNode M3/M7/M9, and Minewsemi ME25LS01. Their radio SPI paths now use
|
||||
16 MHz buffered transfers. Each board keeps its existing TCXO startup delay;
|
||||
the 0.6 ms combined-hop goal is **not** a 600 µs TCXO delay. Only the T1000-E
|
||||
has on-device timing validation so far. Other boards require measured retune
|
||||
timing, packet reception, and TX checks before claiming the 0.6 ms goal.
|
||||
|
||||
`profile_switch_t1000_lr1110` uses the production `CustomLR1110Wrapper` and
|
||||
the same 200 µs TCXO/fast-retune settings as the T1000-E variant. It never
|
||||
transmits, loads an identity, or writes saved settings. Its only frequencies
|
||||
are 909.5 and 909.75 MHz; the `mod`, `freq`, `both`, and `preamble` modes change
|
||||
SF7/SF8, frequency, both, or 32/48 preamble symbols respectively. A run adds
|
||||
eight warmup hops to the requested count and times the remainder with the
|
||||
nRF52840's 64 MHz cycle counter. A separate RSSI read checks that each resumed
|
||||
receiver responds after seven milliseconds of settling; it is outside timing.
|
||||
|
||||
Build with `pio run -e profile_switch_t1000_lr1110`. Flash its application-only
|
||||
ZIP with serial DFU after verifying the board's unique USB serial number and
|
||||
bootloader identity; do not use a whole-chip erase or UF2 copy as a substitute.
|
||||
At 115200 baud the commands are `info`, `spihz 2000000|8000000|16000000`,
|
||||
`bulk 0|1`, `run both 1000 1` (use a new sequence number per run), and
|
||||
`result result 1` to replay the last result without another RF run. `fs 0|1`
|
||||
compares the corrected XOSC standby with the default FS-mode retune.
|
||||
`clear 0|1|2` compares always-clear, never-clear (unsafe diagnostic only), and the
|
||||
default checked-clear path. `inject 1` puts a one-shot RX-timeout IRQ into the
|
||||
next run to verify checked clearing. `tcxo`
|
||||
accepts 50, 100, 150–200 in 10 µs steps, 400, 800, and 1600 µs for RAM-only
|
||||
experiments; reboot restores the compiled 200 µs. Each JSON result reports
|
||||
command counts, busy deferrals, drained test packets, failures, RX/cache/RSSI
|
||||
errors, and both timing directions. The RX-only harness discards pending packets
|
||||
before retrying a deferred hop; otherwise an unprocessed interrupt can stall
|
||||
the test even though production firmware would consume it.
|
||||
|
||||
On the Pi-attached T1000-E, 16 MHz buffered SPI and default 200 µs TCXO,
|
||||
FS-mode retune plus checked IRQ clearing averaged about 0.504–0.508 ms for
|
||||
simultaneous frequency/SF changes, 0.384 ms for frequency only, 0.409 ms for SF
|
||||
only, and 0.361 ms for a preamble change. Three boot/run cycles with the final
|
||||
default HIL image each completed 1,008 combined hops; earlier runs completed
|
||||
three more combined cycles plus 1,008 of each single-change mode. No completed
|
||||
run had a radio, cache, RSSI, or BUSY-timeout error. An injected stale timeout
|
||||
IRQ was detected and cleared before RX re-entry. A corrected XOSC standby path
|
||||
with checked IRQ clearing took about 0.568–0.575 ms; always clearing took about
|
||||
0.60 ms. The old RadioLib `STANDBY_XOSC` constant is erroneously zero (RC mode),
|
||||
whose previous combined result was about 0.87 ms at a requested 180 µs. The
|
||||
unbuffered 2 MHz RC-standby baseline was about 1.36 ms.
|
||||
|
||||
RadioLib converts the requested TCXO delay to 32.768 kHz ticks by truncating
|
||||
`delay / 30.52`, so requests of 160, 170, and 180 µs all programmed five ticks
|
||||
(about 153 µs), while 200 µs programs six (about 183 µs). The earlier 150 µs
|
||||
trial failed after reset; the 160/170 µs runs developed deferrals, but those
|
||||
differences do not establish a precise hardware threshold. With true XOSC or
|
||||
FS-mode retuning, 180 and 200 µs produced indistinguishable warm-hop timing;
|
||||
200 µs remains the selected startup setting. These are room-temperature,
|
||||
single-unit, RX-only retune/command-health measurements, not over-the-air
|
||||
packet reception, TX operation, or voltage/temperature-corner qualification.
|
||||
@@ -0,0 +1,296 @@
|
||||
// Application-only Heltec T096 SX1262 experiment. No autonomous TX, identity,
|
||||
// saved settings, display, or filesystem. RX frequencies: 909.5/909.75 MHz.
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#include <SPI.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#include "ProfileSwitchUsb.h"
|
||||
|
||||
class BenchBoard : public mesh::MainBoard {
|
||||
public:
|
||||
uint16_t getBattMilliVolts() override { return 0; }
|
||||
const char* getManufacturerName() const override { return "Heltec T096 SX1262 bench"; }
|
||||
void reboot() override { NVIC_SystemReset(); }
|
||||
uint8_t getStartupReason() const override { return 0; }
|
||||
} board;
|
||||
|
||||
class CountingHal : public Nrf52BufferedRadioHal {
|
||||
public:
|
||||
explicit CountingHal(SPIClass& bus) : Nrf52BufferedRadioHal(bus, 8000000) {}
|
||||
uint32_t spiHz = 8000000;
|
||||
uint32_t rfWrites = 0, modWrites = 0, packetWrites = 0;
|
||||
uint32_t standbyWrites = 0, fsWrites = 0, clearWrites = 0, rxWrites = 0;
|
||||
void setSpiHz(uint32_t hz) {
|
||||
spiHz = hz;
|
||||
spiSettings = SPISettings(hz, MSBFIRST, SPI_MODE0);
|
||||
}
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
if (len) {
|
||||
switch (out[0]) {
|
||||
case 0x86: ++rfWrites; break;
|
||||
case 0x8B: ++modWrites; break;
|
||||
case 0x8C: ++packetWrites; break;
|
||||
case 0x80: ++standbyWrites; break;
|
||||
case 0xC1: ++fsWrites; break;
|
||||
case 0x02: ++clearWrites; break;
|
||||
case 0x82: ++rxWrites; break;
|
||||
}
|
||||
}
|
||||
Nrf52BufferedRadioHal::spiTransfer(out, len, in);
|
||||
}
|
||||
} radioHal(SPI);
|
||||
|
||||
CustomSX1262 chip = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1,
|
||||
P_LORA_RESET, P_LORA_BUSY);
|
||||
class BenchWrapper : public CustomSX1262Wrapper {
|
||||
public:
|
||||
using CustomSX1262Wrapper::CustomSX1262Wrapper;
|
||||
mesh::RadioParamApplyResult hop(uint8_t profile) { return tuneProfile(profile); }
|
||||
void startRx() { startRecv(); }
|
||||
void warm() { setProfileStandbyWarm(true); }
|
||||
} driver(chip, board);
|
||||
|
||||
struct Timing {
|
||||
uint32_t n = 0, lo = UINT32_MAX, hi = 0;
|
||||
uint64_t sum = 0;
|
||||
void add(uint32_t cycles) { ++n; if (cycles < lo) lo = cycles; if (cycles > hi) hi = cycles; sum += cycles; }
|
||||
double toUs(uint64_t cycles) const { return double(cycles) * 1000000.0 / SystemCoreClock; }
|
||||
double mean() const { return n ? toUs(sum) / n : 0; }
|
||||
double min() const { return n ? toUs(lo) : 0; }
|
||||
double max() const { return n ? toUs(hi) : 0; }
|
||||
};
|
||||
|
||||
bool ready = false, pilotReady = false;
|
||||
int initError = 0;
|
||||
uint32_t lastSequence = 0;
|
||||
|
||||
bool waitBusy(uint32_t limitUs = 20000) {
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
while (chip.isChipBusy())
|
||||
if (uint32_t(DWT->CYCCNT - start) >= uint64_t(limitUs) * SystemCoreClock / 1000000) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void drain() {
|
||||
uint8_t packet[256] = {};
|
||||
driver.recvRaw(packet, sizeof(packet));
|
||||
}
|
||||
|
||||
bool configureProfiles(uint8_t sf, bool varySf) {
|
||||
drain();
|
||||
mesh::RadioProfileParams primary;
|
||||
primary.freq = 909.5f; primary.bw = 125; primary.sf = sf; primary.cr = 5;
|
||||
primary.preamble = 32;
|
||||
auto applied = driver.trySetPrimaryParams(primary, true);
|
||||
for (unsigned retry = 0; applied == mesh::RadioParamApplyResult::BUSY && retry < 20; ++retry) {
|
||||
drain(); delay(20);
|
||||
applied = driver.trySetPrimaryParams(primary, true);
|
||||
}
|
||||
if (applied != mesh::RadioParamApplyResult::APPLIED) return false;
|
||||
mesh::RadioProfileConfig secondary;
|
||||
secondary.params = primary;
|
||||
secondary.params.freq = 909.75f;
|
||||
if (varySf) secondary.params.sf = sf + 1;
|
||||
secondary.mode = mesh::RadioProfileMode::Rx;
|
||||
driver.profiles()->setSecondary(secondary, true);
|
||||
driver.startRx();
|
||||
driver.warm();
|
||||
return driver.isInRecvMode() && waitBusy();
|
||||
}
|
||||
|
||||
void run(const char* mode, unsigned count, uint32_t sequence) {
|
||||
const bool varySf = !strcmp(mode, "both");
|
||||
if ((!varySf && strcmp(mode, "freq")) || !ready || sequence <= lastSequence
|
||||
|| count < 16 || count > 1000) {
|
||||
Serial.println("{\"error\":\"invalid run\"}"); return;
|
||||
}
|
||||
lastSequence = sequence;
|
||||
pilotReady = false;
|
||||
if (!configureProfiles(7, varySf)) { Serial.println("{\"error\":\"setup failed\"}"); return; }
|
||||
const uint32_t rf0 = radioHal.rfWrites, mod0 = radioHal.modWrites;
|
||||
const uint32_t packet0 = radioHal.packetWrites, standby0 = radioHal.standbyWrites;
|
||||
const uint32_t fs0 = radioHal.fsWrites, clear0 = radioHal.clearWrites;
|
||||
const uint32_t rx0 = radioHal.rxWrites, fast0 = chip.getOptimizedProfileSwitches();
|
||||
const uint32_t skip0 = chip.hilPacketSkips, fsEntry0 = chip.hilFsEntries;
|
||||
Timing timing[2];
|
||||
uint32_t completed = 0, deferrals = 0, failures = 0, modeErrors = 0;
|
||||
uint32_t cacheErrors = 0, rssiErrors = 0, busyTimeouts = 0;
|
||||
for (unsigned attempt = 0; completed < count + 8 && attempt < (count + 8) * 4; ++attempt) {
|
||||
drain();
|
||||
if (!waitBusy()) { ++busyTimeouts; break; }
|
||||
delay(10); // host traffic and settling stay outside the timed region
|
||||
const uint8_t target = driver.receiveProfile() ^ 1;
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
const auto result = driver.hop(target);
|
||||
if (result == mesh::RadioParamApplyResult::BUSY) { ++deferrals; drain(); continue; }
|
||||
if (result != mesh::RadioParamApplyResult::APPLIED) { ++failures; break; }
|
||||
if (!waitBusy()) { ++busyTimeouts; break; }
|
||||
const uint32_t elapsed = DWT->CYCCNT - start;
|
||||
if (sx126xReceiveMode(&chip) != 1) ++modeErrors;
|
||||
if (chip.hilFrequencyMHz() != (target ? 909.75f : 909.5f)
|
||||
|| driver.getSpreadingFactor() != (target && varySf ? 8 : 7)) ++cacheErrors;
|
||||
delay(7);
|
||||
const float rssi = driver.getCurrentRSSI();
|
||||
if (!isfinite(rssi) || rssi < -180 || rssi > 20) ++rssiErrors;
|
||||
if (completed++ >= 8) timing[target].add(elapsed);
|
||||
}
|
||||
driver.profiles()->setSecondary({}, true);
|
||||
drain();
|
||||
auto restored = driver.hop(0);
|
||||
for (unsigned retry = 0; restored == mesh::RadioParamApplyResult::BUSY && retry < 20; ++retry) {
|
||||
drain(); delay(20); restored = driver.hop(0);
|
||||
}
|
||||
const bool safe = restored == mesh::RadioParamApplyResult::APPLIED
|
||||
&& driver.isInRecvMode() && chip.hilFrequencyMHz() == 909.5f;
|
||||
recordBenchResult(lastRunResult, sequence,
|
||||
"{\"result\":%lu,\"board\":\"Heltec T096 SX1262\",\"mode\":\"%s\","
|
||||
"\"fs\":%s,\"packet_cache\":%s,\"spi_hz\":%lu,\"requested\":%u,"
|
||||
"\"completed\":%lu,\"deferrals\":%lu,\"failures\":%lu,"
|
||||
"\"mode_errors\":%lu,\"cache_errors\":%lu,\"rssi_errors\":%lu,"
|
||||
"\"busy_timeouts\":%lu,\"fast_resumes\":%lu,\"fs_entries\":%lu,"
|
||||
"\"packet_skips\":%lu,\"rf_commands\":%lu,\"mod_commands\":%lu,"
|
||||
"\"packet_commands\":%lu,\"standby_commands\":%lu,\"fs_commands\":%lu,"
|
||||
"\"clear_commands\":%lu,\"rx_commands\":%lu,\"restored\":%s,"
|
||||
"\"to_primary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f},"
|
||||
"\"to_secondary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f}}\n",
|
||||
(unsigned long)sequence, mode, chip.hilUseFsOnRetune ? "true" : "false",
|
||||
chip.hilSkipUnchangedPacket ? "true" : "false", (unsigned long)radioHal.spiHz,
|
||||
count, (unsigned long)completed, (unsigned long)deferrals, (unsigned long)failures,
|
||||
(unsigned long)modeErrors, (unsigned long)cacheErrors, (unsigned long)rssiErrors,
|
||||
(unsigned long)busyTimeouts, (unsigned long)(chip.getOptimizedProfileSwitches() - fast0),
|
||||
(unsigned long)(chip.hilFsEntries - fsEntry0), (unsigned long)(chip.hilPacketSkips - skip0),
|
||||
(unsigned long)(radioHal.rfWrites - rf0), (unsigned long)(radioHal.modWrites - mod0),
|
||||
(unsigned long)(radioHal.packetWrites - packet0), (unsigned long)(radioHal.standbyWrites - standby0),
|
||||
(unsigned long)(radioHal.fsWrites - fs0), (unsigned long)(radioHal.clearWrites - clear0),
|
||||
(unsigned long)(radioHal.rxWrites - rx0), safe ? "true" : "false",
|
||||
(unsigned long)timing[0].n, timing[0].min(), timing[0].mean(), timing[0].max(),
|
||||
(unsigned long)timing[1].n, timing[1].min(), timing[1].mean(), timing[1].max());
|
||||
}
|
||||
|
||||
void pilot() {
|
||||
if (!ready || !configureProfiles(10, false)) {
|
||||
pilotReady = false;
|
||||
Serial.println("{\"pilot\":false}");
|
||||
return;
|
||||
}
|
||||
pilotReady = driver.receiveProfile() == 0 && driver.isInRecvMode()
|
||||
&& chip.hilFrequencyMHz() == 909.5f && driver.getSpreadingFactor() == 10;
|
||||
Serial.printf("{\"pilot\":%s,\"freq_khz\":909500,\"sf\":10,\"bw_khz\":125}\n",
|
||||
pilotReady ? "true" : "false");
|
||||
}
|
||||
|
||||
void pilotHop(unsigned target) {
|
||||
if (!pilotReady || target > 1 || driver.receiveProfile() == target) {
|
||||
Serial.println("{\"error\":\"invalid pilot hop\"}"); return;
|
||||
}
|
||||
const uint32_t fast0 = chip.getOptimizedProfileSwitches();
|
||||
const uint32_t fs0 = chip.hilFsEntries, skip0 = chip.hilPacketSkips;
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const auto result = driver.hop(target);
|
||||
const uint32_t cycles = DWT->CYCCNT - started;
|
||||
const bool okay = result == mesh::RadioParamApplyResult::APPLIED
|
||||
&& driver.receiveProfile() == target && driver.isInRecvMode()
|
||||
&& chip.hilFrequencyMHz() == (target ? 909.75f : 909.5f)
|
||||
&& driver.getSpreadingFactor() == 10 && waitBusy();
|
||||
if (!okay) pilotReady = false;
|
||||
Serial.printf("{\"pilot_hop\":%u,\"ok\":%s,\"us\":%.3f,\"fast\":%lu,"
|
||||
"\"fs\":%lu,\"packet_skips\":%lu}\n", target,
|
||||
okay ? "true" : "false", double(cycles) * 1000000.0 / SystemCoreClock,
|
||||
(unsigned long)(chip.getOptimizedProfileSwitches() - fast0),
|
||||
(unsigned long)(chip.hilFsEntries - fs0),
|
||||
(unsigned long)(chip.hilPacketSkips - skip0));
|
||||
}
|
||||
|
||||
void poll(uint32_t expected) {
|
||||
if (!pilotReady || !expected) { Serial.println("{\"error\":\"pilot not ready\"}"); return; }
|
||||
uint8_t bytes[256] = {};
|
||||
const int len = driver.recvRaw(bytes, sizeof(bytes));
|
||||
uint32_t seen = 0;
|
||||
if (len == 16 && !memcmp(bytes, "CHS1", 4)) memcpy(&seen, bytes + 4, 4);
|
||||
bool valid = len == 16 && seen == expected && bytes[8] == 0;
|
||||
for (unsigned i = 9; valid && i < 16; ++i) valid = bytes[i] == uint8_t(i ^ expected);
|
||||
Serial.printf("{\"expected\":%lu,\"received\":%lu,\"len\":%d,\"valid\":%s,\"rx_mode\":%s}\n",
|
||||
(unsigned long)expected, (unsigned long)seen, len,
|
||||
valid ? "true" : "false", driver.isInRecvMode() ? "true" : "false");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200); delay(1500);
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
NRF_POWER->DCDCEN = 1;
|
||||
pinMode(30, OUTPUT); digitalWrite(30, HIGH); // T096 RF front-end supply
|
||||
pinMode(12, OUTPUT); digitalWrite(12, HIGH); // KCT8103L enabled
|
||||
pinMode(41, OUTPUT); digitalWrite(41, LOW); // LNA/RX path
|
||||
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
|
||||
SPI.begin();
|
||||
initError = chip.begin(909.5f, 125, 7, 5,
|
||||
RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 0, 32, 1.8f);
|
||||
if (!initError) initError = chip.setCRC(true);
|
||||
if (!initError) initError = chip.explicitHeader();
|
||||
if (!initError) initError = chip.setDio2AsRfSwitch(true);
|
||||
if (!initError) initError = chip.setCurrentLimit(140);
|
||||
if (!initError) initError = chip.setRxBoostedGainMode(true);
|
||||
const uint32_t cycles = DWT->CYCCNT;
|
||||
delayMicroseconds(2);
|
||||
ready = initError == RADIOLIB_ERR_NONE && DWT->CYCCNT != cycles;
|
||||
if (ready) {
|
||||
chip.setProfileSwitchOptimization(true);
|
||||
driver.begin(); driver.startRx(); driver.warm();
|
||||
ready = driver.isInRecvMode() && waitBusy();
|
||||
}
|
||||
Serial.printf("{\"ready\":%s,\"bench\":\"t096-sx1262-fs-packet-v1\",\"init_rc\":%d}\n",
|
||||
ready ? "true" : "false", initError);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (NRF_WDT->RUNSTATUS)
|
||||
for (unsigned i = 0; i < 8; ++i) NRF_WDT->RR[i] = 0x6E524635;
|
||||
static char line[96];
|
||||
static size_t used = 0;
|
||||
while (Serial.available()) {
|
||||
const char c = Serial.read();
|
||||
if (c == '\r' || c == '\n') {
|
||||
if (!used) continue;
|
||||
line[used] = 0; used = 0;
|
||||
unsigned value; unsigned long seq; char trailing, mode[16], kind[12];
|
||||
if (!strcmp(line, "info")) {
|
||||
Serial.printf("{\"ready\":%s,\"bench\":\"t096-sx1262-fs-packet-v1\","
|
||||
"\"board\":\"Heltec T096 SX1262\",\"autonomous_tx\":false,"
|
||||
"\"sd_fwid\":%u,\"app_base\":%lu,\"spi_hz\":%lu,"
|
||||
"\"fs\":%s,\"packet_cache\":%s}\n",
|
||||
ready ? "true" : "false",
|
||||
unsigned(*reinterpret_cast<volatile uint16_t*>(0x300c)),
|
||||
(unsigned long)*reinterpret_cast<volatile uint32_t*>(0x3008),
|
||||
(unsigned long)radioHal.spiHz,
|
||||
chip.hilUseFsOnRetune ? "true" : "false",
|
||||
chip.hilSkipUnchangedPacket ? "true" : "false");
|
||||
} else if (sscanf(line, "fs %u %c", &value, &trailing) == 1 && value <= 1) {
|
||||
chip.hilUseFsOnRetune = value;
|
||||
Serial.printf("{\"fs\":%s}\n", value ? "true" : "false");
|
||||
} else if (sscanf(line, "packetcache %u %c", &value, &trailing) == 1 && value <= 1) {
|
||||
chip.hilSkipUnchangedPacket = value;
|
||||
Serial.printf("{\"packet_cache\":%s}\n", value ? "true" : "false");
|
||||
} else if (sscanf(line, "spihz %u %c", &value, &trailing) == 1
|
||||
&& (value == 8000000 || value == 16000000)) {
|
||||
radioHal.setSpiHz(value);
|
||||
Serial.printf("{\"spi_hz\":%lu}\n", (unsigned long)radioHal.spiHz);
|
||||
} else if (sscanf(line, "run %15s %u %lu %c", mode, &value, &seq, &trailing) == 3) {
|
||||
run(mode, value, seq);
|
||||
} else if (!strcmp(line, "pilot")) {
|
||||
pilot();
|
||||
} else if (sscanf(line, "pilothop %u %c", &value, &trailing) == 1) {
|
||||
pilotHop(value);
|
||||
} else if (sscanf(line, "poll %lu %c", &seq, &trailing) == 1) {
|
||||
poll(seq);
|
||||
} else if (sscanf(line, "result result %lu %c", &seq, &trailing) == 1) {
|
||||
replayBenchResult("result", seq);
|
||||
} else if (!strcmp(line, "reboot")) {
|
||||
Serial.flush(); delay(20); NVIC_SystemReset();
|
||||
} else Serial.println("{\"error\":\"unsupported command\"}");
|
||||
} else if (used + 1 < sizeof(line)) line[used++] = c;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// Application-only T1000-E LR1110 production-wrapper RX retune timing bench.
|
||||
// RAM-only; no Mesh identity, saved settings, autonomous TX or TX command.
|
||||
// RX-only test profiles are 909.5 and 909.75 MHz; never 910.525 MHz.
|
||||
#include <Arduino.h>
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#include <SPI.h>
|
||||
#include <helpers/radiolib/CustomLR1110Wrapper.h>
|
||||
#include "ProfileSwitchUsb.h"
|
||||
|
||||
class BenchBoard : public mesh::MainBoard {
|
||||
public:
|
||||
uint16_t getBattMilliVolts() override { return 0; }
|
||||
const char* getManufacturerName() const override { return "Seeed T1000-E LR1110 timing bench"; }
|
||||
void reboot() override { NVIC_SystemReset(); }
|
||||
uint8_t getStartupReason() const override { return 0; }
|
||||
} board;
|
||||
|
||||
class CountingHal : public ArduinoHal {
|
||||
public:
|
||||
using ArduinoHal::ArduinoHal;
|
||||
bool bulkTransfer = true;
|
||||
uint32_t spiHz = 16000000;
|
||||
void setSpiHz(uint32_t hz) {
|
||||
spiHz = hz;
|
||||
spiSettings = SPISettings(hz, MSBFIRST, SPI_MODE0);
|
||||
}
|
||||
uint32_t rfWrites = 0, modulationWrites = 0, packetWrites = 0;
|
||||
uint32_t standbyWrites = 0, rxWrites = 0, dioWrites = 0, packetTypeReads = 0;
|
||||
uint32_t clearWrites = 0, fsWrites = 0;
|
||||
void spiTransfer(uint8_t* out, size_t len, uint8_t* in) override {
|
||||
if (len >= 2 && out[0] == 2 && out[1] == 0x0B) ++rfWrites;
|
||||
if (len >= 2 && out[0] == 2 && out[1] == 0x0F) ++modulationWrites;
|
||||
if (len >= 2 && out[0] == 2 && out[1] == 0x10) ++packetWrites;
|
||||
if (len >= 2 && out[0] == 1 && out[1] == 0x1C) ++standbyWrites;
|
||||
if (len >= 2 && out[0] == 1 && out[1] == 0x14) ++clearWrites;
|
||||
if (len >= 2 && out[0] == 1 && out[1] == 0x1D) ++fsWrites;
|
||||
if (len >= 2 && out[0] == 2 && out[1] == 0x09) ++rxWrites;
|
||||
if (len >= 2 && out[0] == 1 && out[1] == 0x13) ++dioWrites;
|
||||
if (len >= 2 && out[0] == 2 && out[1] == 0x02) ++packetTypeReads;
|
||||
if (bulkTransfer) spi->transfer(out, in, len);
|
||||
else ArduinoHal::spiTransfer(out, len, in);
|
||||
}
|
||||
} radioHal(SPI, SPISettings(16000000, MSBFIRST, SPI_MODE0));
|
||||
|
||||
class BenchLR1110 : public CustomLR1110 {
|
||||
public:
|
||||
using CustomLR1110::CustomLR1110;
|
||||
uint16_t preambleSymbols() const { return preambleLengthLoRa; }
|
||||
uint64_t standbyCycles = 0, startRxCycles = 0;
|
||||
uint32_t standbyCount = 0, startRxCount = 0;
|
||||
int16_t standby() override {
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const int16_t rc = CustomLR1110::standby();
|
||||
standbyCycles += DWT->CYCCNT - started;
|
||||
++standbyCount;
|
||||
return rc;
|
||||
}
|
||||
int16_t startReceive() override {
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const int16_t rc = CustomLR1110::startReceive();
|
||||
startRxCycles += DWT->CYCCNT - started;
|
||||
++startRxCount;
|
||||
return rc;
|
||||
}
|
||||
} chip = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1,
|
||||
P_LORA_RESET, P_LORA_BUSY);
|
||||
class BenchWrapper : public CustomLR1110Wrapper {
|
||||
public:
|
||||
using CustomLR1110Wrapper::CustomLR1110Wrapper;
|
||||
uint64_t applyCycles = 0, receivingCycles = 0, startModeCycles = 0;
|
||||
uint32_t applyCount = 0, receivingCount = 0, startModeCount = 0;
|
||||
uint32_t busyHighChecks = 0, receivingTrueChecks = 0;
|
||||
mesh::RadioParamApplyResult hop(uint8_t profile) { return tuneProfile(profile); }
|
||||
void startRx() { startRecv(); }
|
||||
void stopRx() { idle(); }
|
||||
void enableWarmStandby() { setProfileStandbyWarm(true); }
|
||||
bool isReceivingPacket() override {
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const bool receiving = CustomLR1110Wrapper::isReceivingPacket();
|
||||
if (receiving) ++receivingTrueChecks;
|
||||
receivingCycles += DWT->CYCCNT - started;
|
||||
++receivingCount;
|
||||
return receiving;
|
||||
}
|
||||
bool isChipBusy() override {
|
||||
const bool busy = CustomLR1110Wrapper::isChipBusy();
|
||||
if (busy) ++busyHighChecks;
|
||||
return busy;
|
||||
}
|
||||
protected:
|
||||
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const bool rc = CustomLR1110Wrapper::applyParams(freq, bw, sf, cr);
|
||||
applyCycles += DWT->CYCCNT - started;
|
||||
++applyCount;
|
||||
return rc;
|
||||
}
|
||||
int startReceiveMode() override {
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const int rc = CustomLR1110Wrapper::startReceiveMode();
|
||||
startModeCycles += DWT->CYCCNT - started;
|
||||
++startModeCount;
|
||||
return rc;
|
||||
}
|
||||
} driver(chip, board);
|
||||
|
||||
static const uint32_t switchPins[Module::RFSWITCH_MAX_PINS] = {
|
||||
RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6,
|
||||
RADIOLIB_LR11X0_DIO7, RADIOLIB_LR11X0_DIO8, RADIOLIB_NC,
|
||||
};
|
||||
static const Module::RfSwitchMode_t switchModes[] = {
|
||||
{LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW}},
|
||||
{LR11x0::MODE_RX, {HIGH, LOW, LOW, HIGH}},
|
||||
{LR11x0::MODE_TX, {HIGH, HIGH, LOW, HIGH}},
|
||||
{LR11x0::MODE_TX_HP, {LOW, HIGH, LOW, HIGH}},
|
||||
{LR11x0::MODE_TX_HF, {LOW, LOW, LOW, LOW}},
|
||||
{LR11x0::MODE_GNSS, {LOW, LOW, HIGH, LOW}},
|
||||
{LR11x0::MODE_WIFI, {LOW, LOW, LOW, LOW}},
|
||||
END_OF_MODE_TABLE,
|
||||
};
|
||||
|
||||
struct Timing {
|
||||
uint32_t n = 0, minCycles = UINT32_MAX, maxCycles = 0;
|
||||
uint64_t sumCycles = 0;
|
||||
void add(uint32_t cycles) {
|
||||
++n;
|
||||
if (cycles < minCycles) minCycles = cycles;
|
||||
if (cycles > maxCycles) maxCycles = cycles;
|
||||
sumCycles += cycles;
|
||||
}
|
||||
double toUs(uint32_t cycles) const { return double(cycles) * 1000000.0 / SystemCoreClock; }
|
||||
double minUs() const { return n ? toUs(minCycles) : 0; }
|
||||
double maxUs() const { return n ? toUs(maxCycles) : 0; }
|
||||
double meanUs() const { return n ? double(sumCycles) * 1000000.0 / n / SystemCoreClock : 0; }
|
||||
};
|
||||
|
||||
bool ready = false;
|
||||
int initError = 0;
|
||||
uint32_t nextSequence = 0;
|
||||
uint32_t configuredTcxoUs = MC_TCXO_DELAY_US;
|
||||
bool injectTimeoutNextRun = false;
|
||||
|
||||
bool waitBusy(uint32_t limitUs = 20000) {
|
||||
const uint32_t start = micros();
|
||||
while (chip.isChipBusy()) {
|
||||
if (uint32_t(micros() - start) >= limitUs) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void run(const char* mode, unsigned count, uint32_t sequence) {
|
||||
const bool modulation = !strcmp(mode, "mod") || !strcmp(mode, "both");
|
||||
const bool frequency = !strcmp(mode, "freq") || !strcmp(mode, "both");
|
||||
const bool preambleChange = !strcmp(mode, "preamble");
|
||||
if (!ready || !sequence || sequence <= nextSequence || count < 16 || count > 1000) {
|
||||
Serial.println("{\"error\":\"invalid or repeated run\"}");
|
||||
return;
|
||||
}
|
||||
if (!modulation && !frequency && !preambleChange) {
|
||||
Serial.println("{\"error\":\"mode must be mod, freq, both, or preamble\"}");
|
||||
return;
|
||||
}
|
||||
nextSequence = sequence;
|
||||
if (!waitBusy()) { Serial.println("{\"error\":\"initial BUSY timeout\"}"); return; }
|
||||
|
||||
mesh::RadioProfileParams primary;
|
||||
primary.freq = 909.5f; primary.bw = 125; primary.sf = 7; primary.cr = 5;
|
||||
primary.preamble = 32;
|
||||
mesh::RadioProfileConfig secondary;
|
||||
secondary.params = primary;
|
||||
if (modulation) secondary.params.sf = 8;
|
||||
if (frequency) secondary.params.freq = 909.75f;
|
||||
if (preambleChange) secondary.params.preamble = 48;
|
||||
secondary.mode = mesh::RadioProfileMode::Rx;
|
||||
uint32_t primaryBusyDeferrals = 0;
|
||||
uint32_t drainedPackets = 0, drainCalls = 0;
|
||||
auto drainPendingRx = [&]() {
|
||||
uint8_t packet[RADIOLIB_LR11X0_MAX_PACKET_LENGTH] = {};
|
||||
const int length = driver.recvRaw(packet, sizeof(packet));
|
||||
++drainCalls;
|
||||
if (length > 0) ++drainedPackets;
|
||||
};
|
||||
auto primaryResult = driver.trySetPrimaryParams(primary, true);
|
||||
while (primaryResult == mesh::RadioParamApplyResult::BUSY && primaryBusyDeferrals < 20) {
|
||||
++primaryBusyDeferrals;
|
||||
drainPendingRx();
|
||||
delay(20); // An unrelated packet can briefly own RX at setup.
|
||||
primaryResult = driver.trySetPrimaryParams(primary, true);
|
||||
}
|
||||
if (primaryResult != mesh::RadioParamApplyResult::APPLIED) {
|
||||
Serial.printf("{\"error\":\"primary rejected\",\"result\":%u,"
|
||||
"\"busy_deferrals\":%lu}\n",
|
||||
unsigned(primaryResult), (unsigned long)primaryBusyDeferrals);
|
||||
return;
|
||||
}
|
||||
driver.profiles()->setSecondary(secondary, true);
|
||||
driver.startRx();
|
||||
if (!driver.isInRecvMode() || !waitBusy()) {
|
||||
Serial.println("{\"error\":\"RX did not start\"}"); return;
|
||||
}
|
||||
chip.hilInjectRxTimeout = injectTimeoutNextRun;
|
||||
injectTimeoutNextRun = false;
|
||||
|
||||
Timing total[2];
|
||||
uint32_t failures = 0, busyDeferrals = 0, rxErrors = 0, cacheErrors = 0;
|
||||
uint32_t busyTimeouts = 0, rssiErrors = 0;
|
||||
const uint32_t rfBefore = radioHal.rfWrites;
|
||||
const uint32_t modBefore = radioHal.modulationWrites;
|
||||
const uint32_t packetBefore = radioHal.packetWrites;
|
||||
const uint32_t standbyBefore = radioHal.standbyWrites;
|
||||
const uint32_t rxBefore = radioHal.rxWrites;
|
||||
const uint32_t dioBefore = radioHal.dioWrites;
|
||||
const uint32_t packetTypeBefore = radioHal.packetTypeReads;
|
||||
const uint32_t clearBefore = radioHal.clearWrites;
|
||||
const uint32_t fsBefore = radioHal.fsWrites;
|
||||
const uint32_t pendingBefore = chip.hilPendingIrqSnapshots;
|
||||
const uint32_t fastBefore = chip.getOptimizedProfileSwitches();
|
||||
const uint64_t standbyCyclesBefore = chip.standbyCycles, startRxCyclesBefore = chip.startRxCycles;
|
||||
const uint64_t applyCyclesBefore = driver.applyCycles, receivingCyclesBefore = driver.receivingCycles;
|
||||
const uint64_t startModeCyclesBefore = driver.startModeCycles;
|
||||
const uint32_t standbyCountBefore = chip.standbyCount, startRxCountBefore = chip.startRxCount;
|
||||
const uint32_t applyCountBefore = driver.applyCount, receivingCountBefore = driver.receivingCount;
|
||||
const uint32_t startModeCountBefore = driver.startModeCount;
|
||||
const uint32_t busyHighBefore = driver.busyHighChecks;
|
||||
const uint32_t receivingTrueBefore = driver.receivingTrueChecks;
|
||||
const uint64_t clearCyclesBefore = chip.fastClearCycles;
|
||||
const uint64_t packetCyclesBefore = chip.fastPacketCycles;
|
||||
const uint64_t setRxCyclesBefore = chip.fastSetRxCycles;
|
||||
unsigned completed = 0;
|
||||
for (unsigned attempt = 0; completed < count + 8 && attempt < (count + 8) * 4; ++attempt) {
|
||||
if (!waitBusy()) { ++busyTimeouts; break; }
|
||||
delay(10); // Keep host I/O and dwell outside the measured retune.
|
||||
const uint8_t target = driver.receiveProfile() ^ 1;
|
||||
const uint32_t started = DWT->CYCCNT;
|
||||
const auto result = driver.hop(target);
|
||||
if (result == mesh::RadioParamApplyResult::BUSY) {
|
||||
++busyDeferrals;
|
||||
drainPendingRx();
|
||||
continue;
|
||||
}
|
||||
if (result != mesh::RadioParamApplyResult::APPLIED) { ++failures; break; }
|
||||
if (!waitBusy()) { ++busyTimeouts; break; }
|
||||
const uint32_t elapsed = DWT->CYCCNT - started;
|
||||
if (!driver.isInRecvMode()) ++rxErrors;
|
||||
if (chip.getSpreadingFactor() != (target && modulation ? 8 : 7)
|
||||
|| chip.getFreqMHz() != (target && frequency ? 909.75f : 909.5f)
|
||||
|| chip.preambleSymbols() != (target && preambleChange ? 48 : 32)) ++cacheErrors;
|
||||
delay(7); // RSSI needs a settled receiver; this is outside switch timing.
|
||||
float rssi = 0;
|
||||
if (chip.getRssiInst(&rssi) != RADIOLIB_ERR_NONE || !isfinite(rssi)
|
||||
|| rssi < -180.0f || rssi > 20.0f) ++rssiErrors;
|
||||
if (completed++ >= 8) total[target].add(elapsed);
|
||||
}
|
||||
|
||||
driver.profiles()->setSecondary({}, true);
|
||||
auto restored = driver.hop(0);
|
||||
for (unsigned retry = 0; restored == mesh::RadioParamApplyResult::BUSY && retry < 20; ++retry) {
|
||||
drainPendingRx();
|
||||
delay(20);
|
||||
restored = driver.hop(0);
|
||||
}
|
||||
const bool safe = restored == mesh::RadioParamApplyResult::APPLIED
|
||||
&& chip.getFreqMHz() == 909.5f && driver.isInRecvMode();
|
||||
recordBenchResult(lastRunResult, sequence,
|
||||
"{\"result\":%lu,\"board\":\"Seeed T1000-E LR1110\",\"mode\":\"%s\","
|
||||
"\"primary\":\"909.5,125,7,5\",\"secondary_freq_mhz\":%.2f,\"secondary_sf\":%u,"
|
||||
"\"secondary_preamble\":%u,"
|
||||
"\"tcxo_us\":%lu,\"spi_hz\":%lu,\"bulk_spi\":%s,"
|
||||
"\"requested\":%u,\"completed\":%u,\"primary_busy_deferrals\":%lu,"
|
||||
"\"busy_deferrals\":%lu,\"drain_calls\":%lu,\"drained_packets\":%lu,"
|
||||
"\"busy_high_checks\":%lu,"
|
||||
"\"receiving_true_checks\":%lu,\"failures\":%lu,"
|
||||
"\"busy_timeouts\":%lu,\"rx_errors\":%lu,\"cache_errors\":%lu,\"rssi_errors\":%lu,"
|
||||
"\"rf_commands\":%lu,\"modulation_commands\":%lu,\"packet_commands\":%lu,"
|
||||
"\"standby_commands\":%lu,\"rx_commands\":%lu,\"dio_commands\":%lu,"
|
||||
"\"packet_type_reads\":%lu,\"clear_commands\":%lu,\"fs_commands\":%lu,"
|
||||
"\"pending_irq_snapshots\":%lu,\"fast_resumes\":%lu,\"restored\":%s,"
|
||||
"\"standby_mean_us\":%.3f,\"apply_mean_us\":%.3f,\"receiving_check_mean_us\":%.3f,"
|
||||
"\"start_mode_mean_us\":%.3f,\"start_rx_mean_us\":%.3f,"
|
||||
"\"clear_mean_us\":%.3f,\"packet_mean_us\":%.3f,\"set_rx_mean_us\":%.3f,"
|
||||
"\"to_primary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f},"
|
||||
"\"to_secondary\":{\"n\":%lu,\"min_us\":%.3f,\"mean_us\":%.3f,\"max_us\":%.3f}}\n",
|
||||
(unsigned long)sequence, mode, double(secondary.params.freq), unsigned(secondary.params.sf),
|
||||
unsigned(secondary.params.preamble),
|
||||
(unsigned long)configuredTcxoUs, (unsigned long)radioHal.spiHz,
|
||||
radioHal.bulkTransfer ? "true" : "false",
|
||||
count, completed, (unsigned long)primaryBusyDeferrals, (unsigned long)busyDeferrals,
|
||||
(unsigned long)drainCalls, (unsigned long)drainedPackets,
|
||||
(unsigned long)(driver.busyHighChecks - busyHighBefore),
|
||||
(unsigned long)(driver.receivingTrueChecks - receivingTrueBefore),
|
||||
(unsigned long)failures, (unsigned long)busyTimeouts, (unsigned long)rxErrors,
|
||||
(unsigned long)cacheErrors, (unsigned long)rssiErrors,
|
||||
(unsigned long)(radioHal.rfWrites - rfBefore),
|
||||
(unsigned long)(radioHal.modulationWrites - modBefore),
|
||||
(unsigned long)(radioHal.packetWrites - packetBefore),
|
||||
(unsigned long)(radioHal.standbyWrites - standbyBefore),
|
||||
(unsigned long)(radioHal.rxWrites - rxBefore),
|
||||
(unsigned long)(radioHal.dioWrites - dioBefore),
|
||||
(unsigned long)(radioHal.packetTypeReads - packetTypeBefore),
|
||||
(unsigned long)(radioHal.clearWrites - clearBefore),
|
||||
(unsigned long)(radioHal.fsWrites - fsBefore),
|
||||
(unsigned long)(chip.hilPendingIrqSnapshots - pendingBefore),
|
||||
(unsigned long)(chip.getOptimizedProfileSwitches() - fastBefore), safe ? "true" : "false",
|
||||
chip.standbyCount == standbyCountBefore ? 0.0 : double(chip.standbyCycles - standbyCyclesBefore)
|
||||
* 1000000.0 / (chip.standbyCount - standbyCountBefore) / SystemCoreClock,
|
||||
driver.applyCount == applyCountBefore ? 0.0 : double(driver.applyCycles - applyCyclesBefore)
|
||||
* 1000000.0 / (driver.applyCount - applyCountBefore) / SystemCoreClock,
|
||||
driver.receivingCount == receivingCountBefore ? 0.0 : double(driver.receivingCycles - receivingCyclesBefore)
|
||||
* 1000000.0 / (driver.receivingCount - receivingCountBefore) / SystemCoreClock,
|
||||
driver.startModeCount == startModeCountBefore ? 0.0 : double(driver.startModeCycles - startModeCyclesBefore)
|
||||
* 1000000.0 / (driver.startModeCount - startModeCountBefore) / SystemCoreClock,
|
||||
chip.startRxCount == startRxCountBefore ? 0.0 : double(chip.startRxCycles - startRxCyclesBefore)
|
||||
* 1000000.0 / (chip.startRxCount - startRxCountBefore) / SystemCoreClock,
|
||||
chip.getOptimizedProfileSwitches() == fastBefore ? 0.0
|
||||
: double(chip.fastClearCycles - clearCyclesBefore) * 1000000.0
|
||||
/ (chip.getOptimizedProfileSwitches() - fastBefore) / SystemCoreClock,
|
||||
chip.getOptimizedProfileSwitches() == fastBefore ? 0.0
|
||||
: double(chip.fastPacketCycles - packetCyclesBefore) * 1000000.0
|
||||
/ (chip.getOptimizedProfileSwitches() - fastBefore) / SystemCoreClock,
|
||||
chip.getOptimizedProfileSwitches() == fastBefore ? 0.0
|
||||
: double(chip.fastSetRxCycles - setRxCyclesBefore) * 1000000.0
|
||||
/ (chip.getOptimizedProfileSwitches() - fastBefore) / SystemCoreClock,
|
||||
(unsigned long)total[0].n, total[0].minUs(), total[0].meanUs(), total[0].maxUs(),
|
||||
(unsigned long)total[1].n, total[1].minUs(), total[1].meanUs(), total[1].maxUs());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1500);
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
NRF_POWER->DCDCEN = 1;
|
||||
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
|
||||
SPI.begin();
|
||||
initError = chip.begin(909.5f, 125, 7, 5,
|
||||
RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, 0, 32, 1.6f);
|
||||
if (!initError) initError = chip.setCRC(2);
|
||||
if (!initError) initError = chip.explicitHeader();
|
||||
if (!initError) chip.setRfSwitchTable(switchPins, switchModes);
|
||||
if (!initError) initError = chip.setRxBoostedGainMode(true);
|
||||
const uint32_t cycles = DWT->CYCCNT;
|
||||
delayMicroseconds(100);
|
||||
ready = initError == RADIOLIB_ERR_NONE && DWT->CYCCNT != cycles;
|
||||
if (ready) { driver.begin(); driver.startRx(); driver.enableWarmStandby(); }
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (NRF_WDT->RUNSTATUS)
|
||||
for (unsigned i = 0; i < 8; ++i) NRF_WDT->RR[i] = 0x6E524635;
|
||||
static char line[96];
|
||||
static size_t used = 0;
|
||||
static bool overflow = false;
|
||||
while (Serial.available()) {
|
||||
const char c = Serial.read();
|
||||
if (c == '\r' || c == '\n') {
|
||||
if (overflow) { used = 0; overflow = false; Serial.println("{\"error\":\"command too long\"}"); continue; }
|
||||
if (!used) continue;
|
||||
line[used] = 0; used = 0;
|
||||
unsigned count; unsigned long seq; char trailing; char mode[16];
|
||||
if (!strcmp(line, "reboot")) {
|
||||
Serial.println("{\"rebooting\":true}");
|
||||
Serial.flush();
|
||||
delay(20);
|
||||
NVIC_SystemReset();
|
||||
} else if (!strcmp(line, "info")) {
|
||||
char info[384];
|
||||
snprintf(info, sizeof(info),
|
||||
"{\"bench\":\"t1000-lr1110-production-retune-v1\",\"ready\":%s,"
|
||||
"\"init_rc\":%d,\"board\":\"Seeed T1000-E LR1110\","
|
||||
"\"rx_frequencies_mhz\":[909.5,909.75],\"autonomous_tx\":false,\"tx_command\":false,"
|
||||
"\"sd_fwid\":%u,\"app_base\":%u,\"cycle_hz\":%lu,\"tcxo_us\":%lu,"
|
||||
"\"spi_hz\":%lu,\"bulk_spi\":%s}\n",
|
||||
ready ? "true" : "false", initError,
|
||||
unsigned(*reinterpret_cast<volatile uint16_t*>(0x300c)),
|
||||
unsigned(*reinterpret_cast<volatile uint32_t*>(0x3008)),
|
||||
(unsigned long)SystemCoreClock, (unsigned long)configuredTcxoUs,
|
||||
(unsigned long)radioHal.spiHz, radioHal.bulkTransfer ? "true" : "false");
|
||||
Serial.print(info);
|
||||
} else if (sscanf(line, "fs %u %c", &count, &trailing) == 1 && count <= 1) {
|
||||
chip.hilUseFsOnRetune = count;
|
||||
Serial.printf("{\"fs_retune\":%s}\n", count ? "true" : "false");
|
||||
} else if (sscanf(line, "clear %u %c", &count, &trailing) == 1 && count <= 2) {
|
||||
chip.hilFastClearMode = count;
|
||||
Serial.printf("{\"fast_clear_mode\":%u}\n", count);
|
||||
} else if (sscanf(line, "inject %u %c", &count, &trailing) == 1 && count <= 1) {
|
||||
injectTimeoutNextRun = count;
|
||||
Serial.printf("{\"inject_timeout_next_run\":%s}\n", count ? "true" : "false");
|
||||
} else if (sscanf(line, "bulk %u %c", &count, &trailing) == 1 && count <= 1) {
|
||||
radioHal.bulkTransfer = count;
|
||||
Serial.printf("{\"bulk_spi\":%s}\n", radioHal.bulkTransfer ? "true" : "false");
|
||||
} else if (sscanf(line, "spihz %u %c", &count, &trailing) == 1
|
||||
&& (count == 2000000 || count == 8000000 || count == 16000000)) {
|
||||
radioHal.setSpiHz(count);
|
||||
Serial.printf("{\"spi_hz\":%lu}\n", (unsigned long)radioHal.spiHz);
|
||||
} else if (sscanf(line, "tcxo %u %c", &count, &trailing) == 1) {
|
||||
if ((!ready && count != 1600) || (count != 0 && count != 50 && count != 100
|
||||
&& (count < 150 || count > 200 || count % 10 != 0)
|
||||
&& count != 400 && count != 800 && count != 1600)) {
|
||||
Serial.println("{\"error\":\"unsupported tcxo delay\"}");
|
||||
} else {
|
||||
driver.stopRx();
|
||||
int rc = chip.setTCXO(1.6f, count);
|
||||
if (!rc) rc = chip.calibrate(0x3F);
|
||||
delay(50);
|
||||
driver.startRx();
|
||||
if (!rc && driver.isInRecvMode() && waitBusy()) {
|
||||
configuredTcxoUs = count;
|
||||
driver.enableWarmStandby();
|
||||
Serial.printf("{\"tcxo_us\":%lu,\"ready\":true}\n", (unsigned long)configuredTcxoUs);
|
||||
} else {
|
||||
ready = false;
|
||||
Serial.printf("{\"error\":\"tcxo reconfigure failed\",\"rc\":%d}\n", rc);
|
||||
}
|
||||
}
|
||||
} else if (sscanf(line, "run %15s %u %lu %c", mode, &count, &seq, &trailing) == 3)
|
||||
run(mode, count, seq);
|
||||
else if (sscanf(line, "result result %lu %c", &seq, &trailing) == 1)
|
||||
replayBenchResult("result", seq);
|
||||
else Serial.println("{\"error\":\"unsupported command\"}");
|
||||
} else if (!overflow) {
|
||||
if (used < sizeof(line) - 1) line[used++] = c;
|
||||
else overflow = true;
|
||||
}
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Receive fixed RAK4631 packets after T096 SX1262 fast profile hops.
|
||||
|
||||
Run only on the MercerMesh fixture with the two matching HIL applications.
|
||||
The RAK source transmits 16-byte packets only when this script requests them.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
|
||||
def fixture_port(serial_number, topology):
|
||||
matches = [p for p in list_ports.comports()
|
||||
if p.serial_number == serial_number and p.location
|
||||
and p.location.startswith(topology)]
|
||||
if len(matches) != 1 or matches[0].vid != 0x239A or matches[0].pid != 0x8029:
|
||||
raise RuntimeError(f"Missing unique HIL application: {serial_number}")
|
||||
return matches[0].device
|
||||
|
||||
|
||||
def request(port, command):
|
||||
port.write((command + "\n").encode("ascii"))
|
||||
raw = port.readline().decode("ascii", errors="replace").strip()
|
||||
if not raw:
|
||||
raise RuntimeError(f"No reply to {command!r}")
|
||||
result = json.loads(raw)
|
||||
if "error" in result:
|
||||
raise RuntimeError(f"{command}: {result}")
|
||||
return result
|
||||
|
||||
|
||||
def run(start, count):
|
||||
rx_path = fixture_port("651F8E496197F882", "1-1.2.3")
|
||||
tx_path = fixture_port("9AB3B64C641BA927", "1-1.3.1")
|
||||
with serial.Serial(rx_path, 115200, timeout=8) as rx, \
|
||||
serial.Serial(tx_path, 115200, timeout=8) as tx:
|
||||
rx.reset_input_buffer()
|
||||
tx.reset_input_buffer()
|
||||
rx_info = request(rx, "info")
|
||||
tx_info = request(tx, "info")
|
||||
if rx_info.get("bench") != "t096-sx1262-fs-packet-v1":
|
||||
raise RuntimeError(f"Unexpected RX application: {rx_info}")
|
||||
if tx_info.get("bench") != "production-profile-switch-v8" or not tx_info.get("fixed_tx"):
|
||||
raise RuntimeError(f"Unexpected TX application: {tx_info}")
|
||||
if not tx_info.get("prepared"):
|
||||
prepared = request(tx, "txprepare 0")
|
||||
if not prepared.get("prepared"):
|
||||
raise RuntimeError(f"TX preparation failed: {prepared}")
|
||||
elif tx_info.get("channel") != 0:
|
||||
raise RuntimeError(f"Wrong fixed TX channel: {tx_info}")
|
||||
|
||||
results = []
|
||||
seq = start
|
||||
for optimized in (False, True):
|
||||
assert request(rx, f"fs {int(optimized)}").get("fs") is optimized
|
||||
assert request(rx, f"packetcache {int(optimized)}").get("packet_cache") is optimized
|
||||
if not request(rx, "pilot").get("pilot"):
|
||||
raise RuntimeError("Receiver pilot setup failed")
|
||||
delivered = 0
|
||||
for _ in range(count):
|
||||
for profile in (1, 0):
|
||||
hop = request(rx, f"pilothop {profile}")
|
||||
if not hop.get("ok") or hop.get("fast") != 1:
|
||||
raise RuntimeError(f"Bad fast hop: {hop}")
|
||||
if hop.get("fs") != int(optimized) or hop.get("packet_skips") != int(optimized):
|
||||
raise RuntimeError(f"Wrong experimental path: {hop}")
|
||||
sent = request(tx, f"scantx 0 {seq} 16 32 10 125")
|
||||
if sent.get("sent") != seq or sent.get("rc") != 0 or sent.get("failed"):
|
||||
raise RuntimeError(f"TX failed: {sent}")
|
||||
if sent.get("rf_commands") or sent.get("modulation_commands"):
|
||||
raise RuntimeError(f"Fixed TX was retuned: {sent}")
|
||||
for _ in range(5):
|
||||
packet = request(rx, f"poll {seq}")
|
||||
if packet.get("valid"):
|
||||
delivered += 1
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise RuntimeError(f"Packet {seq} not received after hop: {packet}")
|
||||
seq += 1
|
||||
results.append({"optimized": optimized, "delivered": delivered, "sent": count,
|
||||
"fast_hops": 2 * count})
|
||||
print(json.dumps({"board": "Heltec T096 SX1262", "spi_hz": rx_info["spi_hz"],
|
||||
"source": "RAK4631", "modes": results}, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--start", type=int, default=2)
|
||||
parser.add_argument("--count", type=int, default=10)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.count <= 50 or args.start < 1:
|
||||
parser.error("use 1-50 packets per mode and a positive start sequence")
|
||||
run(args.start, args.count)
|
||||
@@ -852,6 +852,8 @@ build_flags =
|
||||
; host-powered modem also uses the established V4 160 MHz role override;
|
||||
; normal V4 images retain their lower-power 80 MHz policy.
|
||||
-D MC_SX1262_FAST_PROFILE_SWITCH=1
|
||||
-D MC_SX1262_FAST_FS=1
|
||||
-D MC_SX1262_FAST_PACKET_CACHE=1
|
||||
-D MC_SX1262_BUFFERED_RADIO_HAL=1
|
||||
-D MC_SX1262_SPI_HZ=8000000
|
||||
-D ESP32_CPU_FREQ=160
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/UsbLogging.h>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
|
||||
MinewsemiME25LS01Board board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
Nrf52BufferedRadioHal radioHal(SPI);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
VolatileRTCClock rtc_clock;
|
||||
|
||||
@@ -91,6 +91,10 @@ build_flags =
|
||||
-D SENSECAP_INDICATOR_LORA
|
||||
-D USE_SX1262
|
||||
-D MC_SX1262_FAST_PROFILE_SWITCH=1
|
||||
; Radio control and BUSY/IRQ use a shared I2C expander. Keep the previously
|
||||
; enabled owned-RX path, but not the direct-I/O SetFs/packet shortcuts.
|
||||
-D MC_SX1262_FAST_FS=0
|
||||
-D MC_SX1262_FAST_PACKET_CACHE=0
|
||||
-D MC_SX1262_SPI_HZ=8000000
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=IndicatorSX1262Wrapper
|
||||
|
||||
@@ -177,6 +177,8 @@ build_flags =
|
||||
; through serial host round trips. Keep this fast-RX/buffered-SPI opt-in
|
||||
; isolated to the host-powered modem image.
|
||||
-D MC_SX1262_FAST_PROFILE_SWITCH=1
|
||||
-D MC_SX1262_FAST_FS=1
|
||||
-D MC_SX1262_FAST_PACKET_CACHE=1
|
||||
-D MC_SX1262_BUFFERED_RADIO_HAL=1
|
||||
-D MC_SX1262_SPI_HZ=8000000
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ build_flags = ${rak4631_hw.build_flags}
|
||||
-D PIN_STATUS_LED=24
|
||||
-D RADIO_CLASS=CustomLR1110
|
||||
-D USE_LR1110
|
||||
-D MC_LR1110_FAST_PROFILE_SWITCH=1
|
||||
-D MC_TCXO_DELAY_US=200
|
||||
-D WRAPPER_CLASS=CustomLR1110Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D RF_SWITCH_TABLE
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#include "t1000e_sensors.h"
|
||||
#include "target.h"
|
||||
#include <helpers/UsbLogging.h>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
T1000eBoard board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
Nrf52BufferedRadioHal radioHal(SPI);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/UsbLogging.h>
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
ThinkNodeM3Board board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
Nrf52BufferedRadioHal radioHal(SPI);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
#include <helpers/radiolib/Esp32BufferedRadioHal.h>
|
||||
|
||||
ThinkNodeM7Board board;
|
||||
|
||||
static SPIClass spi;
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
|
||||
Esp32BufferedRadioHal radioHal(spi);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
@@ -83,4 +85,3 @@ mesh::LocalIdentity radio_new_identity() {
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/radiolib/Esp32BufferedRadioHal.h>
|
||||
|
||||
ThinkNodeM9Board board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
Esp32BufferedRadioHal radioHal(SPI);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
@@ -77,4 +79,4 @@ bool radio_init() {
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include "target.h"
|
||||
#include <helpers/radiolib/Nrf52BufferedRadioHal.h>
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
|
||||
WioWM1110Board board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
Nrf52BufferedRadioHal radioHal(SPI);
|
||||
RADIO_CLASS radio = new Module(&radioHal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ build_flags = ${esp32_base.build_flags}
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D USE_SX1262
|
||||
-D MC_SX1262_FAST_PROFILE_SWITCH=1
|
||||
-D MC_SX1262_FAST_FS=1
|
||||
-D MC_SX1262_FAST_PACKET_CACHE=1
|
||||
-D MC_SX1262_SPI_HZ=8000000
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
|
||||
Reference in new issue
Block a user