diff --git a/CHANGELOG.md b/CHANGELOG.md index 18fb438..98605fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.4.0] - 2026-04-25 + +### Added + +- **RAK4631 WisBlock board support** (nRF52840 + SX1262, RAK19007 base + + RAK4631 Core + RAK1921 OLED). The second nRF52840 + SX1262 board + alongside the Wio Tracker L1. + - `SX126X_POWER_EN` (P1.05) driven HIGH at init — the radio is + power-gated on this module; without this the chip has no power + and SPI hangs. + - HFXO + LFXO external crystals selected in `embassy_nrf::Config`. + USB CDC-ACM needs ±500 ppm which the internal RC oscillator + can't provide. + - `SoftwareVbusDetect(true, true)` so USB enumerates without a + `CLOCK_POWER` interrupt handler. + - Green LED on P1.03 (RAK19007 Base LED1, active-HIGH) via + `SimpleLed`. + - Hardware-validated end-to-end: ADVERT round-trip with an external + receiver on US915. + - Added `debug-checkpoint` blink-map coverage at every TX-phase + boundary in `radio.rs`, including a new checkpoint between + `prepare_for_cad` and `cad()` so future bring-ups can distinguish + a modem-setup hang from a DIO1-wait hang. + +- **Adaptive listen-before-talk.** The CAD pre-TX retry loop scales to + the active LoRa config instead of running a fixed 10 retries × 10 ms + (which was roughly one max-length packet at SF7/BW500 and a no-op at + SF12/BW125). + - Per-retry sleep is sized to the time-on-air of an assumed-peer + 80-byte packet under the current SF/BW/CR — cached on + `RadioContext`, recomputed on every `SET_CONFIG`. + - Retry count = `clamp(floor(BUDGET/T)+1, 1, 10)` with `BUDGET = 2.5 + s`. Fast configs still get up to 10 retries; SF11/12 degrade to + single-CAD-and-bail (logged at config-apply). + - Sleeps jittered ±20% per iteration to decorrelate + simultaneously-backing-off contenders. + - ToA math, CR→{1..4} mapping, retry-count derivation, and jitter + helper live in a pure `src/lbt.rs` module — 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 sees zero at the same + SF/BW. + +### Fixed + +- **First-TX-after-boot reliability on SX1262 boards.** Picked up the + upstream `lora-phy` fix that moves the BUSY-handshake check from + *after* to *before* each SPI op, per Semtech SX1261/2 §8.3.1, LR1121 + UM §3, and LR1110 DS §1.2.5. The existing post-op wait is preserved + as the belt that closes the BUSY-assertion latency window + (T_SW ≤ 600 ns) that a single level-poll can miss. Consumed via + `[patch.crates-io]` against `swaits/lora-rs#fix/sx126x-busy-race` + until the upstream PR merges. + - Hardware-verified on Seeed Wio Tracker L1: 20/20 clean-boot TX + (vs ~50% before). + - Renames `InterfaceVariant::wait_on_busy` → `wait_until_ready` to + match the new semantics. +- **BUSY pin pull-down → none on nRF SX1262 boards.** SX1262's BUSY + pin has an internal 20 kΩ pull-up (datasheet p. 53); our + `Input::new(..., Pull::Down)` was contending with it, leaving BUSY + at an indeterminate voltage and intermittently mis-reading the + chip's ready state. Per `lora-rs/lora-rs#260`, switched to + `Pull::None` on both Wio Tracker L1 and RAK4631. The RAK4631 + appeared to work before, but was affected by the same class of bug + per the upstream issue — now deterministically correct rather than + "happens to work". + +### Changed + +- **Build-system DX.** `just flash`, `just flash-probe`, and + `just build` accept an optional comma-separated `extra_features` + argument that is appended to the board feature, e.g. `just flash + wio_tracker_l1 debug-checkpoint`. No more dropping to raw + `cargo + rust-objcopy + uf2 cp` for iteration loops. +- **`just` recipes now resolve all tools through `mise`** via + `set shell := ["mise", "exec", "--", "sh", "-c"]`. `cargo`, + `espflash`, `rust-objcopy`, `cargo-hex-to-uf2` etc. always pick up + the versions pinned in `mise.toml` regardless of `PATH` ordering. +- **`mise.toml` rust pins now carry ARM targets + clippy**, so a fresh + `mise install` is sufficient bootstrapping — no manual + `rustup target add` for `thumbv6m-none-eabi` / + `thumbv7em-none-eabihf` and no separate clippy install. +- **New `just flash-dfu` recipe** for the Adafruit nRF52 bootloader + (VID:PID `239a:0029`) via `adafruit-nrfutil` under `uvx`, with port + autodetect. +- **Per-board `_info` chain in the justfile** collapsed from a long + per-board `if/elif` ladder + per-board `:=` variables into a single + `case` statement — one place per board, two lists (names + + metadata). + ## [1.3.1] - 2026-04-23 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 26f7900..6ad2de1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,7 +586,7 @@ dependencies = [ [[package]] name = "donglora" -version = "1.3.1" +version = "1.4.0" dependencies = [ "cfg-if", "cortex-m", diff --git a/Cargo.toml b/Cargo.toml index 7b7c25a..63a3b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["."] [package] name = "donglora" -version = "1.3.1" +version = "1.4.0" edition = "2021" build = "build.rs" authors = ["Stephen Waits "] diff --git a/TODO.md b/TODO.md index b4ca18a..aab3a93 100644 --- a/TODO.md +++ b/TODO.md @@ -1,82 +1,30 @@ # Firmware TODO List -## `lora-phy` fix shipped locally; upstream PR pending submission +## `lora-phy` fix: drop fork pin once PR #428 merges + releases -Branch: [`swaits/lora-rs @ fix/sx126x-busy-race`](https://github.com/swaits/lora-rs/tree/fix/sx126x-busy-race). -PR not yet opened. Firmware consumes the fix via `[patch.crates-io]` -pointing at `../../lora-rs/lora-phy`; drop the patch line + pre-release -selector once upstream releases. +Upstream PR: [lora-rs/lora-rs#428 — *phy: add pre-command BUSY checks +per Semtech datasheets*](https://github.com/lora-rs/lora-rs/pull/428) +(open). +Fork branch we ship from: +[`swaits/lora-rs @ fix/sx126x-busy-race`](https://github.com/swaits/lora-rs/tree/fix/sx126x-busy-race). + +Firmware consumes the fix via a direct `git` dependency in +`Cargo.toml`: + +```toml +lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fix/sx126x-busy-race", features = ["defmt-03"] } +``` + +When PR #428 merges and a `lora-phy` release ships on crates.io that +contains it, switch this line back to a normal `lora-phy = "X.Y"` +crates.io version and drop the explanatory comment block above it. Hardware-verified on Wio Tracker L1: 20/20 clean-boot TX after the fix -(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 - -``` -Title: phy: pre-command BUSY check per Semtech datasheets - -## Problem - -While bringing up a downstream project (donglora), I hit a race where TX commands would occasionally fail. Tracked it down to lora-phy not following the BUSY-handshake algorithm Semtech recommends: the host should wait on BUSY *before* issuing each command, not after. - -Details: `SpiInterface::{write, write_with_payload, read, -read_with_status}` polls BUSY *after* each SPI op. - -On fast MCUs (nRF52840 at 64 MHz, release build) this is racy — `embedded-hal-async`'s `wait_for_low` returns immediately if the pin is already low, and the chip takes up to T_SW ns (SX1261/2 §8.3, Fig. 8-3) to assert BUSY after NSS rises. The host polls inside that pre-assertion window, sees a stale LOW, fires the next command while the chip is still busy, and the chip silently drops it. First `lora.tx()` after a clean boot on Seeed Wio Tracker L1 hangs ~50% of the time. - -## Authority - -- [SX1261/2 datasheet, DS.SX1261-2.W.APP Rev. 1.2](https://cdn.sparkfun.com/assets/6/b/5/1/4/SX1262_datasheet.pdf) §8.3.1 - "BUSY Control Line": *"it is essential to wait for the BUSY line to - go low before sending an SPI command."* -- [LR1121 User Manual, UM.LR1121.W.APP Rev. 1.2](https://files.waveshare.com/wiki/Core1121/UserManual_LR1121_v1_2.pdf) §3 - "Host-Controller Interface": *"it is necessary to check the status - of BUSY prior to sending a command."* (Applies to the LR11xx family - — see also [LR1110 DS Rev. 1.1](https://download.mikroe.com/documents/datasheets/LR1110_datasheet.pdf) §1.2.5.) -- [RadioLib](https://github.com/jgromes/RadioLib/blob/cffd5af/src/Module.cpp) - `Module::SPIwriteStream` / `SPIreadStream` call `waitForBusy()` at - the start of every transaction. -- The [SX1276/77/78/79 datasheet](https://cdn.sparkfun.com/assets/7/7/3/2/2/SX1276_Datasheet.pdf) - Table 2 exposes no BUSY pin, so - `GenericSx127xInterfaceVariant::wait_on_busy` stays a no-op and the - change is behaviorally identical for SX127x. - -## Fix - -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. - -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. - -## Test - -- `cargo clippy -p lora-phy --features defmt-03 -- -D warnings` clean. -- Hardware: Seeed Wio Tracker L1 (nRF52840 + SX1262), 20 consecutive - clean-boot TX cycles → 20/20 (vs ~50% before). -``` +(vs ~50% before). The fix adds a pre-command BUSY check before each +`SpiInterface` read/write op, keeping the existing post-command wait +in place — belt-and-suspenders bracketing that closes the +BUSY-assertion latency window (T_SW ≤ 600 ns) a single level-poll can +miss. ---