From e09ea274e06f6cf404bf9d7bad32a9cf83a1741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 9 Jul 2026 12:55:24 +0200 Subject: [PATCH] Add RadioMaster Nomad Gemini support --- src/input/InputBroker.cpp | 2 + src/main.cpp | 32 +++++- src/main.h | 2 + src/mesh/LR11x0Interface.cpp | 9 ++ src/mesh/RF95Interface.cpp | 9 +- src/mesh/RadioExternalPa.cpp | 18 ++++ src/mesh/RadioExternalPa.h | 46 +++++++++ src/mesh/RadioInterface.cpp | 57 ++++++----- src/mesh/RadioLibInterface.cpp | 33 +++++++ src/mesh/RadioLibInterface.h | 3 + src/mesh/SX126xInterface.cpp | 5 +- src/mesh/SX128xInterface.cpp | 5 +- src/modules/AdminModule.cpp | 14 ++- .../radiomaster_nomad_gemini/variant.cpp | 97 +++++++++++++++++++ .../esp32/betafpv_2400_tx_micro/variant.h | 2 +- .../esp32/radiomaster_900_bandit/variant.h | 3 +- .../radiomaster_900_bandit_nano/variant.h | 2 +- .../radiomaster_nomad_gemini/platformio.ini | 20 ++++ .../esp32/radiomaster_nomad_gemini/rfswitch.h | 56 +++++++++++ .../esp32/radiomaster_nomad_gemini/variant.h | 68 +++++++++++++ variants/esp32s3/t-beam-1w/variant.h | 2 +- 21 files changed, 436 insertions(+), 49 deletions(-) create mode 100644 src/mesh/RadioExternalPa.cpp create mode 100644 src/mesh/RadioExternalPa.h create mode 100644 src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp create mode 100644 variants/esp32/radiomaster_nomad_gemini/platformio.ini create mode 100644 variants/esp32/radiomaster_nomad_gemini/rfswitch.h create mode 100644 variants/esp32/radiomaster_nomad_gemini/variant.h diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index 7b0f830c59..21ae505280 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -463,8 +463,10 @@ void InputBroker::Init() rotaryEncoderInterruptImpl1 = nullptr; } #endif +#if !MESHTASTIC_EXCLUDE_I2C cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl->init(); +#endif #if defined(M5STACK_UNITC6L) i2cButton = new i2cButtonThread("i2cButtonThread"); #endif diff --git a/src/main.cpp b/src/main.cpp index df87cfcdeb..c63f0e7dc9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -273,6 +273,8 @@ bool pauseBluetoothLogging = false; bool pmu_found; +uint8_t pa_fan_percentage = 50; + #if !MESHTASTIC_EXCLUDE_I2C // Array map of sensor types with i2c address and wire as we'll find in the i2c scan std::pair nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1] = {}; @@ -1180,16 +1182,36 @@ void setup() mqttInit(); #endif -#ifdef RF95_FAN_EN - // Ability to disable FAN if PIN has been set with RF95_FAN_EN. +#ifdef RADIO_FAN_EN + // Ability to disable FAN if PIN has been set with RADIO_FAN_EN. // Make sure LoRa has been started before disabling FAN. - if (config.lora.pa_fan_disabled) - digitalWrite(RF95_FAN_EN, LOW ^ 0); +#ifdef RADIO_FAN_PWM +#if defined(ARCH_ESP32) + // Set up PWM at Channel 1 at 25KHz, using 8-bit resolution + // Turn ON/OFF fan to the specified value if enabled by config. + // code by https://github.com/gjelsoe/ + if (ledcSetup(1, 25000, 8)) { + ledcAttachPin(RADIO_FAN_EN, 1); + LOG_INFO("PWM init C1 P%d\n", RADIO_FAN_EN); + // Set PWM duty cycle based on fan disabled state + ledcWrite(1, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55)); + } else { + LOG_WARN("PWM init fail P%d\n", RADIO_FAN_EN); + } +#elif defined(ARCH_NRF52) + pinMode(RADIO_FAN_EN, OUTPUT); + analogWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55)); +#endif +#else + // Set up as ON/OFF switch of fan; default on unless disabled by config. + pinMode(RADIO_FAN_EN, OUTPUT); + digitalWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? (LOW ^ 0) : (HIGH ^ 0)); +#endif #endif #ifndef ARCH_PORTDUINO - // Initialize Wifi + // Initialize Wifi #if HAS_WIFI initWifi(); #endif diff --git a/src/main.h b/src/main.h index 19b1bace0d..08d99141f4 100644 --- a/src/main.h +++ b/src/main.h @@ -108,6 +108,8 @@ extern volatile bool lockdownDisablePending; extern uint32_t serialSinceMsec; +extern uint8_t pa_fan_percentage; + // If a thread does something that might need for it to be rescheduled ASAP it can set this flag // This will suppress the current delay and instead try to run ASAP. extern bool runASAP; diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 8be0b64139..4bf5488951 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -1,8 +1,10 @@ #if RADIOLIB_EXCLUDE_LR11X0 != 1 #include "LR11x0Interface.h" +#include "RadioExternalPa.h" #include "Throttle.h" #include "configuration.h" #include "error.h" +#include "main.h" #include "mesh/NodeDB.h" // A variant may define LR11X0_UPDATE_FIRMWARE_TO to a Semtech transceiver firmware version (e.g. 0x0402) to @@ -89,6 +91,8 @@ template bool LR11x0Interface::init() digitalWrite(LR11X0_POWER_EN, HIGH); #endif + enableFan(); + // An explicit Vref always wins; TCXO_OPTIONAL only supplies a default for boards that declare a // TCXO may be fitted without saying at what voltage. Both may appear in the same variant file. #if ARCH_PORTDUINO @@ -358,6 +362,7 @@ template void LR11x0Interface::addReceiveMetadata(meshtastic_Mes */ template void LR11x0Interface::configHardwareForSend() { + radioExternalPaTxEnable(); // bias an external PA (if any) before we transmit RadioLibInterface::configHardwareForSend(); } @@ -372,6 +377,8 @@ template void LR11x0Interface::startReceive() setStandby(); + radioExternalPaRxIdle(); // drop external PA bias while receiving/idle + lora.setPreambleLength(preambleLength); // Solve RX ack fail after direct message sent. Not sure why this is needed. // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. @@ -459,6 +466,8 @@ template bool LR11x0Interface::sleep() LOG_DEBUG("LR11x0 entering sleep mode"); setStandby(); // Stop any pending operations + radioExternalPaSleep(); // power down an external PA (if any) + // turn off TCXO if it was powered lora.setTCXO(0); diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 909d47e23e..37c0af8f4d 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -160,10 +160,7 @@ bool RF95Interface::init() digitalWrite(RF95_TXEN, 0); #endif -#ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, 1); -#endif + enableFan(); #ifdef RF95_RXEN pinMode(RF95_RXEN, OUTPUT); @@ -346,9 +343,7 @@ bool RF95Interface::sleep() digitalWrite(RF95_POWER_EN, LOW); #endif -#ifdef RF95_FAN_EN - digitalWrite(RF95_FAN_EN, 0); -#endif + disableFan(); return true; } diff --git a/src/mesh/RadioExternalPa.cpp b/src/mesh/RadioExternalPa.cpp new file mode 100644 index 0000000000..a31aca75f6 --- /dev/null +++ b/src/mesh/RadioExternalPa.cpp @@ -0,0 +1,18 @@ +#include "RadioExternalPa.h" + +// Weak default implementations: no external PA. A board with an external PA +// provides strong overrides (see e.g. +// src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp). + +int8_t __attribute__((weak)) radioExternalPaMapPower(int8_t requestedTotalDbm, float freqHz) +{ + (void)freqHz; + (void)requestedTotalDbm; + return RADIO_EXTERNAL_PA_NO_MAP; +} + +void __attribute__((weak)) radioExternalPaTxEnable() {} + +void __attribute__((weak)) radioExternalPaRxIdle() {} + +void __attribute__((weak)) radioExternalPaSleep() {} diff --git a/src/mesh/RadioExternalPa.h b/src/mesh/RadioExternalPa.h new file mode 100644 index 0000000000..e0a7d5b65c --- /dev/null +++ b/src/mesh/RadioExternalPa.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +/** + * Generic hooks for boards with an external power amplifier whose gain/bias is + * controlled outside the LoRa transceiver (e.g. an analog PA biased through a DAC + * pin, as on the RadioMaster Nomad Gemini). + * + * All four functions have weak no-op / pass-through defaults (see RadioExternalPa.cpp), + * so boards without an external PA are completely unaffected. A board provides an + * external PA by giving STRONG overrides (typically in its + * src/platform/extra_variants//variant.cpp). + * + * Relationship to the existing TX_GAIN_LORA / LoRaFEMInterface mechanisms: + * - TX_GAIN_LORA assumes the chip output index is a non-negative dBm value. + * - LoRaFEMInterface models *digital* front-end modules (discrete enable pins). + * An analog PA that needs the transceiver driven at negative dBm fits neither, so it + * is handled here instead. + */ + +/// Sentinel returned by radioExternalPaMapPower() when the board has no external PA. +#define RADIO_EXTERNAL_PA_NO_MAP INT8_MIN + +/** + * Map a desired *total* radiated output power to the transceiver chip output power. + * + * Called from RadioInterface::limitPower() AFTER regional/regulatory clamping, so + * @p requestedTotalDbm is already the legal total output we want out of the antenna. + * The override returns the chip output power (dBm, may be negative) that, combined + * with the external PA gain, yields that total, and configures/stashes the PA bias. + * + * @param requestedTotalDbm desired total output power in dBm (already region-limited) + * @param freqHz operating frequency in Hz (for band-dependent PAs) + * @return chip output power in dBm, or RADIO_EXTERNAL_PA_NO_MAP if no external PA + */ +int8_t radioExternalPaMapPower(int8_t requestedTotalDbm, float freqHz); + +/// Engage the external PA bias for transmit (called just before a transmission). +void radioExternalPaTxEnable(); + +/// Drop the external PA bias for receive/idle (called when entering RX/standby). +void radioExternalPaRxIdle(); + +/// Power the external PA fully down (called when the radio goes to sleep). +void radioExternalPaSleep(); diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 58bd498c58..edaf13db84 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -10,6 +10,7 @@ #include "MeshService.h" #include "NodeDB.h" #include "RF95Interface.h" +#include "RadioExternalPa.h" #include "Router.h" #include "SX1262Interface.h" #include "SX1268Interface.h" @@ -1426,38 +1427,50 @@ void RadioInterface::limitPower(int8_t loraMaxPower) power = maxPower; } + // Boards with an external PA controlled outside the transceiver (e.g. an analog PA + // biased through a DAC pin) map the desired total output to a possibly-negative chip + // output power here. This is applied even for licensed operators, because it reflects + // a hardware gain stage, not a regulatory limit (the regulatory clamp above already + // honors the license). Pass-through by default; see RadioExternalPa.h. + int8_t externalPaChip = radioExternalPaMapPower(power, getFreq()); + if (externalPaChip != RADIO_EXTERNAL_PA_NO_MAP) { + LOG_INFO("External PA: %d dBm total -> chip %d dBm", power, externalPaChip); + power = externalPaChip; + } else { #if HAS_LORA_FEM - if (!devicestate.owner.is_licensed) { - power = loraFEMInterface.powerConversion(power); - } + 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; + 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}; + size_t num_pa_points = NUM_PA_POINTS; + const uint16_t tx_gain[NUM_PA_POINTS] = {TX_GAIN_LORA}; #endif - if (num_pa_points == 1) { - if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) { - LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]); - power -= tx_gain[0]; - } - } else if (!devicestate.owner.is_licensed) { - // we have an array of PA gain values. Find the highest power setting that works. - for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) { - if (((radio_dbm + tx_gain[radio_dbm]) > power) || - ((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { - // 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", power, tx_gain[radio_dbm]); - power -= tx_gain[radio_dbm]; - break; + if (num_pa_points == 1) { + if (tx_gain[0] > 0 && !devicestate.owner.is_licensed) { + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, tx_gain[0]); + power -= tx_gain[0]; + } + } else if (!devicestate.owner.is_licensed) { + // we have an array of PA gain values. Find the highest power setting that works. + for (int radio_dbm = 0; radio_dbm < (int)num_pa_points; radio_dbm++) { + if (((radio_dbm + tx_gain[radio_dbm]) > power) || + ((radio_dbm == (int)(num_pa_points - 1)) && ((radio_dbm + tx_gain[radio_dbm]) <= power))) { + // 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", power, tx_gain[radio_dbm]); + power -= tx_gain[radio_dbm]; + break; + } } } - } #endif + } + if (power > loraMaxPower) // Clamp power to maximum defined level power = loraMaxPower; diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 195a5738a0..ca5d1310f6 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -807,3 +807,36 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp) return res == RADIOLIB_ERR_NONE; } } + +void RadioLibInterface::enableFan() +{ +#ifdef RADIO_FAN_EN + +#ifdef RADIO_FAN_PWM +#if defined(ARCH_ESP32) + ledcWrite(1, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55)); +#elif defined(ARCH_NRF52) + analogWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55)); +#endif +#else + pinMode(RADIO_FAN_EN, OUTPUT); + digitalWrite(RADIO_FAN_EN, config.lora.pa_fan_disabled ? 0 : 1); +#endif +#endif +} + +void RadioLibInterface::disableFan() +{ +#ifdef RADIO_FAN_EN +#ifdef RADIO_FAN_PWM +#if defined(ARCH_ESP32) + ledcWrite(1, 0); +#elif defined(ARCH_NRF52) + analogWrite(RADIO_FAN_EN, 0); +#endif +#else + pinMode(RADIO_FAN_EN, OUTPUT); + digitalWrite(RADIO_FAN_EN, 0); +#endif +#endif +} diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 0142721789..e1e87d905f 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -227,6 +227,9 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */ virtual bool findInTxQueue(NodeNum from, PacketId id) override; + void enableFan(); + void disableFan(); + /** * Update the noise floor measurement by sampling RSSI from a slow path. * This should not be called from radio interrupt or TX/RX critical paths. diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 2400a8e03f..1a8adf9173 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -64,10 +64,7 @@ template bool SX126xInterface::init() } #endif -#ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, HIGH); -#endif + enableFan(); #if ARCH_PORTDUINO tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index bb1d890247..b52cf86696 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -35,10 +35,7 @@ template bool SX128xInterface::init() digitalWrite(SX128X_POWER_EN, HIGH); #endif -#ifdef RF95_FAN_EN - pinMode(RF95_FAN_EN, OUTPUT); - digitalWrite(RF95_FAN_EN, 1); -#endif + enableFan(); #if ARCH_PORTDUINO if (portduino_config.lora_rxen_pin.pin != RADIOLIB_NC) { diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 80bb799036..165df561fb 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1122,14 +1122,22 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) } #endif -#ifdef RF95_FAN_EN +#ifdef RADIO_FAN_EN +#ifdef RADIO_FAN_PWM +#if defined(ARCH_ESP32) + ledcWrite(1, c.payload_variant.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55)); +#elif defined(ARCH_NRF52) + analogWrite(RADIO_FAN_EN, c.payload_variant.lora.pa_fan_disabled ? 0 : (pa_fan_percentage * 2.55)); +#endif +#else // Turn PA off if disabled by config if (c.payload_variant.lora.pa_fan_disabled) { - digitalWrite(RF95_FAN_EN, LOW ^ 0); + digitalWrite(RADIO_FAN_EN, LOW ^ 0); } else { - digitalWrite(RF95_FAN_EN, HIGH ^ 0); + digitalWrite(RADIO_FAN_EN, HIGH ^ 0); } #endif +#endif #if HAS_LORA_FEM // Apply FEM LNA mode from config (only meaningful on hardware that supports it) diff --git a/src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp b/src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp new file mode 100644 index 0000000000..5a6155e98a --- /dev/null +++ b/src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp @@ -0,0 +1,97 @@ +#include "configuration.h" + +#ifdef RADIOMASTER_NOMAD_GEMINI + +#include "RadioExternalPa.h" +#include + +/** + * RadioMaster Nomad Gemini external analog PA driver. + * + * The Nomad's PA is biased by an analog control voltage on the APC2 pin + * (RADIO_PA_APC2_PIN = GPIO26 = ESP32 DAC channel 2), driven with dacWrite() + * (0-255 -> 0-3.3V). The DAC is essentially a fixed bias; the actual output power + * is set by driving the LR1121 chip at a (negative) output power and letting the PA + * add ~25-31 dB of gain. + * + * The calibration below is derived from the ExpressLRS hardware target + * "TX/Radiomaster Nomad.json" (power_values = APC2 DAC codes, power_values2 = chip + * output dBm). These values reproduce ELRS behaviour but have NOT been validated on + * a bench against a power meter for Meshtastic — verify before trusting at 1 W, both + * for PA safety and for regulatory compliance. + * + * TODO: dual-band (Gemini) operation uses ELRS power_values_dual and a second radio; + * once JANUS_RADIO is implemented, branch on freqHz / active radio here. + */ + +// total dBm corresponds to the ELRS power presets 10/25/50/100/250/500/1000 mW. +static const struct { + int8_t totalDbm; // desired total radiated power + int8_t chipDbm; // LR1121 output power register (ELRS power_values2) + uint8_t apc2; // APC2 DAC code (ELRS power_values) +} NOMAD_PA_CAL[] = { + {10, -17, 120}, // 10 mW + {14, -16, 120}, // 25 mW + {17, -14, 120}, // 50 mW + {20, -11, 120}, // 100 mW + {24, -7, 120}, // 250 mW + {27, -3, 120}, // 500 mW + {30, 5, 95}, // 1000 mW (1 W) +}; +static const size_t NOMAD_PA_CAL_N = sizeof(NOMAD_PA_CAL) / sizeof(NOMAD_PA_CAL[0]); + +// APC2 bias chosen for the currently-configured power level; applied on TX, dropped +// to 0 for RX/idle/sleep so the PA only draws current while transmitting. +static uint8_t nomadApc2Level = 0; + +// Runs at the start of setup(), before the LoRa radio is initialized. +void earlyInitVariant() +{ + dacWrite(RADIO_PA_APC2_PIN, 0); // bias the PA off until we actually transmit + nomadApc2Level = 0; +} + +int8_t radioExternalPaMapPower(int8_t requestedTotalDbm, float freqHz) +{ + (void)freqHz; // single-band for now (see dual-band TODO above) + + if (requestedTotalDbm <= NOMAD_PA_CAL[0].totalDbm) { + nomadApc2Level = NOMAD_PA_CAL[0].apc2; + return NOMAD_PA_CAL[0].chipDbm; + } + const size_t last = NOMAD_PA_CAL_N - 1; + if (requestedTotalDbm >= NOMAD_PA_CAL[last].totalDbm) { + nomadApc2Level = NOMAD_PA_CAL[last].apc2; + return NOMAD_PA_CAL[last].chipDbm; + } + + // Piecewise-linear interpolation of chip dBm between the bracketing points. + for (size_t i = 1; i < NOMAD_PA_CAL_N; i++) { + if (requestedTotalDbm <= NOMAD_PA_CAL[i].totalDbm) { + const int t0 = NOMAD_PA_CAL[i - 1].totalDbm, t1 = NOMAD_PA_CAL[i].totalDbm; + const int c0 = NOMAD_PA_CAL[i - 1].chipDbm, c1 = NOMAD_PA_CAL[i].chipDbm; + const int chip = c0 + ((c1 - c0) * (requestedTotalDbm - t0) + (t1 - t0) / 2) / (t1 - t0); // round-to-nearest + nomadApc2Level = NOMAD_PA_CAL[i - 1].apc2; // only the top point lowers the bias + return (int8_t)chip; + } + } + nomadApc2Level = NOMAD_PA_CAL[last].apc2; // unreachable + return NOMAD_PA_CAL[last].chipDbm; +} + +void radioExternalPaTxEnable() +{ + dacWrite(RADIO_PA_APC2_PIN, nomadApc2Level); +} + +void radioExternalPaRxIdle() +{ + dacWrite(RADIO_PA_APC2_PIN, 0); +} + +void radioExternalPaSleep() +{ + dacWrite(RADIO_PA_APC2_PIN, 0); +} + +#endif // RADIOMASTER_NOMAD_GEMINI diff --git a/variants/esp32/betafpv_2400_tx_micro/variant.h b/variants/esp32/betafpv_2400_tx_micro/variant.h index 8e875a3e66..9c7aa5a7d5 100644 --- a/variants/esp32/betafpv_2400_tx_micro/variant.h +++ b/variants/esp32/betafpv_2400_tx_micro/variant.h @@ -12,7 +12,7 @@ #define LORA_MISO 19 #define LORA_MOSI 23 #define LORA_CS 5 -#define RF95_FAN_EN 17 +#define RADIO_FAN_EN 17 // This is a LED_WS2812 not a standard LED #define HAS_NEOPIXEL // Enable the use of neopixels diff --git a/variants/esp32/radiomaster_900_bandit/variant.h b/variants/esp32/radiomaster_900_bandit/variant.h index 0c7417cacd..a84a1ca4ad 100644 --- a/variants/esp32/radiomaster_900_bandit/variant.h +++ b/variants/esp32/radiomaster_900_bandit/variant.h @@ -46,7 +46,8 @@ FAN is active at 250mW on it's ExpressLRS Firmware. This FAN has TACHO signal on Pin 27 for use with PWM. */ -#define RF95_FAN_EN 2 +#define RADIO_FAN_EN 2 +#define RADIO_FAN_PWM /* LED PIN setup and it has a NeoPixel LED. diff --git a/variants/esp32/radiomaster_900_bandit_nano/variant.h b/variants/esp32/radiomaster_900_bandit_nano/variant.h index 318401f92d..a5145f2ab2 100644 --- a/variants/esp32/radiomaster_900_bandit_nano/variant.h +++ b/variants/esp32/radiomaster_900_bandit_nano/variant.h @@ -32,7 +32,7 @@ This unit has a FAN built-in. FAN is active at 250mW on it's ExpressLRS Firmware. */ -#define RF95_FAN_EN 2 +#define RADIO_FAN_EN 2 /* LED PIN setup. diff --git a/variants/esp32/radiomaster_nomad_gemini/platformio.ini b/variants/esp32/radiomaster_nomad_gemini/platformio.ini new file mode 100644 index 0000000000..c120c9444b --- /dev/null +++ b/variants/esp32/radiomaster_nomad_gemini/platformio.ini @@ -0,0 +1,20 @@ +[env:radiomaster_nomad_gemini] +extends = esp32_base +board = esp32doit-devkit-v1 +build_flags = + ${esp32_base.build_flags} + -DRADIOMASTER_NOMAD_GEMINI + -DPRIVATE_HW + -DVTABLES_IN_FLASH=1 + -DCONFIG_DISABLE_HAL_LOCKS=1 + -O2 + -Ivariants/esp32/radiomaster_nomad_gemini + -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -DMESHTASTIC_EXCLUDE_I2C=1 + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_SX126X=1 +board_build.f_cpu = 240000000L +upload_protocol = esptool +lib_deps = + ${esp32_base.lib_deps} diff --git a/variants/esp32/radiomaster_nomad_gemini/rfswitch.h b/variants/esp32/radiomaster_nomad_gemini/rfswitch.h new file mode 100644 index 0000000000..8c2a56a874 --- /dev/null +++ b/variants/esp32/radiomaster_nomad_gemini/rfswitch.h @@ -0,0 +1,56 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_LR11X0_DIO7, + RADIOLIB_LR11X0_DIO8, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 DIO7 DIO8 + {LR11x0::MODE_STBY, {LOW, LOW, LOW, LOW}}, {LR11x0::MODE_RX, {LOW, LOW, HIGH, LOW}}, + {LR11x0::MODE_TX, {LOW, LOW, LOW, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, LOW, LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, HIGH, LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW, LOW, LOW}}, + {LR11x0::MODE_WIFI, {HIGH, LOW, LOW, LOW}}, END_OF_MODE_TABLE, +}; + +/* + +DIO5: RXEN 2.4GHz +DIO6: TXEN 2.4GHz +DIO7: RXEN 900MHz +DIO8: TXEN 900MHz + + + "radio_dcdc": true, + "radio_rfo_hf": true, + + "power_apc2": 26, + "power_min": 0, + "power_high": 6, + "power_max": 6, + "power_default": 3, + "power_control": 3, POWER_OUTPUT_DACWRITE // use internal dacWrite function to set value on GPIO_PIN_RFamp_APC2 + [0, 1, 2, 3, 4, 5, 6 ] // 0-6 + "power_values": [120, 120, 120, 120, 120, 120, 95] // DAC Value + "power_values2": [-17, -16, -14, -11, -7, -3, 5 ] // 900M + "power_values_dual": [-18, -14, -8, -6, -2, 3, 5 ] // 2.4G + + // default value 0 means direct! +#define POWER_OUTPUT_DACWRITE (hardware_int(HARDWARE_power_control)==3) +#define POWER_OUTPUT_VALUES hardware_i16_array(HARDWARE_power_values) +#define POWER_OUTPUT_VALUES_COUNT hardware_int(HARDWARE_power_values_count) +#define POWER_OUTPUT_VALUES2 hardware_i16_array(HARDWARE_power_values2) +#define POWER_OUTPUT_VALUES_DUAL hardware_i16_array(HARDWARE_power_values_dual) +#define POWER_OUTPUT_VALUES_DUAL_COUNT hardware_int(HARDWARE_power_values_dual_count) + +#define GPIO_PIN_FAN_EN hardware_pin(HARDWARE_misc_fan_en) + + case PWR_10mW: return 10; + case PWR_25mW: return 14; + case PWR_50mW: return 17; + case PWR_100mW: return 20; + case PWR_250mW: return 24; + case PWR_500mW: return 27; + case PWR_1000mW: return 30; + + 95 -> +25dBm + 120 -> +24dBm +*/ \ No newline at end of file diff --git a/variants/esp32/radiomaster_nomad_gemini/variant.h b/variants/esp32/radiomaster_nomad_gemini/variant.h new file mode 100644 index 0000000000..a11af2d610 --- /dev/null +++ b/variants/esp32/radiomaster_nomad_gemini/variant.h @@ -0,0 +1,68 @@ +#define HAS_SCREEN 0 +#define HAS_WIRE 0 +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define PIN_SPI_MISO 33 +#define PIN_SPI_MOSI 32 +#define PIN_SPI_SCK 25 +#define PIN_SPI_NSS 27 + +#define LORA_RESET 15 +#define LORA_DIO1 37 +#define LORA_DIO2 36 +#define LORA_SCK PIN_SPI_SCK +#define LORA_MISO PIN_SPI_MISO +#define LORA_MOSI PIN_SPI_MOSI +#define LORA_CS PIN_SPI_NSS + +// supported modules list +#define USE_LR1121 + +#define LR1121_IRQ_PIN LORA_DIO1 +#define LR1121_NRESET_PIN LORA_RESET +#define LR1121_BUSY_PIN LORA_DIO2 +#define LR1121_SPI_NSS_PIN LORA_CS +#define LR1121_SPI_SCK_PIN LORA_SCK +#define LR1121_SPI_MOSI_PIN LORA_MOSI +#define LR1121_SPI_MISO_PIN LORA_MISO + +// Caps for the LR1121 *chip* output. The external analog PA (driven via the APC2 +// DAC pin, see RADIO_PA_APC2_PIN below) provides the rest of the gain, so the chip +// itself is driven low. See src/platform/extra_variants/radiomaster_nomad_gemini. +#define LR1110_MAX_POWER 5 +// 2.4G Part +#define LR1120_MAX_POWER 5 + +// not yet implemented +#define JANUS_RADIO +#define LR1121_IRQ2_PIN 34 +#define LR1121_NRESET2_PIN 21 +#define LR1121_BUSY2_PIN 39 +#define LR1121_SPI_NSS2_PIN 13 + +// TODO: check if this is correct +// #define LR11X0_DIO3_TCXO_VOLTAGE 1.6 +#define LR11X0_DIO_AS_RF_SWITCH + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 2 // How many neopixels are connected +#define NEOPIXEL_DATA 22 // GPIO pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // Type of neopixels in use +#define ENABLE_AMBIENTLIGHTING // Turn on Ambient Lighting + +// Primary button is GPIO14 (ELRS "button"); GPIO12 is the second button ("button2"). +// NOTE: GPIO34 is the second radio's DIO1 (LR1121_IRQ2_PIN) and is input-only with no +// internal pull resistor, so it must NOT be reused as a button. +#define BUTTON_PIN 14 +#define BUTTON_NEED_PULLUP + +#undef EXT_NOTIFY_OUT + +#define RADIO_FAN_EN 2 + +// Analog PA bias control (ELRS APC2). GPIO26 = ESP32 DAC channel 2, driven with +// dacWrite() (0-255 -> 0-3.3V). Defining this enables the external-PA driver in +// src/platform/extra_variants/radiomaster_nomad_gemini/variant.cpp. +#define RADIO_PA_APC2_PIN 26 diff --git a/variants/esp32s3/t-beam-1w/variant.h b/variants/esp32s3/t-beam-1w/variant.h index 673b7c4c01..ea98651f3d 100644 --- a/variants/esp32s3/t-beam-1w/variant.h +++ b/variants/esp32s3/t-beam-1w/variant.h @@ -84,7 +84,7 @@ // Fan control #define FAN_CTRL_PIN 41 // Meshtastic standard fan control pin macro -#define RF95_FAN_EN FAN_CTRL_PIN +#define RADIO_FAN_EN FAN_CTRL_PIN // PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) // Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U