A compile-time, fixed-packet one-way link layer for the Nordic 2.4 GHz RADIO, over two backends — a bare exclusive RADIO driver and an MPSL timeslot backend. Four heterogeneous boards interoperate on one wire format: nRF52833, nRF52840, nRF5340 (net core), nRF54LM20.
┌──────────────────┐ fixed 6-byte state ┌──────────────────┐
│ Sender │ ─────────────────────▶ │ Receiver │
│ data slots │ │ one long window │
│ feedback listen │ ◀───────────────────── │ 3-byte beacon │
└──────────────────┘ per batch boundary └──────────────────┘
The sender publishes fixed-length state packets on a compile-time slot plan
(OneWayState<PAYLOAD, FEEDBACK_EVERY> + fixed_slot_plan); the receiver
keeps the newest value and answers once per batch with a 3-byte feedback
beacon — timing error, plus an addressed recall that pulls one sender
into a reliable negotiation phase. Details: docs/architecture.md
(protocol/timing), docs/negotiation.md (recall).
| Crate | Role |
|---|---|
thunders |
Protocol core: one-way modes, const slot planner, negotiation codec, StaticTdma, the Phy trait. no_std, board-agnostic. |
thunders-phy-nrf |
nRF PHY: bare (exclusive RADIO, TXIDLE/RXIDLE restart per packet) and mpsl (RADIO inside MPSL timeslots). Registers via nrf-pac. |
examples/{nrf52833,nrf52840,nrf5340,nrf54lm20}/{bare,mpsl} |
Board firmware; sender/receiver roles are separate binaries/Cargo features. |
MPSL is the stock alexmoon/nrf-sdc
git dependency; embassy HAL is upstream embassy-rs/embassy via
[patch.crates-io]. No vendored forks.
| Link | TX | RX | Throughput | Integrity |
|---|---|---|---|---|
| MPSL one-way, default | LM20 3333/s | 52840 | 3332 pkt/s | 0 CRC-bad, 0 invalid |
| MPSL one-way, default | 52833 2767/s | 52840 | 2767 pkt/s | 0 CRC-bad over 8.6M frames (~52 min); rare single-window invalid spikes (~0.003%) |
MPSL one-way, phase-align |
LM20 3306/s | 52840 | 3306 pkt/s | 0 CRC-bad over 83k frames; recall demo echo confirmed |
MPSL one-way, phase-align |
LM20 3331/s | 52833 | 3331 pkt/s | 0 CRC-bad, 0 invalid |
MPSL one-way, hopping |
LM20 3304/s | 52840 | 3267 pkt/s | 0 CRC-bad while hopping 8 channels |
| MPSL multi-sender | LM20 1666/s + 5340 1111/s | 52840 | 1623/s + 898/s | both senders demultiplexed concurrently |
| MPSL multi-sender | 52833 1384/s + 5340 1111/s | LM20 | 1373/s + 900/s | 52833 as sender 0, 99% delivery |
| Bare one-way | LM20 14705/s | 52840 | 14627 pkt/s | 0% loss, feedback flowing |
| Bare one-way | LM20 15183/s | 52833 | 15183 pkt/s | 0% loss (1 frame lost in 380k) |
Bare one-way, hopping |
LM20 14723/s | 52840 | 14625 pkt/s | 0% loss |
| Bare TDMA, 2 senders | 52840 5265/s + 5340 5221/s | LM20 | 5258/s + 5160/s | 0 invalid; ~2% CRC-bad from collisions |
| Bare TDMA, 2 senders | 52833 5264/s + 5340 5221/s | LM20 | 5263/s + 5183/s | 52833 as TDMA master; same collision noise |
| Feature | Effect |
|---|---|
phase-align |
Per-batch feedback event (receiver replies, sender locks). Enables recall. |
hopping |
Synchronized 8-channel hopping at the batch boundary (implies phase-align). |
multi-sender |
nrf52840 receiver demultiplexes up to 8 senders by RADIO logical address. |
sender |
nrf52833 full-rate one-way TX role (default build is the receiver). |
multi-sender-bench / multi-receiver |
The 2-TX/1-RX benchmark roles. |
sender-1 |
nrf54lm20 transmits with the 0xC3 prefix (second-sender role). |
The bare examples add a const-generic TDMA pair (--bin tdma-sender /
--bin tdma-receiver, shared schedule in examples/tdma_config.rs).
Prereqs: probe-rs, Rust targets thumbv7em-none-eabihf (nRF52833/52840),
thumbv8m.main-none-eabi (nRF5340), thumbv8m.main-none-eabihf (nRF54LM20).
# receiver: nRF52840 (DAPLink)
cd examples/nrf52840/mpsl
cargo build --release --no-default-features --features phase-align
probe-rs run --chip nRF52840_xxAA --probe "0d28:0204-3:0700..." \
target/thumbv7em-none-eabihf/release/thunders-mpsl
# sender: nRF54LM20 (J-Link; auto-detect fails, --chip is required)
cd examples/nrf54lm20/mpsl
cargo build --release --no-default-features --features phase-align
probe-rs run --verify --disable-double-buffering --chip nRF54LM20A \
--probe 1366:1069 target/thumbv8m.main-none-eabihf/release/thunders-mpslThe receiver's RTT log runs the recall demo (foreign prefix, addressed
recall, echo, release). The nRF5340 net core is debug-locked: every flash
needs --allow-erase-all.
- TXPOWER is encoded: 0 dBm =
0x18. Raw0x00leaves the PA off (the radio runs its TX state machine but emits no RF). - Event block at 0x200+: the nRF54L radio's events sit at 0x200-0x220 (the nRF5340's at 0x100-0x110).
- TXD/RXD DMA amounts: no SVD registers at 0xEE8/0xED4, but the writes are required on silicon (0-length PDUs / no RX otherwise).
- Errata 54L/49: the first on-air payload bits need a hidden register (0x5008C58C) set.
- Check flash integrity after post-flash boot faults: corrupted flash
is one observed failure mode, not a diagnosis for every boot fault.
With probe-rs's double-buffered flashing, dropped/duplicated words were
observed on the LM20 (RRAMC buffered write) and the 52833 (NVMC).
The next page buffer is downloaded over MEM-AP while the CPU runs the
flash algorithm; bus contention was the suspected mechanism.
Use
--disable-double-bufferingand--verifyon these targets. Corrupted images can fault semi-randomly (blob assert, null call, bus fault); resets cannot repair stored flash content. Both LM20 examples retain precise HardFault diagnostics (CFSR/HFSR/BFAR and stacked PC/R0/LR over defmt) to help distinguish failures. - Retracted: an early
RRAMC.LOWPOWERCONFIG = Standbywrite turned out unnecessary — the boot asserts attributed to fetch latency were flash-corruption artifacts. Don't cargo-cult it back.