no_std async driver for the HopeRF / Semtech RFM69 sub-GHz FSK
transceiver. Generic over embedded-hal and
embedded-hal-async 1.0 traits, so the same
driver runs on any MCU whose HAL provides them.
Two API layers
The crate exposes two ways to drive a radio. Pick whichever fits.
1. Low-level: [Rfm69]
[Rfm69] is the bare transceiver: register setters, send, recv,
and an [Rfm69::reset] that does the post-power-up version check.
Use it directly if your application owns the radio from a single
task and you want minimal indirection.
2. High-level: [Stack] / [Runner] (feature embassy)
For applications where multiple tasks want to share one radio — the
common embedded pattern of "always-listening node that occasionally
sends" — [Stack] and [Runner] provide an embassy-net-style
split:
- A long-running [
Runner] task owns the radio exclusively and arbitrates between TX requests and incoming packets viaembassy_futures::select. - User tasks hold cheap [
Stack] handles (Copy, clone freely) and call [Stack::send] / [Stack::recv] concurrently. The Runner serializes the half-duplex bus underneath. - A small MAC layer above the wire — [
Flags::Ack] retries with [MacTiming] knobs, broadcast filtering, RSSI capture — is provided by the Runner so user code stays simple. - Hardware health surfaces as [
LinkState]: a streak of [LINK_DOWN_STREAK] consecutiveTrxErrors flips the link toDown; the next success flips it back. Observe via [Stack::wait_link_up] / [Stack::wait_link_down].
See examples/rp/src/bin/concurrent_demo.rs in the repository for
the canonical Stack/Runner setup pattern.
The Transceiver boundary
Both layers meet at the [Transceiver] trait — a small async trait
with send / recv and a fixed [TrxError] vocabulary.
[Stack] / [Runner] are generic over TRX: Transceiver, so the
Stack is reusable against any backing radio (or a mock) that
implements the trait, not just Rfm69.
Quick start
Construction follows the pattern below. The full HAL plumbing
(SPI device, pin modes, async DMA setup) is HAL-specific; see the
examples/rp/ binaries for runnable code on the RP2040.
use rfm69_async::{config, Address, Flags, Rfm69, Stack, StackResources, MacTiming};
// 1. Construct the bare driver from your HAL's SPI / GPIO / delay.
let mut rfm = Rfm69::new(spi, reset_pin, Some(dio0_pin), delay);
// 2. Apply a preset configuration in place. The same call can be re-issued
// later (e.g. from a `Transceiver::recover` impl) to re-initialize after
// a hardware fault.
config::my_defaults(&mut rfm, /* network_id */ 0x42, /* freq Hz */ 868_000_000).await?;
// 3. Split into a Stack handle and a Runner task.
static mut RES: StackResources<4> = StackResources::new();
let (stack, mut runner) = Stack::new(rfm, Address::Unicast(1), unsafe { &mut RES }, MacTiming::default());
// 4. Spawn the runner (here in the same task via embassy_futures::join).
// In real apps, spawn it as an embassy_executor task instead.
embassy_futures::join::join(
runner.run(),
async {
stack.send(Address::Unicast(2), Flags::Ack(3), b"hello").await?;
let pkt = stack.recv().await;
Ok::<_, rfm69_async::TxError>(())
},
).await;
For the lower-level direct path, swap step 3+4 for rfm.send(&packet).await?
/ rfm.recv().await? calls in your own loop.
Optional DIO0 pin
[Rfm69::new] takes dio0: Option<DIO0>. With a pin connected, the
driver awaits hardware-driven PacketSent / PayloadReady events
via Wait::wait_for_high
— preferred. With None::<SomePin> the driver falls back to
polling IrqFlags2. Both modes are functionally equivalent; the
pin-driven path is cheaper on the CPU.
Cargo features
All features are off by default — opt in to what you need.
embassy— pulls inembassy-time/embassy-sync/embassy-futuresand enables the [Stack] / [Runner] surface (plus [MacTiming], [LinkState], [StackResources], [TxError]). Required for any of the high-level API.log— routes the driver's internalinfo!/debug!/warn!/error!macros through thelogfacade. Use this when piping output over USB CDC viaembassy-usb-logger, or any otherlog-based sink.defmt— derives [defmt::Format] on the public error / address / packet types and routes the driver's logging throughdefmt. Use fordefmt-rttover a debug probe.
log and defmt are not mutually exclusive — enabling both fans
the driver's log output to both backends. With neither enabled the
logging macros expand to no-ops that still consume their arguments
(so unused_variables warnings don't fire on debug-only locals).
Errors
Two error types deliberately exist:
- [
Error]<SPI, RESET, DIO0>— the parametric error returned by the inherentRfm69::*methods. Preserves the underlying HAL's error types verbatim, useful when you want to inspect the cause. - [
TrxError] — a fixed, lossy enum at the [Transceiver] / [Stack] boundary. The parametric error is collapsed here soStack/Runnerdon't need an extra error-type generic. Mirrors theembassy-net::Stackpattern.
MSRV
1.88. This section is the canonical explanation of the floor;
Cargo.toml, the CHANGELOG, and CI reference it rather than restate
it, so the reasoning lives in exactly one place.
The base crate (no features) builds on 1.87 — the floor
heapless = "0.9" sets; every other base dependency needs less. The
embassy feature is what raises it: the Stack/Runner code in
stack.rs uses a let-chain, stable since 1.88. A package declares a
single rust-version, and it has to cover the most-demanding feature,
so the declared floor is 1.88. No nightly features are used — the
crate builds on any stable toolchain at or above the floor.