Support LR1121 RX duty-cycle power saving
This commit is contained in:
1 parent
eab29a12cd
commit
ff1ccacb2a
11 files changed
+2053
-23
No files matched your search
@@ -431,6 +431,7 @@ jobs:
|
||||
python3 -B test/test_upstream_20261015_wifi_boot.py -v
|
||||
python3 -B test/test_wio_tracker_l2_gps_power.py -v
|
||||
python3 -B test/test_lr1121_power_merge.py -v
|
||||
python3 -B test/test_lr1121_rxps.py -v
|
||||
python3 -B test/test_lr2021_power.py -v
|
||||
python3 -B test/test_lr2021_tx_prearm.py -v
|
||||
python3 -B test/test_lr2021_primary_apply_wait.py -v
|
||||
|
||||
+11
-2
@@ -2106,7 +2106,7 @@ intervals and the 24-hour watchdog policy. An explicitly saved normal-mode
|
||||
available. Returning to slower settings recalculates from the saved RXPS
|
||||
minimum.
|
||||
|
||||
With the measured RXPS model and the default 1600 us TCXO startup delay
|
||||
With the measured SX126x/LR1110 RXPS model and the default 1600 us TCXO startup delay
|
||||
(2600 us total transition), a requested minimum level 1 gives:
|
||||
|
||||
| SF | BW (kHz) | Wire preamble | Effective timing preamble | Effective RXPS level | RX / sleep |
|
||||
@@ -2279,6 +2279,13 @@ Station G2/G3 targets default to `off`.
|
||||
- `state`: `on` or `off`.
|
||||
|
||||
**Notes:**
|
||||
- LR1121 RXPS is supported on Waveshare ESP32-S3 LR1121 and Muzi Base Duo
|
||||
builds, using the same commands. Their existing 5000 us TCXO startup delay
|
||||
is preserved, giving a 6000 us sleep/wake transition. Fast SF/BW tuples may
|
||||
use continuous receive; `get radio.rxps.status` reports the actual mode.
|
||||
- While radio2 profile scanning is enabled, RXPS is suspended so neither
|
||||
profile sleeps through its listening window. The requested RXPS setting
|
||||
is retained and restored after returning to single-profile receive.
|
||||
- `get rxps.wd` reports the radio watchdogs' soft and hard recovery counts.
|
||||
See [receive calibration and recovery](test-results/radio_receive_calibration.md) for
|
||||
noise sampling, CAD handling, and the continuous-RX mode check.
|
||||
@@ -2310,7 +2317,9 @@ Station G2/G3 targets default to `off`.
|
||||
- Named experimental profiles accept `preamble 32`; numeric levels keep the
|
||||
existing automatic-preamble selection. Manual timings bypass the level model.
|
||||
- `MC_TCXO_DELAY_US` defaults to 1600 us. Initialization applies it and checks
|
||||
TCXO/calibration errors. A board can override it for its oscillator.
|
||||
TCXO/calibration errors on the qualified SX126x/LR1110 paths. A board can
|
||||
override it for its oscillator. LR1121 retains RadioLib's 5000 us setting;
|
||||
it does not inherit the LR1110 hardware timing qualification.
|
||||
- Fresh Cascade-profile builds start with RXPS on at level `8` and a 16-symbol preamble. Saved operator settings still take precedence after an upgrade.
|
||||
- Level-based settings automatically recalculate their timings when the spreading factor or bandwidth changes. Custom `<rx_us> <sleep_us>` timings remain fixed.
|
||||
- `get radio.rxps` keeps the legacy on/off, RX, and sleep reply. The new
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<!-- meshcore-hosted-doc-link:start -->
|
||||
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/lr1121_rxps_2026-10-27/">View this page on MeshCore Docs</a>.</p>
|
||||
<!-- meshcore-hosted-doc-link:end -->
|
||||
|
||||
# LR1121 RX power saving, 2026-10-27
|
||||
|
||||
RXPS support is implemented for Waveshare ESP32-S3 LR1121 and Muzi Base Duo.
|
||||
Existing saved settings and commands apply to Repeater, Room Server and
|
||||
Companion roles. No board was flashed during this work; RF reception and
|
||||
current savings have not been physically measured.
|
||||
|
||||
```text
|
||||
set radio.rxps balanced
|
||||
get radio.rxps.config
|
||||
get radio.rxps.status
|
||||
set radio.rxps off
|
||||
```
|
||||
|
||||
Status distinguishes requested power saving from actual armed or continuous
|
||||
receive. Radio2 scanning suspends RXPS while retaining the requested setting;
|
||||
returning to the primary profile restores it. Fast tuples can require
|
||||
continuous receive. Experimental levels 9/10 retain their existing caveats.
|
||||
|
||||
## Driver changes
|
||||
|
||||
The LR1121 uses the same `0x0214` duty-cycle command and 32768 Hz, 24-bit
|
||||
periods as the existing LR11x0 implementation. Arming enters Standby RC,
|
||||
configures the LF clock, stages packet/IRQ parameters, activates the RX RF
|
||||
path and issues the native command. See [Semtech's LR1121 user manual,
|
||||
section 7.2.6](https://storage.googleapis.com/cloud-storage-web/public/t_form3/rzsyE6ulSlqjnJrpxSmZqD5It5wRyeCyyxP6I2Y3.pdf).
|
||||
|
||||
The implementation uses 64-bit timer conversion, rejects zero/sub-tick RX
|
||||
periods, preserves the extended-preamble guard and restores LF-clock behavior
|
||||
when leaving duty cycle. Pinned RadioLib's public implementation uses 32-bit
|
||||
conversion, which overflows above about 131 ms; its staging also needs the
|
||||
explicit RF-path activation. [RadioLib implementation](https://github.com/jgromes/RadioLib/blob/2325a38a89dd0f0e402c19f67fe910111da1e246/src/modules/LR11x0/LR11x0.cpp).
|
||||
|
||||
BUSY-aware activity polling and cached LoRa airtime avoid ordinary SPI reads
|
||||
while asleep. Header errors remain visible until the receive owner clears
|
||||
the stale buffer and rearms. The shared receive path retains packet-metadata
|
||||
ownership, automatic fallback/retries and watchdog behavior. Waveshare now
|
||||
registers its full-radio recovery callback, restoring the board's TCXO and
|
||||
DIO RF-switch table after a reset.
|
||||
|
||||
LR1121 retains RadioLib's existing **5000 us TCXO startup setting**, giving
|
||||
a 6000 us transition. It does not inherit the LR1110's shorter qualified
|
||||
oscillator timing. Both board recipes select the eight-symbol LR11x0 capture
|
||||
cost before CLI/default calculations. PA selection, regulator mode, band
|
||||
configuration and boosted gain keep their existing paths.
|
||||
|
||||
## Checks
|
||||
|
||||
All **68 affected host tests** pass across 13 suites. Three new tests execute
|
||||
the actual driver/wrapper methods in five ASan/UBSan binaries: timer bounds,
|
||||
command ordering and failures, clock restoration, sleeping BUSY, cached
|
||||
airtime, header recovery, profile suspension and reset callbacks. The extended
|
||||
activity-epoch tests retain success/failure and removed-reset negative controls.
|
||||
|
||||
| Packaged firmware | RAM admission | Capability checks |
|
||||
| --- | --- | ---: |
|
||||
| Waveshare Full Repeater | Pass | 15/15 |
|
||||
| Waveshare Full Companion | Pass | 26/26 |
|
||||
| Muzi Duo Full Companion | Pass | 16/16 |
|
||||
| Muzi Duo LoRa-OTA Repeater, Full sensors | Pass | 7/7 |
|
||||
| Muzi Duo LoRa-OTA Repeater, Reduced sensors | Pass | 5/5 |
|
||||
|
||||
All packages pass their size and update-path checks. PlatformIO ran one
|
||||
process at a time. The [machine-readable evidence](lr1121_rxps_2026-10-27.safe.json)
|
||||
contains source hashes, exact commands, artifact hashes and memory reports.
|
||||
After integrating the concurrent Companion trace-handling update at
|
||||
`eab29a12c`, Muzi Full Companion was packaged again at `b34a82acb` and passed
|
||||
all 16 capability checks, RAM admission and UF2 address checks. Its available
|
||||
internal RAM remains 89,292 bytes against 59,200 required. The RXPS source
|
||||
hashes still match the original five packages above.
|
||||
|
||||
These software checks do not establish LR1121 packet-delivery margins,
|
||||
measured power reduction or watchdog recovery on physical hardware.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,7 @@
|
||||
#include <RadioLib.h>
|
||||
#include "RadioRegulatorState.h"
|
||||
#include "MeshCore.h"
|
||||
#include "RXPowerSaving.h"
|
||||
#include "LR11x0MorseRfSwitch.h"
|
||||
#include "../MorseRx.h"
|
||||
|
||||
@@ -15,6 +16,7 @@ class CustomLR1121 : public LR1121 {
|
||||
uint32_t _activityAt = 0;
|
||||
bool _headerSeen = false;
|
||||
bool _rx_boosted = false;
|
||||
bool _lfClockRestoreNeeded = false;
|
||||
LR11x0MorseRfSwitch _morse_rf_switch;
|
||||
int8_t _txPower = 0;
|
||||
bool _txPowerSet = false;
|
||||
@@ -72,17 +74,32 @@ class CustomLR1121 : public LR1121 {
|
||||
return _regulator.finishInitialization(state);
|
||||
}
|
||||
|
||||
// Airtime is a cached LoRa calculation. GetPacketType would wake a
|
||||
// sleeping duty-cycle receiver merely to estimate an outgoing packet.
|
||||
RadioLibTime_t getTimeOnAir(size_t len) override {
|
||||
return getToA(len, ModemType_t::RADIOLIB_MODEM_LORA);
|
||||
}
|
||||
|
||||
int16_t recoverReceivePath() {
|
||||
_activityAt = 0;
|
||||
_headerSeen = false;
|
||||
int16_t state = standby();
|
||||
int16_t next = clearRxBuffer();
|
||||
if (state == RADIOLIB_ERR_NONE) state = next;
|
||||
next = clearIrqState(RADIOLIB_LR11X0_IRQ_ALL);
|
||||
if (state == RADIOLIB_ERR_NONE) state = next;
|
||||
return state;
|
||||
}
|
||||
|
||||
size_t getPacketLength(bool update) override {
|
||||
size_t len = LR1121::getPacketLength(update);
|
||||
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
|
||||
// we've just received a corrupted packet
|
||||
// this may have triggered a bug causing subsequent packets to be shifted
|
||||
// call standby() to return radio to known-good state
|
||||
// recvRaw will call startReceive() to restart rx
|
||||
MESH_DEBUG_PRINTLN("LR1121: got header err, calling standby()");
|
||||
standby();
|
||||
// A header-error event can retain the previous packet's nonzero length.
|
||||
// Recover from the IRQ itself rather than delivering those stale bytes.
|
||||
if (getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
|
||||
MESH_DEBUG_PRINTLN("LR1121: header error, resetting RX path");
|
||||
recoverReceivePath();
|
||||
return 0;
|
||||
}
|
||||
return len;
|
||||
return LR1121::getPacketLength(update);
|
||||
}
|
||||
|
||||
using LR1121::setOutputPower;
|
||||
@@ -113,6 +130,74 @@ class CustomLR1121 : public LR1121 {
|
||||
}
|
||||
|
||||
float getFreqMHz() const { return freqMHz; }
|
||||
uint32_t getTcxoDelay() const { return tcxoDelay; }
|
||||
|
||||
bool needsLfClockRestore() const { return _lfClockRestoreNeeded; }
|
||||
int16_t restoreLfClock() {
|
||||
if (!_lfClockRestoreNeeded) return RADIOLIB_ERR_NONE;
|
||||
// Retain the LF RC clock: DIO11 can also be an IRQ/RF-switch signal.
|
||||
const int16_t state = configLfClock(
|
||||
RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED | RADIOLIB_LR11X0_LF_CLK_RC);
|
||||
if (state == RADIOLIB_ERR_NONE) _lfClockRestoreNeeded = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
int16_t startReceiveDutyCycle(uint32_t rxPeriod, uint32_t sleepPeriod,
|
||||
RadioLibIrqFlags_t irqFlags = RADIOLIB_IRQ_RX_DEFAULT_FLAGS,
|
||||
RadioLibIrqFlags_t irqMask = RADIOLIB_IRQ_RX_DEFAULT_MASK,
|
||||
uint32_t* effectiveRxPeriod = nullptr) {
|
||||
// Reject zero/sub-tick inputs before the preamble guard can enlarge
|
||||
// them: one 32.768 kHz RTC tick requires at least 31 microseconds.
|
||||
if (rxPeriod < 31UL) return RADIOLIB_ERR_INVALID_RX_PERIOD;
|
||||
const uint32_t symbolPeriod = (uint32_t)(((1000.0f * (float)(1UL << spreadingFactor)) /
|
||||
bandwidthKhz) + 0.999f);
|
||||
const uint32_t transitionTime = tcxoDelay + 1000UL;
|
||||
if (sleepPeriod <= transitionTime) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
|
||||
const uint32_t programmedSleepPeriod = sleepPeriod - transitionTime;
|
||||
|
||||
// Use the existing LR11x0 extended-preamble guard. Preamble detection
|
||||
// extends the receive timeout to 2*rx + programmed sleep.
|
||||
const uint64_t requiredExtendedPeriod =
|
||||
((uint64_t)preambleLengthLoRa + 11ULL) * symbolPeriod + 1000ULL;
|
||||
if (2ULL * rxPeriod + programmedSleepPeriod < requiredExtendedPeriod) {
|
||||
rxPeriod = (uint32_t)((requiredExtendedPeriod - programmedSleepPeriod + 1ULL) / 2ULL);
|
||||
}
|
||||
if (effectiveRxPeriod) *effectiveRxPeriod = rxPeriod;
|
||||
|
||||
// LR11xx uses 32.768 kHz RTC ticks. Native RadioLib 7.7.1 overflows
|
||||
// its 32-bit multiplication for periods above roughly 131 ms.
|
||||
const uint32_t rxPeriodRaw = (uint32_t)(((uint64_t)rxPeriod * 32768UL) / 1000000UL);
|
||||
const uint32_t sleepPeriodRaw =
|
||||
(uint32_t)(((uint64_t)programmedSleepPeriod * 32768UL) / 1000000UL);
|
||||
if ((rxPeriodRaw & 0xFF000000) || rxPeriodRaw == 0) return RADIOLIB_ERR_INVALID_RX_PERIOD;
|
||||
if ((sleepPeriodRaw & 0xFF000000) || sleepPeriodRaw == 0) return RADIOLIB_ERR_INVALID_SLEEP_PERIOD;
|
||||
|
||||
// SetRxDutyCycle requires Standby RC and a configured RTC source.
|
||||
int16_t state = standby(RADIOLIB_LR11X0_STANDBY_RC);
|
||||
RADIOLIB_ASSERT(state);
|
||||
_lfClockRestoreNeeded = true;
|
||||
state = configLfClock(
|
||||
RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED);
|
||||
RADIOLIB_ASSERT(state);
|
||||
RadioModeConfig_t cfg = {
|
||||
.receive = {
|
||||
.timeout = RADIOLIB_LR11X0_RX_TIMEOUT_INF,
|
||||
.irqFlags = irqFlags,
|
||||
.irqMask = irqMask,
|
||||
.len = 0,
|
||||
}
|
||||
};
|
||||
state = stageMode(RADIOLIB_RADIO_MODE_RX, &cfg);
|
||||
RADIOLIB_ASSERT(state);
|
||||
// stageMode prepares packet/IRQ state; launch the RX RF path too.
|
||||
mod->setRfSwitchState(Module::MODE_RX);
|
||||
state = setRxDutyCycle(rxPeriodRaw, sleepPeriodRaw, RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
_activityAt = 0;
|
||||
_headerSeen = false;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
int16_t setRxBoostedGainMode(bool en) {
|
||||
_rx_boosted = en;
|
||||
@@ -123,7 +208,9 @@ class CustomLR1121 : public LR1121 {
|
||||
|
||||
int16_t startReceive() override {
|
||||
// include the PREAMBLE_DETECTED irq bit in reported flags.
|
||||
const int16_t status = LR1121::startReceive(RADIOLIB_LR11X0_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0);
|
||||
const int16_t status = LR1121::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);
|
||||
if (status == RADIOLIB_ERR_NONE) {
|
||||
_activityAt = 0;
|
||||
_headerSeen = false;
|
||||
@@ -131,14 +218,21 @@ class CustomLR1121 : public LR1121 {
|
||||
return status;
|
||||
}
|
||||
|
||||
bool isChipBusy() {
|
||||
const uint32_t busy = mod->getGpio();
|
||||
return busy != RADIOLIB_NC && mod->hal->digitalRead(busy);
|
||||
}
|
||||
|
||||
bool isReceiving() {
|
||||
if (isChipBusy()) return false;
|
||||
uint32_t irq = getIrqStatus();
|
||||
bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4
|
||||
bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5
|
||||
bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6
|
||||
uint32_t now = millis();
|
||||
if (hdrErr) {
|
||||
clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR);
|
||||
// The receive wrapper owns recovery. Clearing this during polling
|
||||
// would hide the error before stale RX-buffer length is rejected.
|
||||
_activityAt = 0;
|
||||
_headerSeen = false;
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef USE_LR1121
|
||||
#define USE_LR1121
|
||||
#endif
|
||||
|
||||
#include "CustomLR1121.h"
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "LR11x0Reset.h"
|
||||
@@ -8,6 +12,10 @@ class CustomLR1121Wrapper : public RadioLibWrapper {
|
||||
using DeepInitCallback = bool (*)();
|
||||
DeepInitCallback _deep_init = nullptr;
|
||||
public:
|
||||
static constexpr bool SupportsRxPowerSaving = true;
|
||||
uint32_t rxPowerSavingTransitionUs() const override {
|
||||
return static_cast<CustomLR1121*>(_radio)->getTcxoDelay() + 1000UL;
|
||||
}
|
||||
uint8_t regulatorModes() const override { return static_cast<CustomLR1121*>(_radio)->regulatorModes(); }
|
||||
mesh::power::RegulatorMode defaultRegulatorMode() const override {
|
||||
return static_cast<CustomLR1121*>(_radio)->defaultRegulatorMode();
|
||||
@@ -116,6 +124,9 @@ public:
|
||||
bool isReceivingPacket() override {
|
||||
return ((CustomLR1121 *)_radio)->isReceiving();
|
||||
}
|
||||
bool isChipBusy() override {
|
||||
return static_cast<CustomLR1121*>(_radio)->isChipBusy();
|
||||
}
|
||||
float getCurrentRSSI() override {
|
||||
float rssi = NAN;
|
||||
return ((CustomLR1121 *)_radio)->getRssiInst(&rssi) == RADIOLIB_ERR_NONE ? rssi : NAN;
|
||||
@@ -128,12 +139,47 @@ public:
|
||||
|
||||
uint32_t getEstAirtimeFor(int len_bytes) override {
|
||||
auto airtime = RadioLibWrapper::getEstAirtimeFor(len_bytes);
|
||||
if (airtime == 0) return 0;
|
||||
return airtime < 200 ? 200 : airtime; // at least 200 millis
|
||||
}
|
||||
|
||||
uint8_t getSpreadingFactor() const override { return ((CustomLR1121 *)_radio)->getSpreadingFactor(); }
|
||||
bool supportsRxPowerSaving() const override { return SupportsRxPowerSaving; }
|
||||
void onReceiveProcessed() override { finishReceiveProcessing(); }
|
||||
|
||||
protected:
|
||||
int startReceiveMode() override { return startReceivePowerSaving(); }
|
||||
bool isPacketReady() override {
|
||||
if (_radio->checkIrq(RADIOLIB_IRQ_HEADER_ERR) > 0) {
|
||||
const int16_t error = static_cast<CustomLR1121*>(_radio)->recoverReceivePath();
|
||||
if (error != RADIOLIB_ERR_NONE) {
|
||||
MESH_DEBUG_PRINTLN("CustomLR1121Wrapper: RX recovery error (%d)", error);
|
||||
}
|
||||
_rx_ps_armed = false;
|
||||
_rx_hold_continuous = false;
|
||||
n_recv_errors++;
|
||||
return false;
|
||||
}
|
||||
return RadioLibWrapper::isPacketReady();
|
||||
}
|
||||
int16_t armDutyCycle(RadioLibIrqFlags_t irq_flags, RadioLibIrqFlags_t irq_mask,
|
||||
uint32_t* effective_rx_us, uint32_t* effective_sleep_us) override {
|
||||
*effective_sleep_us = _rx_ps_sleep_us;
|
||||
return static_cast<CustomLR1121*>(_radio)->startReceiveDutyCycle(
|
||||
_rx_ps_rx_us, _rx_ps_sleep_us, irq_flags, irq_mask, effective_rx_us);
|
||||
}
|
||||
int16_t stopDutyCycleHardware() override {
|
||||
const int16_t standby_error = _radio->standby();
|
||||
const int16_t clock_error = static_cast<CustomLR1121*>(_radio)->restoreLfClock();
|
||||
return standby_error != RADIOLIB_ERR_NONE ? standby_error : clock_error;
|
||||
}
|
||||
int16_t startContinuousReceive() override {
|
||||
auto* radio = static_cast<CustomLR1121*>(_radio);
|
||||
if (!radio->needsLfClockRestore()) return _radio->startReceive();
|
||||
int16_t error = _radio->standby();
|
||||
if (error == RADIOLIB_ERR_NONE) error = radio->restoreLfClock();
|
||||
return error == RADIOLIB_ERR_NONE ? _radio->startReceive() : error;
|
||||
}
|
||||
bool radioDeepInit() override {
|
||||
return _deep_init && prepareRadioHardReset() && _deep_init()
|
||||
&& _board->finishRadioHardReset();
|
||||
|
||||
@@ -226,17 +226,25 @@ inline bool rxPowerSavingUsesContinuousFallback(uint32_t rx_us, uint32_t sleep_u
|
||||
}
|
||||
|
||||
inline uint32_t rxPowerSavingDefaultTransitionUs() {
|
||||
#if defined(SX126X_DIO3_TCXO_VOLTAGE)
|
||||
return SX126X_DIO3_TCXO_VOLTAGE > 0.0f ? MC_TCXO_DELAY_US + 1000UL : 1000UL;
|
||||
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
||||
return LR11X0_DIO3_TCXO_VOLTAGE > 0.0f ? MC_TCXO_DELAY_US + 1000UL : 1000UL;
|
||||
#if defined(USE_LR1121)
|
||||
// Retain RadioLib's existing 5 ms LR1121 TCXO startup time. The shorter
|
||||
// LR1110 setting has not been qualified for these boards. Driver arming
|
||||
// uses its actual cached delay; CLI/default calculations must agree.
|
||||
const uint32_t tcxo_delay_us = 5000UL;
|
||||
#else
|
||||
return MC_TCXO_DELAY_US + 1000UL;
|
||||
const uint32_t tcxo_delay_us = MC_TCXO_DELAY_US;
|
||||
#endif
|
||||
#if defined(SX126X_DIO3_TCXO_VOLTAGE)
|
||||
return SX126X_DIO3_TCXO_VOLTAGE > 0.0f ? tcxo_delay_us + 1000UL : 1000UL;
|
||||
#elif defined(LR11X0_DIO3_TCXO_VOLTAGE)
|
||||
return LR11X0_DIO3_TCXO_VOLTAGE > 0.0f ? tcxo_delay_us + 1000UL : 1000UL;
|
||||
#else
|
||||
return tcxo_delay_us + 1000UL;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float rxPowerSavingDefaultCaptureCost() {
|
||||
#ifdef USE_LR1110
|
||||
#if defined(USE_LR1110) || defined(USE_LR1121)
|
||||
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS_LR11X0;
|
||||
#else
|
||||
return RX_POWERSAVING_CAPTURE_COST_SYMBOLS;
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
"""Execute LR1121 RXPS production methods with faultable RadioLib doubles."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <helpers/radiolib/RXPowerSaving.h>
|
||||
#include <helpers/radiolib/RadioRegulatorState.h>
|
||||
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNSUPPORTED -2
|
||||
#define RADIOLIB_ERR_INVALID_RX_PERIOD -3
|
||||
#define RADIOLIB_ERR_INVALID_SLEEP_PERIOD -4
|
||||
#define RADIOLIB_ERR_UNKNOWN -5
|
||||
#define RADIOLIB_NC 0xffffffffUL
|
||||
#define RADIOLIB_LR11X0_STANDBY_RC 0
|
||||
#define RADIOLIB_LR11X0_LF_CLK_RC 0
|
||||
#define RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED 0
|
||||
#define RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED 16
|
||||
#define RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX 1
|
||||
#define RADIOLIB_LR11X0_RX_TIMEOUT_INF 0xffffff
|
||||
#define RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE 0x12
|
||||
#define RADIOLIB_LR11X0_IRQ_ALL 0xffffffffUL
|
||||
#define RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED (1UL << 4)
|
||||
#define RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID (1UL << 5)
|
||||
#define RADIOLIB_LR11X0_IRQ_HEADER_ERR (1UL << 6)
|
||||
#define RADIOLIB_IRQ_RX_DONE 2
|
||||
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 4
|
||||
#define RADIOLIB_IRQ_HEADER_ERR 6
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS (1UL << RADIOLIB_IRQ_RX_DONE)
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_MASK (1UL << RADIOLIB_IRQ_RX_DONE)
|
||||
#define RADIOLIB_RADIO_MODE_RX 1
|
||||
#define RADIOLIB_ASSERT(state) do { if ((state) != 0) return (state); } while (0)
|
||||
#define STATE_IDLE 0
|
||||
#define STATE_RX 1
|
||||
#define STATE_TX_WAIT 3
|
||||
|
||||
using RadioLibTime_t = uint32_t;
|
||||
using RadioLibIrqFlags_t = uint32_t;
|
||||
enum class ModemType_t { RADIOLIB_MODEM_LORA };
|
||||
struct RadioModeConfig_t {
|
||||
struct { uint32_t timeout, irqFlags, irqMask; size_t len; } receive;
|
||||
};
|
||||
static uint32_t now_ms = 100;
|
||||
static unsigned delays;
|
||||
uint32_t millis() { return now_ms; }
|
||||
void delay(uint32_t value) { assert(value == 50); ++delays; }
|
||||
|
||||
struct Hal {
|
||||
bool busy = false;
|
||||
unsigned reads = 0;
|
||||
bool digitalRead(uint32_t pin) { assert(pin == 9); ++reads; return busy; }
|
||||
};
|
||||
struct Module {
|
||||
enum { MODE_RX = 1 };
|
||||
Hal hal_value;
|
||||
Hal* hal = &hal_value;
|
||||
std::string* trace = nullptr;
|
||||
uint32_t gpio = 9;
|
||||
uint32_t getGpio() { return gpio; }
|
||||
void setRfSwitchState(unsigned mode) { assert(mode == MODE_RX && trace); *trace += 'W'; }
|
||||
};
|
||||
|
||||
class LR1121 {
|
||||
public:
|
||||
virtual ~LR1121() = default;
|
||||
Module module;
|
||||
Module* mod = &module;
|
||||
float tcxoVoltage = 1.8f;
|
||||
uint32_t tcxoDelay = 5000;
|
||||
uint8_t spreadingFactor = 8;
|
||||
float bandwidthKhz = 62.5f;
|
||||
uint16_t preambleLengthLoRa = 32;
|
||||
std::string trace;
|
||||
int16_t standby_error = 0, lf_error = 0, stage_error = 0, duty_error = 0;
|
||||
int16_t buffer_error = 0, irq_error = 0, continuous_error = 0;
|
||||
int16_t tcxo_error = 0, calibration_error = 0, begin_error = 0;
|
||||
uint32_t programmed_rx = 0, programmed_sleep = 0, irq = 0;
|
||||
uint32_t continuous_flags = 0, continuous_mask = 0;
|
||||
unsigned stage_calls = 0, duty_calls = 0, irq_reads = 0, irq_clears = 0;
|
||||
unsigned base_length_calls = 0, modem_queries = 0, toa_calls = 0;
|
||||
unsigned tcxo_calls = 0, calibration_calls = 0;
|
||||
uint32_t tcxo_programmed = 0;
|
||||
size_t stale_length = 17;
|
||||
RadioModeConfig_t staged = {};
|
||||
LR1121() { module.trace = &trace; }
|
||||
|
||||
int16_t standby(uint8_t mode = RADIOLIB_LR11X0_STANDBY_RC) {
|
||||
assert(mode == RADIOLIB_LR11X0_STANDBY_RC);
|
||||
trace += 'S'; return standby_error;
|
||||
}
|
||||
int16_t configLfClock(uint8_t setup) {
|
||||
assert(setup == (RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED)
|
||||
|| setup == (RADIOLIB_LR11X0_LF_CLK_RC | RADIOLIB_LR11X0_LF_BUSY_RELEASE_DISABLED));
|
||||
trace += setup & RADIOLIB_LR11X0_LF_BUSY_RELEASE_ENABLED ? 'L' : 'R';
|
||||
return lf_error;
|
||||
}
|
||||
int16_t stageMode(int mode, RadioModeConfig_t* config) {
|
||||
assert(mode == RADIOLIB_RADIO_MODE_RX);
|
||||
trace += 'G'; ++stage_calls; staged = *config; return stage_error;
|
||||
}
|
||||
int16_t setRxDutyCycle(uint32_t rx, uint32_t sleep, uint8_t mode) {
|
||||
assert(mode == RADIOLIB_LR11X0_RX_DUTY_CYCLE_MODE_RX);
|
||||
trace += 'D'; ++duty_calls; programmed_rx = rx; programmed_sleep = sleep;
|
||||
return duty_error;
|
||||
}
|
||||
int16_t clearRxBuffer() { trace += 'B'; stale_length = 0; return buffer_error; }
|
||||
int16_t clearIrqState(uint32_t bits) {
|
||||
trace += 'C'; ++irq_clears; irq &= ~bits; return irq_error;
|
||||
}
|
||||
uint32_t getIrqStatus() { ++irq_reads; return irq; }
|
||||
int16_t checkIrq(unsigned bit) { return (getIrqStatus() & (1UL << bit)) ? 1 : 0; }
|
||||
virtual size_t getPacketLength(bool) { ++base_length_calls; return stale_length; }
|
||||
virtual int16_t startReceive() { return startReceive(0, 0, 0, 0); }
|
||||
int16_t startReceive(uint32_t, uint32_t flags, uint32_t mask, size_t) {
|
||||
trace += 'X'; continuous_flags = flags; continuous_mask = mask;
|
||||
return continuous_error;
|
||||
}
|
||||
virtual RadioLibTime_t getTimeOnAir(size_t) { ++modem_queries; return 0xffffffffUL; }
|
||||
RadioLibTime_t getToA(size_t length, ModemType_t modem) {
|
||||
assert(modem == ModemType_t::RADIOLIB_MODEM_LORA);
|
||||
++toa_calls; return uint32_t(length * 900);
|
||||
}
|
||||
int16_t setTCXO(float voltage, uint32_t value) {
|
||||
assert(voltage == tcxoVoltage); trace += 'T'; ++tcxo_calls;
|
||||
tcxo_programmed = value;
|
||||
if (!tcxo_error) tcxoDelay = value;
|
||||
return tcxo_error;
|
||||
}
|
||||
int16_t calibrate(uint8_t mask) {
|
||||
assert(mask == 0x3f); trace += 'A'; ++calibration_calls;
|
||||
return calibration_error;
|
||||
}
|
||||
int16_t begin(float, float, uint8_t, uint8_t, uint8_t, int8_t, uint16_t, float voltage) {
|
||||
trace += 'I'; tcxoVoltage = voltage; tcxoDelay = voltage > 0 ? 5000 : 0;
|
||||
return begin_error;
|
||||
}
|
||||
int16_t setRegulatorLDO() { trace += 'O'; return 0; }
|
||||
int16_t setRegulatorDCDC() { trace += 'V'; return 0; }
|
||||
};
|
||||
|
||||
class CustomLR1121 : public LR1121 {
|
||||
public:
|
||||
mesh::power::RadioRegulatorState _regulator{
|
||||
mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
|
||||
mesh::power::RegulatorMode::Dcdc};
|
||||
uint32_t _preambleMillis = 66, _maxPayloadMillis = 3934, _activityAt = 0;
|
||||
bool _headerSeen = false, _lfClockRestoreNeeded = false;
|
||||
@DRIVER_METHODS@
|
||||
};
|
||||
|
||||
namespace mesh { enum class RadioParamApplyResult : uint8_t { APPLIED, BUSY, FAILED }; }
|
||||
struct Board {
|
||||
bool finish_ok = true;
|
||||
unsigned finish_calls = 0;
|
||||
bool finishRadioHardReset() { ++finish_calls; return finish_ok; }
|
||||
};
|
||||
struct RadioLibWrapper {
|
||||
virtual ~RadioLibWrapper() = default;
|
||||
static constexpr bool SupportsRxPowerSaving = false;
|
||||
CustomLR1121* _radio;
|
||||
Board board;
|
||||
Board* _board = &board;
|
||||
bool prepare_ok = true, hard_reset_path = true;
|
||||
unsigned prepare_calls = 0;
|
||||
bool _rx_ps_enabled = true, _rx_ps_armed = true, _rx_hold_continuous = true;
|
||||
bool _rx_ps_continuous_fallback = false, _nf_calib_active = false;
|
||||
bool _profile_rxps_suspended = false, _profile_saved_rxps = false;
|
||||
bool _profile_repair_required = false, _cw_active = false, _beacon_owned = false;
|
||||
bool _loop_event_pending = false, interrupt_pending = false;
|
||||
uint8_t _irq_probe_failures = 0, state = STATE_RX;
|
||||
uint32_t _rx_ps_rx_us = 78000, _rx_ps_sleep_us = 26851;
|
||||
int16_t _rx_ps_last_error = 0;
|
||||
unsigned n_recv_errors = 0, reconfigure_calls = 0, rearm_calls = 0;
|
||||
RxPowerSavingArmRetryState _rx_ps_arm_retry;
|
||||
explicit RadioLibWrapper(CustomLR1121& value) : _radio(&value) {}
|
||||
virtual bool supportsRxPowerSaving() const { return false; }
|
||||
virtual uint32_t rxPowerSavingTransitionUs() const { return 0; }
|
||||
virtual bool isChipBusy() { return false; }
|
||||
virtual bool isReceivingPacket() { return false; }
|
||||
virtual int startReceiveMode() { return -1; }
|
||||
virtual int16_t armDutyCycle(RadioLibIrqFlags_t, RadioLibIrqFlags_t, uint32_t*, uint32_t*) { return -1; }
|
||||
virtual int16_t stopDutyCycleHardware() { return -1; }
|
||||
virtual int16_t startContinuousReceive() { return -1; }
|
||||
virtual void onReceiveProcessed() {}
|
||||
virtual bool radioDeepInit() { return false; }
|
||||
virtual bool supportsRadioDeepInit() const { return false; }
|
||||
bool prepareRadioHardReset() { ++prepare_calls; return prepare_ok; }
|
||||
bool supportsRadioHardResetPath() const { return hard_reset_path; }
|
||||
bool hasPendingRadioInterrupt() const { return interrupt_pending; }
|
||||
uint8_t beginReconfigure() { ++reconfigure_calls; return 1; }
|
||||
void endReconfigure(uint8_t) { ++reconfigure_calls; }
|
||||
void startRecv() { ++rearm_calls; }
|
||||
int startReceivePowerSaving() { return 77; }
|
||||
@BASE_METHODS@
|
||||
};
|
||||
struct CustomLR1121Wrapper : RadioLibWrapper {
|
||||
using DeepInitCallback = bool (*)();
|
||||
DeepInitCallback _deep_init = nullptr;
|
||||
explicit CustomLR1121Wrapper(CustomLR1121& value) : RadioLibWrapper(value) {}
|
||||
@CAPABILITY@
|
||||
@WRAPPER_METHODS@
|
||||
};
|
||||
|
||||
static bool reinit_ok = true;
|
||||
static unsigned reinit_calls = 0;
|
||||
static bool board_reinit() { ++reinit_calls; return reinit_ok; }
|
||||
|
||||
static void arm(CustomLR1121& radio) {
|
||||
uint32_t effective = 0;
|
||||
assert(radio.startReceiveDutyCycle(1000000, 500000, 0x35, 0x46, &effective) == 0);
|
||||
assert(effective == 1000000 && radio.programmed_rx == 32768);
|
||||
assert(radio.programmed_sleep == uint32_t((500000ULL - 6000) * 32768 / 1000000));
|
||||
assert(radio.trace == "SLGWD" && radio.needsLfClockRestore());
|
||||
assert(radio.staged.receive.timeout == RADIOLIB_LR11X0_RX_TIMEOUT_INF);
|
||||
assert(radio.staged.receive.irqFlags == 0x35 && radio.staged.receive.irqMask == 0x46);
|
||||
}
|
||||
|
||||
int main() {
|
||||
static_assert(CustomLR1121Wrapper::SupportsRxPowerSaving, "LR1121 advertises RXPS");
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
arm(radio);
|
||||
radio.trace.clear();
|
||||
assert(radio.restoreLfClock() == 0 && !radio.needsLfClockRestore());
|
||||
assert(radio.trace == "R");
|
||||
assert(radio.restoreLfClock() == 0 && radio.trace == "R");
|
||||
}
|
||||
// The maximum CLI periods must survive the native 32768 Hz conversion.
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
uint32_t effective = 0;
|
||||
assert(radio.startReceiveDutyCycle(30000000, 30000000, 1, 2, &effective) == 0);
|
||||
assert(effective == 30000000 && radio.programmed_rx == 983040);
|
||||
assert(radio.programmed_sleep == uint32_t((30000000ULL - 6000) * 32768 / 1000000));
|
||||
}
|
||||
// Invalid periods fail before changing hardware or underflowing the sleep.
|
||||
for (uint32_t sleep : {0U, 5999U, 6000U, 6001U}) {
|
||||
CustomLR1121 radio;
|
||||
assert(radio.startReceiveDutyCycle(78000, sleep, 1, 2, nullptr)
|
||||
== RADIOLIB_ERR_INVALID_SLEEP_PERIOD);
|
||||
assert(radio.trace.empty() && !radio.needsLfClockRestore());
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
assert(radio.startReceiveDutyCycle(1000000, 6031, 1, 2, nullptr) == 0);
|
||||
assert(radio.programmed_sleep == 1);
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
assert(radio.startReceiveDutyCycle(0, 1000000, 1, 2, nullptr)
|
||||
== RADIOLIB_ERR_INVALID_RX_PERIOD);
|
||||
assert(radio.trace.empty());
|
||||
for (uint32_t rx : {0U, 30U}) {
|
||||
assert(radio.startReceiveDutyCycle(rx, 60000, 1, 2, nullptr)
|
||||
== RADIOLIB_ERR_INVALID_RX_PERIOD);
|
||||
assert(radio.trace.empty());
|
||||
}
|
||||
assert(radio.startReceiveDutyCycle(512000000, 1000000, 1, 2, nullptr)
|
||||
== RADIOLIB_ERR_INVALID_RX_PERIOD);
|
||||
assert(radio.trace.empty());
|
||||
assert(radio.startReceiveDutyCycle(1000000, 512006000, 1, 2, nullptr)
|
||||
== RADIOLIB_ERR_INVALID_SLEEP_PERIOD);
|
||||
assert(radio.trace.empty());
|
||||
uint32_t effective = 0;
|
||||
assert(radio.startReceiveDutyCycle(31, 30000, 1, 2, &effective) == 0);
|
||||
assert(effective == 76564); // SF8/BW62.5: 43 symbols plus 1 ms, less sleep.
|
||||
}
|
||||
// Stop at the first failed command and remember LF restoration after any
|
||||
// attempt to enable BUSY release, including a failing configuration command.
|
||||
for (unsigned fault = 0; fault < 4; ++fault) {
|
||||
CustomLR1121 radio;
|
||||
if (fault == 0) radio.standby_error = -71;
|
||||
if (fault == 1) radio.lf_error = -72;
|
||||
if (fault == 2) radio.stage_error = -73;
|
||||
if (fault == 3) radio.duty_error = -74;
|
||||
assert(radio.startReceiveDutyCycle(1000000, 500000, 1, 2, nullptr) == -71 - int(fault));
|
||||
const char* expected[] = {"S", "SL", "SLG", "SLGWD"};
|
||||
assert(radio.trace == expected[fault]);
|
||||
assert(radio.needsLfClockRestore() == (fault != 0));
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
radio._lfClockRestoreNeeded = true; radio.lf_error = -75;
|
||||
assert(radio.restoreLfClock() == -75 && radio.needsLfClockRestore());
|
||||
radio.lf_error = 0;
|
||||
assert(radio.restoreLfClock() == 0 && !radio.needsLfClockRestore());
|
||||
}
|
||||
for (unsigned fault = 0; fault < 3; ++fault) {
|
||||
CustomLR1121 radio;
|
||||
radio._activityAt = 90; radio._headerSeen = true;
|
||||
if (fault == 0) radio.standby_error = -81;
|
||||
if (fault == 1) radio.buffer_error = -82;
|
||||
if (fault == 2) radio.irq_error = -83;
|
||||
assert(radio.recoverReceivePath() == -81 - int(fault));
|
||||
assert(radio.trace == "SBC" && !radio._activityAt && !radio._headerSeen);
|
||||
}
|
||||
// Driver activity polling does no SPI in a duty-cycle sleep window, and
|
||||
// leaves HEADER_ERR visible to the receive owner when awake.
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
radio.module.hal->busy = true;
|
||||
radio.irq = RADIOLIB_LR11X0_IRQ_HEADER_ERR;
|
||||
assert(!radio.isReceiving() && radio.irq_reads == 0 && radio.irq_clears == 0);
|
||||
radio.module.hal->busy = false;
|
||||
assert(!radio.isReceiving() && radio.irq == RADIOLIB_LR11X0_IRQ_HEADER_ERR);
|
||||
assert(radio.irq_clears == 0);
|
||||
assert(radio.getPacketLength(true) == 0 && radio.trace == "SBC");
|
||||
assert(radio.base_length_calls == 0 && radio.stale_length == 0 && radio.irq == 0);
|
||||
radio.module.gpio = RADIOLIB_NC;
|
||||
const unsigned reads = radio.module.hal->reads;
|
||||
assert(!radio.isChipBusy() && radio.module.hal->reads == reads);
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
radio.module.hal->busy = true;
|
||||
assert(radio.getTimeOnAir(32) == 28800 && radio.toa_calls == 1 && radio.modem_queries == 0);
|
||||
radio._activityAt = 99; radio._headerSeen = true;
|
||||
assert(radio.startReceive() == 0 && !radio._activityAt && !radio._headerSeen);
|
||||
assert(radio.continuous_flags & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED);
|
||||
assert(radio.continuous_mask & RADIOLIB_LR11X0_IRQ_HEADER_ERR);
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
assert(radio.begin() == 0 && radio.getTcxoDelay() == 5000);
|
||||
assert(radio.trace == "IV" && radio.tcxo_calls == 0 && delays == 0);
|
||||
radio.trace.clear();
|
||||
assert(radio.begin(434,125,9,7,0x12,10,8,0.0f) == 0);
|
||||
assert(radio.getTcxoDelay() == 0 && radio.trace == "IV" && radio.tcxo_calls == 0);
|
||||
CustomLR1121Wrapper xo_wrapper(radio);
|
||||
assert(xo_wrapper.rxPowerSavingTransitionUs() == 1000);
|
||||
radio.trace.clear();
|
||||
assert(radio.begin() == 0 && radio.getTcxoDelay() == 5000 && radio.trace == "IV");
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
CustomLR1121Wrapper wrapper(radio);
|
||||
assert(wrapper.supportsRxPowerSaving() && wrapper.rxPowerSavingTransitionUs() == 6000);
|
||||
assert(wrapper.startReceiveMode() == 77);
|
||||
uint32_t effective_rx = 0, effective_sleep = 0;
|
||||
assert(wrapper.armDutyCycle(0x31,0x42,&effective_rx,&effective_sleep) == 0);
|
||||
assert(effective_rx >= wrapper._rx_ps_rx_us && effective_sleep == wrapper._rx_ps_sleep_us);
|
||||
assert(radio.staged.receive.irqFlags == 0x31 && radio.staged.receive.irqMask == 0x42);
|
||||
radio.trace.clear();
|
||||
assert(wrapper.stopDutyCycleHardware() == 0 && radio.trace == "SR");
|
||||
assert(!radio.needsLfClockRestore());
|
||||
radio._lfClockRestoreNeeded = true; radio.trace.clear(); radio.lf_error = -78;
|
||||
assert(wrapper.startContinuousReceive() == -78 && radio.trace == "SR");
|
||||
assert(radio.needsLfClockRestore());
|
||||
radio.trace.clear(); radio.lf_error = 0;
|
||||
assert(wrapper.startContinuousReceive() == 0 && radio.trace == "SRX");
|
||||
assert(!radio.needsLfClockRestore());
|
||||
radio.trace.clear();
|
||||
assert(wrapper.startContinuousReceive() == 0 && radio.trace == "X");
|
||||
radio._lfClockRestoreNeeded = true; radio.standby_error = -79; radio.trace.clear();
|
||||
assert(wrapper.startContinuousReceive() == -79 && radio.trace == "S");
|
||||
assert(radio.needsLfClockRestore());
|
||||
radio.lf_error = -80; radio.trace.clear();
|
||||
assert(wrapper.stopDutyCycleHardware() == -79 && radio.trace == "SR");
|
||||
assert(radio.needsLfClockRestore());
|
||||
radio.standby_error = 0; radio.lf_error = 0; radio.trace.clear();
|
||||
assert(wrapper.stopDutyCycleHardware() == 0 && radio.trace == "SR");
|
||||
assert(!radio.needsLfClockRestore());
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
CustomLR1121Wrapper wrapper(radio);
|
||||
radio.module.hal->busy = true;
|
||||
radio.irq = RADIOLIB_LR11X0_IRQ_HEADER_ERR;
|
||||
assert(!wrapper.isReceivingPacket() && radio.irq_reads == 0);
|
||||
radio.module.hal->busy = false;
|
||||
assert(!wrapper.isPacketReady() && radio.trace == "SBC");
|
||||
assert(wrapper.n_recv_errors == 1 && !wrapper._rx_ps_armed && !wrapper._rx_hold_continuous);
|
||||
assert(!wrapper.isPacketReady()); // A non-RxDone IRQ cannot replay stale FIFO data.
|
||||
radio.irq = 1UL << RADIOLIB_IRQ_RX_DONE;
|
||||
assert(wrapper.isPacketReady());
|
||||
}
|
||||
// The driver's completion callback must retain continuous RX while another
|
||||
// packet owns it, then hand control back to the generic duty-cycle rearm.
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
CustomLR1121Wrapper wrapper(radio);
|
||||
wrapper._rx_ps_armed = false; wrapper.interrupt_pending = true;
|
||||
wrapper.onReceiveProcessed();
|
||||
assert(wrapper._rx_hold_continuous && wrapper.rearm_calls == 0);
|
||||
wrapper.interrupt_pending = false;
|
||||
wrapper.onReceiveProcessed();
|
||||
assert(!wrapper._rx_hold_continuous && wrapper.rearm_calls == 1 && wrapper.state == STATE_IDLE);
|
||||
}
|
||||
// Dual-profile scanning keeps RX continuous while changing only the saved
|
||||
// requested RXPS setting. Existing generic scan teardown restores that intent.
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
CustomLR1121Wrapper wrapper(radio);
|
||||
wrapper._profile_rxps_suspended = true; wrapper._rx_ps_enabled = false;
|
||||
assert(wrapper.trySetRxPowerSaving(true,100000,50000) == mesh::RadioParamApplyResult::APPLIED);
|
||||
assert(wrapper._profile_saved_rxps && !wrapper._rx_ps_enabled && wrapper.reconfigure_calls == 0);
|
||||
assert(wrapper._rx_ps_rx_us == 100000 && wrapper._rx_ps_sleep_us == 50000);
|
||||
assert(wrapper.trySetRxPowerSaving(false,100000,50000) == mesh::RadioParamApplyResult::APPLIED);
|
||||
assert(!wrapper._profile_saved_rxps && radio.trace.empty());
|
||||
wrapper._beacon_owned = true;
|
||||
assert(wrapper.trySetRxPowerSaving(true,100000,50000) == mesh::RadioParamApplyResult::BUSY);
|
||||
assert(!wrapper._profile_saved_rxps);
|
||||
}
|
||||
{
|
||||
CustomLR1121 radio;
|
||||
CustomLR1121Wrapper wrapper(radio);
|
||||
assert(!wrapper.supportsRadioDeepInit() && !wrapper.radioDeepInit());
|
||||
assert(wrapper.prepare_calls == 0 && wrapper.board.finish_calls == 0 && reinit_calls == 0);
|
||||
wrapper.setDeepInitCallback(board_reinit);
|
||||
assert(wrapper.supportsRadioDeepInit());
|
||||
wrapper.hard_reset_path = false;
|
||||
assert(!wrapper.supportsRadioDeepInit());
|
||||
wrapper.hard_reset_path = true;
|
||||
wrapper.prepare_ok = false;
|
||||
assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 1);
|
||||
assert(wrapper.board.finish_calls == 0 && reinit_calls == 0);
|
||||
wrapper.prepare_ok = true; reinit_ok = false;
|
||||
assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 2 && reinit_calls == 1);
|
||||
assert(wrapper.board.finish_calls == 0);
|
||||
reinit_ok = true; wrapper.board.finish_ok = false;
|
||||
assert(!wrapper.radioDeepInit() && wrapper.prepare_calls == 3 && reinit_calls == 2);
|
||||
assert(wrapper.board.finish_calls == 1);
|
||||
wrapper.board.finish_ok = true;
|
||||
assert(wrapper.radioDeepInit() && wrapper.prepare_calls == 4 && reinit_calls == 3);
|
||||
assert(wrapper.board.finish_calls == 2);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class LR1121RxpsTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if not cls.compiler:
|
||||
raise unittest.SkipTest("C++ compiler unavailable")
|
||||
|
||||
def compile_and_run(self, source, flags=()):
|
||||
with tempfile.TemporaryDirectory(prefix="lr1121-rxps-") as directory:
|
||||
folder = Path(directory)
|
||||
cpp, binary = folder / "probe.cpp", folder / "probe"
|
||||
cpp.write_text(source, encoding="ascii")
|
||||
command = [self.compiler, "-std=gnu++17", "-O1", "-g", "-Wall", "-Wextra",
|
||||
"-Werror", "-I", str(ROOT / "src"), *flags]
|
||||
if os.name != "nt":
|
||||
command += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"]
|
||||
built = subprocess.run([*command, str(cpp), "-o", str(binary)],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_driver_and_wrapper_hardware_lifecycle(self):
|
||||
driver = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text()
|
||||
wrapper = (ROOT / "src/helpers/radiolib/CustomLR1121Wrapper.h").read_text()
|
||||
generic = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
|
||||
driver_methods = "\n".join(body(driver, signature) for signature in (
|
||||
"uint32_t getTcxoDelay()", "bool needsLfClockRestore()",
|
||||
"int16_t restoreLfClock()", "int16_t startReceiveDutyCycle(",
|
||||
"RadioLibTime_t getTimeOnAir(",
|
||||
"bool isChipBusy()", "int16_t recoverReceivePath()",
|
||||
"size_t getPacketLength(", "int16_t startReceive()", "bool isReceiving()",
|
||||
"int16_t begin(float",
|
||||
))
|
||||
wrapper_methods = "\n".join(body(wrapper, signature) for signature in (
|
||||
"uint32_t rxPowerSavingTransitionUs()", "bool supportsRxPowerSaving()",
|
||||
"bool isChipBusy()", "bool isReceivingPacket()", "int startReceiveMode()",
|
||||
"bool isPacketReady()", "int16_t armDutyCycle(", "int16_t stopDutyCycleHardware()",
|
||||
"int16_t startContinuousReceive()", "void onReceiveProcessed()",
|
||||
"bool radioDeepInit()", "bool supportsRadioDeepInit()",
|
||||
"void setDeepInitCallback(",
|
||||
))
|
||||
capability = re.search(r"static constexpr bool SupportsRxPowerSaving\s*=\s*true;", wrapper)
|
||||
self.assertIsNotNone(capability)
|
||||
base_methods = "\n".join(body(generic, signature).replace("RadioLibWrapper::", "")
|
||||
for signature in (
|
||||
"bool RadioLibWrapper::isPacketReady()",
|
||||
"void RadioLibWrapper::finishReceiveProcessing()",
|
||||
"mesh::RadioParamApplyResult RadioLibWrapper::trySetRxPowerSaving(",
|
||||
))
|
||||
base_methods = base_methods.replace("bool isPacketReady()", "virtual bool isPacketReady()", 1)
|
||||
source = (HARNESS.replace("@DRIVER_METHODS@", driver_methods)
|
||||
.replace("@WRAPPER_METHODS@", wrapper_methods)
|
||||
.replace("@BASE_METHODS@", base_methods)
|
||||
.replace("@CAPABILITY@", capability.group(0)))
|
||||
self.compile_and_run(source, ["-DMESH_ENABLE_TX_BEACONS=1", "-DUSE_LR1121"])
|
||||
|
||||
def test_board_recipes_select_lr11x0_math_before_target_headers(self):
|
||||
recipes = [
|
||||
("variants/waveshare_esp32s3_lr1121/platformio.ini", "Waveshare_S3_LR1121"),
|
||||
("variants/muzi_base/platformio.ini", "muzi_base_duo"),
|
||||
]
|
||||
source = r'''
|
||||
#include <cassert>
|
||||
// Intentionally included before any target/driver header: recipe flags must
|
||||
// select family-specific math in CommonCLI and role constructors too.
|
||||
#include <helpers/radiolib/RXPowerSaving.h>
|
||||
int main() {
|
||||
assert(rxPowerSavingDefaultCaptureCost() == 8.0f);
|
||||
assert(rxPowerSavingDefaultTransitionUs() == EXPECT_TRANSITION);
|
||||
uint32_t rx = 0, sleep = 0, explicit_rx = 0, explicit_sleep = 0;
|
||||
assert(calcRxPowerSavingLevel(6,8,62.5f,32,&rx,&sleep));
|
||||
assert(calcRxPowerSavingLevel(6,8,62.5f,32,&explicit_rx,&explicit_sleep,
|
||||
8.0f,EXPECT_TRANSITION));
|
||||
assert(rx == explicit_rx && sleep == explicit_sleep);
|
||||
#if defined(USE_LR1121) && EXPECT_TRANSITION == 6000
|
||||
assert(!calcRxPowerSavingLevel(6,8,812.5f,16,&rx,&sleep));
|
||||
uint8_t effective_level = 0, effective_preamble = 0;
|
||||
assert(recalcRxPowerSavingFromLevel(6,8,812.5f,16,&rx,&sleep,
|
||||
&effective_level,&effective_preamble));
|
||||
// Preserve the existing policy of promoting to the actual longer wire
|
||||
// preamble when a short configured preamble cannot cover TCXO startup.
|
||||
assert(!rxPowerSavingUsesContinuousFallback(rx,sleep));
|
||||
assert(effective_level >= 6 && effective_preamble > 16);
|
||||
assert(recalcRxPowerSavingFromLevel(6,5,812.5f,16,&rx,&sleep));
|
||||
assert(rxPowerSavingUsesContinuousFallback(rx,sleep));
|
||||
assert(recalcRxPowerSavingFromLevel(6,10,812.5f,16,&rx,&sleep));
|
||||
assert(!rxPowerSavingUsesContinuousFallback(rx,sleep));
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
for relative, section in recipes:
|
||||
text = (ROOT / relative).read_text()
|
||||
section_text = text.split("[" + section + "]", 1)[1].split("\n[", 1)[0]
|
||||
self.assertRegex(section_text, r"(?m)^\s*-D\s+USE_LR1121\b", relative)
|
||||
voltage = re.search(r"-D\s+LR11X0_DIO3_TCXO_VOLTAGE=([0-9.]+)", section_text)
|
||||
self.assertIsNotNone(voltage)
|
||||
with self.subTest(recipe=relative):
|
||||
self.compile_and_run(source, ["-DUSE_LR1121",
|
||||
"-DEXPECT_TRANSITION=6000",
|
||||
"-DLR11X0_DIO3_TCXO_VOLTAGE=" + voltage.group(1)])
|
||||
with self.subTest(family="LR1110"):
|
||||
self.compile_and_run(source, ["-DUSE_LR1110", "-DEXPECT_TRANSITION=2600",
|
||||
"-DLR11X0_DIO3_TCXO_VOLTAGE=1.8"])
|
||||
with self.subTest(oscillator="LR1121 XO"):
|
||||
self.compile_and_run(source, ["-DUSE_LR1121", "-DEXPECT_TRANSITION=1000",
|
||||
"-DLR11X0_DIO3_TCXO_VOLTAGE=0.0"])
|
||||
|
||||
def test_boards_register_deep_init_after_restoring_rf_switch_and_gain(self):
|
||||
for relative in ("variants/waveshare_esp32s3_lr1121/target.cpp",
|
||||
"variants/muzi_base/target.cpp"):
|
||||
with self.subTest(board=relative):
|
||||
initialization = body((ROOT / relative).read_text(), "bool radio_init()")
|
||||
callback = initialization.index("radio_driver.setDeepInitCallback(radio_init)")
|
||||
switch = initialization.index("radio.setRfSwitchTable(")
|
||||
gain = initialization.index("radio.setRxBoostedGainMode(")
|
||||
self.assertLess(initialization.index("radio.begin("), switch)
|
||||
self.assertLess(switch, gain)
|
||||
self.assertLess(gain, callback)
|
||||
self.assertLess(callback, initialization.index("return true", callback))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -220,8 +220,8 @@ int main(){
|
||||
++now_ms;assert(!chip.isReceiving()&&!chip._headerSeen);
|
||||
chip.irq=ACTIVITY_HEADER|ACTIVITY_HEADER_ERROR;
|
||||
assert(!chip.isReceiving()&&!chip._headerSeen&&chip._activityAt==0);
|
||||
#if MESH_RADIO_PROFILE_CHIP == 1110
|
||||
// LR1110 deliberately leaves its error IRQ for wrapper-owned recovery.
|
||||
#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121
|
||||
// LR11x0 deliberately leaves its error IRQ for wrapper-owned recovery.
|
||||
assert(chip.irq&ACTIVITY_HEADER_ERROR);
|
||||
#else
|
||||
assert(!(chip.irq&ACTIVITY_HEADER_ERROR));
|
||||
@@ -261,7 +261,7 @@ int main(){
|
||||
assert(chip._headerSeen&&chip._activityAt==37);
|
||||
chip.duty_failure=0;
|
||||
assert(chip.startReceiveDutyCycle(100000,100000)==0&&!chip._headerSeen&&chip._activityAt==0);
|
||||
#if MESH_RADIO_PROFILE_CHIP == 1110
|
||||
#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121
|
||||
// Bounds and every preparatory command can reject duty-cycle entry. None
|
||||
// acknowledged a new RX epoch, so none may retire the old local activity.
|
||||
chip._headerSeen=true;chip._activityAt=37;
|
||||
@@ -357,7 +357,7 @@ class RadioRxActivityEpochTests(unittest.TestCase):
|
||||
compile_run(build_source(family)+FAST_CHECKS,['-DESP32_PLATFORM=1','-DRADIO_CLASS='+family])
|
||||
|
||||
def test_actual_duty_cycle_entry_resets_only_after_acknowledged_entry(self):
|
||||
for family in ('CustomSX1262','CustomLR1110'):
|
||||
for family in ('CustomSX1262','CustomLR1110','CustomLR1121'):
|
||||
for remove in (False,True):
|
||||
with self.subTest(family=family,removed_reset=remove):
|
||||
source=build_source(family,remove_reset=remove,include_duty=True)
|
||||
|
||||
@@ -20,6 +20,7 @@ build_flags = ${esp32_base.build_flags}
|
||||
-D P_LORA_MOSI=45
|
||||
-D RADIO_CLASS=CustomLR1121
|
||||
-D WRAPPER_CLASS=CustomLR1121Wrapper
|
||||
-D USE_LR1121
|
||||
-D LR11X0_DIO3_TCXO_VOLTAGE=1.8
|
||||
-D LR1121_FORCE_HP_PA ; RFO_LP_LF is not fitted (R4 NC)
|
||||
-D RX_BOOSTED_GAIN=true
|
||||
|
||||
@@ -56,6 +56,8 @@ bool radio_init() {
|
||||
radio.setRxBoostedGainMode(RX_BOOSTED_GAIN);
|
||||
#endif
|
||||
|
||||
// Full RX recovery must restore this board's TCXO and DIO RF-switch table.
|
||||
radio_driver.setDeepInitCallback(radio_init);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user