From 366f6ab73d8a8c0b550b3ca06d0ed20ea98949e6 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Fri, 14 Aug 2026 17:45:51 -0700 Subject: [PATCH] Add T-Beam 1W external LNA control --- src/helpers/radiolib/CustomSX1262.h | 39 ++++++++++++++++++++++ src/helpers/radiolib/CustomSX1262Wrapper.h | 30 ++++++++++++++++- variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 29 +++++++++++++++- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 9 +++++ variants/lilygo_tbeam_1w/target.cpp | 1 + 5 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 4772c596..d6635d65 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -156,6 +156,45 @@ class CustomSX1262 : public SX1262 { return _rx_ps_rf_rx_disabled; } + // Select whether a host-controlled RXEN pin is asserted in receive mode. + // Some FEM modules (notably the LilyGo T-Beam 1W XY16P35) expose their + // external LNA supply on RXEN while DIO2 independently selects the RF + // switch. Swapping RadioLib's table keeps the requested state intact + // across RX, CAD, TX, standby, and duty-cycle transitions; a one-shot + // digitalWrite() would be overwritten on the next mode change. + bool setExternalRxLnaEnabled(bool enabled) { + #if defined(SX126X_RXEN) + #if defined(SX126X_TXEN) + static constexpr uint32_t tx_en = SX126X_TXEN; + #else + static constexpr uint32_t tx_en = RADIOLIB_NC; + #endif + + static const uint32_t pins[Module::RFSWITCH_MAX_PINS] = { + SX126X_RXEN, tx_en, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, + }; + static const Module::RfSwitchMode_t lna_enabled_table[] = { + { Module::MODE_IDLE, { LOW, LOW } }, + { Module::MODE_RX, { HIGH, LOW } }, + { Module::MODE_TX, { LOW, HIGH } }, + END_OF_MODE_TABLE, + }; + static const Module::RfSwitchMode_t lna_disabled_table[] = { + { Module::MODE_IDLE, { LOW, LOW } }, + { Module::MODE_RX, { LOW, LOW } }, + { Module::MODE_TX, { LOW, HIGH } }, + END_OF_MODE_TABLE, + }; + + this->mod->setRfSwitchTable( + pins, enabled ? lna_enabled_table : lna_disabled_table); + return true; + #else + (void)enabled; + return false; + #endif + } + // BUSY high means the chip is asleep (RX duty-cycle sleep window) or mid // command; any SPI access would stall until the chip's next listen window. bool isChipBusy() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index a3daac05..8ce3a03d 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -78,6 +78,25 @@ public: return ((CustomSX1262 *)_radio)->isRxPowerSavingRfRxDisabled(); } + // Reconfigure through the normal bounded radio transition so RXEN is never + // asserted while a transmission or unread packet owns the radio. The + // selected table is then honored by every RadioLib mode transition. + bool setExternalRxLnaEnabled(bool enabled) { + #if defined(SX126X_RXEN) + if (SX126X_RXEN == RADIOLIB_NC) return false; + + uint8_t resume_rx = beginReconfigure(); + if (resume_rx > 1) return false; + + bool success = ((CustomSX1262 *)_radio)->setExternalRxLnaEnabled(enabled); + endReconfigure(resume_rx); + return success; + #else + (void)enabled; + return false; + #endif + } + protected: int startReceiveMode() override { if (_rx_ps_armed) { @@ -118,7 +137,16 @@ protected: bool radioDeepInit() override { // std_init() re-runs RadioLib begin(), which starts with a hardware reset // via NRST - the only way out of a hard-locked chip (BUSY stuck high). - return ((CustomSX1262 *)_radio)->std_init(); + bool success = ((CustomSX1262 *)_radio)->std_init(); +#if defined(TBEAM_1W) + // std_init() restores RadioLib's default RXEN table. Reapply the T-Beam + // 1W's persisted external-LNA selection before receive mode is re-armed. + if (success && _board->canControlLoRaFemLna()) { + success = ((CustomSX1262 *)_radio)->setExternalRxLnaEnabled( + _board->isLoRaFemLnaEnabled()); + } +#endif + return success; } bool supportsRadioDeepInit() const override { return true; } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index 1719d733..69c0b1c6 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -1,4 +1,5 @@ #include "TBeam1WBoard.h" +#include void TBeam1WBoard::begin() { ESP32Board::begin(); @@ -9,7 +10,10 @@ void TBeam1WBoard::begin() { radio_powered = true; delay(10); // Allow radio to power up - // RF switch RXEN pin handled by RadioLib via setRfSwitchPins() + // Keep the external LNA off until RadioLib deliberately enters RX. GPIO21 + // is subsequently controlled by RadioLib's target-specific RF switch table. + pinMode(SX126X_RXEN, OUTPUT); + digitalWrite(SX126X_RXEN, LOW); // Initialize LED pinMode(LED_PIN, OUTPUT); @@ -62,6 +66,29 @@ void TBeam1WBoard::powerOff() { ESP32Board::powerOff(); } +void TBeam1WBoard::attachRadioDriver(CustomSX1262Wrapper* driver) { + radio_driver = driver; +} + +bool TBeam1WBoard::setLoRaFemLnaEnabled(bool enable) { + if (radio_driver == nullptr || !radio_driver->setExternalRxLnaEnabled(enable)) { + return false; + } + + // This is the requested RX-mode state. TX and standby always force CTRL low + // regardless of this preference, protecting both the LNA and PA paths. + lna_enabled = enable; + return true; +} + +bool TBeam1WBoard::canControlLoRaFemLna() const { + return radio_driver != nullptr; +} + +bool TBeam1WBoard::isLoRaFemLnaEnabled() const { + return lna_enabled; +} + void TBeam1WBoard::setFanEnabled(bool enabled) { digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); } diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index 670f9d40..5362601a 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -4,6 +4,8 @@ #include #include "variant.h" +class CustomSX1262Wrapper; + // LilyGo T-Beam 1W with SX1262 + external PA (XY16P35 module) // // Power architecture (LDO is separate chip on T-Beam board, not inside XY16P35): @@ -30,6 +32,8 @@ class TBeam1WBoard : public ESP32Board { private: bool radio_powered = false; + bool lna_enabled = true; + CustomSX1262Wrapper* radio_driver = nullptr; public: void begin(); @@ -39,6 +43,11 @@ public: const char* getManufacturerName() const override; void powerOff() override; + void attachRadioDriver(CustomSX1262Wrapper* driver); + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; + // Fan control methods void setFanEnabled(bool enabled); bool isFanEnabled() const; diff --git a/variants/lilygo_tbeam_1w/target.cpp b/variants/lilygo_tbeam_1w/target.cpp index ad672c3d..669fdf80 100644 --- a/variants/lilygo_tbeam_1w/target.cpp +++ b/variants/lilygo_tbeam_1w/target.cpp @@ -36,6 +36,7 @@ bool radio_init() { bool success = radio.std_init(&spi); if (success) { + board.attachRadioDriver(&radio_driver); // T-Beam 1W has external PA requiring longer ramp time (>800us recommended) // RADIOLIB_SX126X_PA_RAMP_800U = 0x05 radio.setTxParams(LORA_TX_POWER, RADIOLIB_SX126X_PA_RAMP_800U);