Compare commits

..

29 Commits

Author SHA1 Message Date
Thomas Göttgens ce23bcf91f Merge branch 'master' into nomad-gemini 2026-06-07 10:54:37 +02:00
Thomas Göttgens d3d606c1d5 Nomad fixes: honor pa_fan_disabled, drop RADIOLIB_LOW_LEVEL, relocate variant
Non-PWM fan paths (main.cpp setup + enableFan) now respect pa_fan_disabled instead of forcing the fan off/on. Remove the unneeded global RADIOLIB_LOW_LEVEL build flag (no code requires it; Module low-level methods are already public). Move the variant to variants/esp32/radiomaster_nomad_gemini/ for layout consistency with sibling boards.
2026-06-07 10:53:41 +02:00
Thomas Göttgens b5940fc9f0 trunk it real good 2026-06-03 16:54:56 +02:00
Thomas Göttgens 9b11352d42 Fix build for I2C-excluded boards with input broker
Guard cardKbI2cImpl usage with !MESHTASTIC_EXCLUDE_I2C (its include was already guarded).
2026-06-03 15:02:16 +02:00
Thomas Göttgens 1daeb2edce Add RadioMaster Nomad Gemini analog PA support
External PA via APC2 DAC (GPIO26) + LR1121 negative-dBm drive, behind a generic weak-hook API. Also fixes BUTTON_PIN 34->14.
2026-06-03 15:01:13 +02:00
Thomas Göttgens f6956160bb Unify PA fan macro: RF95_FAN_EN -> RADIO_FAN_EN
The nomad branch generalized RF95_FAN_EN into RADIO_FAN_EN with
enableFan()/disableFan() helpers, but master independently added new
RF95_FAN_EN usages. Converge on the generalized macro:

- t-beam-1w variant.h: define RADIO_FAN_EN instead of RF95_FAN_EN
- SX126xInterface: drop the inline RF95_FAN_EN block now covered by
  the enableFan() call directly below it (avoids double-driving the
  pin and the PWM/digitalWrite conflict on PWM variants)

All radio interfaces now drive the fan exclusively through
enableFan()/disableFan().
2026-06-03 13:27:41 +02:00
Thomas Göttgens 63d0781e29 Merge remote-tracking branch 'origin/master' into nomad-gemini
# Conflicts:
#	platformio.ini
#	src/mesh/RadioLibInterface.cpp
#	src/mesh/RadioLibInterface.h
#	src/modules/AdminModule.cpp
2026-06-03 12:52:35 +02:00
Thomas Göttgens bfc536f96c Merge branch 'master' into nomad-gemini 2025-07-13 18:18:32 +02:00
Thomas Göttgens 7d7bfbcf24 Merge branch 'master' into nomad-gemini 2025-06-25 20:12:00 +02:00
Thomas Göttgens 8fcaf5ff30 Merge branch 'master' into nomad-gemini 2025-06-05 14:30:38 +02:00
Thomas Göttgens 3773683df6 Merge branch 'master' into nomad-gemini 2025-05-24 22:08:42 +02:00
Thomas Göttgens 2c782223a5 Merge branch 'master' into nomad-gemini 2025-03-31 11:21:44 +02:00
Thomas Göttgens dcabb85000 WIP and taking Notes, trunk'd 2025-03-02 12:22:15 +01:00
Thomas Göttgens 9c3ceaf6e9 Merge branch 'master' into nomad-gemini 2025-03-02 12:01:43 +01:00
Thomas Göttgens b2c07708bd Merge branch 'master' into nomad-gemini 2025-02-19 13:22:04 +01:00
Thomas Göttgens b5737d089b Merge branch 'master' into nomad-gemini 2025-01-18 14:11:19 +01:00
Thomas Göttgens 3ccec98b58 Merge branch 'master' into nomad-gemini 2024-12-29 22:50:31 +01:00
Thomas Göttgens 30a3f39ed0 Merge branch 'master' into nomad-gemini 2024-11-24 15:54:12 +01:00
Thomas Göttgens 55da30ab58 Merge branch 'master' into nomad-gemini 2024-11-20 20:29:44 +01:00
Thomas Göttgens 078cd68191 Merge branch 'master' into nomad-gemini 2024-11-19 12:44:45 +01:00
Thomas Göttgens e932c9786f Merge branch 'master' into nomad-gemini 2024-11-13 12:28:06 +01:00
Thomas Göttgens 8d1659a993 Merge branch 'master' into nomad-gemini 2024-11-12 20:55:36 +01:00
Thomas Göttgens d04509bbf0 fix nullptr 2024-11-12 18:39:33 +01:00
Thomas Göttgens 8d1674c3e5 typo 2024-11-12 18:33:33 +01:00
Thomas Göttgens af87c55530 Merge branch 'nomad-gemini' of https://github.com/meshtastic/firmware into nomad-gemini 2024-11-12 18:06:24 +01:00
Thomas Göttgens 1898aec281 Update Radiomaster target 2024-11-12 18:05:55 +01:00
Thomas Göttgens dd2a4a3121 WIP. Needs PA and GPIO verification 2024-11-12 18:05:55 +01:00
Thomas Göttgens a77836279a Update Radiomaster target 2024-11-12 16:53:36 +01:00
Thomas Göttgens ebc41e571c WIP. Needs PA and GPIO verification 2024-11-11 23:40:58 +01:00
21 changed files with 440 additions and 52 deletions
+2
View File
@@ -383,8 +383,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
+27 -5
View File
@@ -221,6 +221,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<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1] = {};
@@ -994,16 +996,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
+2
View File
@@ -90,6 +90,8 @@ extern bool suppressRebootBanner;
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;
+13 -2
View File
@@ -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"
#ifdef LR11X0_DIO_AS_RF_SWITCH
#include "rfswitch.h"
@@ -54,6 +56,8 @@ template <typename T> bool LR11x0Interface<T>::init()
digitalWrite(LR11X0_POWER_EN, HIGH);
#endif
enableFan();
#if ARCH_PORTDUINO
float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000;
// FIXME: correct logic to default to not using TCXO if no voltage is specified for LR11x0_DIO3_TCXO_VOLTAGE
@@ -184,8 +188,10 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
if (err != RADIOLIB_ERR_NONE)
RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING);
if (power > LR1110_MAX_POWER) // This chip has lower power limits than some
power = LR1110_MAX_POWER;
// Re-apply regulatory limits and any external-PA power mapping. applyModemConfig()
// (run by the base reconfigure()) reset `power` to the requested total, so without
// this a runtime tx_power change would skip region clamping and external-PA mapping.
limitPower(LR1110_MAX_POWER);
if ((power > LR1120_MAX_POWER) && (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24)) // 2.4G power limit
power = LR1120_MAX_POWER;
@@ -236,6 +242,7 @@ template <typename T> void LR11x0Interface<T>::addReceiveMetadata(meshtastic_Mes
*/
template <typename T> void LR11x0Interface<T>::configHardwareForSend()
{
radioExternalPaTxEnable(); // bias an external PA (if any) before we transmit
RadioLibInterface::configHardwareForSend();
}
@@ -250,6 +257,8 @@ template <typename T> void LR11x0Interface<T>::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.
@@ -337,6 +346,8 @@ template <typename T> bool LR11x0Interface<T>::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);
+2 -8
View File
@@ -154,10 +154,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);
@@ -335,10 +332,7 @@ bool RF95Interface::sleep()
// put chipset into sleep mode
setStandby(); // First cancel any active receiving/sending
lora->sleep();
#ifdef RF95_FAN_EN
digitalWrite(RF95_FAN_EN, 0);
#endif
disableFan();
return true;
}
+18
View File
@@ -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() {}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <stdint.h>
/**
* 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/<board>/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();
+35 -22
View File
@@ -9,6 +9,7 @@
#include "MeshService.h"
#include "NodeDB.h"
#include "RF95Interface.h"
#include "RadioExternalPa.h"
#include "Router.h"
#include "SX1262Interface.h"
#include "SX1268Interface.h"
@@ -917,38 +918,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;
+33
View File
@@ -691,3 +691,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
}
+3
View File
@@ -203,6 +203,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.
+1 -4
View File
@@ -64,10 +64,7 @@ template <typename T> bool SX126xInterface<T>::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;
+1 -4
View File
@@ -35,10 +35,7 @@ template <typename T> bool SX128xInterface<T>::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) {
+11 -3
View File
@@ -793,13 +793,21 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
}
#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
config.lora = validatedLora;
@@ -0,0 +1,97 @@
#include "configuration.h"
#ifdef RADIOMASTER_NOMAD_GEMINI
#include "RadioExternalPa.h"
#include <Arduino.h>
/**
* 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
@@ -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
@@ -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.
@@ -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.
@@ -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}
@@ -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
*/
@@ -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
+1 -1
View File
@@ -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