From 2ae435f5893696b04ad30dceeb649a9ab236c680 Mon Sep 17 00:00:00 2001 From: hardened_malloc <288914967+0x00001312@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:04:52 +0200 Subject: [PATCH 1/2] fix(radio): issue CalibrateImage from STDBY_RC, not STDBY_XOSC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit calibrate_image() called the generic standby() helper before issuing CalibrateImage, which enters STDBY_XOSC on this TCXO-equipped board. The SX1262 datasheet requires CalibrateImage to run from STDBY_RC, same as the general Calibrate() command a few lines above it (which already forces STDBY_RC explicitly, with a comment to that effect) — calibrate() had the fix, calibrate_image() didn't. Symptom this produces: image/PLL calibration only actually runs when imageCalParams() reports a new band (_imageCalBand is cached per band), so switching regions (e.g. default Americas 915MHz to a configured Europe 868MHz frequency) is exactly when the wrong-mode calibration fires. The chip's subsequent SetRfFrequency write appears to apply in software, but the radio stays locked on the previously-calibrated band's frequency. Re-entering Settings and reapplying any LoRa value — even the same one — calls setFrequency() again, but by then _imageCalBand already matches the target band, so the broken calibration step is skipped and a clean SetRfFrequency goes through on an otherwise-idle chip, which is why that "fixes" it. Configured region was stuck on the boot default until a no-op Settings save; this fix resolves it. (cherry picked from commit a2ffda05c6105dca8d0b419f10d384e43c6159c7) --- src/radio/SX1262.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/radio/SX1262.cpp b/src/radio/SX1262.cpp index 9e333d8..4786582 100644 --- a/src/radio/SX1262.cpp +++ b/src/radio/SX1262.cpp @@ -242,7 +242,15 @@ bool SX1262::calibrate_image(uint32_t frequency) { if (_imageCalBand == band) return true; - standby(); + // CalibrateImage must be issued from STDBY_RC per datasheet, same as + // Calibrate() above. standby() would enter STDBY_XOSC on this TCXO + // board, which leaves the image/PLL calibration for the new band + // invalid — symptom: after switching bands (e.g. Americas -> Europe), + // SetRfFrequency below appears to apply, but the chip stays locked on + // the previously-calibrated band's frequency until a later call skips + // this (now band-cached) step and re-issues a clean SetRfFrequency. + uint8_t mode_byte = MODE_STDBY_RC_6X; + executeOpcode(OP_STANDBY_6X, &mode_byte, 1); executeOpcode(OP_CALIBRATE_IMAGE_6X, image_freq, 2); waitOnBusy(); _imageCalBand = band; From 5fa63d71cd4741932a26c20f168c101461cd8308 Mon Sep 17 00:00:00 2001 From: hardened_malloc <288914967+0x00001312@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:29:29 +0200 Subject: [PATCH 2/2] fix(radio): fix LoRa frequency stuck at default after first band switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setFrequency() to a new ISM band (e.g. Americas 915MHz -> Europe 868MHz on first applying a saved custom frequency) appeared to apply in software and logs, but the chip kept transmitting/receiving on the old frequency until the same value was reapplied a second time (e.g. by reopening Settings and confirming any radio value). The second call "worked" only because calibrate_image() caches per-band and skips recalibration once a band has been calibrated, landing a clean SetRfFrequency on an otherwise idle chip — masking that the first, real calibration attempt never actually got there. Root cause: waitOnBusy() capped its poll at 100ms. CalibrateImage on a fresh (uncached) band can legitimately run longer than that on this hardware; once the cap was hit, the function returned while BUSY was still genuinely asserted. The SX126x ignores SPI while busy, so the SetRfFrequency write issued immediately after by setFrequency() landed on a still-busy chip and was silently dropped, leaving the old frequency in effect even though _imageCalBand now (wrongly) recorded the new band as calibrated. Raise the waitOnBusy() cap to 1000ms (well beyond any documented SX126x opcode duration) and add the same settle delay calibrate() already has before its own waitOnBusy(), since CalibrateImage has the same BUSY-assertion-propagation gap. (cherry picked from commit 5e03e0df912aac592936f1e3b87902e71e80722a) --- src/radio/SX1262.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/radio/SX1262.cpp b/src/radio/SX1262.cpp index 4786582..a8526a7 100644 --- a/src/radio/SX1262.cpp +++ b/src/radio/SX1262.cpp @@ -197,7 +197,15 @@ void SX1262::waitOnBusy() { unsigned long t = millis(); if (_busy != -1) { while (digitalRead(_busy) == HIGH) { - if (millis() >= (t + 100)) break; + // CalibrateImage on a fresh (uncached) band can legitimately run + // longer than other opcodes. The old 100ms cap could give up + // while BUSY was still genuinely asserted — the chip ignores SPI + // while busy, so the SetRfFrequency write right after a band + // switch would land on a still-busy chip and get silently + // dropped, leaving the old frequency in effect. 1000ms is well + // beyond any documented SX126x opcode duration, so this only + // changes behavior in the case that was previously broken. + if (millis() >= (t + 1000)) break; yield(); } } @@ -252,6 +260,14 @@ bool SX1262::calibrate_image(uint32_t frequency) { uint8_t mode_byte = MODE_STDBY_RC_6X; executeOpcode(OP_STANDBY_6X, &mode_byte, 1); executeOpcode(OP_CALIBRATE_IMAGE_6X, image_freq, 2); + // Same as calibrate() above: BUSY needs a moment to assert after the + // opcode before waitOnBusy()'s poll loop means anything. Without this, + // waitOnBusy() can return immediately (BUSY hasn't risen yet) while + // CalibrateImage is still running, and the SetRfFrequency that + // setFrequency() issues right after lands mid-calibration and gets + // dropped — chip stays on the old band's frequency even though + // _imageCalBand now (wrongly) claims the new band is cached. + delay(5); waitOnBusy(); _imageCalBand = band; Serial.printf("[SX1262] Image calibrated for %lu Hz (0x%02X 0x%02X)\n",