diff --git a/docs/radio_profile_switch_validation.md b/docs/radio_profile_switch_validation.md index 78005e9b2..875844b2c 100644 --- a/docs/radio_profile_switch_validation.md +++ b/docs/radio_profile_switch_validation.md @@ -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. diff --git a/platformio.ini b/platformio.ini index 148c4208e..017246f90 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index b0a8013cd..6c8efe79c 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -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 diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 45186b2bc..604d68064 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -19,6 +19,19 @@ public: void setDeepInitCallback(DeepInitCallback callback) { _deep_init = callback; } +protected: + void beginProfileRetune(bool continuousRx) override { + static_cast(_radio)->beginProfileSwitch(continuousRx); + } + void endProfileRetune(bool success) override { + static_cast(_radio)->endProfileSwitch(success); + } + void setProfileStandbyWarm(bool enabled) override { + static_cast(_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(_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)); diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 257e7d5c2..dacea2448 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -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; } diff --git a/src/helpers/radiolib/Esp32BufferedRadioHal.h b/src/helpers/radiolib/Esp32BufferedRadioHal.h new file mode 100644 index 000000000..83a61e165 --- /dev/null +++ b/src/helpers/radiolib/Esp32BufferedRadioHal.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +// 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); + } +}; diff --git a/src/helpers/radiolib/LR1110ProfileSwitchState.h b/src/helpers/radiolib/LR1110ProfileSwitchState.h new file mode 100644 index 000000000..198bb8efa --- /dev/null +++ b/src/helpers/radiolib/LR1110ProfileSwitchState.h @@ -0,0 +1,68 @@ +#pragma once +#include + +// 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; + } +}; diff --git a/src/helpers/radiolib/Nrf52BufferedRadioHal.h b/src/helpers/radiolib/Nrf52BufferedRadioHal.h new file mode 100644 index 000000000..e564b8f5f --- /dev/null +++ b/src/helpers/radiolib/Nrf52BufferedRadioHal.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +// 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); + } +}; diff --git a/src/helpers/radiolib/SX1262ProfileSwitchState.h b/src/helpers/radiolib/SX1262ProfileSwitchState.h index 492e39579..07b4b9cb7 100644 --- a/src/helpers/radiolib/SX1262ProfileSwitchState.h +++ b/src/helpers/radiolib/SX1262ProfileSwitchState.h @@ -1,4 +1,5 @@ #pragma once +#include #include // 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) { diff --git a/test/test_lr1110_profile_switch.py b/test/test_lr1110_profile_switch.py new file mode 100644 index 000000000..356ea73f1 --- /dev/null +++ b/test/test_lr1110_profile_switch.py @@ -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 +#include +#include +#include +#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 +#include +#include +#include +@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 +#include +@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 +#include +#include +#include +@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<=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<=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() diff --git a/test/test_sx1262_profile_switch.py b/test/test_sx1262_profile_switch.py index b2d16380a..9c9a70a41 100644 --- a/test/test_sx1262_profile_switch.py +++ b/test/test_sx1262_profile_switch.py @@ -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 +#include +#include +@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 #include @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__": diff --git a/tools/hil/profile_four_tx_fixture.py b/tools/hil/profile_four_tx_fixture.py index c0e0094de..cab9ac4c1 100644 --- a/tools/hil/profile_four_tx_fixture.py +++ b/tools/hil/profile_four_tx_fixture.py @@ -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', diff --git a/tools/hil/profile_switch.ini b/tools/hil/profile_switch.ini index 3ee4afee6..bcf86d288 100644 --- a/tools/hil/profile_switch.ini +++ b/tools/hil/profile_switch.ini @@ -154,3 +154,89 @@ build_flags = build_src_filter = + +<../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 = + + + + + +<../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 = + + + + + +<../tools/hil/profile_switch_t096_sx1262.cpp> diff --git a/tools/hil/profile_switch_README.md b/tools/hil/profile_switch_README.md index fdc30402f..40abc7162 100644 --- a/tools/hil/profile_switch_README.md +++ b/tools/hil/profile_switch_README.md @@ -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. diff --git a/tools/hil/profile_switch_t096_sx1262.cpp b/tools/hil/profile_switch_t096_sx1262.cpp new file mode 100644 index 000000000..3193ec506 --- /dev/null +++ b/tools/hil/profile_switch_t096_sx1262.cpp @@ -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 +#include +#include +#include +#include +#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(0x300c)), + (unsigned long)*reinterpret_cast(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); +} diff --git a/tools/hil/profile_switch_t1000_lr1110.cpp b/tools/hil/profile_switch_t1000_lr1110.cpp new file mode 100644 index 000000000..ddde8b10f --- /dev/null +++ b/tools/hil/profile_switch_t1000_lr1110.cpp @@ -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 +#include +#include +#include +#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(0x300c)), + unsigned(*reinterpret_cast(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); +} diff --git a/tools/hil/t096_sx1262_packet_probe.py b/tools/hil/t096_sx1262_packet_probe.py new file mode 100644 index 000000000..611957b92 --- /dev/null +++ b/tools/hil/t096_sx1262_packet_probe.py @@ -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) diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index b3cad89bd..31a51cfdc 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -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 diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp index 58b48d9d5..254240a60 100644 --- a/variants/minewsemi_me25ls01/target.cpp +++ b/variants/minewsemi_me25ls01/target.cpp @@ -1,10 +1,12 @@ #include #include "target.h" #include +#include 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; diff --git a/variants/sensecap_indicator-espnow/platformio.ini b/variants/sensecap_indicator-espnow/platformio.ini index adcf0e6e6..97ab09ed5 100644 --- a/variants/sensecap_indicator-espnow/platformio.ini +++ b/variants/sensecap_indicator-espnow/platformio.ini @@ -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 diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index 5c1ca9d4f..a06819cf1 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -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 diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index a358cf40c..8f357199c 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -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 diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index dcadb53e5..192d7564d 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -2,11 +2,13 @@ #include "t1000e_sensors.h" #include "target.h" #include +#include #include 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); diff --git a/variants/thinknode_m3/target.cpp b/variants/thinknode_m3/target.cpp index f6ca7eefa..b36ff11c4 100644 --- a/variants/thinknode_m3/target.cpp +++ b/variants/thinknode_m3/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" #include +#include #include 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); diff --git a/variants/thinknode_m7/target.cpp b/variants/thinknode_m7/target.cpp index a2d7b40c4..9069b11e4 100644 --- a/variants/thinknode_m7/target.cpp +++ b/variants/thinknode_m7/target.cpp @@ -1,11 +1,13 @@ #include #include "target.h" #include +#include 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 } - diff --git a/variants/thinknode_m9/target.cpp b/variants/thinknode_m9/target.cpp index 06bf68146..fd49eee05 100644 --- a/variants/thinknode_m9/target.cpp +++ b/variants/thinknode_m9/target.cpp @@ -1,9 +1,11 @@ #include #include "target.h" +#include 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 -} \ No newline at end of file +} diff --git a/variants/wio_wm1110/target.cpp b/variants/wio_wm1110/target.cpp index 764a86ad0..b18f880ea 100644 --- a/variants/wio_wm1110/target.cpp +++ b/variants/wio_wm1110/target.cpp @@ -1,10 +1,12 @@ #include -#include "target.h" +#include "target.h" +#include #include 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); diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index 283e6d88e..d29c78382 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -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