diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index b4ee6c97aa..5add5c16b2 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -2,6 +2,7 @@ #include #include "MeshCore.h" +#include "SX126xReset.h" class CustomSX1262 : public SX1262 { uint32_t _preambleMillis = 66; @@ -89,10 +90,7 @@ class CustomSX1262 : public SX1262 { // for improved RX with Heltec v4 #ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - writeRegister(0x8B5, &r_data, 1); + sx126xApplyRegisterPatch(this); #endif MESH_DEBUG_PRINTLN("SX1262 status=0x%02X device_errors=0x%04X", getStatus(), getDeviceErrors()); diff --git a/src/helpers/radiolib/LinuxSX1262.h b/src/helpers/radiolib/LinuxSX1262.h index bed50ee92e..1bc188bce9 100644 --- a/src/helpers/radiolib/LinuxSX1262.h +++ b/src/helpers/radiolib/LinuxSX1262.h @@ -1,26 +1,48 @@ #pragma once #include +#include "MeshCore.h" +#include "CustomSX1262.h" +#include "SX126xReset.h" +// For the LinuxBoard definition behind `board` below: std_init() and +// rxSettings() both read board.config, so the complete type is needed here. +// Reached transitively via target.h today, but named so this header does not +// depend on include order. +#include "LinuxBoard.h" -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 #define SX126X_PREAMBLE_LENGTH 16 extern LinuxBoard board; -class LinuxSX1262 : public SX1262 { +// The Linux build's SX1262. +// +// Everything that is not Linux-specific comes from CustomSX1262 unchanged -- +// the RX watchdog (startReceive/isReceiving and their preamble/header +// deadlines), the millis setters, the RX-boost readback. That logic is shared +// with every SX126x board and upstream keeps fixing it, so forking it here +// would mean re-copying each fix by hand and silently missing the ones nobody +// notices. +// +// What genuinely differs is initialisation: the MCU variants pick frequency, +// regulator, RF-switch and gain per board at compile time, but one binary here +// serves every HAT, so all of it comes from meshcored.ini at runtime. +class LinuxSX1262 : public CustomSX1262 { public: - LinuxSX1262(Module *mod) : SX1262(mod) { } + LinuxSX1262(Module *mod) : CustomSX1262(mod) { } + // Shadows (not overrides) CustomSX1262::std_init(), which is non-virtual + // and reads LORA_*/SX126X_* build flags this variant does not define. bool std_init(SPIClass* spi = NULL) { - LinuxConfig config = board.config; + const LinuxConfig& config = board.config; Serial.printf("Radio begin %f %f %d %d %f\n", config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, config.lora_tcxo); - int status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, config.lora_tcxo); + MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", config.use_regulator_ldo ? "LDO" : "DC-DC"); + int status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, config.lora_tcxo, config.use_regulator_ldo); // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { - status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, 0.0f); + MESH_DEBUG_PRINTLN("SX1262 init failed with error %d, retrying with TCXO at 0.0V", status); + status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, 0.0f, config.use_regulator_ldo); } if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); @@ -31,24 +53,24 @@ class LinuxSX1262 : public SX1262 { setCRC(1); setCurrentLimit(config.current_limit); - setDio2AsRfSwitch(config.dio2_as_rf_switch); - setRxBoostedGainMode(config.rx_boosted_gain); + sx126xApplyRxSettings(this, rxSettings()); if (config.lora_rxen_pin != RADIOLIB_NC || config.lora_txen_pin != RADIOLIB_NC) { setRfSwitchPins(config.lora_rxen_pin, config.lora_txen_pin); } - return true; - } + MESH_DEBUG_PRINTLN("SX1262 status=0x%02X device_errors=0x%04X", getStatus(), getDeviceErrors()); - bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + return true; } - bool getRxBoostedGainMode() { - uint8_t rxGain = 0; - readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1); - return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED); + // The RX settings for this node, as configured in meshcored.ini. Applied at + // init and re-applied after every AGC reset, both via sx126xApplyRxSettings() + // -- which is where the MCU variants instead read their SX126X_* build flags. + SX126xRxSettings rxSettings() const { + SX126xRxSettings s; + s.dio2_as_rf_switch = board.config.dio2_as_rf_switch; + s.rx_boosted_gain = board.config.rx_boosted_gain; + s.register_patch = board.config.rx_register_patch; + return s; } }; diff --git a/src/helpers/radiolib/LinuxSX1262Wrapper.h b/src/helpers/radiolib/LinuxSX1262Wrapper.h index 78c1e7cefb..f63968dce3 100644 --- a/src/helpers/radiolib/LinuxSX1262Wrapper.h +++ b/src/helpers/radiolib/LinuxSX1262Wrapper.h @@ -1,39 +1,45 @@ #pragma once +#include "CustomSX1262Wrapper.h" #include "LinuxSX1262.h" -#include "RadioLibWrappers.h" +#include "SX126xReset.h" -class LinuxSX1262Wrapper : public RadioLibWrapper { -public: - LinuxSX1262Wrapper(LinuxSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } - - void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { - ((LinuxSX1262 *)_radio)->setFrequency(freq); - ((LinuxSX1262 *)_radio)->setSpreadingFactor(sf); - ((LinuxSX1262 *)_radio)->setBandwidth(bw); - ((LinuxSX1262 *)_radio)->setCodingRate(cr); - updatePreamble(sf); - } +// LinuxSX1262 is a CustomSX1262, so its wrapper is a CustomSX1262Wrapper. +// +// setParams(), isReceivingPacket(), the RSSI/SNR readers, packetScore(), +// getSpreadingFactor(), set/getRxBoostedGainMode() and powerOff() were all +// byte-for-byte the base class's implementation with a different cast spelled +// out, so they are inherited rather than restated. That is also what keeps this +// file honest against upstream: a method added to RadioLibWrapper now arrives +// here implemented, the same way it arrives on every other SX126x board, +// instead of breaking the Linux build (pure virtual) or silently no-opping on +// it alone (virtual with a default). +class LinuxSX1262Wrapper : public CustomSX1262Wrapper { + // _radio is held as the base mesh::Radio. The inherited members downcast it to + // CustomSX1262, which is as far as they need to see; this names the + // LinuxSX1262 downcast for the parts only the Linux subclass has. It is always + // a LinuxSX1262 -- the constructor takes one by reference -- so this is a + // naming convenience, not a checked conversion. + LinuxSX1262* r() const { return (LinuxSX1262 *)_radio; } - bool isReceivingPacket() override { - return ((LinuxSX1262 *)_radio)->isReceiving(); - } - float getCurrentRSSI() override { - return ((LinuxSX1262 *)_radio)->getRSSI(false); - } - float getLastRSSI() const override { return ((LinuxSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((LinuxSX1262 *)_radio)->getSNR(); } - - float packetScore(float snr, int packet_len) override { - int sf = ((LinuxSX1262 *)_radio)->spreadingFactor; - return packetScoreInt(snr, sf, packet_len); - } - uint8_t getSpreadingFactor() const override { return ((LinuxSX1262 *)_radio)->spreadingFactor; } +public: + LinuxSX1262Wrapper(LinuxSX1262& radio, mesh::MainBoard& board) : CustomSX1262Wrapper(radio, board) { } - bool setRxBoostedGainMode(bool en) override { - return ((LinuxSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; - } - bool getRxBoostedGainMode() const override { - return ((LinuxSX1262 *)_radio)->getRxBoostedGainMode(); + // The one override that is not the inherited behaviour. Recalibration drops + // DIO2-as-RF-switch, RX boosted gain and the 0x8B5 patch, and the inherited + // version restores the first and third from SX126X_* build flags -- none of + // which this variant defines, so on Linux it would silently drop both. + // rxSettings() supplies them from meshcored.ini instead. + // + // The gain is read back off the chip, exactly as the inherited version does + // it, and deliberately not taken from rxSettings(): the ini value is only the + // starting point, and both the persisted pref and `set radio.rxgain` change + // it afterwards. Restoring the ini value here would revert them at the first + // agc.reset.interval tick, with `get radio.rxgain` still reporting the value + // the radio had stopped using. + void doResetAGC() override { + SX126xRxSettings rx = r()->rxSettings(); + rx.rx_boosted_gain = getRxBoostedGainMode(); + sx126xResetAGC(r(), rx); } }; diff --git a/src/helpers/radiolib/SX126xReset.h b/src/helpers/radiolib/SX126xReset.h index 472eb33bca..0616f6cf2d 100644 --- a/src/helpers/radiolib/SX126xReset.h +++ b/src/helpers/radiolib/SX126xReset.h @@ -2,10 +2,41 @@ #include +// RX settings that Calibrate(0x7F) does not preserve, so every reset has to +// restate them. Boards that fix these at build time (every MCU variant) do not +// need this -- see sx126xResetAGC() below, which reads their SX126X_* flags +// directly. It exists for targets configured at runtime instead: the Linux +// daemon serves every HAT from one binary, so its values come from +// meshcored.ini and cannot be macros. +struct SX126xRxSettings { + bool dio2_as_rf_switch = false; + bool rx_boosted_gain = false; + bool register_patch = false; // 0x8B5 RX-sensitivity patch +}; + +// The RX-sensitivity patch upstream added for the Heltec v4. Undocumented by +// Semtech, hence the bare register number. +inline void sx126xApplyRegisterPatch(SX126x* radio) { + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +} + +// Apply the RX settings calibration does not preserve. Both initial +// configuration and every later AGC reset go through here, so a setting added +// to SX126xRxSettings reaches both rather than having to be remembered twice. +inline void sx126xApplyRxSettings(SX126x* radio, const SX126xRxSettings& rx) { + radio->setDio2AsRfSwitch(rx.dio2_as_rf_switch); + radio->setRxBoostedGainMode(rx.rx_boosted_gain); + if (rx.register_patch) sx126xApplyRegisterPatch(radio); +} + // Full receiver reset for all SX126x-family chips (SX1262, SX1268, LLCC68, STM32WLx). -// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration, -// then re-applies RX settings that calibration may reset. -inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { +// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration. +// The caller then re-applies the RX settings calibration may have reset, via one of +// the two sx126xResetAGC() entry points below. +inline void sx126xRecalibrate(SX126x* radio) { radio->sleep(true); radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); @@ -21,6 +52,13 @@ inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { // Calibrate(0x7F) defaults image calibration to 902-928MHz band. // Re-calibrate for the actual operating frequency. radio->calibrateImage(radio->freqMHz); +} + +// MCU variants. The SX126X_* build flags still decide *which* settings apply, +// but the boosted-gain value is passed in -- callers read it back off the chip +// -- so a reset no longer clobbers a gain mode that was changed at runtime. +inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { + sx126xRecalibrate(radio); #ifdef SX126X_DIO2_AS_RF_SWITCH radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); @@ -29,9 +67,16 @@ inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { radio->setRxBoostedGainMode(rx_boost_gain); #endif #ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - radio->readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - radio->writeRegister(0x8B5, &r_data, 1); + sx126xApplyRegisterPatch(radio); #endif } + +// Runtime-configured targets. Every setting comes from the caller and none of +// the SX126X_* macros are consulted, which makes this the single place they are +// applied rather than having the caller re-apply them afterwards: a setting +// added to SX126xRxSettings then reaches these targets too, instead of being +// silently dropped on them until someone notices. +inline void sx126xResetAGC(SX126x* radio, const SX126xRxSettings& rx) { + sx126xRecalibrate(radio); + sx126xApplyRxSettings(radio, rx); +} diff --git a/variants/linux/LinuxBoard.cpp b/variants/linux/LinuxBoard.cpp index d48f16a0eb..2f4c08465c 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -445,6 +445,10 @@ LinuxConfig::LoadResult LinuxConfig::load(const char *filename) { if (parse_bool(key, value, &bval, &result.bad_values)) dio2_as_rf_switch = bval; } else if (strcmp(key, "rx_boosted_gain") == 0) { if (parse_bool(key, value, &bval, &result.bad_values)) rx_boosted_gain = bval; + } else if (strcmp(key, "use_regulator_ldo") == 0) { + if (parse_bool(key, value, &bval, &result.bad_values)) use_regulator_ldo = bval; + } else if (strcmp(key, "rx_register_patch") == 0) { + if (parse_bool(key, value, &bval, &result.bad_values)) rx_register_patch = bval; } else if (strcmp(key, "lora_irq_pin") == 0) { if (parse_pin(key, value, PIN_REQUIRED, &pin, &result.bad_values)) lora_irq_pin = (uint32_t) pin; } else if (strcmp(key, "lora_reset_pin") == 0) { diff --git a/variants/linux/LinuxBoard.h b/variants/linux/LinuxBoard.h index 8c2916d0b8..cfe5cffd37 100644 --- a/variants/linux/LinuxBoard.h +++ b/variants/linux/LinuxBoard.h @@ -34,6 +34,13 @@ class LinuxConfig { bool dio2_as_rf_switch = false; bool rx_boosted_gain = true; + // The MCU variants pick these per board at compile time, via the + // SX126X_USE_REGULATOR_LDO and SX126X_REGISTER_PATCH build flags. One binary + // here serves every HAT, so they are runtime config instead. Defaults match + // the compile-time defaults: DC-DC, no patch. + bool use_regulator_ldo = false; + bool rx_register_patch = false; + const char* spidev = "/dev/spidev0.0"; const char* lora_gpiochip = "gpiochip0"; diff --git a/variants/linux/README.md b/variants/linux/README.md index 0c8912bc0b..e2405b4973 100644 --- a/variants/linux/README.md +++ b/variants/linux/README.md @@ -119,14 +119,16 @@ Key settings: | `current_limit` | `140` | Radio over-current protection limit in mA | | `dio2_as_rf_switch` | `0` | `1` = use DIO2 to drive the TX/RX RF switch. **Required for the Waveshare Core1262** (without it the radio inits but TX/RX are dead); depends on module wiring | | `rx_boosted_gain` | `1` | `1` enables the SX126x RX boosted-gain mode; `0` disables | +| `use_regulator_ldo` | `0` | `1` powers the radio from the LDO instead of the DC-DC converter. Only for modules built without the DC-DC inductor | +| `rx_register_patch` | `0` | `1` applies the SX126x RX-sensitivity patch (bit 0 of register `0x8B5`). Try it if a HAT receives poorly | | `advert_name` | `"Linux Repeater"` | Node name, first-run default only | | `admin_password` | `"password"` | Admin password, **change this**, first-run default only | | `lat` / `lon` | `0.0` | GPS coordinates for advertisement, first-run default only | Comments (`#`, `;`), blank lines and `[section]` headers are ignored. Boolean -settings (`dio2_as_rf_switch`, `rx_boosted_gain`) accept `1`/`0`, -`true`/`false`, `on`/`off` or `yes`/`no`, case-insensitively; anything else is -a fatal invalid value. +settings (`dio2_as_rf_switch`, `rx_boosted_gain`, `use_regulator_ldo`, +`rx_register_patch`) accept `1`/`0`, `true`/`false`, `on`/`off` or `yes`/`no`, +case-insensitively; anything else is a fatal invalid value. #### Config validation @@ -285,4 +287,3 @@ sudo systemctl start meshcored - **Only repeater firmware**, there is no `linux_companion` target yet; companion radio support (BLE/serial interface to a phone app) is not implemented for Linux. - **Serial `erase` command is a no-op**, `formatFileSystem()` returns `false` on Linux, so the interactive serial `erase` command reports failure. To wipe the filesystem, use the `--erase` *startup* flag (or clear the VFS dir) instead, see step 5. - **No power management**, `board.sleep()` is a no-op; the power-saving loop in `main.cpp` never actually sleeps. -- **Upstream-sync fragility**, the radio wrapper (`LinuxSX1262Wrapper`) implements the `RadioLibWrapper` interface by hand, so it can drift from upstream in two ways: a new **pure-virtual** method breaks the Linux build (e.g. `setParams()`), and a new **virtual-with-default** method silently no-ops on Linux until overridden (e.g. `set`/`getRxBoostedGainMode()`, which reported and applied the wrong state until added). Mirror `CustomSX1262Wrapper` when syncing. diff --git a/variants/linux/meshcored.ini b/variants/linux/meshcored.ini index d7b17d790c..fdf65b8d13 100644 --- a/variants/linux/meshcored.ini +++ b/variants/linux/meshcored.ini @@ -27,3 +27,5 @@ lora_tcxo = 1.8 #current_limit = 140 #dio2_as_rf_switch = 1 #rx_boosted_gain = 1 +#use_regulator_ldo = 1 # LDO instead of DC-DC; only for modules wired without a DC-DC inductor +#rx_register_patch = 1 # RX sensitivity patch (register 0x8B5); harmless, helps some modules