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:
mikecarper committed 2026-09-21 22:57:22 -07:00
1 parent 082466f71b
commit 8c7f4df1cd
28 files changed
+1954 -33

No files matched your search

+64
View File
@@ -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
View File
@@ -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
+223 -1
View File
@@ -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));
+76 -5
View File
@@ -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) {
+340
View File
@@ -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()
+93 -11
View File
@@ -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 -1
View File
@@ -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',
+86
View File
@@ -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>
+70 -4
View File
@@ -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.
+296
View File
@@ -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);
}
+427
View File
@@ -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);
}
+97
View File
@@ -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)
+2
View File
@@ -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
+3 -1
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+3 -1
View File
@@ -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);
+3 -1
View File
@@ -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);
+3 -2
View File
@@ -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
}
+4 -2
View File
@@ -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
}
}
+4 -2
View File
@@ -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);
+2
View File
@@ -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