From 5ef992fa3193945a10f96e946f6835bc76962023 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 21 Feb 2026 14:40:33 +0100 Subject: [PATCH 1/4] Implement 'agc' reset for SX126x chip family There's no actual agc on SX126x chips but you can reset the analog registers by doing a warm sleep & running calibration. --- src/main.cpp | 7 ++++++ src/mesh/RadioLibInterface.cpp | 5 ++++ src/mesh/RadioLibInterface.h | 9 +++++++ src/mesh/SX126xInterface.cpp | 45 ++++++++++++++++++++++++++++++++++ src/mesh/SX126xInterface.h | 2 ++ 5 files changed, 68 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index b4c1f651925..5e5bee8676f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1125,6 +1125,13 @@ void loop() lastRadioMissedIrqPoll = millis(); RadioLibInterface::instance->pollMissedIrqs(); } + + // Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain + static uint32_t lastAgcReset; + if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) { + lastAgcReset = millis(); + RadioLibInterface::instance->resetAGC(); + } } #ifdef DEBUG_STACK diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 78e0fc5b4dc..30cd587da23 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -529,6 +529,11 @@ void RadioLibInterface::pollMissedIrqs() } } +void RadioLibInterface::resetAGC() +{ + // Base implementation: no-op. Override in chip-specific subclasses. +} + void RadioLibInterface::checkRxDoneIrqFlag() { if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) { diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 4bca99b5f30..ca3d78503ed 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -19,6 +19,8 @@ // In addition to the default Rx flags, we need the PREAMBLE_DETECTED flag to detect whether we are actively receiving #define MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS (RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1 << RADIOLIB_IRQ_PREAMBLE_DETECTED)) +#define AGC_RESET_INTERVAL_MS (60 * 1000) // 60 seconds + /** * We need to override the RadioLib ArduinoHal class to add mutex protection for SPI bus access */ @@ -117,6 +119,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ void pollMissedIrqs(); + /** + * Reset AGC by power-cycling the analog frontend. + * Subclasses override with chip-specific calibration sequences. + * Safe to call periodically — skips if currently sending or receiving. + */ + virtual void resetAGC(); + /** * Debugging counts */ diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 5b2fb3ac3ed..fdcc054a342 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -434,6 +434,51 @@ template bool SX126xInterface::sleep() return true; } +template void SX126xInterface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_INFO("SX126x AGC reset: warm sleep + Calibrate(0x7F)"); + + // 1. Warm sleep — powers down the entire analog frontend, resetting AGC state. + // A plain standby→startReceive cycle does NOT reset the AGC. + lora.sleep(true); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_SX126X_STANDBY_RC, true); + + // 3. Calibrate all blocks (ADC, PLL, image, RC oscillators) + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + + // 4. Wait for calibration to complete (BUSY pin goes low) + module.hal->delay(5); + uint32_t start = millis(); + while (module.hal->digitalRead(module.getGpio())) { + if (millis() - start > 50) + break; + module.hal->yield(); + } + + // 5. Re-apply settings that calibration may have reset + + // DIO2 as RF switch +#ifdef SX126X_DIO2_AS_RF_SWITCH + lora.setDio2AsRfSwitch(true); +#elif defined(ARCH_PORTDUINO) + if (portduino_config.dio2_as_rf_switch) + lora.setDio2AsRfSwitch(true); +#endif + + // RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 6. Resume receiving + startReceive(); +} + /** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index b8f16ac6da9..67625e1154a 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -28,6 +28,8 @@ template class SX126xInterface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } + void resetAGC() override; + void setTCXOVoltage(float voltage) { tcxoVoltage = voltage; } protected: From 721a0e69431254289fa5e6ba922da6ef36d7fb2d Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 21 Feb 2026 15:46:56 +0100 Subject: [PATCH 2/4] Address PR comments & implement for LR11x0 too --- src/mesh/LR11x0Interface.cpp | 26 ++++++++++++++++++++++++++ src/mesh/LR11x0Interface.h | 2 ++ src/mesh/SX126xInterface.cpp | 8 +++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index a6ac4f41829..800aa43cf80 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -299,6 +299,32 @@ template bool LR11x0Interface::isActivelyReceiving() RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); } +template void LR11x0Interface::resetAGC() +{ + // Safety: don't reset mid-packet + if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) + return; + + LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)"); + + // 1. Warm sleep — powers down the analog frontend, resetting AGC state + lora.sleep(true, 0); + + // 2. Wake to RC standby for stable calibration + lora.standby(RADIOLIB_LR11X0_STANDBY_RC, true); + + // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) + // calibrate() is protected on LR11x0, so use raw SPI (same as internal implementation) + uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL; + module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true); + + // 4. Re-apply RX boosted gain mode + lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + + // 5. Resume receiving + startReceive(); +} + template bool LR11x0Interface::sleep() { // \todo Display actual typename of the adapter, not just `LR11x0` diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 840184bbf60..2bd7f374948 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -27,6 +27,8 @@ template class LR11x0Interface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } + void resetAGC() override; + protected: /** * Specific module instance diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index fdcc054a342..23fb41211f1 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -440,7 +440,7 @@ template void SX126xInterface::resetAGC() if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) return; - LOG_INFO("SX126x AGC reset: warm sleep + Calibrate(0x7F)"); + LOG_DEBUG("SX126x AGC reset: warm sleep + Calibrate(0x7F)"); // 1. Warm sleep — powers down the entire analog frontend, resetting AGC state. // A plain standby→startReceive cycle does NOT reset the AGC. @@ -462,6 +462,12 @@ template void SX126xInterface::resetAGC() module.hal->yield(); } + if (module.hal->digitalRead(module.getGpio())) { + LOG_WARN("SX126x AGC reset: calibration did not complete within 50ms"); + startReceive(); + return; + } + // 5. Re-apply settings that calibration may have reset // DIO2 as RF switch From 63c7391f4d4c5c0459196c223217614793edab99 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 21 Feb 2026 17:55:06 +0100 Subject: [PATCH 3/4] calibrate for configured frequency band --- src/mesh/LR11x0Interface.cpp | 8 ++++++-- src/mesh/SX126xInterface.cpp | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 800aa43cf80..f9e4348832b 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -318,10 +318,14 @@ template void LR11x0Interface::resetAGC() uint8_t calData = RADIOLIB_LR11X0_CALIBRATE_ALL; module.SPIwriteStream(RADIOLIB_LR11X0_CMD_CALIBRATE, &calData, 1, true, true); - // 4. Re-apply RX boosted gain mode + // 4. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); + + // 5. Re-apply RX boosted gain mode lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); - // 5. Resume receiving + // 6. Resume receiving startReceive(); } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 23fb41211f1..a9ee16545e5 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -468,7 +468,11 @@ template void SX126xInterface::resetAGC() return; } - // 5. Re-apply settings that calibration may have reset + // 5. Re-calibrate image rejection for actual operating frequency + // Calibrate(0x7F) defaults to 902-928 MHz which is wrong for other regions. + lora.calibrateImage(getFreq()); + + // Re-apply settings that calibration may have reset // DIO2 as RF switch #ifdef SX126X_DIO2_AS_RF_SWITCH From 22d173c2dd483c6047732d57561744dc184eab07 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 23 Feb 2026 12:57:20 +0100 Subject: [PATCH 4/4] Gate LR11X0_AGC_RESET --- src/mesh/LR11x0Interface.cpp | 2 ++ src/mesh/LR11x0Interface.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index f9e4348832b..4fec06da4b1 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -299,6 +299,7 @@ template bool LR11x0Interface::isActivelyReceiving() RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); } +#ifdef LR11X0_AGC_RESET template void LR11x0Interface::resetAGC() { // Safety: don't reset mid-packet @@ -328,6 +329,7 @@ template void LR11x0Interface::resetAGC() // 6. Resume receiving startReceive(); } +#endif template bool LR11x0Interface::sleep() { diff --git a/src/mesh/LR11x0Interface.h b/src/mesh/LR11x0Interface.h index 2bd7f374948..1a6b925206b 100644 --- a/src/mesh/LR11x0Interface.h +++ b/src/mesh/LR11x0Interface.h @@ -27,7 +27,9 @@ template class LR11x0Interface : public RadioLibInterface bool isIRQPending() override { return lora.getIrqFlags() != 0; } +#ifdef LR11X0_AGC_RESET void resetAGC() override; +#endif protected: /**