mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-10 15:43:36 -04:00
Whitespace and comment-alignment only: the output of `trunk fmt --all` on
develop @ d49cf21c3 with the pinned clang-format@20.1.0 and prettier. 48
files had drifted; trunk-action only checks the files a PR touches, so the
drift never fails CI and instead lands as noise in the next PR to edit any of
them. No code change.
730 lines
30 KiB
C++
730 lines
30 KiB
C++
#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 <modules/LR2021/LR2021_registers.h>
|
|
#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 <typename T>
|
|
LR20x0Interface<T>::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 <typename T> bool LR20x0Interface<T>::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 <typename T> bool LR20x0Interface<T>::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 <typename T> bool LR20x0Interface<T>::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 <typename T> void LR20x0Interface<T>::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 <typename T> void LR20x0Interface<T>::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 <typename T> void LR20x0Interface<T>::clearRadioIsr()
|
|
{
|
|
lora.clearIrqAction();
|
|
}
|
|
|
|
template <typename T> int16_t LR20x0Interface<T>::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 <typename T> void LR20x0Interface<T>::setStandby()
|
|
{
|
|
int16_t err = trySetStandby();
|
|
assert(err == RADIOLIB_ERR_NONE);
|
|
}
|
|
|
|
/**
|
|
* Add SNR data to received messages
|
|
*/
|
|
template <typename T> void LR20x0Interface<T>::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 <typename T> void LR20x0Interface<T>::configHardwareForSend()
|
|
{
|
|
RadioLibInterface::configHardwareForSend();
|
|
}
|
|
|
|
// For power draw measurements, helpful to force radio to stay sleeping
|
|
// #define SLEEP_ONLY
|
|
|
|
template <typename T> void LR20x0Interface<T>::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 <typename T> bool LR20x0Interface<T>::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 <typename T> bool LR20x0Interface<T>::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 <typename T> void LR20x0Interface<T>::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 <typename T> bool LR20x0Interface<T>::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 <typename T> int16_t LR20x0Interface<T>::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
|