From 717c6e26f64307876702d6d24a6850ba686e363e Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 26 Feb 2026 15:10:52 +0800 Subject: [PATCH 01/14] Add heltec-v4.3 board --- src/configuration.h | 4 + src/graphics/draw/MenuHandler.cpp | 78 ++++++++++- src/graphics/draw/MenuHandler.h | 7 +- src/mesh/LoRaFEMInterface.cpp | 160 ++++++++++++++++++++++ src/mesh/LoRaFEMInterface.h | 34 +++++ src/mesh/RadioInterface.cpp | 8 +- src/mesh/RadioInterface.h | 4 + src/mesh/SX126xInterface.cpp | 65 +++------ src/sleep.cpp | 18 +-- variants/esp32s3/heltec_v4/platformio.ini | 1 + variants/esp32s3/heltec_v4/variant.h | 12 +- 11 files changed, 319 insertions(+), 72 deletions(-) create mode 100644 src/mesh/LoRaFEMInterface.cpp create mode 100644 src/mesh/LoRaFEMInterface.h diff --git a/src/configuration.h b/src/configuration.h index 53ae30d51d2..ee754f3227b 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -163,6 +163,10 @@ along with this program. If not, see . #define TX_GAIN_LORA 0 #endif +#ifndef HAS_LORA_FEM +#define HAS_LORA_FEM 0 +#endif + // ----------------------------------------------------------------------------- // Feature toggles // ----------------------------------------------------------------------------- diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index f57c3951250..eea8c04d92b 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -65,12 +65,17 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { +#if HAS_LORA_FEM + static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region", "LoRa FEM LNA"}; + enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4, LoraFemLna = 5 }; +#else static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; +#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 5; + bannerOptions.optionsCount = sizeof(optionsArray) / sizeof((optionsArray)[0]); bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action @@ -83,6 +88,11 @@ void menuHandler::loraMenu() } else if (selected == LoraPicker) { menuHandler::menuQueue = menuHandler::LoraPicker; } +#if HAS_LORA_FEM + else if (selected == LoraFemLna) { + menuHandler::menuQueue = menuHandler::LoraFemLnaToggleMenu; + } +#endif }; screen->showOverlayBanner(bannerOptions); } @@ -2632,6 +2642,67 @@ void menuHandler::messageBubblesMenu() screen->showOverlayBanner(bannerOptions); } +#if HAS_LORA_FEM +void menuHandler::LoRaFEMLNAToggleMenu() +{ + static const LoRaFEMLNAToggleOption femToggleOptions[] = { + {"Back", OptionsAction::Back}, + {"Enabled", OptionsAction::Select, meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED}, + {"Disabled", OptionsAction::Select, meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED}, + }; + constexpr size_t toggleCount = sizeof(femToggleOptions) / sizeof(femToggleOptions[0]); + static std::array toggleLabels{}; + BannerOverlayOptions bannerOptions; + + if(loraFEMInterface.isLnaCanControl()) { + bannerOptions = createStaticBannerOptions("Toggle FEM LNA", femToggleOptions, toggleLabels, [](const LoRaFEMLNAToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = LoraMenu; + screen->runNow(); + return; + } + + if (!option.hasValue) { + return; + } + + if (config.lora.fem_lna_mode == option.value) { + return; + } + + config.lora.fem_lna_mode = option.value; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); + + int initialSelection = 0; + for (size_t i = 0; i < toggleCount; ++i) { + if (femToggleOptions[i].hasValue && config.lora.fem_lna_mode == femToggleOptions[i].value) { + initialSelection = static_cast(i); + break; + } + } + bannerOptions.InitialSelected = initialSelection; + } else { + static const char *optionsArray[] = {"Back"}; + enum optionsNumbers {Back = 0}; + + // BannerOverlayOptions bannerOptions; + bannerOptions.message = "LNA Disable Unsupported"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 1; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + // No action + } + }; + LOG_INFO("LNA cannot be disabled on this device"); + } + + screen->showOverlayBanner(bannerOptions); +} +#endif + void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != MenuNone) @@ -2782,6 +2853,11 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case MessageBubblesMenu: messageBubblesMenu(); break; +#if HAS_LORA_FEM + case LoraFemLnaToggleMenu: + LoRaFEMLNAToggleMenu(); + break; +#endif } menuQueue = MenuNone; } diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 4a0360412dd..5b10f8b44fa 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -55,7 +55,8 @@ class menuHandler NodeNameLengthMenu, FrameToggles, DisplayUnits, - MessageBubblesMenu + MessageBubblesMenu, + LoraFemLnaToggleMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu @@ -111,6 +112,9 @@ class menuHandler static void displayUnitsMenu(); static void messageBubblesMenu(); static void textMessageMenu(); +#if HAS_LORA_FEM + static void LoRaFEMLNAToggleMenu(); +#endif private: static void saveUIConfig(); @@ -159,6 +163,7 @@ using NodeNameOption = MenuOption; using PositionMenuOption = MenuOption; using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; +using LoRaFEMLNAToggleOption = MenuOption; } // namespace graphics #endif diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp new file mode 100644 index 00000000000..49b38e2a9be --- /dev/null +++ b/src/mesh/LoRaFEMInterface.cpp @@ -0,0 +1,160 @@ +#if HAS_LORA_FEM +#include "LoRaFEMInterface.h" + +#if defined(ARCH_ESP32) +#include +#include +#endif + +LoRaFEMInterface loraFEMInterface; +void LoRaFEMInterface::init(void) +{ + setLnaCanControl(false);// Default is uncontrollable +#ifdef HELTEC_V4 + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); + + pinMode(LORA_PA_POWER,OUTPUT); + digitalWrite(LORA_PA_POWER,HIGH); + delay(1); + pinMode(LORA_KCT8103L_PA_CSD,INPUT); // detect which FEM is used + delay(1); + if(digitalRead(LORA_KCT8103L_PA_CSD)==HIGH) { + // FEM is KCT8103L + fem_type= KCT8103L_PA; + pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + setLnaCanControl(true); + } else if(digitalRead(LORA_KCT8103L_PA_CSD)==LOW) { + // FEM is GC1109 + fem_type= GC1109_PA; + pinMode(LORA_GC1109_PA_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_EN, HIGH); + pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else { + fem_type= OTHER_FEM_TYPES; + } +#endif +} + +void LoRaFEMInterface::setSleepModeEnable(void) +{ +#ifdef HELTEC_V4 + if(fem_type==GC1109_PA) { + /* + * Do not switch the power on and off frequently. + * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. + */ + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if(fem_type==KCT8103L_PA) { + // shutdown the PA + digitalWrite(LORA_KCT8103L_PA_CSD, LOW); + } +#endif +} + +void LoRaFEMInterface::setTxModeEnable(void) +{ +#ifdef HELTEC_V4 + if(fem_type==GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) + } else if(fem_type==KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } +#endif +} + +void LoRaFEMInterface::setRxModeEnable(void) +{ +#ifdef HELTEC_V4 + if(fem_type==GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if(fem_type==KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + if(lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } + } +#endif +} + +void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void) +{ + +#ifdef HELTEC_V4 + // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. + // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), + // then latch with RTC hold so the state survives deep sleep. + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + if(fem_type==GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); + } else if(fem_type==KCT8103L_PA) { + digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); + if(lna_enabled) { + digitalWrite(LORA_KCT8103L_PA_CTX, LOW); + } else { + digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); + } + rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); + } +#endif +} + +void LoRaFEMInterface::setLNAEnable(bool enabled) +{ + lna_enabled = enabled; +} + +int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) +{ +#ifdef HELTEC_V4 + const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; + const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10,9, 8, 7}; + uint16_t *tx_gain,tx_gain_num; + if(fem_type == GC1109_PA) { + tx_gain = (uint16_t*)gc1109_tx_gain; + tx_gain_num = sizeof(gc1109_tx_gain)/sizeof(gc1109_tx_gain[0]); + } else if(fem_type == KCT8103L_PA) { + tx_gain = (uint16_t*)kct8103l_tx_gain; + tx_gain_num = sizeof(kct8103l_tx_gain)/sizeof(kct8103l_tx_gain[0]); + } else { + return loraOutputPower; + } +#else +#ifdef ARCH_PORTDUINO + size_t num_pa_points = portduino_config.num_pa_points; + const uint16_t *tx_gain = portduino_config.tx_gain_lora; +#else + size_t num_pa_points = NUM_PA_POINTS; + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; +#endif +#endif + for (int radio_dbm = 0; radio_dbm < tx_gain_num; radio_dbm++) { + if (((radio_dbm + tx_gain[radio_dbm]) > loraOutputPower) || + ((radio_dbm == (tx_gain_num - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= loraOutputPower))) { + // we've exceeded the power limit, or hit the max we can do + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", loraOutputPower, tx_gain[radio_dbm]); + loraOutputPower -= tx_gain[radio_dbm]; + break; + } + } + return loraOutputPower; +} + +#endif \ No newline at end of file diff --git a/src/mesh/LoRaFEMInterface.h b/src/mesh/LoRaFEMInterface.h new file mode 100644 index 00000000000..5ea0e95736c --- /dev/null +++ b/src/mesh/LoRaFEMInterface.h @@ -0,0 +1,34 @@ +#if HAS_LORA_FEM +#pragma once +#include +#include "configuration.h" +#include "NodeDB.h" + +typedef enum { + GC1109_PA, + KCT8103L_PA, + OTHER_FEM_TYPES +} LoRaFEMType; + +class LoRaFEMInterface +{ + public: + LoRaFEMInterface(){ } + virtual ~LoRaFEMInterface(){ } + void init(void); + void setSleepModeEnable(void); + void setTxModeEnable(void); + void setRxModeEnable(void); + void setRxModeEnableWhenMCUSleep(void); + void setLNAEnable(bool enabled); + int8_t powerConversion(int8_t loraOutputPower); + bool isLnaCanControl(void) { return lna_can_control; } + void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + private: + LoRaFEMType fem_type; + bool lna_enabled=false; + bool lna_can_control=false; +}; +extern LoRaFEMInterface loraFEMInterface; + +#endif \ No newline at end of file diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index e8202d9b0b9..f72630cf740 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -920,6 +920,12 @@ void RadioInterface::limitPower(int8_t loraMaxPower) power = maxPower; } +#if HAS_LORA_FEM + if (!devicestate.owner.is_licensed) { + power = loraFEMInterface.powerConversion(power); + } +#else +// todo:All entries containing "lora fem" are grouped together above. #ifdef ARCH_PORTDUINO size_t num_pa_points = portduino_config.num_pa_points; const uint16_t *tx_gain = portduino_config.tx_gain_lora; @@ -945,7 +951,7 @@ void RadioInterface::limitPower(int8_t loraMaxPower) } } } - +#endif if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 1fe3dd7b0f5..3ed2bfde7f1 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -8,6 +8,10 @@ #include "error.h" #include +#ifdef HAS_LORA_FEM +#include "LoRaFEMInterface.h" +#endif + // Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file. typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index a9ee16545e5..b96b966d582 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -6,7 +6,7 @@ #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" #endif -#if defined(USE_GC1109_PA) && defined(ARCH_ESP32) +#if defined(ARCH_ESP32) #include #include #endif @@ -56,41 +56,13 @@ template bool SX126xInterface::init() pinMode(SX126X_POWER_EN, OUTPUT); #endif -#if defined(USE_GC1109_PA) - // GC1109 FEM chip initialization - // See variant.h for full pin mapping and control logic documentation - // - // On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in - // enableLoraInterrupt). We configure GPIO registers before releasing the hold - // so the pad transitions atomically from held-HIGH to register-HIGH with no - // power glitch. On cold boot the hold_dis is a harmless no-op. - - // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) - pinMode(LORA_PA_POWER, OUTPUT); - digitalWrite(LORA_PA_POWER, HIGH); - rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); - - // TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait - // for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement: - // "VBAT must be prior to CSD/CPS/CTX for the power on sequence" - // On deep sleep wake the LDO was held on via RTC latch, so no delay needed. -#if defined(ARCH_ESP32) - if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) { - delayMicroseconds(1000); +#if HAS_LORA_FEM + loraFEMInterface.init(); + if((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED) && loraFEMInterface.isLnaCanControl()) { + loraFEMInterface.setLNAEnable(true); + } else if ((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED) && loraFEMInterface.isLnaCanControl()) { + loraFEMInterface.setLNAEnable(false); } -#else - delayMicroseconds(1000); -#endif - - // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX - pinMode(LORA_PA_EN, OUTPUT); - digitalWrite(LORA_PA_EN, HIGH); - rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN); - - // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) - // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH - pinMode(LORA_PA_TX_EN, OUTPUT); - digitalWrite(LORA_PA_TX_EN, LOW); // Start in RX-ready state #endif #ifdef RF95_FAN_EN @@ -194,7 +166,7 @@ template bool SX126xInterface::init() LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); } -#ifdef USE_GC1109_PA +#if HAS_LORA_FEM // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity // on boards with the GC1109 FEM. Sets bit 0 of register 0x8B5. // Reference: https://github.com/meshcore-dev/MeshCore/pull/1398 @@ -422,15 +394,10 @@ template bool SX126xInterface::sleep() digitalWrite(SX126X_POWER_EN, LOW); #endif -#if defined(USE_GC1109_PA) - /* - * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. - * // digitalWrite(LORA_PA_POWER, LOW); - */ - digitalWrite(LORA_PA_EN, LOW); - digitalWrite(LORA_PA_TX_EN, LOW); +#if HAS_LORA_FEM + loraFEMInterface.setSleepModeEnable(); #endif + return true; } @@ -492,10 +459,12 @@ template void SX126xInterface::resetAGC() /** Control PA mode for GC1109 FEM - CPS pin selects full PA (txon=true) or bypass mode (txon=false) */ template void SX126xInterface::setTransmitEnable(bool txon) { -#if defined(USE_GC1109_PA) - digitalWrite(LORA_PA_POWER, HIGH); // Ensure LDO is on - digitalWrite(LORA_PA_EN, HIGH); // CSD=1: Chip enabled - digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) +#if HAS_LORA_FEM + if (txon) { + loraFEMInterface.setTxModeEnable(); + } else { + loraFEMInterface.setRxModeEnable(); + } #endif } diff --git a/src/sleep.cpp b/src/sleep.cpp index 8470e9273c7..8603603eaf5 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -163,13 +163,6 @@ void initDeepSleep() if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { LOG_DEBUG("Disable any holds on RTC IO pads"); for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { -#if defined(USE_GC1109_PA) - // Skip GC1109 FEM power pins - they are held HIGH during deep sleep to keep - // the LNA active for RX wake. Released later in SX126xInterface::init() after - // GPIO registers are set HIGH first, avoiding a power glitch. - if (i == LORA_PA_POWER || i == LORA_PA_EN) - continue; -#endif if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) rtc_gpio_hold_dis((gpio_num_t)i); @@ -567,15 +560,8 @@ void enableLoraInterrupt() gpio_pullup_en((gpio_num_t)LORA_CS); #endif -#if defined(USE_GC1109_PA) - // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. - // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), - // then latch with RTC hold so the state survives deep sleep. - digitalWrite(LORA_PA_POWER, HIGH); - rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); - digitalWrite(LORA_PA_EN, HIGH); - rtc_gpio_hold_en((gpio_num_t)LORA_PA_EN); - gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); +#if HAS_LORA_FEM + loraFEMInterface.setRxModeEnableWhenMCUSleep(); #endif LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini index 72c53ded095..9591f2dc1bc 100644 --- a/variants/esp32s3/heltec_v4/platformio.ini +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -6,6 +6,7 @@ board_build.partitions = default_16MB.csv build_flags = ${esp32s3_base.build_flags} -D HELTEC_V4 + -D HAS_LORA_FEM=1 -I variants/esp32s3/heltec_v4 diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index 1c1168d9459..b6a5b2777bc 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -47,15 +47,17 @@ // CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) // CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 -#define USE_GC1109_PA -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable -#define LORA_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) -#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) - // GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) // GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp // Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_GC1109_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + +#define LORA_KCT8103L_PA_CSD 2 +#define LORA_KCT8103L_PA_CTX 5 // enable tx + #if HAS_TFT #define USE_TFTDISPLAY 1 #endif From aa66dd13e046b2b19ac5560acfa47cfd6e932b40 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Thu, 26 Feb 2026 15:42:06 +0800 Subject: [PATCH 02/14] Modify LNA control display content --- src/graphics/draw/MenuHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index eea8c04d92b..ff92f291181 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -2688,7 +2688,7 @@ void menuHandler::LoRaFEMLNAToggleMenu() enum optionsNumbers {Back = 0}; // BannerOverlayOptions bannerOptions; - bannerOptions.message = "LNA Disable Unsupported"; + bannerOptions.message = "LNA Control Unsupported"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 1; bannerOptions.bannerCallback = [](int selected) -> void { From 788725cd0b3902b7db62ee7d904a6219d0bc22e3 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 27 Feb 2026 16:00:28 +0800 Subject: [PATCH 03/14] Fix Heltec Tracker v2 FEM control --- src/graphics/draw/MenuHandler.h | 3 + src/mesh/LoRaFEMInterface.cpp | 107 ++++++++++++------ .../heltec_wireless_tracker_v2/platformio.ini | 1 + .../heltec_wireless_tracker_v2/variant.h | 6 +- 4 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 5b10f8b44fa..1c147ce0115 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -163,7 +163,10 @@ using NodeNameOption = MenuOption; using PositionMenuOption = MenuOption; using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; + +#if HAS_LORA_FEM using LoRaFEMLNAToggleOption = MenuOption; +#endif } // namespace graphics #endif diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp index 49b38e2a9be..e04ae9fd605 100644 --- a/src/mesh/LoRaFEMInterface.cpp +++ b/src/mesh/LoRaFEMInterface.cpp @@ -9,7 +9,7 @@ LoRaFEMInterface loraFEMInterface; void LoRaFEMInterface::init(void) { - setLnaCanControl(false);// Default is uncontrollable + setLnaCanControl(false); // Default is uncontrollable #ifdef HELTEC_V4 rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); @@ -17,76 +17,99 @@ void LoRaFEMInterface::init(void) rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); - pinMode(LORA_PA_POWER,OUTPUT); - digitalWrite(LORA_PA_POWER,HIGH); + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); delay(1); - pinMode(LORA_KCT8103L_PA_CSD,INPUT); // detect which FEM is used + pinMode(LORA_KCT8103L_PA_CSD, INPUT); // detect which FEM is used delay(1); - if(digitalRead(LORA_KCT8103L_PA_CSD)==HIGH) { + if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) { // FEM is KCT8103L - fem_type= KCT8103L_PA; + fem_type = KCT8103L_PA; pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); setLnaCanControl(true); - } else if(digitalRead(LORA_KCT8103L_PA_CSD)==LOW) { + } else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) { // FEM is GC1109 - fem_type= GC1109_PA; + fem_type = GC1109_PA; pinMode(LORA_GC1109_PA_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_EN, HIGH); pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_TX_EN, LOW); } else { - fem_type= OTHER_FEM_TYPES; + fem_type = OTHER_FEM_TYPES; } +#elif defined(USE_GC1109_PA) + fem_type = GC1109_PA; +#if defined(ARCH_ESP32) + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); + rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN); +#endif + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); + delay(1); + pinMode(LORA_GC1109_PA_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_EN, HIGH); + pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); #endif } void LoRaFEMInterface::setSleepModeEnable(void) { #ifdef HELTEC_V4 - if(fem_type==GC1109_PA) { - /* - * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. - */ - digitalWrite(LORA_GC1109_PA_EN, LOW); - digitalWrite(LORA_GC1109_PA_TX_EN, LOW); - } else if(fem_type==KCT8103L_PA) { + if (fem_type == GC1109_PA) { + /* + * Do not switch the power on and off frequently. + * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. + */ + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if (fem_type == KCT8103L_PA) { // shutdown the PA digitalWrite(LORA_KCT8103L_PA_CSD, LOW); } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, LOW); + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); #endif } void LoRaFEMInterface::setTxModeEnable(void) { #ifdef HELTEC_V4 - if(fem_type==GC1109_PA) { - digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) - } else if(fem_type==KCT8103L_PA) { + } else if (fem_type == KCT8103L_PA) { digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, HIGH); // CPS: 1=full PA, 0=bypass (for RX, CPS is don't care) #endif } void LoRaFEMInterface::setRxModeEnable(void) { #ifdef HELTEC_V4 - if(fem_type==GC1109_PA) { - digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled - digitalWrite(LORA_GC1109_PA_TX_EN, LOW); - } else if(fem_type==KCT8103L_PA) { + if (fem_type == GC1109_PA) { + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); + } else if (fem_type == KCT8103L_PA) { digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); - if(lna_enabled) { + if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_GC1109_PA_EN, HIGH); // CSD=1: Chip enabled + digitalWrite(LORA_GC1109_PA_TX_EN, LOW); #endif } @@ -99,20 +122,28 @@ void LoRaFEMInterface::setRxModeEnableWhenMCUSleep(void) // then latch with RTC hold so the state survives deep sleep. digitalWrite(LORA_PA_POWER, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); - if(fem_type==GC1109_PA) { + if (fem_type == GC1109_PA) { digitalWrite(LORA_GC1109_PA_EN, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); - } else if(fem_type==KCT8103L_PA) { + } else if (fem_type == KCT8103L_PA) { digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CSD); - if(lna_enabled) { + if (lna_enabled) { digitalWrite(LORA_KCT8103L_PA_CTX, LOW); } else { digitalWrite(LORA_KCT8103L_PA_CTX, HIGH); } rtc_gpio_hold_en((gpio_num_t)LORA_KCT8103L_PA_CTX); } +#elif defined(USE_GC1109_PA) + digitalWrite(LORA_PA_POWER, HIGH); + digitalWrite(LORA_GC1109_PA_EN, HIGH); +#if defined(ARCH_ESP32) + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + rtc_gpio_hold_en((gpio_num_t)LORA_GC1109_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_GC1109_PA_TX_EN); +#endif #endif } @@ -124,15 +155,15 @@ void LoRaFEMInterface::setLNAEnable(bool enabled) int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) { #ifdef HELTEC_V4 - const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; - const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10,9, 8, 7}; - uint16_t *tx_gain,tx_gain_num; - if(fem_type == GC1109_PA) { - tx_gain = (uint16_t*)gc1109_tx_gain; - tx_gain_num = sizeof(gc1109_tx_gain)/sizeof(gc1109_tx_gain[0]); - } else if(fem_type == KCT8103L_PA) { - tx_gain = (uint16_t*)kct8103l_tx_gain; - tx_gain_num = sizeof(kct8103l_tx_gain)/sizeof(kct8103l_tx_gain[0]); + const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; + const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7}; + uint16_t *tx_gain, tx_gain_num; + if (fem_type == GC1109_PA) { + tx_gain = (uint16_t *)gc1109_tx_gain; + tx_gain_num = sizeof(gc1109_tx_gain) / sizeof(gc1109_tx_gain[0]); + } else if (fem_type == KCT8103L_PA) { + tx_gain = (uint16_t *)kct8103l_tx_gain; + tx_gain_num = sizeof(kct8103l_tx_gain) / sizeof(kct8103l_tx_gain[0]); } else { return loraOutputPower; } @@ -140,9 +171,11 @@ int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) #ifdef ARCH_PORTDUINO size_t num_pa_points = portduino_config.num_pa_points; const uint16_t *tx_gain = portduino_config.tx_gain_lora; + uint16_t tx_gain_num = NUM_PA_POINTS; #else size_t num_pa_points = NUM_PA_POINTS; const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; + uint16_t tx_gain_num = NUM_PA_POINTS; #endif #endif for (int radio_dbm = 0; radio_dbm < tx_gain_num; radio_dbm++) { diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini index a5277ba196d..ebf0118bbed 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini +++ b/variants/esp32s3/heltec_wireless_tracker_v2/platformio.ini @@ -17,6 +17,7 @@ build_flags = ${esp32s3_base.build_flags} -I variants/esp32s3/heltec_wireless_tracker_v2 -D HELTEC_WIRELESS_TRACKER_V2 + -D HAS_LORA_FEM=1 lib_deps = ${esp32s3_base.lib_deps} # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX diff --git a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h index 0ca2dfc033f..7937039ba69 100644 --- a/variants/esp32s3/heltec_wireless_tracker_v2/variant.h +++ b/variants/esp32s3/heltec_wireless_tracker_v2/variant.h @@ -92,9 +92,9 @@ // CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 #define USE_GC1109_PA -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable -#define LORA_PA_EN 4 // CSD - GC1109 chip enable (HIGH=on) -#define LORA_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_GC1109_PA_EN 4 // CSD - GC1109 chip enable (HIGH=on) +#define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) // GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) // GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp From fc9651c7fc67cef837a0520ce048ae60c34dbda1 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Fri, 27 Feb 2026 16:18:45 +0800 Subject: [PATCH 04/14] Use trunk to fix formatting issues. --- src/graphics/draw/MenuHandler.cpp | 48 ++++++++++++++++------------ src/mesh/LoRaFEMInterface.h | 19 +++++------ src/mesh/SX126xInterface.cpp | 5 +-- variants/esp32s3/heltec_v4/variant.h | 6 ++-- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index ff92f291181..9871d0068e5 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -67,7 +67,14 @@ void menuHandler::loraMenu() { #if HAS_LORA_FEM static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region", "LoRa FEM LNA"}; - enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4, LoraFemLna = 5 }; + enum optionsNumbers { + Back = 0, + DeviceRolePicker = 1, + RadioPresetPicker = 2, + FrequencySlot = 3, + LoraPicker = 4, + LoraFemLna = 5 + }; #else static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; @@ -2654,27 +2661,28 @@ void menuHandler::LoRaFEMLNAToggleMenu() static std::array toggleLabels{}; BannerOverlayOptions bannerOptions; - if(loraFEMInterface.isLnaCanControl()) { - bannerOptions = createStaticBannerOptions("Toggle FEM LNA", femToggleOptions, toggleLabels, [](const LoRaFEMLNAToggleOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = LoraMenu; - screen->runNow(); - return; - } + if (loraFEMInterface.isLnaCanControl()) { + bannerOptions = createStaticBannerOptions("Toggle FEM LNA", femToggleOptions, toggleLabels, + [](const LoRaFEMLNAToggleOption &option, int) -> void { + if (option.action == OptionsAction::Back) { + menuQueue = LoraMenu; + screen->runNow(); + return; + } - if (!option.hasValue) { - return; - } + if (!option.hasValue) { + return; + } - if (config.lora.fem_lna_mode == option.value) { - return; - } + if (config.lora.fem_lna_mode == option.value) { + return; + } + + config.lora.fem_lna_mode = option.value; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); - config.lora.fem_lna_mode = option.value; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }); - int initialSelection = 0; for (size_t i = 0; i < toggleCount; ++i) { if (femToggleOptions[i].hasValue && config.lora.fem_lna_mode == femToggleOptions[i].value) { @@ -2685,7 +2693,7 @@ void menuHandler::LoRaFEMLNAToggleMenu() bannerOptions.InitialSelected = initialSelection; } else { static const char *optionsArray[] = {"Back"}; - enum optionsNumbers {Back = 0}; + enum optionsNumbers { Back = 0 }; // BannerOverlayOptions bannerOptions; bannerOptions.message = "LNA Control Unsupported"; diff --git a/src/mesh/LoRaFEMInterface.h b/src/mesh/LoRaFEMInterface.h index 5ea0e95736c..ef469b27900 100644 --- a/src/mesh/LoRaFEMInterface.h +++ b/src/mesh/LoRaFEMInterface.h @@ -1,20 +1,16 @@ #if HAS_LORA_FEM #pragma once -#include -#include "configuration.h" #include "NodeDB.h" +#include "configuration.h" +#include -typedef enum { - GC1109_PA, - KCT8103L_PA, - OTHER_FEM_TYPES -} LoRaFEMType; +typedef enum { GC1109_PA, KCT8103L_PA, OTHER_FEM_TYPES } LoRaFEMType; class LoRaFEMInterface { public: - LoRaFEMInterface(){ } - virtual ~LoRaFEMInterface(){ } + LoRaFEMInterface() {} + virtual ~LoRaFEMInterface() {} void init(void); void setSleepModeEnable(void); void setTxModeEnable(void); @@ -24,10 +20,11 @@ class LoRaFEMInterface int8_t powerConversion(int8_t loraOutputPower); bool isLnaCanControl(void) { return lna_can_control; } void setLnaCanControl(bool can_control) { lna_can_control = can_control; } + private: LoRaFEMType fem_type; - bool lna_enabled=false; - bool lna_can_control=false; + bool lna_enabled = false; + bool lna_can_control = false; }; extern LoRaFEMInterface loraFEMInterface; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index b96b966d582..2f94b0f8355 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -58,9 +58,10 @@ template bool SX126xInterface::init() #if HAS_LORA_FEM loraFEMInterface.init(); - if((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED) && loraFEMInterface.isLnaCanControl()) { + if ((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED) && loraFEMInterface.isLnaCanControl()) { loraFEMInterface.setLNAEnable(true); - } else if ((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED) && loraFEMInterface.isLnaCanControl()) { + } else if ((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED) && + loraFEMInterface.isLnaCanControl()) { loraFEMInterface.setLNAEnable(false); } #endif diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index b6a5b2777bc..c5fe874e7d8 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -51,12 +51,12 @@ // GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp // Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable #define LORA_GC1109_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) #define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) -#define LORA_KCT8103L_PA_CSD 2 -#define LORA_KCT8103L_PA_CTX 5 // enable tx +#define LORA_KCT8103L_PA_CSD 2 +#define LORA_KCT8103L_PA_CTX 5 // enable tx #if HAS_TFT #define USE_TFTDISPLAY 1 From 11162170d1a3dee232618d5bd189cc99c3b9391f Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 3 Mar 2026 16:49:35 +0800 Subject: [PATCH 05/14] Optimize the fem initialization control logic. --- src/mesh/LoRaFEMInterface.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp index e04ae9fd605..497f3df7100 100644 --- a/src/mesh/LoRaFEMInterface.cpp +++ b/src/mesh/LoRaFEMInterface.cpp @@ -11,20 +11,17 @@ void LoRaFEMInterface::init(void) { setLnaCanControl(false); // Default is uncontrollable #ifdef HELTEC_V4 - rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); - rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); - rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_TX_EN); - rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); - rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); - pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); delay(1); + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CSD); pinMode(LORA_KCT8103L_PA_CSD, INPUT); // detect which FEM is used delay(1); if (digitalRead(LORA_KCT8103L_PA_CSD) == HIGH) { // FEM is KCT8103L fem_type = KCT8103L_PA; + rtc_gpio_hold_dis((gpio_num_t)LORA_KCT8103L_PA_CTX); pinMode(LORA_KCT8103L_PA_CSD, OUTPUT); digitalWrite(LORA_KCT8103L_PA_CSD, HIGH); pinMode(LORA_KCT8103L_PA_CTX, OUTPUT); @@ -33,6 +30,8 @@ void LoRaFEMInterface::init(void) } else if (digitalRead(LORA_KCT8103L_PA_CSD) == LOW) { // FEM is GC1109 fem_type = GC1109_PA; + // LORA_GC1109_PA_EN and LORA_KCT8103L_PA_CSD are the same pin and do not need to be repeatedly turned off and held. + // rtc_gpio_hold_dis((gpio_num_t)LORA_GC1109_PA_EN); pinMode(LORA_GC1109_PA_EN, OUTPUT); digitalWrite(LORA_GC1109_PA_EN, HIGH); pinMode(LORA_GC1109_PA_TX_EN, OUTPUT); From 609694ea5d07c31fe0d7314caff2927fd38044be Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:33:35 +0800 Subject: [PATCH 06/14] Update src/mesh/RadioInterface.h change #ifdef to #if Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/RadioInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 3ed2bfde7f1..05825dce1c9 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -8,7 +8,7 @@ #include "error.h" #include -#ifdef HAS_LORA_FEM +#if HAS_LORA_FEM #include "LoRaFEMInterface.h" #endif From 09ba52784a4ed230ac965dded142186907a8e89d Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:01:23 +0800 Subject: [PATCH 07/14] Change LORA_PA_EN to LORA_GC1109_PA_EN. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/LoRaFEMInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp index 497f3df7100..a9f1f7c7b17 100644 --- a/src/mesh/LoRaFEMInterface.cpp +++ b/src/mesh/LoRaFEMInterface.cpp @@ -62,7 +62,7 @@ void LoRaFEMInterface::setSleepModeEnable(void) if (fem_type == GC1109_PA) { /* * Do not switch the power on and off frequently. - * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. + * After turning off LORA_GC1109_PA_EN, the power consumption has dropped to the uA level. */ digitalWrite(LORA_GC1109_PA_EN, LOW); digitalWrite(LORA_GC1109_PA_TX_EN, LOW); From f2720fa6c14c5f82e419091317631d5d2e192a41 Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:06:04 +0800 Subject: [PATCH 08/14] Remove the NodeDB.h include. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/LoRaFEMInterface.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mesh/LoRaFEMInterface.h b/src/mesh/LoRaFEMInterface.h index ef469b27900..82366a929c8 100644 --- a/src/mesh/LoRaFEMInterface.h +++ b/src/mesh/LoRaFEMInterface.h @@ -1,6 +1,5 @@ #if HAS_LORA_FEM #pragma once -#include "NodeDB.h" #include "configuration.h" #include From 5285c43c75e7612cb9116c02de1cc6b2cbbdafca Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:12:09 +0800 Subject: [PATCH 09/14] Change tx_gain to a const variable. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/LoRaFEMInterface.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp index a9f1f7c7b17..adb07641c6d 100644 --- a/src/mesh/LoRaFEMInterface.cpp +++ b/src/mesh/LoRaFEMInterface.cpp @@ -156,12 +156,13 @@ int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) #ifdef HELTEC_V4 const uint16_t gc1109_tx_gain[] = {11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7}; const uint16_t kct8103l_tx_gain[] = {13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7}; - uint16_t *tx_gain, tx_gain_num; + const uint16_t *tx_gain; + uint16_t tx_gain_num; if (fem_type == GC1109_PA) { - tx_gain = (uint16_t *)gc1109_tx_gain; + tx_gain = gc1109_tx_gain; tx_gain_num = sizeof(gc1109_tx_gain) / sizeof(gc1109_tx_gain[0]); } else if (fem_type == KCT8103L_PA) { - tx_gain = (uint16_t *)kct8103l_tx_gain; + tx_gain = kct8103l_tx_gain; tx_gain_num = sizeof(kct8103l_tx_gain) / sizeof(kct8103l_tx_gain[0]); } else { return loraOutputPower; From 81a5ef539a7068bb08cdfbb6135cfd56affac825 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 3 Mar 2026 18:17:34 +0800 Subject: [PATCH 10/14] Fixed the issue where ARCH_PORTDUINO lacked the NUM_PA_POINTS macro. --- src/mesh/LoRaFEMInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/LoRaFEMInterface.cpp b/src/mesh/LoRaFEMInterface.cpp index adb07641c6d..a1b56320e20 100644 --- a/src/mesh/LoRaFEMInterface.cpp +++ b/src/mesh/LoRaFEMInterface.cpp @@ -171,7 +171,7 @@ int8_t LoRaFEMInterface::powerConversion(int8_t loraOutputPower) #ifdef ARCH_PORTDUINO size_t num_pa_points = portduino_config.num_pa_points; const uint16_t *tx_gain = portduino_config.tx_gain_lora; - uint16_t tx_gain_num = NUM_PA_POINTS; + uint16_t tx_gain_num = num_pa_points; #else size_t num_pa_points = NUM_PA_POINTS; const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; From 020e95f870eeac82c2910bf5b2ba8c53592c7f84 Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:22:40 +0800 Subject: [PATCH 11/14] Remove the comment. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/graphics/draw/MenuHandler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 9871d0068e5..3a4880c60fd 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -2695,7 +2695,6 @@ void menuHandler::LoRaFEMLNAToggleMenu() static const char *optionsArray[] = {"Back"}; enum optionsNumbers { Back = 0 }; - // BannerOverlayOptions bannerOptions; bannerOptions.message = "LNA Control Unsupported"; bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 1; From c06afb3ba85a368a27a3cac91a6263af2f33da3f Mon Sep 17 00:00:00 2001 From: Quency-D Date: Tue, 3 Mar 2026 18:26:26 +0800 Subject: [PATCH 12/14] Move #pragma once to the first line. --- src/mesh/LoRaFEMInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/LoRaFEMInterface.h b/src/mesh/LoRaFEMInterface.h index 82366a929c8..0a7c810ef8b 100644 --- a/src/mesh/LoRaFEMInterface.h +++ b/src/mesh/LoRaFEMInterface.h @@ -1,5 +1,5 @@ -#if HAS_LORA_FEM #pragma once +#if HAS_LORA_FEM #include "configuration.h" #include From fc467bf3f75d58e763a40f9d80847cbf28f2f031 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 4 Mar 2026 10:30:51 +0800 Subject: [PATCH 13/14] Remove the FEM LNA control menu. --- src/graphics/draw/MenuHandler.cpp | 85 +------------------------------ src/graphics/draw/MenuHandler.h | 10 +--- src/mesh/SX126xInterface.cpp | 6 --- 3 files changed, 2 insertions(+), 99 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 3a4880c60fd..f57c3951250 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -65,24 +65,12 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { -#if HAS_LORA_FEM - static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region", "LoRa FEM LNA"}; - enum optionsNumbers { - Back = 0, - DeviceRolePicker = 1, - RadioPresetPicker = 2, - FrequencySlot = 3, - LoraPicker = 4, - LoraFemLna = 5 - }; -#else static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; -#endif BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = sizeof(optionsArray) / sizeof((optionsArray)[0]); + bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action @@ -95,11 +83,6 @@ void menuHandler::loraMenu() } else if (selected == LoraPicker) { menuHandler::menuQueue = menuHandler::LoraPicker; } -#if HAS_LORA_FEM - else if (selected == LoraFemLna) { - menuHandler::menuQueue = menuHandler::LoraFemLnaToggleMenu; - } -#endif }; screen->showOverlayBanner(bannerOptions); } @@ -2649,67 +2632,6 @@ void menuHandler::messageBubblesMenu() screen->showOverlayBanner(bannerOptions); } -#if HAS_LORA_FEM -void menuHandler::LoRaFEMLNAToggleMenu() -{ - static const LoRaFEMLNAToggleOption femToggleOptions[] = { - {"Back", OptionsAction::Back}, - {"Enabled", OptionsAction::Select, meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED}, - {"Disabled", OptionsAction::Select, meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED}, - }; - constexpr size_t toggleCount = sizeof(femToggleOptions) / sizeof(femToggleOptions[0]); - static std::array toggleLabels{}; - BannerOverlayOptions bannerOptions; - - if (loraFEMInterface.isLnaCanControl()) { - bannerOptions = createStaticBannerOptions("Toggle FEM LNA", femToggleOptions, toggleLabels, - [](const LoRaFEMLNAToggleOption &option, int) -> void { - if (option.action == OptionsAction::Back) { - menuQueue = LoraMenu; - screen->runNow(); - return; - } - - if (!option.hasValue) { - return; - } - - if (config.lora.fem_lna_mode == option.value) { - return; - } - - config.lora.fem_lna_mode = option.value; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - }); - - int initialSelection = 0; - for (size_t i = 0; i < toggleCount; ++i) { - if (femToggleOptions[i].hasValue && config.lora.fem_lna_mode == femToggleOptions[i].value) { - initialSelection = static_cast(i); - break; - } - } - bannerOptions.InitialSelected = initialSelection; - } else { - static const char *optionsArray[] = {"Back"}; - enum optionsNumbers { Back = 0 }; - - bannerOptions.message = "LNA Control Unsupported"; - bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 1; - bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == Back) { - // No action - } - }; - LOG_INFO("LNA cannot be disabled on this device"); - } - - screen->showOverlayBanner(bannerOptions); -} -#endif - void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != MenuNone) @@ -2860,11 +2782,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case MessageBubblesMenu: messageBubblesMenu(); break; -#if HAS_LORA_FEM - case LoraFemLnaToggleMenu: - LoRaFEMLNAToggleMenu(); - break; -#endif } menuQueue = MenuNone; } diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1c147ce0115..4a0360412dd 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -55,8 +55,7 @@ class menuHandler NodeNameLengthMenu, FrameToggles, DisplayUnits, - MessageBubblesMenu, - LoraFemLnaToggleMenu + MessageBubblesMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu @@ -112,9 +111,6 @@ class menuHandler static void displayUnitsMenu(); static void messageBubblesMenu(); static void textMessageMenu(); -#if HAS_LORA_FEM - static void LoRaFEMLNAToggleMenu(); -#endif private: static void saveUIConfig(); @@ -164,9 +160,5 @@ using PositionMenuOption = MenuOption; using ManageNodeOption = MenuOption; using ClockFaceOption = MenuOption; -#if HAS_LORA_FEM -using LoRaFEMLNAToggleOption = MenuOption; -#endif - } // namespace graphics #endif diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index e87e00f084a..5c9ab35979c 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -58,12 +58,6 @@ template bool SX126xInterface::init() #if HAS_LORA_FEM loraFEMInterface.init(); - if ((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED) && loraFEMInterface.isLnaCanControl()) { - loraFEMInterface.setLNAEnable(true); - } else if ((config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED) && - loraFEMInterface.isLnaCanControl()) { - loraFEMInterface.setLNAEnable(false); - } #endif #ifdef RF95_FAN_EN From 5464984907bde5515e0f1d20b3d3f5f5bd9e00d6 Mon Sep 17 00:00:00 2001 From: Quency-D Date: Wed, 4 Mar 2026 11:03:39 +0800 Subject: [PATCH 14/14] Add description for KCT8103L. --- variants/esp32s3/heltec_v4/variant.h | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h index c5fe874e7d8..8843f75c9fb 100644 --- a/variants/esp32s3/heltec_v4/variant.h +++ b/variants/esp32s3/heltec_v4/variant.h @@ -30,8 +30,8 @@ #define SX126X_DIO3_TCXO_VOLTAGE 1.8 // ---- GC1109 RF FRONT END CONFIGURATION ---- -// The Heltec V4 uses a GC1109 FEM chip with integrated PA and LNA -// RF path: SX1262 -> GC1109 PA -> Pi attenuator -> Antenna +// The Heltec V4.2 uses a GC1109 FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> GC1109 PA -> Antenna // Measured net TX gain (non-linear due to PA compression): // +11dB at 0-15dBm input (e.g., 10dBm in -> 21dBm out) // +10dB at 16-17dBm input @@ -48,15 +48,29 @@ // CPS (pin 5) -> GPIO46: PA mode select (HIGH=full PA, LOW=bypass) // VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 // GC1109 FEM: TX/RX path switching is handled by DIO2 -> CTX pin (via SX126X_DIO2_AS_RF_SWITCH) -// GPIO46 is CPS (PA mode), not TX control - setTransmitEnable() handles it in SX126xInterface.cpp // Do NOT use SX126X_TXEN/RXEN as that would cause double-control of GPIO46 -#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 LDO power enable +#define LORA_PA_POWER 7 // VFEM_Ctrl - GC1109 and KCT8103L LDO power enable #define LORA_GC1109_PA_EN 2 // CSD - GC1109 chip enable (HIGH=on) #define LORA_GC1109_PA_TX_EN 46 // CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) -#define LORA_KCT8103L_PA_CSD 2 -#define LORA_KCT8103L_PA_CTX 5 // enable tx +// ---- KCT8103L RF FRONT END CONFIGURATION ---- +// The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA +// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna +// Control logic (from KCT8103L datasheet): +// Transmit PA: CSD=1, CTX=1, CPS=1 +// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF) +// Receive bypass: CSD=1, CTX=1, CPS=0 +// Shutdown: CSD=0, CTX=X, CPS=X +// Pin mapping: +// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH) +// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown) +// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=bypass PA, LOW=LNA) +// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7 +// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH) + +#define LORA_KCT8103L_PA_CSD 2 // CSD - KCT8103L chip enable (HIGH=on) +#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=bypass PA, LOW=LNA) #if HAS_TFT #define USE_TFTDISPLAY 1