Skip to content

fix(radio): fix SX1262 image calibration leaving radio on the wrong frequency#21

Open
0x00001312 wants to merge 2 commits into
ratspeak:mainfrom
cryptspeak:fix/sx1262-image-calibration
Open

fix(radio): fix SX1262 image calibration leaving radio on the wrong frequency#21
0x00001312 wants to merge 2 commits into
ratspeak:mainfrom
cryptspeak:fix/sx1262-image-calibration

Conversation

@0x00001312

@0x00001312 0x00001312 commented Jun 28, 2026

Copy link
Copy Markdown

Summary

Fixes the root cause behind #19: after switching to a configured frequency in a different ISM band from the 915MHz boot default, the SX1262 silently stays on the old frequency until any radio setting is reconfirmed in Settings without a reboot. That "fix" doesn't actually fix anything — it just skips the broken calibration path on the second call.

Two bugs in calibrate_image() / waitOnBusy() combine to cause this, both in src/radio/SX1262.cpp:

  1. calibrate_image() issued CalibrateImage from STDBY_XOSC, not STDBY_RC. The SX1262 datasheet requires STDBY_RC for this opcode, same as the general Calibrate() a few lines above it (which already forced STDBY_RC explicitly) — calibrate_image() was missing the same fix.

  2. waitOnBusy() capped its poll at 100ms. CalibrateImage on a fresh, never-calibrated band can legitimately run longer than that on this hardware. When the cap 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 — frequency stayed on the old band even though _imageCalBand now (wrongly) recorded the new band as calibrated.

Because _imageCalBand caches per-band and skips recalibration once a band is marked done, both bugs produce the same masking symptom: reopening Settings and reconfirming any value calls setFrequency() again, but _imageCalBand already matches the target band, so the broken calibration step is skipped entirely and a clean SetRfFrequency goes through on an otherwise idle chip — "fixing" it until the next reboot.

Changes

  • Issue CalibrateImage from STDBY_RC explicitly, matching Calibrate().
  • Raise the waitOnBusy() poll cap from 100ms to 1000ms (well beyond any documented SX126x opcode duration).
  • Add the same settle delay before waitOnBusy() that calibrate() already has, since CalibrateImage has the same BUSY-assertion-propagation gap.

Both fixes have been running on a downstream fork (rsCardputer-CE) since v0.0.5 with no recurrence of the stuck-frequency symptom.

Test plan

  • Builds clean for standalone_915 via pio run -e standalone_915
  • Switched from boot default (915MHz) to a custom 868MHz frequency via Settings, rebooted, radio came up on the new frequency immediately — no reconfirm-without-reboot workaround needed

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 a2ffda0)
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 5e03e0d)
@0x00001312

Copy link
Copy Markdown
Author

Tested both fixes on hardware: switched from the 915MHz boot default to a custom 868MHz frequency via Settings, rebooted, and the radio came up on the new frequency immediately — no reconfirm-without-reboot workaround needed.

@0x00001312

Copy link
Copy Markdown
Author

Worth calling out separately from the Settings-reconfirm symptom: this isn't a Settings-page quirk, it's the chip's actual RF tuning being wrong. SetRfFrequency sets the LO frequency used for both TX and RX on this half-duplex transceiver, so when the calibration/write gets dropped, the radio is stuck on the old band in both directions at once, not just on transmit.

That matches what was reported in #19: announces not getting through, and not receiving anything from other nodes on the same settings either, plus the SDR captures showing a faint/off-looking signal that grew into a vague "cloud" the more the announce button got spammed — consistent with the chip transmitting off the configured frequency the whole time.

I wouldn't extend this further without testing it directly — the bandwidth-reverting-after-reboot symptom mentioned earlier in the thread goes through a different opcode (SetModulationParams, not calibration), so I can't confirm it shares this exact root cause, just that it's the same general class of bug (same waitOnBusy() helper).

@0x00001312

Copy link
Copy Markdown
Author

One nuance worth flagging for review: this is a timing race, not a deterministic failure. Whether the SetRfFrequency write actually lands depends on how long CalibrateImage happens to take on a given chip versus the old (too-short) polling window, so it won't reproduce 100% of the time on every device or every boot. That's consistent with the original report in #19 — wiping the SD card and rebooting fresh worked once, then the same symptom came back after another reboot — and it's also why the reconfirm-without-reboot workaround doesn't reliably "fix" it for everyone: if a given boot's calibration happens to land within the timing window on its own, there's nothing for the workaround (or this fix) to correct in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant