diff --git a/src/mesh/LR20x0Band.h b/src/mesh/LR20x0Band.h new file mode 100644 index 000000000..032f3d3eb --- /dev/null +++ b/src/mesh/LR20x0Band.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +constexpr bool isLr20x0HighBand(float frequencyMHz) +{ + return frequencyMHz > 1500.0f; +} + +constexpr bool isLr20x0BandHop(float previousMHz, float requestedMHz) +{ + return previousMHz > 0.0f && requestedMHz > 0.0f && + isLr20x0HighBand(previousMHz) != isLr20x0HighBand(requestedMHz); +} + +// Path taken by LR20x0Interface::reconfigure() for a frequency change. +enum class Lr20x0ReconfigurePath : uint8_t { Incremental, FullBegin }; + +constexpr Lr20x0ReconfigurePath lr20x0ReconfigurePath(float previousMHz, float requestedMHz) +{ + return isLr20x0BandHop(previousMHz, requestedMHz) ? Lr20x0ReconfigurePath::FullBegin + : Lr20x0ReconfigurePath::Incremental; +} diff --git a/src/mesh/LR20x0Interface.cpp b/src/mesh/LR20x0Interface.cpp index c7d00e3ab..88ab392e7 100644 --- a/src/mesh/LR20x0Interface.cpp +++ b/src/mesh/LR20x0Interface.cpp @@ -1,6 +1,7 @@ #include "configuration.h" #if (defined(USE_LR2021) || defined(ARCH_PORTDUINO)) && RADIOLIB_EXCLUDE_LR2021 != 1 +#include "LR20x0Band.h" #include "LR20x0Interface.h" #include "error.h" #include "mesh/NodeDB.h" @@ -42,6 +43,9 @@ static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = { #define LR2021_MAX_POWER_HF 12 #endif +// Last programmed carrier; LF/HF hops use full begin() (live setOutputPower returns -706). +static float lr20x0LastFreqMHz = 0; + template LR20x0Interface::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) @@ -98,14 +102,14 @@ template bool LR20x0Interface::init() #ifdef LR2021_RF_SWITCH_SUBGHZ pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT); - digitalWrite(LR2021_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); - LOG_DEBUG("Set RF0 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); + digitalWrite(LR2021_RF_SWITCH_SUBGHZ, isLr20x0HighBand(getFreq()) ? LOW : HIGH); + LOG_DEBUG("Set RF0 switch to %s", isLr20x0HighBand(getFreq()) ? "2.4GHz" : "SubGHz"); #endif #ifdef LR2021_RF_SWITCH_2_4GHZ pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT); - digitalWrite(LR2021_RF_SWITCH_2_4GHZ, getFreq() < 1e9 ? LOW : HIGH); - LOG_DEBUG("Set RF1 switch to %s", getFreq() < 1e9 ? "SubGHz" : "2.4GHz"); + digitalWrite(LR2021_RF_SWITCH_2_4GHZ, isLr20x0HighBand(getFreq()) ? HIGH : LOW); + LOG_DEBUG("Set RF1 switch to %s", isLr20x0HighBand(getFreq()) ? "2.4GHz" : "SubGHz"); #endif // Allow extra time for TCXO to stabilize after power-on @@ -169,56 +173,154 @@ template bool LR20x0Interface::init() if (res == RADIOLIB_ERR_NONE) startReceive(); // start receiving + lr20x0LastFreqMHz = getFreq(); return res == RADIOLIB_ERR_NONE; } template bool LR20x0Interface::reconfigure() { - RadioLibInterface::reconfigure(); + bool success = RadioLibInterface::reconfigure(); - // set mode to standby - setStandby(); - - // configure publicly accessible settings - int err = lora.setSpreadingFactor(sf); - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - - err = lora.setBandwidth(bw); // different form than LR11xx - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - - err = lora.setCodingRate(cr, cr != 7); // use long interleaving except if CR is 4/7 which doesn't support it - if (err != RADIOLIB_ERR_NONE) - RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - - err = lora.setSyncWord(syncWord); - assert(err == RADIOLIB_ERR_NONE); - - if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range + if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { limitPower(LR2021_MAX_POWER_HF); } else { - limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range + limitPower(LR2021_MAX_POWER); + } + + const float freq = getFreq(); + const bool bandHop = lr20x0ReconfigurePath(lr20x0LastFreqMHz, freq) == Lr20x0ReconfigurePath::FullBegin; + + if (bandHop) { + LOG_INFO("LR20x0 LF/HF band hop %.1f -> %.1f MHz, full begin()", lr20x0LastFreqMHz, freq); + setStandby(); + + // Match init(): external LF/HF front-end GPIOs (if board defines them). +#ifdef LR2021_RF_SWITCH_SUBGHZ + pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT); + digitalWrite(LR2021_RF_SWITCH_SUBGHZ, isLr20x0HighBand(freq) ? LOW : HIGH); + LOG_DEBUG("Set RF0 switch to %s", isLr20x0HighBand(freq) ? "2.4GHz" : "SubGHz"); +#endif +#ifdef LR2021_RF_SWITCH_2_4GHZ + pinMode(LR2021_RF_SWITCH_2_4GHZ, OUTPUT); + digitalWrite(LR2021_RF_SWITCH_2_4GHZ, isLr20x0HighBand(freq) ? HIGH : LOW); + LOG_DEBUG("Set RF1 switch to %s", isLr20x0HighBand(freq) ? "2.4GHz" : "SubGHz"); +#endif + +#if ARCH_PORTDUINO + float tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; +#elif defined(LR2021_DIO3_TCXO_VOLTAGE) + float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE; +#elif defined(TCXO_OPTIONAL) + float tcxoVoltage = 1.6f; +#else + float tcxoVoltage = 0; +#endif + + delay(10); // same TCXO settle window as init() + + int res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { + LOG_WARN("LR20x0 band-hop begin SPI_CMD_FAILED, retrying..."); + delay(100); + res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + } +#if defined(TCXO_OPTIONAL) + if (res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) { + LOG_WARN("LR20x0 band-hop begin TCXO failed (%s%d), retry without TCXO", radioLibErr, res); + tcxoVoltage = 0; + res = lora.begin(freq, bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); + } +#endif + if (res != RADIOLIB_ERR_NONE) { + LOG_ERROR("LR20x0 band-hop begin %s%d", radioLibErr, res); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + return false; + } + lr20x0LastFreqMHz = freq; + + res = lora.setCRC(2); + if (res != RADIOLIB_ERR_NONE) { + LOG_ERROR("LR20x0 band-hop setCRC %s%d", radioLibErr, res); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + return false; + } + +#ifdef LR2021_DIO_AS_RF_SWITCH + lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table); +#elif ARCH_PORTDUINO + if (portduino_config.has_rfswitch_table) + lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table); +#endif + + res = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); + if (res != RADIOLIB_ERR_NONE) { + LOG_ERROR("LR20x0 band-hop setRxBoostedGainMode %s%d", radioLibErr, res); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + return false; + } + + startReceive(); + return true; + } + + // Same-band reconfigure (previous incremental path) + setStandby(); + + int err = lora.setFrequency(freq); + if (err != RADIOLIB_ERR_NONE) { + LOG_ERROR("LR20x0 setFrequency %.3f MHz %s%d", freq, radioLibErr, err); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } + + err = lora.setSpreadingFactor(sf); + if (err != RADIOLIB_ERR_NONE) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } + + err = lora.setBandwidth(bw); + if (err != RADIOLIB_ERR_NONE) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } + + err = lora.setCodingRate(cr, cr != 7); + if (err != RADIOLIB_ERR_NONE) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } + + err = lora.setSyncWord(syncWord); + if (err != RADIOLIB_ERR_NONE) { + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; } err = lora.setPreambleLength(preambleLength); - assert(err == RADIOLIB_ERR_NONE); - - err = lora.setFrequency(getFreq()); - if (err != RADIOLIB_ERR_NONE) + if (err != RADIOLIB_ERR_NONE) { RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } err = lora.setOutputPower(power); - assert(err == RADIOLIB_ERR_NONE); + if (err != RADIOLIB_ERR_NONE) { + LOG_ERROR("LR20x0 setOutputPower %d dBm @ %.3f MHz %s%d", power, freq, radioLibErr, err); + RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); + success = false; + } - // Apply RX gain mode - valid in STDBY, matches resetAGC() pattern err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain); - if (err != RADIOLIB_ERR_NONE) + if (err != RADIOLIB_ERR_NONE) { LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err); + success = false; + } - startReceive(); // restart receiving - - return true; + if (success) { + startReceive(); + lr20x0LastFreqMHz = freq; + } + return success; } template void LR20x0Interface::disableInterrupt() diff --git a/test/test_radio/test_main.cpp b/test/test_radio/test_main.cpp index df2442164..4fb75631e 100644 --- a/test/test_radio/test_main.cpp +++ b/test/test_radio/test_main.cpp @@ -1,3 +1,4 @@ +#include "LR20x0Band.h" #include "MeshRadio.h" #include "MeshService.h" #include "RadioInterface.h" @@ -9,6 +10,47 @@ static MockMeshService *mockMeshService; +static void test_lr20x0BandClassification() +{ + TEST_ASSERT_FALSE(isLr20x0HighBand(906.875f)); + TEST_ASSERT_FALSE(isLr20x0HighBand(1500.0f)); + TEST_ASSERT_TRUE(isLr20x0HighBand(2400.0f)); + TEST_ASSERT_TRUE(isLr20x0HighBand(2420.71875f)); +} + +static void test_lr20x0BandHopDetection() +{ + TEST_ASSERT_FALSE(isLr20x0BandHop(0.0f, 2420.71875f)); + TEST_ASSERT_FALSE(isLr20x0BandHop(906.875f, 915.0f)); + TEST_ASSERT_FALSE(isLr20x0BandHop(2400.0f, 2420.71875f)); + TEST_ASSERT_TRUE(isLr20x0BandHop(906.875f, 2420.71875f)); + TEST_ASSERT_TRUE(isLr20x0BandHop(2420.71875f, 906.875f)); + // Invalid requested frequency must not look like a band hop. + TEST_ASSERT_FALSE(isLr20x0BandHop(2420.71875f, 0.0f)); + TEST_ASSERT_FALSE(isLr20x0BandHop(906.875f, 0.0f)); + TEST_ASSERT_FALSE(isLr20x0BandHop(2420.71875f, -1.0f)); + TEST_ASSERT_FALSE(isLr20x0BandHop(906.875f, -915.0f)); +} + +static void test_lr20x0ReconfigurePathSelection() +{ + // LF -> HF and HF -> LF take full begin(); same-band stays incremental. + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::FullBegin), + static_cast(lr20x0ReconfigurePath(906.875f, 2420.71875f))); + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::FullBegin), + static_cast(lr20x0ReconfigurePath(2420.71875f, 906.875f))); + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::Incremental), + static_cast(lr20x0ReconfigurePath(906.875f, 915.0f))); + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::Incremental), + static_cast(lr20x0ReconfigurePath(2400.0f, 2420.71875f))); + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::Incremental), + static_cast(lr20x0ReconfigurePath(0.0f, 2420.71875f))); + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::Incremental), + static_cast(lr20x0ReconfigurePath(2420.71875f, 0.0f))); + TEST_ASSERT_EQUAL(static_cast(Lr20x0ReconfigurePath::Incremental), + static_cast(lr20x0ReconfigurePath(906.875f, -1.0f))); +} + // Test shim to expose protected radio parameters set by applyModemConfig() class TestableRadioInterface : public RadioInterface { @@ -359,6 +401,9 @@ void setup() initializeTestEnvironment(); UNITY_BEGIN(); + RUN_TEST(test_lr20x0BandClassification); + RUN_TEST(test_lr20x0BandHopDetection); + RUN_TEST(test_lr20x0ReconfigurePathSelection); RUN_TEST(test_bwCodeToKHz_specialMappings); RUN_TEST(test_bwCodeToKHz_passthrough); RUN_TEST(test_bwCodeToKHz_roundTrip);