#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" #if defined(LR2021_DCDC_WORKAROUND) && RADIOLIB_GODMODE // The DCDC sensitivity workaround pokes RadioLib-internal DCDC registers that are NOT exposed via the // public LR2021.h, so pull in the internal register map explicitly. Opt-in only (see LR2021_DCDC_WORKAROUND). #include #endif // Keep LR20x0 naming while RadioLib exposes LR2021 symbols. #ifndef LR20x0 #define LR20x0 LR2021 #endif #ifdef LR2021_DIO_AS_RF_SWITCH #include "rfswitch.h" #elif ARCH_PORTDUINO #include "PortduinoGlue.h" // Switch-capable DIOs in slot order with this part's constants. static const int8_t lr20x0_switch_dio_nums[] = {5, 6, 7, 8, 9, 10, 11}; static const uint32_t lr20x0_switch_dio_consts[] = {RADIOLIB_LR2021_DIO5, RADIOLIB_LR2021_DIO6, RADIOLIB_LR2021_DIO7, RADIOLIB_LR2021_DIO8, RADIOLIB_LR2021_DIO9, RADIOLIB_LR2021_DIO10, RADIOLIB_LR2021_DIO11}; static_assert(sizeof(lr20x0_switch_dio_nums) / sizeof(lr20x0_switch_dio_nums[0]) == sizeof(lr20x0_switch_dio_consts) / sizeof(lr20x0_switch_dio_consts[0]), "LR20x0 switch DIO numbers and constants must describe the same slots"); // This part has MODE_RX_HF and no MODE_TX_HP/MODE_GNSS/MODE_WIFI. static const int32_t lr20x0_rfswitch_mode_map[RFSW_MODE_COUNT] = { LR20x0::MODE_STBY, LR20x0::MODE_RX, LR20x0::MODE_TX, RFSW_MODE_UNSUPPORTED, LR20x0::MODE_TX_HF, LR20x0::MODE_RX_HF, RFSW_MODE_UNSUPPORTED, RFSW_MODE_UNSUPPORTED, }; static uint32_t lr20x0_rfswitch_dio_pins[Module::RFSWITCH_MAX_PINS]; static Module::RfSwitchMode_t lr20x0_rfswitch_table[RFSW_MODE_COUNT + 1]; #else static const uint32_t lr20x0_rfswitch_dio_pins[] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; static const Module::RfSwitchMode_t lr20x0_rfswitch_table[] = { {LR20x0::MODE_STBY, {}}, {LR20x0::MODE_RX, {}}, {LR20x0::MODE_TX, {}}, {LR20x0::MODE_RX_HF, {}}, {LR20x0::MODE_TX_HF, {}}, END_OF_MODE_TABLE, }; #endif #ifdef LR2021_CUSTOM_PA_TABLE #include "pa_table.h" #endif // Particular boards might define a different max power based on what their hardware can do, default to max power output if not // specified (may be dangerous if using external PA and LR20x0 power config forgotten) #if ARCH_PORTDUINO #define LR2021_MAX_POWER portduino_config.lr2021_max_power #endif #ifndef LR2021_MAX_POWER #define LR2021_MAX_POWER 22 #endif // the 2.4G part maxes at 12dBm #if ARCH_PORTDUINO #define LR2021_MAX_POWER_HF portduino_config.lr2021_max_power_hf #endif #ifndef LR2021_MAX_POWER_HF #define LR2021_MAX_POWER_HF 12 #endif // Last programmed carrier; LF/HF hops use full begin() (live setOutputPower returns -706). static float lr20x0LastFreqMHz = 0; // Unlike SX126x/LR11x0 (on/off bool), the LR2021 RX gain boost is a 0-7 level (0 = disabled, 7 = max boost). // Map the historical on/off sx126x_rx_boosted_gain flag to max boost when enabled. #ifndef LR2021_RX_GAIN_BOOST_LEVEL #define LR2021_RX_GAIN_BOOST_LEVEL 7 #endif template LR20x0Interface::LR20x0Interface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) : RadioLibInterface(hal, cs, irq, rst, busy, &lora), lora(&module) { LOG_WARN("LR20x0Interface(cs=%d, irq=%d, rst=%d, busy=%d)", cs, irq, rst, busy); } /// Initialise the Driver transport hardware and software. /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. template bool LR20x0Interface::init() { #ifdef LR2021_POWER_EN pinMode(LR2021_POWER_EN, OUTPUT); digitalWrite(LR2021_POWER_EN, HIGH); #endif #if ARCH_PORTDUINO // An explicit Vref wins; probing with none given tries the radio default first. float tcxoVoltage; if (portduino_config.dio3_tcxo_voltage > 0) tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; else if (TCXO_OPTIONAL_ENABLED) tcxoVoltage = TCXO_OPTIONAL_DEFAULT_VOLTAGE; else tcxoVoltage = 0; if (portduino_config.dio3_tcxo_voltage <= 0 && TCXO_OPTIONAL_ENABLED) LOG_DEBUG("TCXO_OPTIONAL: no Lora.DIO3_TCXO_VOLTAGE set, trying default TCXO Vref %f V first", tcxoVoltage); #elif defined(LR2021_DIO3_TCXO_VOLTAGE) float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE; LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE defined, DIO3 as TCXO Vref %f V", LR2021_DIO3_TCXO_VOLTAGE); // (DIO3 is not free to be used as an IRQ) #elif defined(TCXO_OPTIONAL) float tcxoVoltage = 1.6f; // TCXO_OPTIONAL: try default 1.6 V first, fall back to XTAL on failure LOG_DEBUG("TCXO_OPTIONAL: no LR2021_DIO3_TCXO_VOLTAGE, try default TCXO Vref 1.6 V first"); #else float tcxoVoltage = 0; // "TCXO reference voltage to be set on DIO3. Defaults to 1.6 V, set to 0 to skip." per // https://github.com/jgromes/RadioLib/blob/690a050ebb46e6097c5d00c371e961c1caa3b52e/src/modules/LR11x0/LR11x0.h#L471C26-L471C104 // (DIO3 is free to be used as an IRQ) LOG_DEBUG("LR2021_DIO3_TCXO_VOLTAGE not defined, DIO3 not used as TCXO Vref"); #endif RadioLibInterface::init(); #ifdef LR2021_IRQ_DIO_NUM lora.irqDioNum = LR2021_IRQ_DIO_NUM; LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum); #elif defined(IRQ_DIO_NUM) lora.irqDioNum = IRQ_DIO_NUM; LOG_DEBUG("Set irqDioNum %d", lora.irqDioNum); #elif defined(ARCH_PORTDUINO) // Unset keeps RadioLib's default of DIO5, which many carriers also drive as a switch line. The // range is checked again here because a DIO the radio cannot drive is a silently dead receiver. if (portduino_config.irq_dio_num < 0) { LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum); } else if (portduino_config.irq_dio_num >= kLr20x0IrqDioMin && portduino_config.irq_dio_num <= kLr20x0IrqDioMax) { lora.irqDioNum = portduino_config.irq_dio_num; LOG_DEBUG("Set irqDioNum %d from config", lora.irqDioNum); } else { LOG_WARN("Config irqDioNum %d outside DIO%d-DIO%d, using default irqDioNum %d", portduino_config.irq_dio_num, kLr20x0IrqDioMin, kLr20x0IrqDioMax, lora.irqDioNum); } #else LOG_DEBUG("Use default irqDioNum %d", lora.irqDioNum); #endif if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { // clamp if wide freq range limitPower(LR2021_MAX_POWER_HF); } else { limitPower(LR2021_MAX_POWER); // default clamp for non-wide freq range } #ifdef LR2021_RF_SWITCH_SUBGHZ pinMode(LR2021_RF_SWITCH_SUBGHZ, OUTPUT); 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, 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 delay(10); int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); // Retry if we get SPI command failed - some units need extra TCXO stabilization time if (res == RADIOLIB_ERR_SPI_CMD_FAILED) { LOG_WARN("LR20x0 init failed with %d (SPI_CMD_FAILED), retry after delay", res); delay(100); res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); } // If init failed for any reason other than chip not found, retry without TCXO (XTAL mode) if (TCXO_OPTIONAL_ENABLED && res != RADIOLIB_ERR_NONE && res != RADIOLIB_ERR_CHIP_NOT_FOUND && tcxoVoltage > 0) { LOG_WARN("LR20x0 init failed with TCXO Vref %f V (err %d), retry without TCXO", tcxoVoltage, res); tcxoVoltage = 0; res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); if (res == RADIOLIB_ERR_NONE) LOG_INFO("LR20x0 init success without TCXO (XTAL mode)"); } // \todo Display actual typename of the adapter, not just `LR20x0` LOG_INFO("LR20x0 init result %d", res); if (res == RADIOLIB_ERR_CHIP_NOT_FOUND || res == RADIOLIB_ERR_SPI_CMD_FAILED) return false; // Some basic info about the module's explicit firmware version - no other info available // Currently requires radiolib godmode #if RADIOLIB_GODMODE if (res == RADIOLIB_ERR_NONE) { uint8_t fwMajor = 0; uint8_t fwMinor = 0; int versionRes = lora.getVersion(&fwMajor, &fwMinor); if (versionRes == RADIOLIB_ERR_NONE) LOG_DEBUG("LR20x0 FW %d.%d", fwMajor, fwMinor); } #endif // Semtech DCDC sensitivity workaround for sub-GHz operation - applied here after lora.begin() has set the // packet type and modulation params. reconfigure() reapplies it after its own modulation changes. if (res == RADIOLIB_ERR_NONE) applyDcdcWorkaround(); applyCustomLfPaTable(getFreq()); LOG_INFO("Frequency set to %f", getFreq()); LOG_INFO("Bandwidth set to %f", bw); LOG_INFO("Power output set to %d", power); if (res == RADIOLIB_ERR_NONE) res = lora.setCRC(2); // Standard DCDC ramp timing from RadioLib workarounds (register 0x00F20024) // Currently requires radiolib godmode #if RADIOLIB_GODMODE if (res == RADIOLIB_ERR_NONE) { uint8_t rampTimes[4] = {15, 15, 15, 15}; // Standard case for all conditions // godmode-only DCDC ramp tuning: log failures but don't fail init (radio is already up) int16_t rmRes = lora.setRegMode(RADIOLIB_LR2021_REG_MODE_SIMO_NORMAL, rampTimes); if (rmRes != RADIOLIB_ERR_NONE) LOG_WARN("LR2021 setRegMode failed: %d", rmRes); } #endif #ifdef LR2021_DIO_AS_RF_SWITCH bool dioAsRfSwitch = true; #elif defined(ARCH_PORTDUINO) bool dioAsRfSwitch = portduino_config.has_rfswitch_table; if (dioAsRfSwitch) buildRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table, RFSW_MODE_COUNT + 1, lr20x0_switch_dio_nums, lr20x0_switch_dio_consts, sizeof(lr20x0_switch_dio_nums) / sizeof(lr20x0_switch_dio_nums[0]), lr20x0_rfswitch_mode_map); #else bool dioAsRfSwitch = false; #endif if (dioAsRfSwitch) { lora.setRfSwitchTable(lr20x0_rfswitch_dio_pins, lr20x0_rfswitch_table); LOG_DEBUG("Set DIO RF switch"); } if (res == RADIOLIB_ERR_NONE) { if (config.lora.sx126x_rx_boosted_gain) { // the name is unfortunate but historically accurate res = lora.setRxBoostedGainMode(LR2021_RX_GAIN_BOOST_LEVEL); LOG_INFO("Set RX gain to boosted mode (level %d); result: %d", LR2021_RX_GAIN_BOOST_LEVEL, res); } else { res = lora.setRxBoostedGainMode(0); LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", res); } } if (res == RADIOLIB_ERR_NONE) startReceive(); // start receiving lr20x0LastFreqMHz = getFreq(); return res == RADIOLIB_ERR_NONE; } template bool LR20x0Interface::reconfigure() { // Propagated to the return value below, separately from the chip-programming outcome, so a // base-class failure isn't masked as success. const bool reconfigureSuccess = RadioLibInterface::reconfigure(); if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) { limitPower(LR2021_MAX_POWER_HF); } else { 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); // fullBegin() hardware-resets the chip, so a standby failure is survivable here (void)trySetStandby(); if (!fullBegin(freq)) return false; startReceive(); return reconfigureSuccess; } // Same-band reconfigure (previous incremental path) bool standbySuccess = true; int16_t standbyErr = trySetStandby(); if (standbyErr != RADIOLIB_ERR_NONE) standbySuccess = false; if (standbyErr == RADIOLIB_ERR_NONE) { 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); standbySuccess = false; } err = lora.setSpreadingFactor(sf); if (err != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 setSpreadingFactor(%u) %s%d", sf, radioLibErr, err); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); standbySuccess = false; } err = lora.setBandwidth(bw); if (err != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 setBandwidth(%.1f) %s%d", bw, radioLibErr, err); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); standbySuccess = false; } err = lora.setCodingRate(cr, cr != 7); if (err != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 setCodingRate(%u) %s%d", cr, radioLibErr, err); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); standbySuccess = false; } err = lora.setSyncWord(syncWord); if (err != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 setSyncWord %s%d", radioLibErr, err); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); standbySuccess = false; } err = lora.setPreambleLength(preambleLength); if (err != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 setPreambleLength(%u) %s%d", preambleLength, radioLibErr, err); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); standbySuccess = false; } err = lora.setOutputPower(power); 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); standbySuccess = false; } // Warn-only, as in LR11x0: a rejected gain mode is cosmetic and not a lost-state signature, so // it must not drag reconfigure() into a full chip reset. err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain ? LR2021_RX_GAIN_BOOST_LEVEL : 0); if (err != RADIOLIB_ERR_NONE) LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err); } if (!standbySuccess) { // A chip that fails standby or rejects parameter programming (typically WRONG_MODEM, -20) has // lost its runtime configuration to a chip-internal reset or brownout. Recover in place with the // same full begin() the band-hop path uses - it hardware-resets the chip. Crashing here instead // would reboot before MeshService persists the config change that triggered us. LOG_ERROR("LR20x0 rejected modem params, chip state lost? Full re-init"); if (!fullBegin(freq)) { LOG_ERROR("LR20x0 unrecoverable, radio down until reboot"); return false; } LOG_INFO("LR20x0 recovered after re-init"); } // setSpreadingFactor/setBandwidth/setCodingRate each re-run setLoRaModulationParams(), which resets // the DCDC configure state, so reapply the workaround before we resume receiving. if (standbySuccess) applyDcdcWorkaround(); startReceive(); lr20x0LastFreqMHz = freq; return reconfigureSuccess; } // The chip-side re-init the band-hop and recovery paths share: front-end switch GPIOs for the target // band, a fresh begin() (which hardware-resets the chip), CRC, DIO RF-switch table, and RX gain. template bool LR20x0Interface::fullBegin(float freq) { { // 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; if (portduino_config.dio3_tcxo_voltage > 0) tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; else if (TCXO_OPTIONAL_ENABLED) tcxoVoltage = TCXO_OPTIONAL_DEFAULT_VOLTAGE; else tcxoVoltage = 0; #elif defined(LR2021_DIO3_TCXO_VOLTAGE) float tcxoVoltage = LR2021_DIO3_TCXO_VOLTAGE; #elif defined(TCXO_OPTIONAL) float tcxoVoltage = TCXO_OPTIONAL_DEFAULT_VOLTAGE; #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 (TCXO_OPTIONAL_ENABLED && 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); } if (res != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 band-hop begin %s%d", radioLibErr, res); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); return false; } applyCustomLfPaTable(freq); 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 ? LR2021_RX_GAIN_BOOST_LEVEL : 0); if (res != RADIOLIB_ERR_NONE) { LOG_ERROR("LR20x0 band-hop setRxBoostedGainMode %s%d", radioLibErr, res); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); return false; } // begin() above reprogrammed the modulation params, so the DCDC configure state is reset here too. applyDcdcWorkaround(); return true; } } // Board LF PA table after begin(); pointer is retained. HF keeps the RadioLib default. // Warn-only: a calibration miss must not fail init/fullBegin, keep the begin() PA config. template void LR20x0Interface::applyCustomLfPaTable(float freq) { #ifdef LR2021_CUSTOM_PA_TABLE if (isLr20x0HighBand(freq)) return; lora.setPaTable(lr2021_pa_table_lf, false); int16_t paRes = lora.setOutputPower(power); if (paRes != RADIOLIB_ERR_NONE) LOG_WARN("LR2021 custom LF PA table setOutputPower failed (%s%d)", radioLibErr, paRes); else LOG_DEBUG("LR2021 custom LF PA table installed"); #else (void)freq; #endif } // Semtech DCDC sensitivity workaround for sub-GHz operation on engineering sample date code 2513. // Worthy of note is that we tested this on non-engineering samples and it didn't make any difference, but // we went to the trouble of writing this, so it can stay in, albeit gated behind a compile-time option. // lr20xx_workarounds_dcdc_reset must follow setPacketType; lr20xx_workarounds_dcdc_configure must follow // setModulationParams. In init() both hold once lora.begin() returns; in reconfigure() the caller invokes this // after setSpreadingFactor/setBandwidth/setCodingRate, whose RadioLib implementations re-run // setLoRaModulationParams() and reset the DCDC configure state. Only applies to sub-GHz; 2.4 GHz (LORA_24) is // excluded. Opt-in only: requires -DLR2021_DCDC_WORKAROUND (and RADIOLIB_GODMODE for the internal register access). template void LR20x0Interface::applyDcdcWorkaround() { #if defined(LR2021_DCDC_WORKAROUND) && RADIOLIB_GODMODE if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_LORA_24) return; // Helper: set DCDC LF frequency register and re-apply the current RF frequency. auto dcdcSetFreq = [&](uint32_t freqHz) -> int16_t { const uint32_t freqLf = (uint32_t)((float)freqHz * 1.048576f); int16_t s = lora.writeRegMem32(RADIOLIB_LR2021_REG_DCDC_FREQ_LF, &freqLf, 1); if (s != RADIOLIB_ERR_NONE) return s; uint32_t rawRfFreq = 0; s = lora.readRegMem32(RADIOLIB_LR2021_REG_RTTOF_RF_FREQ, &rawRfFreq, 1); if (s != RADIOLIB_ERR_NONE) return s; // Convert PLL steps to Hz: (steps * 15625 + 16383) / 16384 uint32_t rfHz = (uint32_t)(((uint64_t)rawRfFreq * 15625ULL + 16383ULL) / 16384ULL); return lora.setRfFrequency(rfHz); }; // dcdc_reset: reset RISE/FALL ramp fields to conservative 15/15 at 2.8 MHz int16_t dcdcRes = lora.writeRegMemMask32(RADIOLIB_LR2021_REG_DCDC_SWITCHER, 0xFu << 20, 15u << 20); if (dcdcRes == RADIOLIB_ERR_NONE) dcdcRes = lora.writeRegMemMask32(RADIOLIB_LR2021_REG_DCDC_SWITCHER, 0xFu << 16, 15u << 16); if (dcdcRes == RADIOLIB_ERR_NONE) dcdcRes = dcdcSetFreq(2800000); // dcdc_configure: tune RISE/FALL and DC freq based on ADC decimation and RX path. // Matches lr20xx_workarounds_dcdc_configure() exactly. if (dcdcRes == RADIOLIB_ERR_NONE) { uint32_t adcCtrl = 0, rxPath = 0; dcdcRes = lora.readRegMem32(RADIOLIB_LR2021_REG_DCDC_ADC_CTRL, &adcCtrl, 1); if (dcdcRes == RADIOLIB_ERR_NONE) dcdcRes = lora.readRegMem32(RADIOLIB_LR2021_REG_DCDC_RX_PATH, &rxPath, 1); if (dcdcRes == RADIOLIB_ERR_NONE) { const uint32_t anaDec = (adcCtrl >> 8) & 0x7; const bool isRxHf = (rxPath & 0x3) == 1; // Narrowband sub-GHz path (ana_dec 1 or 2): use tighter RISE=11/FALL=13 timing const uint32_t rise = (!isRxHf && (anaDec == 1 || anaDec == 2)) ? 11u : 15u; const uint32_t fall = (!isRxHf && (anaDec == 1 || anaDec == 2)) ? 13u : 15u; dcdcRes = lora.writeRegMemMask32(RADIOLIB_LR2021_REG_DCDC_SWITCHER, 0xFu << 20, rise << 20); if (dcdcRes == RADIOLIB_ERR_NONE) dcdcRes = lora.writeRegMemMask32(RADIOLIB_LR2021_REG_DCDC_SWITCHER, 0xFu << 16, fall << 16); if (dcdcRes == RADIOLIB_ERR_NONE) dcdcRes = dcdcSetFreq(anaDec == 1 ? 4300000 : 2800000); } } if (dcdcRes != RADIOLIB_ERR_NONE) LOG_WARN("LR20x0 DCDC workaround failed: %d", dcdcRes); else LOG_DEBUG("LR20x0 DCDC workaround applied"); #endif } template void LR20x0Interface::clearRadioIsr() { lora.clearIrqAction(); } template int16_t LR20x0Interface::trySetStandby() { checkNotification(); // handle any pending interrupts before we force standby int16_t err = lora.standby(); if (err != RADIOLIB_ERR_NONE) { LOG_DEBUG("LR20x0 standby failed, err %d", err); } isReceiving = false; // If we were receiving, not any more activeReceiveStart = 0; disableInterrupt(); completeSending(); // If we were sending, not anymore RadioLibInterface::setStandby(); return err; } template void LR20x0Interface::setStandby() { int16_t err = trySetStandby(); assert(err == RADIOLIB_ERR_NONE); } /** * Add SNR data to received messages */ template void LR20x0Interface::addReceiveMetadata(meshtastic_MeshPacket *mp) { // LOG_DEBUG("PacketStatus %x", lora.getPacketStatus()); mp->rx_snr = lora.getSNR(); mp->rx_rssi = lround(lora.getRSSI()); mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding // LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0 // too(!) } /** We override to turn on transmitter power as needed. */ template void LR20x0Interface::configHardwareForSend() { RadioLibInterface::configHardwareForSend(); } // For power draw measurements, helpful to force radio to stay sleeping // #define SLEEP_ONLY template void LR20x0Interface::startReceive() { #ifdef SLEEP_ONLY sleep(); #else int16_t err = trySetStandby(); if (err == RADIOLIB_ERR_NONE) { 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. err = lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } if (err != RADIOLIB_ERR_NONE) { LOG_ERROR("StartReceive error: %d", err); if (maybeRecoverChipStateLoss()) { lora.setPreambleLength(preambleLength); err = lora.startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, MESHTASTIC_RADIOLIB_IRQ_RX_FLAGS, RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); } } if (err != RADIOLIB_ERR_NONE) { // No assert: leave RX off rather than reboot; periodicRadioMaintenance() re-arms it, throttled LOG_ERROR("LR20x0 RX offline %s%d", radioLibErr, err); rxOffline = true; return; } RadioLibInterface::startReceive(); // Must be done AFTER starting receive, because startReceive clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); checkRxDoneIrqFlag(); #endif } /** Is the channel currently active? */ template bool LR20x0Interface::isChannelActive() { // check if we can detect a LoRa preamble on the current channel ChannelScanConfig_t cfg = {.cad = {.symNum = NUM_SYM_CAD, .detPeak = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, .detMin = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, .exitMode = RADIOLIB_LR2021_CAD_PARAM_DEFAULT, .timeout = 0, .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result = trySetStandby(); if (result == RADIOLIB_ERR_NONE) { result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) return true; if (result != RADIOLIB_ERR_WRONG_MODEM) return false; } // standby failed or the LoRa modem type is gone - the chip lost its runtime state maybeRecoverChipStateLoss(); return false; // report the channel free: a recovered chip can TX, a dead one fails startSend safely } /** Could we send right now (i.e. either not actively receiving or transmitting)? */ template bool LR20x0Interface::isActivelyReceiving() { // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet // received and handled the interrupt for reading the packet/handling errors. return receiveDetected(lora.getIrqStatus(), RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID, RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED); } #ifdef LR20X0_AGC_RESET template void LR20x0Interface::resetAGC() { // Safety: don't reset mid-packet if (sendingPacket != NULL || (isReceiving && isActivelyReceiving())) return; LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)"); // 1. Warm sleep - powers down the analog frontend, resetting AGC state lora.sleep(true, 0); // 2. Wake to RC standby for stable calibration lora.standby(RADIOLIB_LR20X0_STANDBY_RC, true); // 3. Calibrate all blocks (PLL, ADC, image, RC oscillators) // calibrate() is protected on LR20x0, so use raw SPI (same as internal implementation) uint8_t calData = RADIOLIB_LR20X0_CALIBRATE_ALL; module.SPIwriteStream(RADIOLIB_LR20X0_CMD_CALIBRATE, &calData, 1, true, true); // 4. Re-calibrate image rejection for actual operating frequency // Calibrate(0x3F) defaults to 902-928 MHz which is wrong for other regions. lora.calibrateImageRejection(getFreq() - 4.0f, getFreq() + 4.0f); // 5. Re-apply RX boosted gain mode lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain ? LR2021_RX_GAIN_BOOST_LEVEL : 0); // 6. Resume receiving startReceive(); } #endif template bool LR20x0Interface::sleep() { // \todo Display actual typename of the adapter, not just `LR20x0` LOG_DEBUG("LR20x0 entering sleep mode"); (void)trySetStandby(); // Stop any pending operations - the chip is being put to sleep, a failure must not crash // turn off TCXO if it was powered lora.setTCXO(0); // put chipset into sleep mode (we've already disabled interrupts by now) bool keepConfig = false; lora.sleep(keepConfig, 0); // Note: we do not keep the config, full reinit will be needed #ifdef LR2021_POWER_EN digitalWrite(LR2021_POWER_EN, LOW); #endif return true; } template int16_t LR20x0Interface::getCurrentRSSI() { float rssi = lora.getRSSI(false, true); return (int16_t)round(rssi); } // Don't leak the alias into the files InterfacesTemplates.cpp includes after this one. #undef LR20x0 #endif