feat(radio): adaptive LBT — scale CAD retries to current LoRa config
The CAD pre-TX loop previously ran up to 10 retries with a fixed 10 ms
sleep — roughly one max-length packet only at SF7/BW500. At SF10/BW125
the 100 ms total was ~4% of one packet; at SF12/BW125 LBT was a no-op.
Now the per-retry sleep is sized to one time-on-air of an assumed-peer
80-byte packet under the active config, cached on RadioContext and
recomputed on every SET_CONFIG. Retries scale as
clamp(floor(BUDGET/T)+1, 1, 10) with BUDGET=2.5 s, so fast configs
still get up to 10 retries while SF11/12 degrade to single-CAD-and-bail
(logged at config-apply). Sleeps are jittered ±20% per iteration to
decorrelate simultaneously-backing-off contenders.
The ToA math, CR→{1..4} mapping, retry-count derivation, and jitter
helper live in a pure src/lbt.rs module so they're host-testable via
DONGLORA_HOST_TEST=1 cargo test (13 tests).
Validated on-device: 20 iterations of the AI Bot test flow that
previously saw ~2 ChannelBusy errors now see zero at the same SF/BW.
This commit is contained in:
6 files changed
+370
-21
No files matched your search
@@ -8,8 +8,11 @@ pointing at `../../lora-rs/lora-phy`; drop the patch line + pre-release
|
||||
selector once upstream releases.
|
||||
|
||||
Hardware-verified on Wio Tracker L1: 20/20 clean-boot TX after the fix
|
||||
(vs ~50% before). Additive fix: adds pre-command BUSY check, keeps
|
||||
existing post-command wait in place.
|
||||
(vs ~50% before). Adds a pre-command BUSY check before each
|
||||
SpiInterface read/write op, keeping the existing post-command wait in
|
||||
place. Belt-and-suspenders bracketing: pre-wait per spec, post-wait
|
||||
closes the BUSY-assertion latency window (T_SW ≤ 600 ns) that a
|
||||
single level-poll can miss.
|
||||
|
||||
### PR body
|
||||
|
||||
@@ -44,9 +47,27 @@ On fast MCUs (nRF52840 at 64 MHz, release build) this is racy — `embedded-hal-
|
||||
|
||||
## Fix
|
||||
|
||||
Replace the post-command BUSY wait with a pre-command BUSY check at the start of each `SpiInterface` / `Lr1110SpiInterface` read/write method. Drop the redundant explicit `wait_on_busy()` calls in `sx126x` (after Calibrate, `ensure_ready` non-sleep arm) and `lr1110::mod` (4 regmem/bootloader sites) — all now covered by the interface pre-wait.
|
||||
Bracket each `SpiInterface` / `Lr1110SpiInterface` read/write op with
|
||||
both a pre- and post-command BUSY wait. The pre-wait is the new
|
||||
addition required by the spec; the existing post-wait is preserved as
|
||||
the belt that closes the BUSY-assertion latency window.
|
||||
|
||||
The `is_sleep_command` flag keeps its name but now gates the pre-wait; set `true` only when BUSY is held HIGH by design (the wake-from-sleep GetStatus pulse in `sx126x::ensure_ready`). Two callsite flips as a result: `ensure_ready` Sleep arm `false`→`true`; `set_sleep` `true`→`false`.
|
||||
Why pre-wait alone isn't enough: `embedded-hal-async`'s
|
||||
`wait_for_low` returns immediately when the pin is already low. The
|
||||
chip takes up to T_SW ns to assert BUSY after NSS rises, so a single
|
||||
polling chance — at any side of the SPI op — can see a stale LOW and
|
||||
race the next command into a still-busy chip. The repro is
|
||||
back-to-back writes inside `sx126x::do_cad` (SetCADParams immediately
|
||||
followed by SetCAD): pre-wait alone races, SetCAD is dropped, CADDone
|
||||
never fires, `lora.cad()` hangs forever. Keeping the post-wait gives a
|
||||
second polling chance after enough time has elapsed for BUSY to be
|
||||
reliably observable.
|
||||
|
||||
The `is_sleep_command` flag now gates both waits. One callsite flip:
|
||||
`sx126x::ensure_ready`'s wake-from-sleep GetStatus pulse passes
|
||||
`is_sleep_command=true` so the pre-wait doesn't hang on the
|
||||
BUSY-stuck-HIGH chip-asleep state. The next SPI op's pre-wait absorbs
|
||||
the wake-completion latency.
|
||||
|
||||
No public API signature changes.
|
||||
|
||||
|
||||
Reference in new issue
Block a user