Compare commits
10 Commits
f9356d5f30
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4295dd2aab | |||
| 91a9b4a311 | |||
| 085e791ec6 | |||
| 757111afda | |||
| 709bff6d5d | |||
| 62e586a776 | |||
| da308fe23e | |||
| 82010fc745 | |||
| f41a7b9002 | |||
| a789b5a881 |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "thumbv7em-none-eabi"
|
||||
175
Cargo.lock
generated
175
Cargo.lock
generated
@@ -80,6 +80,15 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "cortex-m"
|
||||
version = "0.7.7"
|
||||
@@ -88,6 +97,7 @@ checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
|
||||
dependencies = [
|
||||
"bare-metal",
|
||||
"bitfield",
|
||||
"critical-section",
|
||||
"embedded-hal 0.2.7",
|
||||
"volatile-register",
|
||||
]
|
||||
@@ -118,6 +128,50 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "defmt"
|
||||
version = "1.0.1"
|
||||
@@ -150,6 +204,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
@@ -165,9 +229,11 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8"
|
||||
dependencies = [
|
||||
"defmt 1.0.1",
|
||||
"embassy-futures",
|
||||
"embassy-hal-internal 0.3.0",
|
||||
"embassy-sync",
|
||||
"embassy-time",
|
||||
"embedded-hal 0.2.7",
|
||||
"embedded-hal 1.0.0",
|
||||
"embedded-hal-async",
|
||||
@@ -176,6 +242,38 @@ dependencies = [
|
||||
"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]]
|
||||
name = "embassy-futures"
|
||||
version = "0.1.2"
|
||||
@@ -199,6 +297,7 @@ checksum = "7f10ce10a4dfdf6402d8e9bd63128986b96a736b1a0a6680547ed2ac55d55dba"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"critical-section",
|
||||
"defmt 1.0.1",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
@@ -207,6 +306,9 @@ name = "embassy-net-driver"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524eb3c489760508f71360112bca70f6e53173e6fe48fc5f0efd0f5ab217751d"
|
||||
dependencies = [
|
||||
"defmt 0.3.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embassy-stm32"
|
||||
@@ -219,15 +321,20 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block-device-driver",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"cortex-m",
|
||||
"cortex-m-rt",
|
||||
"critical-section",
|
||||
"defmt 1.0.1",
|
||||
"document-features",
|
||||
"embassy-embedded-hal",
|
||||
"embassy-futures",
|
||||
"embassy-hal-internal 0.4.0",
|
||||
"embassy-net-driver",
|
||||
"embassy-sync",
|
||||
"embassy-time",
|
||||
"embassy-time-driver",
|
||||
"embassy-time-queue-utils",
|
||||
"embassy-usb-driver",
|
||||
"embassy-usb-synopsys-otg",
|
||||
"embedded-can",
|
||||
@@ -263,6 +370,7 @@ checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"defmt 1.0.1",
|
||||
"embedded-io-async 0.6.1",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -277,6 +385,7 @@ checksum = "f4fa65b9284d974dad7a23bb72835c4ec85c0b540d86af7fc4098c88cff51d65"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"critical-section",
|
||||
"defmt 1.0.1",
|
||||
"document-features",
|
||||
"embassy-time-driver",
|
||||
"embedded-hal 0.2.7",
|
||||
@@ -294,12 +403,23 @@ dependencies = [
|
||||
"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]]
|
||||
name = "embassy-usb-driver"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a"
|
||||
dependencies = [
|
||||
"defmt 1.0.1",
|
||||
"embedded-io-async 0.6.1",
|
||||
]
|
||||
|
||||
@@ -310,6 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "288751f8eaa44a5cf2613f13cee0ca8e06e6638cb96e897e6834702c79084b23"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"defmt 1.0.1",
|
||||
"embassy-sync",
|
||||
"embassy-usb-driver",
|
||||
]
|
||||
@@ -369,6 +490,9 @@ name = "embedded-io"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7"
|
||||
dependencies = [
|
||||
"defmt 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io-async"
|
||||
@@ -385,6 +509,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0"
|
||||
dependencies = [
|
||||
"defmt 1.0.1",
|
||||
"embedded-io 0.7.1",
|
||||
]
|
||||
|
||||
@@ -403,6 +528,12 @@ dependencies = [
|
||||
"embedded-storage",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
@@ -461,6 +592,12 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
@@ -491,6 +628,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -608,6 +755,7 @@ checksum = "a411079520dbccc613af73172f944b7cf97ba84e3bd7381a0352b6ec7bfef03b"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"cortex-m-rt",
|
||||
"defmt 0.3.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -615,13 +763,38 @@ name = "stm32wl-subghz"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"defmt",
|
||||
"defmt 1.0.1",
|
||||
"embassy-stm32",
|
||||
"embassy-time",
|
||||
"embedded-hal 1.0.0",
|
||||
"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]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
||||
55
Cargo.toml
55
Cargo.toml
@@ -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]
|
||||
members = [ "stm32wl-subghz" ]
|
||||
exclude = [ "examples/stm32wle5jc" ]
|
||||
members = ["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
|
||||
|
||||
17
README.md
17
README.md
@@ -4,12 +4,25 @@
|
||||
|
||||
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
|
||||
- [`stm32wl-subghz`](./stm32wl-subghz/) - the driver library
|
||||
- [`stm32wl-subghz`](.) - the driver library
|
||||
- [`examples/stm32wle5jc`](./examples/stm32wle5jc/) - usage examples for the STM32WLE5JC-based boards
|
||||
|
||||
## License
|
||||
MIT
|
||||
|
||||
[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
|
||||
|
||||
@@ -6,7 +6,7 @@ license = "MIT"
|
||||
publish = false
|
||||
|
||||
[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-sync = { version = "0.7.2", features = ["defmt"] }
|
||||
|
||||
@@ -9,7 +9,6 @@ use embassy_stm32::{
|
||||
rcc::{MSIRange, Sysclk, mux},
|
||||
spi::Spi,
|
||||
};
|
||||
use embassy_time::{Duration, Timer};
|
||||
use stm32wl_subghz::{
|
||||
Configure, PaSelection, Radio, SubGhzSpiDevice, Transmit,
|
||||
modulations::bpsk::{Bitrate, BpskConfig, BpskRadio},
|
||||
@@ -39,19 +38,15 @@ async fn main(_spawner: Spawner) {
|
||||
frequency: 868_100_000,
|
||||
bitrate: Bitrate::Bps600,
|
||||
pa: PaSelection::HighPower,
|
||||
power_dbm: 17,
|
||||
power_dbm: 22,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("sending stuffs");
|
||||
match bpsk.tx(b"hiiiii!").await {
|
||||
Ok(_) => info!("yay :3"),
|
||||
info!("sending bpsk stuffs");
|
||||
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 tx done :3"),
|
||||
Err(e) => error!("tx error: {:?}", e),
|
||||
}
|
||||
|
||||
loop {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
56
examples/stm32wle5jc/src/bin/fsk_rx.rs
Normal file
56
examples/stm32wle5jc/src/bin/fsk_rx.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
53
examples/stm32wle5jc/src/bin/fsk_tx.rs
Normal file
53
examples/stm32wle5jc/src/bin/fsk_tx.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
60
examples/stm32wle5jc/src/bin/lora_rx.rs
Normal file
60
examples/stm32wle5jc/src/bin/lora_rx.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
53
examples/stm32wle5jc/src/bin/lora_tx.rs
Normal file
53
examples/stm32wle5jc/src/bin/lora_tx.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
55
examples/stm32wle5jc/src/bin/msk_rx.rs
Normal file
55
examples/stm32wle5jc/src/bin/msk_rx.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
53
examples/stm32wle5jc/src/bin/msk_tx.rs
Normal file
53
examples/stm32wle5jc/src/bin/msk_tx.rs
Normal 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
1
rust-analyzer.toml
Normal file
@@ -0,0 +1 @@
|
||||
linkedProjects = ["Cargo.toml", "examples/stm32wle5jc/Cargo.toml"]
|
||||
@@ -1,13 +1,22 @@
|
||||
#![no_std]
|
||||
#![cfg_attr(feature = "hal", no_std)]
|
||||
#![allow(async_fn_in_trait)]
|
||||
|
||||
pub mod error;
|
||||
pub mod modulations;
|
||||
|
||||
#[cfg(feature = "hal")]
|
||||
pub mod error;
|
||||
#[cfg(feature = "hal")]
|
||||
pub mod radio;
|
||||
#[cfg(feature = "hal")]
|
||||
pub mod spi;
|
||||
#[cfg(feature = "hal")]
|
||||
pub mod traits;
|
||||
|
||||
#[cfg(feature = "hal")]
|
||||
pub use error::RadioError;
|
||||
#[cfg(feature = "hal")]
|
||||
pub use radio::{PaSelection, Radio};
|
||||
#[cfg(feature = "hal")]
|
||||
pub use spi::SubGhzSpiDevice;
|
||||
#[cfg(feature = "hal")]
|
||||
pub use traits::{Configure, Receive, Transmit};
|
||||
500
src/modulations/bpsk.rs
Normal file
500
src/modulations/bpsk.rs
Normal 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
380
src/modulations/fsk.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use embedded_hal::digital::OutputPin;
|
||||
use embedded_hal_async::spi::SpiDevice;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(Clone, Copy, defmt::Format)]
|
||||
@@ -57,6 +57,7 @@ pub struct LoraConfig {
|
||||
pub crc_on: bool,
|
||||
pub iq_inverted: bool,
|
||||
pub sync_word: u16,
|
||||
pub rx_gain: RxGain,
|
||||
pub pa: PaSelection,
|
||||
pub power_dbm: i8,
|
||||
pub ramp: RampTime,
|
||||
@@ -75,6 +76,7 @@ impl Default for LoraConfig {
|
||||
crc_on: true,
|
||||
iq_inverted: false,
|
||||
sync_word: 0x1424, // private LoRa network
|
||||
rx_gain: RxGain::PowerSaving,
|
||||
pa: PaSelection::LowPower,
|
||||
power_dbm: 14,
|
||||
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)
|
||||
self.radio.set_stop_rx_timer_on_preamble(true).await?;
|
||||
|
||||
// Set RX gain (0x94 = normal, 0x96 = boosted)
|
||||
self.radio.write_register(0x08AC, &[0x94]).await?;
|
||||
// Set RX gain
|
||||
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
|
||||
let timeout_steps = if timeout_ms == 0 {
|
||||
7
src/modulations/mod.rs
Normal file
7
src/modulations/mod.rs
Normal 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
261
src/modulations/msk.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,16 @@ pub enum PaSelection {
|
||||
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
|
||||
#[derive(Clone, Copy, defmt::Format)]
|
||||
#[repr(u8)]
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod bpsk;
|
||||
pub mod lora;
|
||||
Reference in New Issue
Block a user