Compare commits

...

10 Commits

28 changed files with 1756 additions and 220 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
target = "thumbv7em-none-eabi"

175
Cargo.lock generated
View File

@@ -80,6 +80,15 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "cortex-m" name = "cortex-m"
version = "0.7.7" version = "0.7.7"
@@ -88,6 +97,7 @@ checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
dependencies = [ dependencies = [
"bare-metal", "bare-metal",
"bitfield", "bitfield",
"critical-section",
"embedded-hal 0.2.7", "embedded-hal 0.2.7",
"volatile-register", "volatile-register",
] ]
@@ -118,6 +128,50 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.117",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.117",
]
[[package]]
name = "defmt"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
dependencies = [
"defmt 1.0.1",
]
[[package]] [[package]]
name = "defmt" name = "defmt"
version = "1.0.1" version = "1.0.1"
@@ -150,6 +204,16 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "defmt-rtt"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e"
dependencies = [
"critical-section",
"defmt 1.0.1",
]
[[package]] [[package]]
name = "document-features" name = "document-features"
version = "0.2.12" version = "0.2.12"
@@ -165,9 +229,11 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8" checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8"
dependencies = [ dependencies = [
"defmt 1.0.1",
"embassy-futures", "embassy-futures",
"embassy-hal-internal 0.3.0", "embassy-hal-internal 0.3.0",
"embassy-sync", "embassy-sync",
"embassy-time",
"embedded-hal 0.2.7", "embedded-hal 0.2.7",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-hal-async", "embedded-hal-async",
@@ -176,6 +242,38 @@ dependencies = [
"nb 1.1.0", "nb 1.1.0",
] ]
[[package]]
name = "embassy-executor"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06070468370195e0e86f241c8e5004356d696590a678d47d6676795b2e439c6b"
dependencies = [
"cortex-m",
"critical-section",
"defmt 1.0.1",
"document-features",
"embassy-executor-macros",
"embassy-executor-timer-queue",
]
[[package]]
name = "embassy-executor-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfdddc3a04226828316bf31393b6903ee162238576b1584ee2669af215d55472"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "embassy-executor-timer-queue"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c"
[[package]] [[package]]
name = "embassy-futures" name = "embassy-futures"
version = "0.1.2" version = "0.1.2"
@@ -199,6 +297,7 @@ checksum = "7f10ce10a4dfdf6402d8e9bd63128986b96a736b1a0a6680547ed2ac55d55dba"
dependencies = [ dependencies = [
"cortex-m", "cortex-m",
"critical-section", "critical-section",
"defmt 1.0.1",
"num-traits", "num-traits",
] ]
@@ -207,6 +306,9 @@ name = "embassy-net-driver"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d" checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d"
dependencies = [
"defmt 0.3.100",
]
[[package]] [[package]]
name = "embassy-stm32" name = "embassy-stm32"
@@ -219,15 +321,20 @@ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"block-device-driver", "block-device-driver",
"cfg-if", "cfg-if",
"chrono",
"cortex-m", "cortex-m",
"cortex-m-rt", "cortex-m-rt",
"critical-section", "critical-section",
"defmt 1.0.1",
"document-features", "document-features",
"embassy-embedded-hal", "embassy-embedded-hal",
"embassy-futures", "embassy-futures",
"embassy-hal-internal 0.4.0", "embassy-hal-internal 0.4.0",
"embassy-net-driver", "embassy-net-driver",
"embassy-sync", "embassy-sync",
"embassy-time",
"embassy-time-driver",
"embassy-time-queue-utils",
"embassy-usb-driver", "embassy-usb-driver",
"embassy-usb-synopsys-otg", "embassy-usb-synopsys-otg",
"embedded-can", "embedded-can",
@@ -263,6 +370,7 @@ checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"critical-section", "critical-section",
"defmt 1.0.1",
"embedded-io-async 0.6.1", "embedded-io-async 0.6.1",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@@ -277,6 +385,7 @@ checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"critical-section", "critical-section",
"defmt 1.0.1",
"document-features", "document-features",
"embassy-time-driver", "embassy-time-driver",
"embedded-hal 0.2.7", "embedded-hal 0.2.7",
@@ -294,12 +403,23 @@ dependencies = [
"document-features", "document-features",
] ]
[[package]]
name = "embassy-time-queue-utils"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454"
dependencies = [
"embassy-executor-timer-queue",
"heapless 0.8.0",
]
[[package]] [[package]]
name = "embassy-usb-driver" name = "embassy-usb-driver"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a" checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a"
dependencies = [ dependencies = [
"defmt 1.0.1",
"embedded-io-async 0.6.1", "embedded-io-async 0.6.1",
] ]
@@ -310,6 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288751f8eaa44a5cf2613f13cee0ca8e06e6638cb96e897e6834702c79084b23" checksum = "288751f8eaa44a5cf2613f13cee0ca8e06e6638cb96e897e6834702c79084b23"
dependencies = [ dependencies = [
"critical-section", "critical-section",
"defmt 1.0.1",
"embassy-sync", "embassy-sync",
"embassy-usb-driver", "embassy-usb-driver",
] ]
@@ -369,6 +490,9 @@ name = "embedded-io"
version = "0.7.1" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7"
dependencies = [
"defmt 1.0.1",
]
[[package]] [[package]]
name = "embedded-io-async" name = "embedded-io-async"
@@ -385,6 +509,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0"
dependencies = [ dependencies = [
"defmt 1.0.1",
"embedded-io 0.7.1", "embedded-io 0.7.1",
] ]
@@ -403,6 +528,12 @@ dependencies = [
"embedded-storage", "embedded-storage",
] ]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.32" version = "0.3.32"
@@ -461,6 +592,12 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "litrs" name = "litrs"
version = "1.0.0" version = "1.0.0"
@@ -491,6 +628,16 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "panic-probe"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a"
dependencies = [
"cortex-m",
"defmt 1.0.1",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -608,6 +755,7 @@ checksum = "a411079520dbccc613af73172f944b7cf97ba84e3bd7381a0352b6ec7bfef03b"
dependencies = [ dependencies = [
"cortex-m", "cortex-m",
"cortex-m-rt", "cortex-m-rt",
"defmt 0.3.100",
] ]
[[package]] [[package]]
@@ -615,13 +763,38 @@ name = "stm32wl-subghz"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cortex-m", "cortex-m",
"defmt", "defmt 1.0.1",
"embassy-stm32", "embassy-stm32",
"embassy-time", "embassy-time",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-hal-async", "embedded-hal-async",
] ]
[[package]]
name = "stm32wl-subghz-examples"
version = "0.1.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"defmt 1.0.1",
"defmt-rtt",
"embassy-embedded-hal",
"embassy-executor",
"embassy-stm32",
"embassy-sync",
"embassy-time",
"embedded-hal 1.0.0",
"embedded-hal-async",
"panic-probe",
"stm32wl-subghz",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"

View File

@@ -1,3 +1,54 @@
[package]
name = "stm32wl-subghz"
version = "0.1.0"
edition = "2024"
license = "MIT"
repository = "https://git.lusia.moe/lukrecja/stm32wl-subghz"
categories = [ "no-std", "embedded", "asynchronous" ]
keywords = [ "stm32wl", "stm32wle5jc", "lora", "fsk", "gfsk", "msk", "gmsk", "bpsk", "radio", "embedded-hal-async" ]
description = "Sub-GHz radio driver for STM32WL-series microcontrollers"
[workspace] [workspace]
members = [ "stm32wl-subghz" ] members = ["examples/stm32wle5jc"]
exclude = [ "examples/stm32wle5jc" ]
[features]
# Default chip for development/CI. Downstream users should use default-features = false
# and enable their specific chip feature.
default = ["stm32wle5jc"]
# Hardware abstraction layer, which enables embedded radio driver and all hardware deps
# When it's disabled, some parts of the code, such as BPSK frame decoding, can be used
hal = ["dep:embassy-stm32", "dep:embassy-time", "dep:defmt", "dep:cortex-m", "dep:embedded-hal", "dep:embedded-hal-async"]
# Single-core variants
stm32wle4c8 = ["hal", "embassy-stm32/stm32wle4c8"]
stm32wle4cb = ["hal", "embassy-stm32/stm32wle4cb"]
stm32wle4cc = ["hal", "embassy-stm32/stm32wle4cc"]
stm32wle4j8 = ["hal", "embassy-stm32/stm32wle4j8"]
stm32wle4jb = ["hal", "embassy-stm32/stm32wle4jb"]
stm32wle4jc = ["hal", "embassy-stm32/stm32wle4jc"]
stm32wle5c8 = ["hal", "embassy-stm32/stm32wle5c8"]
stm32wle5cb = ["hal", "embassy-stm32/stm32wle5cb"]
stm32wle5cc = ["hal", "embassy-stm32/stm32wle5cc"]
stm32wle5j8 = ["hal", "embassy-stm32/stm32wle5j8"]
stm32wle5jb = ["hal", "embassy-stm32/stm32wle5jb"]
stm32wle5jc = ["hal", "embassy-stm32/stm32wle5jc"]
# Dual-core variants
stm32wl54cc-cm0p = ["hal", "embassy-stm32/stm32wl54cc-cm0p"]
stm32wl54cc-cm4 = ["hal", "embassy-stm32/stm32wl54cc-cm4"]
stm32wl54jc-cm0p = ["hal", "embassy-stm32/stm32wl54jc-cm0p"]
stm32wl54jc-cm4 = ["hal", "embassy-stm32/stm32wl54jc-cm4"]
stm32wl55cc-cm0p = ["hal", "embassy-stm32/stm32wl55cc-cm0p"]
stm32wl55cc-cm4 = ["hal", "embassy-stm32/stm32wl55cc-cm4"]
stm32wl55jc-cm0p = ["hal", "embassy-stm32/stm32wl55jc-cm0p"]
stm32wl55jc-cm4 = ["hal", "embassy-stm32/stm32wl55jc-cm4"]
[dependencies]
embassy-stm32 = { version = "0.5.0", features = ["unstable-pac"], optional = true }
embassy-time = { version = "0.5.0", optional = true }
defmt = { version = "1.0.1", optional = true }
cortex-m = { version = "0.7.6", features = ["inline-asm"], optional = true }
embedded-hal = { version = "1.0.0", optional = true }
embedded-hal-async = { version = "1.0.0", optional = true }
[profile.release]
debug = 2
opt-level = 3

View File

@@ -4,12 +4,25 @@
Sub-GHZ SPI device radio driver for STM32WL-series microcontrollers, currently supporting LoRa and BPSK. Sub-GHZ SPI device radio driver for STM32WL-series microcontrollers, currently supporting LoRa and BPSK.
## Usage
Add the dependency with `default-features = false` and enable your chip's feature:
```toml
[dependencies]
stm32wl-subghz = { version = "0.1.0", default-features = false, features = ["stm32wle5jc"] }
```
A chip feature **must** be enabled. See `Cargo.toml` for the full list of supported chips (all STM32WLE single-core and STM32WL5x dual-core variants).
> The default feature enables `stm32wle5jc` for development convenience. Always use `default-features = false` in your project to avoid feature conflicts.
## Crates ## Crates
- [`stm32wl-subghz`](./stm32wl-subghz/) - the driver library - [`stm32wl-subghz`](.) - the driver library
- [`examples/stm32wle5jc`](./examples/stm32wle5jc/) - usage examples for the STM32WLE5JC-based boards - [`examples/stm32wle5jc`](./examples/stm32wle5jc/) - usage examples for the STM32WLE5JC-based boards
## License ## License
MIT MIT
[Latest Version]: https://img.shields.io/crates/v/stm32wl-subghz.svg [Latest Version]: https://img.shields.io/crates/v/stm32wl-subghz.svg
[crates.io]: https://crates.io/crates/stm32wl-subghz [crates.io]: https://crates.io/crates/stm32wl-subghz

View File

@@ -6,7 +6,7 @@ license = "MIT"
publish = false publish = false
[dependencies] [dependencies]
stm32wl-subghz = { path = "../../stm32wl-subghz" } stm32wl-subghz = { path = "../.." }
embassy-stm32 = { version = "0.5.0", features = ["defmt", "stm32wle5jc", "time-driver-tim1", "memory-x", "unstable-pac", "exti", "chrono"] } embassy-stm32 = { version = "0.5.0", features = ["defmt", "stm32wle5jc", "time-driver-tim1", "memory-x", "unstable-pac", "exti", "chrono"] }
embassy-sync = { version = "0.7.2", features = ["defmt"] } embassy-sync = { version = "0.7.2", features = ["defmt"] }

View File

@@ -9,7 +9,6 @@ use embassy_stm32::{
rcc::{MSIRange, Sysclk, mux}, rcc::{MSIRange, Sysclk, mux},
spi::Spi, spi::Spi,
}; };
use embassy_time::{Duration, Timer};
use stm32wl_subghz::{ use stm32wl_subghz::{
Configure, PaSelection, Radio, SubGhzSpiDevice, Transmit, Configure, PaSelection, Radio, SubGhzSpiDevice, Transmit,
modulations::bpsk::{Bitrate, BpskConfig, BpskRadio}, modulations::bpsk::{Bitrate, BpskConfig, BpskRadio},
@@ -39,19 +38,15 @@ async fn main(_spawner: Spawner) {
frequency: 868_100_000, frequency: 868_100_000,
bitrate: Bitrate::Bps600, bitrate: Bitrate::Bps600,
pa: PaSelection::HighPower, pa: PaSelection::HighPower,
power_dbm: 17, power_dbm: 22,
..Default::default() ..Default::default()
}) })
.await .await
.unwrap(); .unwrap();
info!("sending stuffs"); info!("sending bpsk stuffs");
match bpsk.tx(b"hiiiii!").await { match bpsk.tx(b"hiiiii hello :3 :3 :3 this is a looooooooooooooooong text! very long :> and cute! :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 ummmm urghhh awwwooooooo woof wooooof woof").await {
Ok(_) => info!("yay :3"), Ok(_) => info!("yay tx done :3"),
Err(e) => error!("tx error: {:?}", e), Err(e) => error!("tx error: {:?}", e),
} }
loop {
Timer::after(Duration::from_secs(1)).await;
}
} }

View File

@@ -0,0 +1,56 @@
#![no_std]
#![no_main]
use defmt::{error, info, warn};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use stm32wl_subghz::{
Configure, PaSelection, Radio, RadioError, Receive, SubGhzSpiDevice,
modulations::fsk::{Bandwidth, Bitrate, FreqDev, FskConfig, FskRadio},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
let rf_tx = Output::new(p.PC4, Level::Low, Speed::High);
let rf_rx = Output::new(p.PC5, Level::Low, Speed::High);
let rf_en = Output::new(p.PC3, Level::Low, Speed::High);
let mut radio = Radio::new(spi, rf_tx, rf_rx, rf_en);
radio.init().await.unwrap();
let mut fsk = FskRadio::new(&mut radio);
fsk.configure(&FskConfig {
frequency: 868_100_000,
bitrate: Bitrate::Custom(600),
fdev: FreqDev::Hz(380),
bandwidth: Bandwidth::Bw4_8kHz, // >= 2 * (380 + 600/2) = 1.36kHz
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
info!("waiting for fsk stuffs...");
let mut buf = [0u8; 255];
match fsk.rx(&mut buf, 5_000).await {
Ok(len) => info!("yay :3 got {} bytes: {:x}", len, &buf[..len]),
Err(RadioError::Timeout) => warn!("nobody is talking to me :<"),
Err(e) => error!("rx error: {:?}", e),
}
}

View File

@@ -0,0 +1,53 @@
#![no_std]
#![no_main]
use defmt::{error, info};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use stm32wl_subghz::{
Configure, PaSelection, Radio, SubGhzSpiDevice, Transmit,
modulations::fsk::{Bitrate, FreqDev, FskConfig, FskRadio},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
let rf_tx = Output::new(p.PC4, Level::Low, Speed::High);
let rf_rx = Output::new(p.PC5, Level::Low, Speed::High);
let rf_en = Output::new(p.PC3, Level::Low, Speed::High);
let mut radio = Radio::new(spi, rf_tx, rf_rx, rf_en);
radio.init().await.unwrap();
let mut fsk = FskRadio::new(&mut radio);
fsk.configure(&FskConfig {
frequency: 868_100_000,
bitrate: Bitrate::Custom(600),
fdev: FreqDev::Hz(380),
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
info!("sending fsk stuffs");
match fsk.tx(b"hiiiii hello :3 :3 :3 this is a looooooooooooooooong text! very long :> and cute! :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 ummmm urghhh awwwooooooo woof wooooof woof").await {
Ok(_) => info!("yay tx done :3"),
Err(e) => error!("tx error: {:?}", e),
}
}

View File

@@ -0,0 +1,60 @@
#![no_std]
#![no_main]
use defmt::{error, info, warn};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use embassy_time::{Duration, Timer};
use stm32wl_subghz::{
Configure, PaSelection, Radio, RadioError, Receive, SubGhzSpiDevice,
modulations::lora::{Bandwidth, LoraConfig, LoraRadio, SpreadingFactor},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
let rf_tx = Output::new(p.PC4, Level::Low, Speed::High);
let rf_rx = Output::new(p.PC5, Level::Low, Speed::High);
let rf_en = Output::new(p.PC3, Level::Low, Speed::High);
let mut radio = Radio::new(spi, rf_tx, rf_rx, rf_en);
radio.init().await.unwrap();
let mut lora = LoraRadio::new(&mut radio);
lora.configure(&LoraConfig {
frequency: 868_100_000,
sf: SpreadingFactor::SF9,
bw: Bandwidth::Bw20_83kHz,
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
info!("waiting for lora stuffs...");
let mut buf = [0u8; 255];
match lora.rx(&mut buf, 5_000).await {
Ok(len) => info!("yay :3 got {} bytes: {:x}", len, &buf[..len]),
Err(RadioError::Timeout) => warn!("nobody is talking to me :<"),
Err(e) => error!("rx error: {:?}", e),
}
loop {
Timer::after(Duration::from_secs(1)).await;
}
}

View File

@@ -0,0 +1,53 @@
#![no_std]
#![no_main]
use defmt::{error, info};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use stm32wl_subghz::{
Configure, PaSelection, Radio, SubGhzSpiDevice, Transmit,
modulations::lora::{Bandwidth, LoraConfig, LoraRadio, SpreadingFactor},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
let rf_tx = Output::new(p.PC4, Level::Low, Speed::High);
let rf_rx = Output::new(p.PC5, Level::Low, Speed::High);
let rf_en = Output::new(p.PC3, Level::Low, Speed::High);
let mut radio = Radio::new(spi, rf_tx, rf_rx, rf_en);
radio.init().await.unwrap();
let mut lora = LoraRadio::new(&mut radio);
lora.configure(&LoraConfig {
frequency: 868_100_000,
sf: SpreadingFactor::SF9,
bw: Bandwidth::Bw20_83kHz,
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
info!("sending lora stuffs");
match lora.tx(b"hiiiii hello :3 :3 :3 this is a looooooooooooooooong text! very long :> and cute! :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 ummmm urghhh awwwooooooo woof wooooof woof").await {
Ok(_) => info!("yay tx done :3"),
Err(e) => error!("tx error: {:?}", e),
}
}

View File

@@ -0,0 +1,55 @@
#![no_std]
#![no_main]
use defmt::{error, info, warn};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use stm32wl_subghz::{
Configure, PaSelection, Radio, RadioError, Receive, SubGhzSpiDevice,
modulations::msk::{Bitrate, MskConfig, MskRadio, PulseShape},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
let rf_tx = Output::new(p.PC4, Level::Low, Speed::High);
let rf_rx = Output::new(p.PC5, Level::Low, Speed::High);
let rf_en = Output::new(p.PC3, Level::Low, Speed::High);
let mut radio = Radio::new(spi, rf_tx, rf_rx, rf_en);
radio.init().await.unwrap();
let mut msk = MskRadio::new(&mut radio);
msk.configure(&MskConfig {
frequency: 868_100_000,
bitrate: Bitrate::Custom(600),
pulse_shape: PulseShape::GaussianBt05,
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
info!("waiting for msk stuffs...");
let mut buf = [0u8; 255];
match msk.rx(&mut buf, 5_000).await {
Ok(len) => info!("yay :3 got {} bytes: {:x}", len, &buf[..len]),
Err(RadioError::Timeout) => warn!("nobody is talking to me :<"),
Err(e) => error!("rx error: {:?}", e),
}
}

View File

@@ -0,0 +1,53 @@
#![no_std]
#![no_main]
use defmt::{error, info};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use stm32wl_subghz::{
Configure, PaSelection, Radio, SubGhzSpiDevice, Transmit,
modulations::msk::{Bitrate, MskConfig, MskRadio, PulseShape},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
{
config.rcc.msi = Some(MSIRange::RANGE48M);
config.rcc.sys = Sysclk::MSI;
config.rcc.mux.rngsel = mux::Rngsel::MSI;
config.enable_debug_during_sleep = true;
}
let p = embassy_stm32::init(config);
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
let rf_tx = Output::new(p.PC4, Level::Low, Speed::High);
let rf_rx = Output::new(p.PC5, Level::Low, Speed::High);
let rf_en = Output::new(p.PC3, Level::Low, Speed::High);
let mut radio = Radio::new(spi, rf_tx, rf_rx, rf_en);
radio.init().await.unwrap();
let mut msk = MskRadio::new(&mut radio);
msk.configure(&MskConfig {
frequency: 868_100_000,
bitrate: Bitrate::Custom(600),
pulse_shape: PulseShape::GaussianBt05,
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
info!("sending msk stuffs");
match msk.tx(b"hiiiii hello :3 :3 :3 this is a looooooooooooooooong text! very long :> and cute! :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 ummmm urghhh awwwooooooo woof wooooof woof").await {
Ok(_) => info!("yay tx done :3"),
Err(e) => error!("tx error: {:?}", e),
}
}

1
rust-analyzer.toml Normal file
View File

@@ -0,0 +1 @@
linkedProjects = ["Cargo.toml", "examples/stm32wle5jc/Cargo.toml"]

View File

@@ -1,13 +1,22 @@
#![no_std] #![cfg_attr(feature = "hal", no_std)]
#![allow(async_fn_in_trait)] #![allow(async_fn_in_trait)]
pub mod error;
pub mod modulations; pub mod modulations;
#[cfg(feature = "hal")]
pub mod error;
#[cfg(feature = "hal")]
pub mod radio; pub mod radio;
#[cfg(feature = "hal")]
pub mod spi; pub mod spi;
#[cfg(feature = "hal")]
pub mod traits; pub mod traits;
#[cfg(feature = "hal")]
pub use error::RadioError; pub use error::RadioError;
#[cfg(feature = "hal")]
pub use radio::{PaSelection, Radio}; pub use radio::{PaSelection, Radio};
#[cfg(feature = "hal")]
pub use spi::SubGhzSpiDevice; pub use spi::SubGhzSpiDevice;
#[cfg(feature = "hal")]
pub use traits::{Configure, Receive, Transmit}; pub use traits::{Configure, Receive, Transmit};

500
src/modulations/bpsk.rs Normal file
View File

@@ -0,0 +1,500 @@
#[cfg(feature = "hal")]
use defmt::debug;
#[cfg(feature = "hal")]
use embedded_hal::digital::OutputPin;
#[cfg(feature = "hal")]
use embedded_hal_async::spi::SpiDevice;
#[cfg(feature = "hal")]
use crate::{
RadioError,
radio::{PaSelection, PacketType, Radio, RampTime, irq},
traits::{Configure, Transmit},
};
#[cfg(not(feature = "hal"))]
use std::vec::Vec;
/// BPSK bitrate
/// Formula: register = (32 * 32_000_000) / bps
#[derive(Clone, Copy)]
#[cfg_attr(feature = "hal", derive(defmt::Format))]
pub enum Bitrate {
/// 100 bits per second
Bps100,
/// 600 bits per second
Bps600,
/// Arbitrary bitrate in bits per second
Custom(u32),
}
impl Bitrate {
/// Get the 3-byte register value for this bitrate
pub fn to_bytes(self) -> [u8; 3] {
let val = match self {
Bitrate::Bps100 => 0x9C4000,
Bitrate::Bps600 => 0x1A0AAA,
Bitrate::Custom(bps) => (32 * 32_000_000) / bps,
};
[(val >> 16) as u8, (val >> 8) as u8, val as u8]
}
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "hal", derive(defmt::Format))]
pub enum CrcType {
None,
/// Using a common 0x07 polynomial
Crc8,
/// CRC-16 CCITT using 0x1021 polynomial
Crc16,
}
impl CrcType {
pub fn compute(self, data: &[u8]) -> (u16, usize) {
match self {
CrcType::None => (0, 0),
CrcType::Crc8 => {
let mut crc: u8 = 0x00;
for &byte in data {
crc ^= byte;
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0x07;
} else {
crc <<= 1;
}
}
}
(crc as u16, 1)
}
CrcType::Crc16 => {
let mut crc: u16 = 0xFFFF;
for &byte in data {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
(crc, 2)
}
}
}
pub fn write(self, crc: u16, buf: &mut [u8]) {
match self {
CrcType::None => {}
CrcType::Crc8 => {
buf[0] = crc as u8;
}
CrcType::Crc16 => {
buf[0] = (crc >> 8) as u8;
buf[1] = crc as u8;
}
}
}
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "hal", derive(defmt::Format))]
pub enum Whitening {
None,
Ccitt,
}
impl Whitening {
pub fn apply(self, seed: u16, data: &mut [u8]) {
match self {
Whitening::None => return,
Whitening::Ccitt => {}
}
// Calculate CCITT whitening using x^9 + x^4 + 1 polynomial and LFSR
let mut lfsr: u16 = seed & 0x1FF;
for byte in data.iter_mut() {
let mut mask = 0u8;
for bit in 0..8 {
let feedback = ((lfsr >> 8) ^ (lfsr >> 3)) & 1;
lfsr = ((lfsr << 1) | feedback) & 0x1FF;
mask |= (feedback as u8) << (7 - bit);
}
*byte ^= mask;
}
}
}
#[cfg(not(feature = "hal"))]
pub struct DecodeResult {
/// On which bit the sync word was found
pub bit_offset: usize,
/// Signal was phase-inverted
pub inverted: bool,
/// Decoded payload
pub payload: Vec<u8>,
/// CRC matches
pub crc_valid: bool,
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "hal", derive(defmt::Format))]
pub enum BpskPacket {
/// No framing, just send raw data
Raw,
/// Use a configurable framing
Framing {
/// Length of the preamble (0xAA) in bytes
preamble_len: usize,
/// Synchronization word (max 32 bytes)
sync_word: [u8; 32],
/// Sync word length
sync_word_len: usize,
/// CRC size (0, 1 or 2 bytes)
crc_type: CrcType,
/// Whitening algorithm
whitening: Whitening,
/// Whitening LFSR seed (9-bit, 0x000..0x1FF)
whitening_seed: u16,
},
}
impl BpskPacket {
pub fn default() -> Self {
Self::Framing {
preamble_len: 32,
// Baker-13 code (2 bytes)
sync_word: [
0x1F, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
],
sync_word_len: 2,
crc_type: CrcType::Crc16,
whitening: Whitening::Ccitt,
whitening_seed: 0x1FF,
}
}
#[cfg(feature = "hal")]
pub fn to_bytes(self, payload: &[u8], buf: &mut [u8]) -> Result<u8, RadioError> {
match self {
BpskPacket::Raw => {
// Simple copy operation, no modifications made
buf[..payload.len()].copy_from_slice(payload);
payload
.len()
.try_into()
.map_err(|_| RadioError::PayloadTooLarge)
}
BpskPacket::Framing {
preamble_len,
sync_word,
sync_word_len,
crc_type,
whitening,
whitening_seed,
} => {
let len_field_size = 1;
let crc_size = match crc_type {
CrcType::None => 0,
CrcType::Crc8 => 1,
CrcType::Crc16 => 2,
};
// Validate packet length
let total =
preamble_len + sync_word_len + len_field_size + payload.len() + crc_size;
if total > buf.len() {
return Err(RadioError::PayloadTooLarge);
}
// Keeps track of the current position in the buffer
let mut pos = 0;
// Write preamble which consists of 0xAA symbols
buf[pos..pos + preamble_len].fill(0xAA);
pos += preamble_len;
// Write sync word
buf[pos..pos + sync_word_len].copy_from_slice(&sync_word[..sync_word_len]);
pos += sync_word_len;
// Actual payload starts here
let data_start = pos;
// Write length info
let payload_len: u8 = payload
.len()
.try_into()
.map_err(|_| RadioError::PayloadTooLarge)?;
buf[pos] = payload_len;
pos += 1;
// Copy the original payload itself
buf[pos..pos + payload.len()].copy_from_slice(payload);
pos += payload.len();
// Compute CRC before the whitening
let (crc, crc_len) = crc_type.compute(&buf[data_start..pos]);
crc_type.write(crc, &mut buf[pos..]);
pos += crc_len;
// Apply whitening
whitening.apply(whitening_seed, &mut buf[data_start..pos]);
// Additional validation - if buffer position can't fit in u8, it's invalid
pos.try_into().map_err(|_| RadioError::PayloadTooLarge)
}
}
}
#[cfg(not(feature = "hal"))]
pub fn decode(&self, data: &[u8]) -> Vec<DecodeResult> {
match self {
// Can't do much with Raw packets so just return the same data and accept as valid
BpskPacket::Raw => vec![DecodeResult {
bit_offset: 0,
inverted: false,
payload: data.to_vec(),
crc_valid: true,
}],
BpskPacket::Framing {
preamble_len: _,
sync_word,
sync_word_len,
crc_type,
whitening,
whitening_seed,
} => {
let sync_word = &sync_word[..*sync_word_len];
let sync_bits = sync_word_len * 8;
let total_bits = data.len() * 8;
let mut results = Vec::new();
let crc_len = match crc_type {
CrcType::None => 0,
CrcType::Crc8 => 1,
CrcType::Crc16 => 2,
};
// Go through every bit, giving space to the sync word length
for i in 0..total_bits.saturating_sub(sync_bits) {
let mut matching: usize = 0;
for j in 0..sync_bits {
// Compare the sync word bit-by-bit against the data stream
let data_bit = (data[(i + j) / 8] >> (7 - ((i + j) % 8))) & 1;
let sync_bit = (sync_word[j / 8] >> (7 - (j % 8))) & 1;
if data_bit == sync_bit {
matching += 1;
}
}
// Allow up to 2 bit errors, if <= 2 bits match or unmatch, it's an accepted
// normal or phase-inverted data
let inverted = if sync_bits - matching <= 2 {
false
} else if matching <= 2 {
true
} else {
continue;
};
// Extract bytes that happen after the sync word is found at any bit offset
let bit_start = i + sync_bits;
let remaining_bytes = (total_bits - bit_start) / 8;
if remaining_bytes == 0 {
continue;
}
// Reassemble bytes from arbitrary bit positions after sync word is found at offset `i`
let mut raw = vec![0u8; remaining_bytes];
for b in 0..remaining_bytes {
let mut byte = 0u8;
for bit in 0..8 {
let idx = bit_start + b * 8 + bit;
let mut val = (data[idx / 8] >> (7 - (idx % 8))) & 1;
// Handle the 180 degree phase ambiguity
if inverted {
val ^= 1;
}
byte |= val << (7 - bit);
}
raw[b] = byte;
}
// De-whiten by applying the same whitening operation again (it's reversible)
whitening.apply(*whitening_seed, &mut raw);
// Extract payload length
let (payload_start, payload_len) = (1, raw[0] as usize);
if payload_len == 0 {
continue;
}
if payload_start + payload_len + crc_len > raw.len() {
continue;
}
// Verify CRC over len field and payload
let crc_data = &raw[..payload_start + payload_len];
let (crc_computed, _) = crc_type.compute(crc_data);
let crc_valid = match crc_type {
CrcType::None => true,
CrcType::Crc8 => raw[payload_start + payload_len] == crc_computed as u8,
CrcType::Crc16 => {
// Assemble u16 from two bytes
let received = ((raw[payload_start + payload_len] as u16) << 8)
| raw[payload_start + payload_len + 1] as u16;
received == crc_computed
}
};
results.push(DecodeResult {
bit_offset: i,
inverted,
payload: raw[payload_start..payload_start + payload_len].to_vec(),
crc_valid,
});
}
results
}
}
}
}
#[cfg(feature = "hal")]
#[derive(Clone, Copy, defmt::Format)]
pub struct BpskConfig {
pub frequency: u32,
pub bitrate: Bitrate,
pub pa: PaSelection,
pub power_dbm: i8,
pub ramp: RampTime,
pub packet: BpskPacket,
}
#[cfg(feature = "hal")]
impl Default for BpskConfig {
fn default() -> Self {
Self {
frequency: 868_100_000,
bitrate: Bitrate::Bps600,
pa: PaSelection::LowPower,
power_dbm: 14,
ramp: RampTime::Us40,
packet: BpskPacket::default(),
}
}
}
/// BPSK modulation - borrows a Radio, implements Configure + Transmit
#[cfg(feature = "hal")]
pub struct BpskRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
radio: &'a mut Radio<SPI, TX, RX, EN>,
payload_len: u8,
config: BpskConfig,
}
#[cfg(feature = "hal")]
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
BpskRadio<'a, SPI, TX, RX, EN>
{
pub fn new(radio: &'a mut Radio<SPI, TX, RX, EN>) -> Self {
Self {
radio,
payload_len: 0,
config: BpskConfig::default(),
}
}
/// Re-send SetPacketParams with updated payload length (called before each tx/rx)
async fn update_payload_len(&mut self, len: u8) -> Result<(), RadioError> {
debug!("Updating payload length to {}", len);
if len == self.payload_len {
return Ok(());
}
self.payload_len = len;
self.send_packet_params(len).await
}
/// Send the full SetPacketParams command with the given payload length
async fn send_packet_params(&mut self, payload_len: u8) -> Result<(), RadioError> {
self.radio.set_packet_params(&[payload_len]).await
}
}
#[cfg(feature = "hal")]
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
for BpskRadio<'_, SPI, TX, RX, EN>
{
type Config = BpskConfig;
async fn configure(&mut self, config: &Self::Config) -> Result<(), RadioError> {
self.config = *config;
// Select BPSK packet type
self.radio.set_packet_type(PacketType::Bpsk).await?;
// Payload length updated per tx
self.send_packet_params(0).await?;
// RF frequency
self.radio.set_rf_frequency(config.frequency).await?;
// Set modulation params (bitrate + Gaussian BT 0.5 pulse shape)
let br = config.bitrate.to_bytes();
self.radio
.set_modulation_params(&[br[0], br[1], br[2], 0x16])
.await?;
// PA config + TX power
self.radio
.set_output_power(config.pa, config.power_dbm, config.ramp)
.await?;
Ok(())
}
}
#[cfg(feature = "hal")]
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Transmit
for BpskRadio<'_, SPI, TX, RX, EN>
{
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError> {
let mut buf = [0u8; 255];
// Convert buffer to packet with chosen framing
let len = self.config.packet.to_bytes(data, &mut buf)?;
// Write payload to radio buffer
self.radio.set_buffer_base(0x00, 0x00).await?;
self.radio.write_buffer(0x00, &buf[..len as usize]).await?;
// Update packet params with actual payload length
self.update_payload_len(len).await?;
// Clear any stale IRQ flags before starting TX
self.radio.clear_irq(irq::ALL).await?;
// Enable TxDone IRQ on DIO1
self.radio.set_dio1_irq(irq::TX_DONE | irq::TIMEOUT).await?;
// Start TX
self.radio.set_tx(0).await?;
// Wait until it's done or until timeout
let status = self.radio.poll_irq(irq::TX_DONE | irq::TIMEOUT).await?;
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
Ok(())
}
}

380
src/modulations/fsk.rs Normal file
View File

@@ -0,0 +1,380 @@
use defmt::{debug, trace};
use embedded_hal::digital::OutputPin;
use embedded_hal_async::spi::SpiDevice;
use crate::{
RadioError,
radio::{PaSelection, PacketType, Radio, RampTime, RxGain, irq},
traits::{Configure, Receive, Transmit},
};
/// (G)FSK bitrate
/// Formula: register = 32 * 32 MHz / bitrate
#[derive(Clone, Copy, defmt::Format)]
pub enum Bitrate {
/// Arbitrary bitrate in bits per second
Custom(u32),
}
impl Bitrate {
/// Get the 3-byte BR register value (FSK formula: 32 * fxosc / bitrate)
pub fn to_bytes(self) -> [u8; 3] {
let val = (32u64 * 32_000_000) / self.bps() as u64;
[(val >> 16) as u8, (val >> 8) as u8, val as u8]
}
/// Get the raw bitrate in bps
pub fn bps(self) -> u32 {
match self {
Bitrate::Custom(bps) => bps,
}
}
}
/// Gaussian pulse shape filter
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum PulseShape {
/// No filter applied
None = 0x00,
/// Gaussian filter BT 0.3
GaussianBt03 = 0x08,
/// Gaussian filter BT 0.5
GaussianBt05 = 0x09,
/// Gaussian filter BT 0.7
GaussianBt07 = 0x0A,
/// Gaussian filter BT 1.0
GaussianBt10 = 0x0B,
}
/// FSK receiver bandwidth
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum Bandwidth {
Bw4_8kHz = 0x1F,
Bw5_8kHz = 0x17,
Bw7_3kHz = 0x0F,
Bw9_7kHz = 0x1E,
Bw11_7kHz = 0x16,
Bw14_6kHz = 0x0E,
Bw19_5kHz = 0x1D,
Bw23_4kHz = 0x15,
Bw29_3kHz = 0x0D,
Bw39kHz = 0x1C,
Bw46_9kHz = 0x14,
Bw58_6kHz = 0x0C,
Bw78_2kHz = 0x1B,
Bw93_8kHz = 0x13,
Bw117_3kHz = 0x0B,
Bw156_2kHz = 0x1A,
Bw187_2kHz = 0x12,
Bw234_3kHz = 0x0A,
Bw312kHz = 0x19,
Bw373_6kHz = 0x11,
Bw467kHz = 0x09,
}
/// CRC type for FSK packet params
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum CrcType {
Off = 0x01,
/// 1-byte CRC
Crc1Byte = 0x00,
/// 2-byte CRC
Crc2Byte = 0x02,
/// 1-byte CRC inverted
Crc1ByteInv = 0x04,
/// 2-byte CRC inverted
Crc2ByteInv = 0x06,
}
/// Preamble detection length
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum PreambleDetLength {
/// Preamble detection disabled
Off = 0x00,
/// 8-bit preamble detection
Bits8 = 0x04,
/// 16-bit preamble detection
Bits16 = 0x05,
/// 24-bit preamble detection
Bits24 = 0x06,
/// 32-bit preamble detection
Bits32 = 0x07,
}
/// Address comparison/filtering mode
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum AddrComp {
/// Address filtering disabled
Off = 0x00,
/// Filter on node address
Node = 0x01,
/// Filter on node and broadcast addresses
NodeBroadcast = 0x02,
}
/// Packet length type
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum PacketLengthType {
/// Fixed payload length, no header
Fixed = 0x00,
/// Variable payload length, header added to packet
Variable = 0x01,
}
/// Frequency deviation
/// Formula: register = deviation_hz * 2^25 / 32 MHz
#[derive(Clone, Copy, defmt::Format)]
pub enum FreqDev {
/// Deviation in Hz
Hz(u32),
}
impl FreqDev {
/// Get the 3-byte Fdev register value
pub fn to_bytes(self) -> [u8; 3] {
let FreqDev::Hz(hz) = self;
let val = ((hz as u64) * (1 << 25)) / 32_000_000;
[(val >> 16) as u8, (val >> 8) as u8, val as u8]
}
}
#[derive(Clone, Copy, defmt::Format)]
pub struct FskConfig {
pub frequency: u32,
pub bitrate: Bitrate,
pub pulse_shape: PulseShape,
pub bandwidth: Bandwidth,
pub fdev: FreqDev,
pub preamble_len: u16,
pub preamble_det: PreambleDetLength,
/// Sync word bytes (1-8) written to SUBGHZ_GSYNCR (0x06C0)
pub sync_word: [u8; 8],
pub addr_comp: AddrComp,
pub packet_type: PacketLengthType,
pub crc: CrcType,
pub whitening: bool,
pub rx_gain: RxGain,
pub pa: PaSelection,
pub power_dbm: i8,
pub ramp: RampTime,
}
impl Default for FskConfig {
fn default() -> Self {
Self {
frequency: 868_100_000,
bitrate: Bitrate::Custom(9600),
pulse_shape: PulseShape::GaussianBt05,
bandwidth: Bw46_9kHz,
fdev: FreqDev::Hz(25_000),
preamble_len: 32,
preamble_det: PreambleDetLength::Bits8,
// default values taken from RF0461 reference manual
sync_word: [0x97, 0x23, 0x52, 0x25, 0x56, 0x53, 0x65, 0x64],
addr_comp: AddrComp::Off,
packet_type: PacketLengthType::Variable,
crc: CrcType::Crc2Byte,
whitening: true,
rx_gain: RxGain::Boosted,
pa: PaSelection::LowPower,
power_dbm: 14,
ramp: RampTime::Us40,
}
}
}
// Import for default
use Bandwidth::Bw46_9kHz;
/// (G)FSK modulation - borrows a Radio, implements Configure + Transmit + Receive
pub struct FskRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
radio: &'a mut Radio<SPI, TX, RX, EN>,
payload_len: u8,
config: FskConfig,
}
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
FskRadio<'a, SPI, TX, RX, EN>
{
pub fn new(radio: &'a mut Radio<SPI, TX, RX, EN>) -> Self {
Self {
radio,
payload_len: 0,
config: FskConfig::default(),
}
}
/// Re-send SetPacketParams with updated payload length
async fn update_payload_len(&mut self, len: u8) -> Result<(), RadioError> {
debug!("Updating payload length to {}", len);
if len == self.payload_len {
return Ok(());
}
self.payload_len = len;
self.send_packet_params(len).await
}
/// Send FSK SetPacketParams
async fn send_packet_params(&mut self, payload_len: u8) -> Result<(), RadioError> {
let cfg = &self.config;
self.radio
.set_packet_params(&[
(cfg.preamble_len >> 8) as u8,
cfg.preamble_len as u8,
cfg.preamble_det as u8,
cfg.sync_word.len() as u8 * 8, // SyncWordLen in bits
cfg.addr_comp as u8,
cfg.packet_type as u8,
payload_len,
cfg.crc as u8,
if cfg.whitening { 0x01 } else { 0x00 },
])
.await
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
for FskRadio<'_, SPI, TX, RX, EN>
{
type Config = FskConfig;
async fn configure(&mut self, config: &Self::Config) -> Result<(), RadioError> {
self.config = *config;
self.radio.set_packet_type(PacketType::Fsk).await?;
// Write sync word to SUBGHZ_GSYNCR (0x06C0)
self.radio.write_register(0x06C0, &config.sync_word).await?;
// Set FSK packet params
self.send_packet_params(0).await?;
// RF frequency
self.radio.set_rf_frequency(config.frequency).await?;
// Modulation params
let br = config.bitrate.to_bytes();
let fdev = config.fdev.to_bytes();
self.radio
.set_modulation_params(&[
br[0],
br[1],
br[2],
config.pulse_shape as u8,
config.bandwidth as u8,
fdev[0],
fdev[1],
fdev[2],
])
.await?;
// PA config + TX power
self.radio
.set_output_power(config.pa, config.power_dbm, config.ramp)
.await?;
Ok(())
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Transmit
for FskRadio<'_, SPI, TX, RX, EN>
{
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError> {
if data.len() > 255 {
return Err(RadioError::PayloadTooLarge);
}
// Write payload to radio buffer
self.radio.set_buffer_base(0x00, 0x00).await?;
self.radio.write_buffer(0x00, data).await?;
// Update packet params with actual payload length
self.update_payload_len(data.len() as u8).await?;
// Clear any stale IRQ flags before starting TX
self.radio.clear_irq(irq::ALL).await?;
// Enable IRQs on DIO1
self.radio.set_dio1_irq(irq::TX_DONE | irq::TIMEOUT).await?;
// Start TX
self.radio.set_tx(0).await?;
// Wait until it's done or until timeout
let status = self.radio.poll_irq(irq::TX_DONE | irq::TIMEOUT).await?;
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
Ok(())
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Receive
for FskRadio<'_, SPI, TX, RX, EN>
{
async fn rx(&mut self, buf: &mut [u8], timeout_ms: u32) -> Result<usize, RadioError> {
// Set max payload length we can accept
let max_len = buf.len().min(255) as u8;
self.update_payload_len(max_len).await?;
// Set buffer base addresses
self.radio.set_buffer_base(0x00, 0x00).await?;
// Clear any stale IRQ flags before starting RX
self.radio.clear_irq(irq::ALL).await?;
// Enable RX-related IRQs on DIO1
self.radio
.set_dio1_irq(irq::RX_DONE | irq::TIMEOUT | irq::CRC_ERR | irq::SYNC_WORD_VALID)
.await?;
// Stop RX timer on preamble detection (required for proper RX behavior)
self.radio.set_stop_rx_timer_on_preamble(true).await?;
// Set RX gain
self.radio
.write_register(0x08AC, &[self.config.rx_gain as u8])
.await?;
// Convert ms to 15.625µs steps, 0 = single, 0xFFFFFF = continuous
let timeout_steps = if timeout_ms == 0 {
0
} else {
timeout_ms.saturating_mul(64).min(0xFFFFFF)
};
self.radio.set_rx(timeout_steps).await?;
// Wait for something to happen
let status = self
.radio
.poll_irq(irq::RX_DONE | irq::TIMEOUT | irq::CRC_ERR)
.await?;
// Check what happened
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
if status & irq::CRC_ERR != 0 {
return Err(RadioError::CrcInvalid);
}
// Read received data from the radio buffer
let (len, offset) = self.radio.get_rx_buffer_status().await?;
let read_len = len.min(buf.len() as u8);
self.radio
.read_buffer(offset, &mut buf[..read_len as usize])
.await?;
trace!("Got data {:x}", &mut buf[..read_len as usize]);
Ok(read_len as usize)
}
}

View File

@@ -3,7 +3,7 @@ use embedded_hal::digital::OutputPin;
use embedded_hal_async::spi::SpiDevice; use embedded_hal_async::spi::SpiDevice;
use crate::error::RadioError; use crate::error::RadioError;
use crate::radio::{PaSelection, PacketType, Radio, RampTime, irq}; use crate::radio::{PaSelection, PacketType, Radio, RampTime, RxGain, irq};
use crate::traits::{Configure, Receive, Transmit}; use crate::traits::{Configure, Receive, Transmit};
#[derive(Clone, Copy, defmt::Format)] #[derive(Clone, Copy, defmt::Format)]
@@ -57,6 +57,7 @@ pub struct LoraConfig {
pub crc_on: bool, pub crc_on: bool,
pub iq_inverted: bool, pub iq_inverted: bool,
pub sync_word: u16, pub sync_word: u16,
pub rx_gain: RxGain,
pub pa: PaSelection, pub pa: PaSelection,
pub power_dbm: i8, pub power_dbm: i8,
pub ramp: RampTime, pub ramp: RampTime,
@@ -75,6 +76,7 @@ impl Default for LoraConfig {
crc_on: true, crc_on: true,
iq_inverted: false, iq_inverted: false,
sync_word: 0x1424, // private LoRa network sync_word: 0x1424, // private LoRa network
rx_gain: RxGain::PowerSaving,
pa: PaSelection::LowPower, pa: PaSelection::LowPower,
power_dbm: 14, power_dbm: 14,
ramp: RampTime::Us40, ramp: RampTime::Us40,
@@ -235,8 +237,10 @@ impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Receive
// Stop RX timer on preamble detection (required for proper RX behavior) // Stop RX timer on preamble detection (required for proper RX behavior)
self.radio.set_stop_rx_timer_on_preamble(true).await?; self.radio.set_stop_rx_timer_on_preamble(true).await?;
// Set RX gain (0x94 = normal, 0x96 = boosted) // Set RX gain
self.radio.write_register(0x08AC, &[0x94]).await?; self.radio
.write_register(0x08AC, &[self.config.rx_gain as u8])
.await?;
// Convert ms to 15.625µs steps (ms * 64), 0 = single mode, 0xFFFFFF = continuous // Convert ms to 15.625µs steps (ms * 64), 0 = single mode, 0xFFFFFF = continuous
let timeout_steps = if timeout_ms == 0 { let timeout_steps = if timeout_ms == 0 {

7
src/modulations/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod bpsk;
#[cfg(feature = "hal")]
pub mod fsk;
#[cfg(feature = "hal")]
pub mod lora;
#[cfg(feature = "hal")]
pub mod msk;

261
src/modulations/msk.rs Normal file
View File

@@ -0,0 +1,261 @@
use defmt::{debug, trace};
use embedded_hal::digital::OutputPin;
use embedded_hal_async::spi::SpiDevice;
use crate::{
RadioError,
radio::{PaSelection, PacketType, Radio, RampTime, RxGain, irq},
traits::{Configure, Receive, Transmit},
};
// Re-export shared FSK types
pub use super::fsk::{
AddrComp, Bandwidth, Bitrate, CrcType, PacketLengthType, PreambleDetLength, PulseShape,
};
impl Bitrate {
/// Get the 3-byte Fdev register for MSK (Fdev = bitrate / 4)
/// Register = deviation_hz * 2^25 / 32 MHz
fn fdev_bytes(self) -> [u8; 3] {
let deviation_hz = self.bps() / 4;
let val = ((deviation_hz as u64) * (1 << 25)) / 32_000_000;
[(val >> 16) as u8, (val >> 8) as u8, val as u8]
}
}
#[derive(Clone, Copy, defmt::Format)]
pub struct MskConfig {
pub frequency: u32,
pub bitrate: Bitrate,
pub pulse_shape: PulseShape,
/// Bandwidth of the rx side
/// Should be >= 1.5 * bitrate for MSK
/// (Carson's rule: BW = 2 * (Fdev + bitrate/2), with Fdev = bitrate/4)
/// So for 10kbps signal, it should be
/// 2 * (10000/4 + 10000/2) = 15000 Hz
pub bandwidth: Bandwidth,
pub preamble_len: u16,
pub preamble_det: PreambleDetLength,
/// Sync word bytes (1-8) written to SUBGHZ_GSYNCR (0x06C0)
pub sync_word: [u8; 8],
pub addr_comp: AddrComp,
pub packet_type: PacketLengthType,
pub crc: CrcType,
pub whitening: bool,
pub rx_gain: RxGain,
pub pa: PaSelection,
pub power_dbm: i8,
pub ramp: RampTime,
}
impl Default for MskConfig {
fn default() -> Self {
Self {
frequency: 868_100_000,
bitrate: Bitrate::Custom(600),
pulse_shape: PulseShape::GaussianBt05,
bandwidth: Bandwidth::Bw4_8kHz,
preamble_len: 32,
preamble_det: PreambleDetLength::Bits8,
// default values taken from RF0461 reference manual
sync_word: [0x97, 0x23, 0x52, 0x25, 0x56, 0x53, 0x65, 0x64],
addr_comp: AddrComp::Off,
packet_type: PacketLengthType::Variable,
crc: CrcType::Crc2Byte,
whitening: true,
rx_gain: RxGain::Boosted,
pa: PaSelection::LowPower,
power_dbm: 14,
ramp: RampTime::Us40,
}
}
}
/// (G)MSK modulation implemented via FSK with modulation index 0.5
/// Borrows a Radio, implements Configure + Transmit + Receive
pub struct MskRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
radio: &'a mut Radio<SPI, TX, RX, EN>,
payload_len: u8,
config: MskConfig,
}
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
MskRadio<'a, SPI, TX, RX, EN>
{
pub fn new(radio: &'a mut Radio<SPI, TX, RX, EN>) -> Self {
Self {
radio,
payload_len: 0,
config: MskConfig::default(),
}
}
/// Re-send SetPacketParams with updated payload length
async fn update_payload_len(&mut self, len: u8) -> Result<(), RadioError> {
debug!("Updating payload length to {}", len);
if len == self.payload_len {
return Ok(());
}
self.payload_len = len;
self.send_packet_params(len).await
}
/// Send FSK SetPacketParams with the given payload length
async fn send_packet_params(&mut self, payload_len: u8) -> Result<(), RadioError> {
let cfg = &self.config;
self.radio
.set_packet_params(&[
(cfg.preamble_len >> 8) as u8,
cfg.preamble_len as u8,
cfg.preamble_det as u8,
cfg.sync_word.len() as u8 * 8, // SyncWordLen in bits
cfg.addr_comp as u8,
cfg.packet_type as u8,
payload_len,
cfg.crc as u8,
if cfg.whitening { 0x01 } else { 0x00 },
])
.await
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
for MskRadio<'_, SPI, TX, RX, EN>
{
type Config = MskConfig;
async fn configure(&mut self, config: &Self::Config) -> Result<(), RadioError> {
self.config = *config;
// Use FSK packet type - MSK is FSK with modulation index 0.5
self.radio.set_packet_type(PacketType::Fsk).await?;
// Write sync word to SUBGHZ_GSYNCR (0x06C0)
self.radio.write_register(0x06C0, &config.sync_word).await?;
// Set FSK packet params
self.send_packet_params(0).await?;
// RF frequency
self.radio.set_rf_frequency(config.frequency).await?;
// Modulation params: FSK format with Fdev = bitrate/4 for MSK
let br = config.bitrate.to_bytes();
let fdev = config.bitrate.fdev_bytes();
self.radio
.set_modulation_params(&[
br[0],
br[1],
br[2],
config.pulse_shape as u8,
config.bandwidth as u8,
fdev[0],
fdev[1],
fdev[2],
])
.await?;
// PA config + TX power
self.radio
.set_output_power(config.pa, config.power_dbm, config.ramp)
.await?;
Ok(())
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Transmit
for MskRadio<'_, SPI, TX, RX, EN>
{
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError> {
if data.len() > 255 {
return Err(RadioError::PayloadTooLarge);
}
// Write payload to radio buffer
self.radio.set_buffer_base(0x00, 0x00).await?;
self.radio.write_buffer(0x00, data).await?;
// Update packet params with actual payload length
self.update_payload_len(data.len() as u8).await?;
// Clear any stale IRQ flags before starting TX
self.radio.clear_irq(irq::ALL).await?;
// Enable IRQs on DIO1
self.radio.set_dio1_irq(irq::TX_DONE | irq::TIMEOUT).await?;
// Start TX
self.radio.set_tx(0).await?;
// Wait until it's done or until timeout
let status = self.radio.poll_irq(irq::TX_DONE | irq::TIMEOUT).await?;
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
Ok(())
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Receive
for MskRadio<'_, SPI, TX, RX, EN>
{
async fn rx(&mut self, buf: &mut [u8], timeout_ms: u32) -> Result<usize, RadioError> {
// Set max payload length we can accept
let max_len = buf.len().min(255) as u8;
self.update_payload_len(max_len).await?;
// Set buffer base addresses
self.radio.set_buffer_base(0x00, 0x00).await?;
// Clear any stale IRQ flags before starting RX
self.radio.clear_irq(irq::ALL).await?;
// Enable RX-related IRQs on DIO1
self.radio
.set_dio1_irq(irq::RX_DONE | irq::TIMEOUT | irq::CRC_ERR | irq::SYNC_WORD_VALID)
.await?;
// Stop RX timer on preamble detection (required for proper RX behavior)
self.radio.set_stop_rx_timer_on_preamble(true).await?;
// Set RX gain
self.radio
.write_register(0x08AC, &[self.config.rx_gain as u8])
.await?;
// Convert ms to 15.625µs steps, 0 = single, 0xFFFFFF = continuous
let timeout_steps = if timeout_ms == 0 {
0
} else {
timeout_ms.saturating_mul(64).min(0xFFFFFF)
};
self.radio.set_rx(timeout_steps).await?;
// Wait for something to happen
let status = self
.radio
.poll_irq(irq::RX_DONE | irq::TIMEOUT | irq::CRC_ERR)
.await?;
// Check what happened
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
if status & irq::CRC_ERR != 0 {
return Err(RadioError::CrcInvalid);
}
// Read received data from the radio buffer
let (len, offset) = self.radio.get_rx_buffer_status().await?;
let read_len = len.min(buf.len() as u8);
self.radio
.read_buffer(offset, &mut buf[..read_len as usize])
.await?;
trace!("Got data {:x}", &mut buf[..read_len as usize]);
Ok(read_len as usize)
}
}

View File

@@ -88,6 +88,16 @@ pub enum PaSelection {
HighPower, HighPower,
} }
/// RX gain setting (register 0x08AC)
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum RxGain {
/// Power saving gain
PowerSaving = 0x94,
/// Boosted gain (better sensitivity)
Boosted = 0x96,
}
/// Standby mode clock source /// Standby mode clock source
#[derive(Clone, Copy, defmt::Format)] #[derive(Clone, Copy, defmt::Format)]
#[repr(u8)] #[repr(u8)]

View File

@@ -1,17 +0,0 @@
[package]
name = "stm32wl-subghz"
version = "0.1.0"
edition = "2024"
license = "MIT"
repository = "https://git.lusia.moe/lukrecja/stm32wl-subghz"
categories = [ "no-std", "embedded", "asynchronous" ]
keywords = [ "stm32wl", "stm32wle5jc", "lora", "fsk", "gfsk", "msk", "gmsk", "bpsk", "radio", "embedded-hal-async" ]
description = "Sub-GHz radio driver for STM32WL-series microcontrollers"
[dependencies]
embassy-stm32 = { version = "0.5.0", features = ["unstable-pac"] }
embassy-time = "0.5.0"
defmt = "1.0.1"
cortex-m = { version = "0.7.6", features = ["inline-asm"] }
embedded-hal = "1.0.0"
embedded-hal-async = "1.0.0"

View File

@@ -1,7 +0,0 @@
Copyright © 2026 Lukrecja Pleskaczyńska
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,20 +0,0 @@
# stm32wl-subghz
[![Latest Version]][crates.io]
Sub-GHZ SPI device radio driver for STM32WL-series microcontrollers, currently supporting LoRa and BPSK.
Built on `embedded-hal-async` and `embassy-stm32`.
## Supported hardware
- STM32WLE5JC (single-core) - tested
- Possibly all the other variants in STM32WL5x and STM32WLEx families
## Usage
See the [examples](../examples/) directory.
## License
MIT
[Latest Version]: https://img.shields.io/crates/v/stm32wl-subghz.svg
[crates.io]: https://crates.io/crates/stm32wl-subghz

View File

@@ -1,154 +0,0 @@
use defmt::debug;
use embedded_hal::digital::OutputPin;
use embedded_hal_async::spi::SpiDevice;
use crate::{
RadioError,
radio::{PaSelection, PacketType, Radio, RampTime, irq},
traits::{Configure, Transmit},
};
/// BPSK bitrate
/// Formula: register = (32 * 32_000_000) / bps
#[derive(Clone, Copy, defmt::Format)]
pub enum Bitrate {
/// 100 bits per second
Bps100,
/// 600 bits per second
Bps600,
/// Arbitrary bitrate in bits per second
Custom(u32),
}
impl Bitrate {
/// Get the 3-byte register value for this bitrate
fn to_bytes(self) -> [u8; 3] {
let val = match self {
Bitrate::Bps100 => 0x9C4000,
Bitrate::Bps600 => 0x1A0AAA,
Bitrate::Custom(bps) => (32 * 32_000_000) / bps,
};
[(val >> 16) as u8, (val >> 8) as u8, val as u8]
}
}
#[derive(Clone, Copy, defmt::Format)]
pub struct BpskConfig {
pub frequency: u32,
pub bitrate: Bitrate,
pub pa: PaSelection,
pub power_dbm: i8,
pub ramp: RampTime,
}
impl Default for BpskConfig {
fn default() -> Self {
Self {
frequency: 868_100_000,
bitrate: Bitrate::Bps600,
pa: PaSelection::LowPower,
power_dbm: 14,
ramp: RampTime::Us40,
}
}
}
/// BPSK modulation - borrows a Radio, implements Configure + Transmit
pub struct BpskRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
radio: &'a mut Radio<SPI, TX, RX, EN>,
payload_len: u8,
config: BpskConfig,
}
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
BpskRadio<'a, SPI, TX, RX, EN>
{
pub fn new(radio: &'a mut Radio<SPI, TX, RX, EN>) -> Self {
Self {
radio,
payload_len: 0,
config: BpskConfig::default(),
}
}
/// Re-send SetPacketParams with updated payload length (called before each tx/rx)
async fn update_payload_len(&mut self, len: u8) -> Result<(), RadioError> {
debug!("Updating payload length to {}", len);
if len == self.payload_len {
return Ok(());
}
self.payload_len = len;
self.send_packet_params(len).await
}
/// Send the full SetPacketParams command with the given payload length
async fn send_packet_params(&mut self, payload_len: u8) -> Result<(), RadioError> {
self.radio.set_packet_params(&[payload_len]).await
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
for BpskRadio<'_, SPI, TX, RX, EN>
{
type Config = BpskConfig;
async fn configure(&mut self, config: &Self::Config) -> Result<(), RadioError> {
self.config = *config;
// Select BPSK packet type
self.radio.set_packet_type(PacketType::Bpsk).await?;
// Payload length updated per tx
self.send_packet_params(0).await?;
// RF frequency
self.radio.set_rf_frequency(config.frequency).await?;
// Set modulation params (bitrate + Gaussian BT 0.5 pulse shape)
let br = config.bitrate.to_bytes();
self.radio
.set_modulation_params(&[br[0], br[1], br[2], 0x16])
.await?;
// PA config + TX power
self.radio
.set_output_power(config.pa, config.power_dbm, config.ramp)
.await?;
Ok(())
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Transmit
for BpskRadio<'_, SPI, TX, RX, EN>
{
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError> {
if data.len() > 255 {
return Err(RadioError::PayloadTooLarge);
}
// Write payload to radio buffer
self.radio.set_buffer_base(0x00, 0x00).await?;
self.radio.write_buffer(0x00, data).await?;
// Update packet params with actual payload length
self.update_payload_len(data.len() as u8).await?;
// Clear any stale IRQ flags before starting TX
self.radio.clear_irq(irq::ALL).await?;
// Enable TxDone IRQ on DIO1
self.radio.set_dio1_irq(irq::TX_DONE | irq::TIMEOUT).await?;
// Start TX
self.radio.set_tx(0).await?;
// Wait until it's done or until timeout
let status = self.radio.poll_irq(irq::TX_DONE | irq::TIMEOUT).await?;
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
Ok(())
}
}

View File

@@ -1,2 +0,0 @@
pub mod bpsk;
pub mod lora;