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..889603dcca 100644 --- a/src/helpers/radiolib/LinuxSX1262.h +++ b/src/helpers/radiolib/LinuxSX1262.h @@ -1,26 +1,47 @@ #pragma once #include +#include "MeshCore.h" +#include "CustomSX1262.h" +#include "SX126xReset.h" +// For the LinuxBoard definition behind `board` below. Reached transitively via +// target.h today, but named here so this header does not depend on include +// order -- LinuxSX1262Wrapper.h calls methods on it. +#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 +52,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..7bf90d2f3b 100644 --- a/src/helpers/radiolib/LinuxSX1262Wrapper.h +++ b/src/helpers/radiolib/LinuxSX1262Wrapper.h @@ -2,38 +2,68 @@ #include "LinuxSX1262.h" #include "RadioLibWrappers.h" +#include "SX126xReset.h" class LinuxSX1262Wrapper : public RadioLibWrapper { + // _radio is held as the base mesh::Radio, so every use here needs the + // downcast. 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; } + 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); + r()->setFrequency(freq); + r()->setSpreadingFactor(sf); + r()->setBandwidth(bw); + r()->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + r()->setPreambleMillis(pm.preambleMillis); + r()->setMaxPayloadMillis(pm.payloadMillis); + } + + // Full SX126x receiver reset (warm sleep, recalibrate, re-image the configured + // band), the same one every other SX126x board gets. Without this override the + // base class falls back to a bare sleep(), so `set agc_int` was quietly a + // weaker knob on Linux than everywhere else. + // + // The settings argument is the Linux-specific part: recalibration drops + // DIO2-as-RF-switch, RX boosted gain and the 0x8B5 patch, and the shared + // helper restores them from the SX126X_* build flags, none of which are + // defined here -- those come from meshcored.ini. Handing it the runtime values + // makes it restore the right ones, rather than restoring the wrong ones for + // this caller to redo afterwards. + void doResetAGC() override { + sx126xResetAGC(r(), r()->rxSettings()); + } + + // Cold sleep, matching CustomSX1262Wrapper. The only caller shuts the daemon + // down straight afterwards, so there is no configuration worth retaining and + // the deeper state is the better one to leave the modem in. + void powerOff() override { + r()->sleep(false); } bool isReceivingPacket() override { - return ((LinuxSX1262 *)_radio)->isReceiving(); + return r()->isReceiving(); } float getCurrentRSSI() override { - return ((LinuxSX1262 *)_radio)->getRSSI(false); + return r()->getRSSI(false); } - float getLastRSSI() const override { return ((LinuxSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((LinuxSX1262 *)_radio)->getSNR(); } + float getLastRSSI() const override { return r()->getRSSI(); } + float getLastSNR() const override { return r()->getSNR(); } float packetScore(float snr, int packet_len) override { - int sf = ((LinuxSX1262 *)_radio)->spreadingFactor; - return packetScoreInt(snr, sf, packet_len); + return packetScoreInt(snr, r()->spreadingFactor, packet_len); } - uint8_t getSpreadingFactor() const override { return ((LinuxSX1262 *)_radio)->spreadingFactor; } + uint8_t getSpreadingFactor() const override { return r()->spreadingFactor; } bool setRxBoostedGainMode(bool en) override { - return ((LinuxSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; + return r()->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { - return ((LinuxSX1262 *)_radio)->getRxBoostedGainMode(); + return r()->getRxBoostedGainMode(); } }; 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 4ff8b3ea3c..b0fb94ad18 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -390,6 +390,8 @@ LinuxConfig::LoadResult LinuxConfig::load(const char *filename) { else if (strcmp(key, "current_limit") == 0) { if (parse_float(key, value, &fval, &result.bad_values)) current_limit = fval; } else if (strcmp(key, "dio2_as_rf_switch") == 0) { 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, 0, &pin, &result.bad_values)) lora_irq_pin = (uint32_t) pin; } else if (strcmp(key, "lora_reset_pin") == 0) { if (parse_pin(key, value, 0, &pin, &result.bad_values)) lora_reset_pin = (uint32_t) pin; } 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 bd6ae1d778..65e1f767ae 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 @@ -276,4 +278,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