Compare commits
7 Commits
82010fc745
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4295dd2aab | |||
| 91a9b4a311 | |||
| 085e791ec6 | |||
| 757111afda | |||
| 709bff6d5d | |||
| 62e586a776 | |||
| da308fe23e |
55
Cargo.toml
55
Cargo.toml
@@ -15,36 +15,39 @@ members = ["examples/stm32wle5jc"]
|
|||||||
# Default chip for development/CI. Downstream users should use default-features = false
|
# Default chip for development/CI. Downstream users should use default-features = false
|
||||||
# and enable their specific chip feature.
|
# and enable their specific chip feature.
|
||||||
default = ["stm32wle5jc"]
|
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
|
# Single-core variants
|
||||||
stm32wle4c8 = ["embassy-stm32/stm32wle4c8"]
|
stm32wle4c8 = ["hal", "embassy-stm32/stm32wle4c8"]
|
||||||
stm32wle4cb = ["embassy-stm32/stm32wle4cb"]
|
stm32wle4cb = ["hal", "embassy-stm32/stm32wle4cb"]
|
||||||
stm32wle4cc = ["embassy-stm32/stm32wle4cc"]
|
stm32wle4cc = ["hal", "embassy-stm32/stm32wle4cc"]
|
||||||
stm32wle4j8 = ["embassy-stm32/stm32wle4j8"]
|
stm32wle4j8 = ["hal", "embassy-stm32/stm32wle4j8"]
|
||||||
stm32wle4jb = ["embassy-stm32/stm32wle4jb"]
|
stm32wle4jb = ["hal", "embassy-stm32/stm32wle4jb"]
|
||||||
stm32wle4jc = ["embassy-stm32/stm32wle4jc"]
|
stm32wle4jc = ["hal", "embassy-stm32/stm32wle4jc"]
|
||||||
stm32wle5c8 = ["embassy-stm32/stm32wle5c8"]
|
stm32wle5c8 = ["hal", "embassy-stm32/stm32wle5c8"]
|
||||||
stm32wle5cb = ["embassy-stm32/stm32wle5cb"]
|
stm32wle5cb = ["hal", "embassy-stm32/stm32wle5cb"]
|
||||||
stm32wle5cc = ["embassy-stm32/stm32wle5cc"]
|
stm32wle5cc = ["hal", "embassy-stm32/stm32wle5cc"]
|
||||||
stm32wle5j8 = ["embassy-stm32/stm32wle5j8"]
|
stm32wle5j8 = ["hal", "embassy-stm32/stm32wle5j8"]
|
||||||
stm32wle5jb = ["embassy-stm32/stm32wle5jb"]
|
stm32wle5jb = ["hal", "embassy-stm32/stm32wle5jb"]
|
||||||
stm32wle5jc = ["embassy-stm32/stm32wle5jc"]
|
stm32wle5jc = ["hal", "embassy-stm32/stm32wle5jc"]
|
||||||
# Dual-core variants
|
# Dual-core variants
|
||||||
stm32wl54cc-cm0p = ["embassy-stm32/stm32wl54cc-cm0p"]
|
stm32wl54cc-cm0p = ["hal", "embassy-stm32/stm32wl54cc-cm0p"]
|
||||||
stm32wl54cc-cm4 = ["embassy-stm32/stm32wl54cc-cm4"]
|
stm32wl54cc-cm4 = ["hal", "embassy-stm32/stm32wl54cc-cm4"]
|
||||||
stm32wl54jc-cm0p = ["embassy-stm32/stm32wl54jc-cm0p"]
|
stm32wl54jc-cm0p = ["hal", "embassy-stm32/stm32wl54jc-cm0p"]
|
||||||
stm32wl54jc-cm4 = ["embassy-stm32/stm32wl54jc-cm4"]
|
stm32wl54jc-cm4 = ["hal", "embassy-stm32/stm32wl54jc-cm4"]
|
||||||
stm32wl55cc-cm0p = ["embassy-stm32/stm32wl55cc-cm0p"]
|
stm32wl55cc-cm0p = ["hal", "embassy-stm32/stm32wl55cc-cm0p"]
|
||||||
stm32wl55cc-cm4 = ["embassy-stm32/stm32wl55cc-cm4"]
|
stm32wl55cc-cm4 = ["hal", "embassy-stm32/stm32wl55cc-cm4"]
|
||||||
stm32wl55jc-cm0p = ["embassy-stm32/stm32wl55jc-cm0p"]
|
stm32wl55jc-cm0p = ["hal", "embassy-stm32/stm32wl55jc-cm0p"]
|
||||||
stm32wl55jc-cm4 = ["embassy-stm32/stm32wl55jc-cm4"]
|
stm32wl55jc-cm4 = ["hal", "embassy-stm32/stm32wl55jc-cm4"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embassy-stm32 = { version = "0.5.0", features = ["unstable-pac"] }
|
embassy-stm32 = { version = "0.5.0", features = ["unstable-pac"], optional = true }
|
||||||
embassy-time = "0.5.0"
|
embassy-time = { version = "0.5.0", optional = true }
|
||||||
defmt = "1.0.1"
|
defmt = { version = "1.0.1", optional = true }
|
||||||
cortex-m = { version = "0.7.6", features = ["inline-asm"] }
|
cortex-m = { version = "0.7.6", features = ["inline-asm"], optional = true }
|
||||||
embedded-hal = "1.0.0"
|
embedded-hal = { version = "1.0.0", optional = true }
|
||||||
embedded-hal-async = "1.0.0"
|
embedded-hal-async = { version = "1.0.0", optional = true }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = 2
|
debug = 2
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ async fn main(_spawner: Spawner) {
|
|||||||
lora.configure(&LoraConfig {
|
lora.configure(&LoraConfig {
|
||||||
frequency: 868_100_000,
|
frequency: 868_100_000,
|
||||||
sf: SpreadingFactor::SF9,
|
sf: SpreadingFactor::SF9,
|
||||||
bw: Bandwidth::Bw7_8kHz,
|
bw: Bandwidth::Bw20_83kHz,
|
||||||
pa: PaSelection::HighPower,
|
pa: PaSelection::HighPower,
|
||||||
power_dbm: 22,
|
power_dbm: 22,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -46,11 +46,11 @@ async fn main(_spawner: Spawner) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("waiting for packet...");
|
info!("waiting for lora stuffs...");
|
||||||
let mut buf = [0u8; 255];
|
let mut buf = [0u8; 255];
|
||||||
match lora.rx(&mut buf, 5_000).await {
|
match lora.rx(&mut buf, 5_000).await {
|
||||||
Ok(len) => info!("rx {} bytes: {:x}", len, &buf[..len]),
|
Ok(len) => info!("yay :3 got {} bytes: {:x}", len, &buf[..len]),
|
||||||
Err(RadioError::Timeout) => warn!("no packet received (timeout)"),
|
Err(RadioError::Timeout) => warn!("nobody is talking to me :<"),
|
||||||
Err(e) => error!("rx error: {:?}", e),
|
Err(e) => error!("rx error: {:?}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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::lora::{Bandwidth, LoraConfig, LoraRadio, SpreadingFactor},
|
modulations::lora::{Bandwidth, LoraConfig, LoraRadio, SpreadingFactor},
|
||||||
@@ -38,7 +37,7 @@ async fn main(_spawner: Spawner) {
|
|||||||
lora.configure(&LoraConfig {
|
lora.configure(&LoraConfig {
|
||||||
frequency: 868_100_000,
|
frequency: 868_100_000,
|
||||||
sf: SpreadingFactor::SF9,
|
sf: SpreadingFactor::SF9,
|
||||||
bw: Bandwidth::Bw7_8kHz,
|
bw: Bandwidth::Bw20_83kHz,
|
||||||
pa: PaSelection::HighPower,
|
pa: PaSelection::HighPower,
|
||||||
power_dbm: 22,
|
power_dbm: 22,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -46,13 +45,9 @@ async fn main(_spawner: Spawner) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("sending stuffs");
|
info!("sending lora stuffs");
|
||||||
match lora.tx(b"hiiiiiII!").await {
|
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 :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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/lib.rs
13
src/lib.rs
@@ -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};
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
|
#[cfg(feature = "hal")]
|
||||||
use defmt::debug;
|
use defmt::debug;
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
use embedded_hal::digital::OutputPin;
|
use embedded_hal::digital::OutputPin;
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
use embedded_hal_async::spi::SpiDevice;
|
use embedded_hal_async::spi::SpiDevice;
|
||||||
|
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
use crate::{
|
use crate::{
|
||||||
RadioError,
|
RadioError,
|
||||||
radio::{PaSelection, PacketType, Radio, RampTime, irq},
|
radio::{PaSelection, PacketType, Radio, RampTime, irq},
|
||||||
traits::{Configure, Transmit},
|
traits::{Configure, Transmit},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "hal"))]
|
||||||
|
use std::vec::Vec;
|
||||||
|
|
||||||
/// BPSK bitrate
|
/// BPSK bitrate
|
||||||
/// Formula: register = (32 * 32_000_000) / bps
|
/// Formula: register = (32 * 32_000_000) / bps
|
||||||
#[derive(Clone, Copy, defmt::Format)]
|
#[derive(Clone, Copy)]
|
||||||
|
#[cfg_attr(feature = "hal", derive(defmt::Format))]
|
||||||
pub enum Bitrate {
|
pub enum Bitrate {
|
||||||
/// 100 bits per second
|
/// 100 bits per second
|
||||||
Bps100,
|
Bps100,
|
||||||
@@ -22,7 +30,7 @@ pub enum Bitrate {
|
|||||||
|
|
||||||
impl Bitrate {
|
impl Bitrate {
|
||||||
/// Get the 3-byte register value for this bitrate
|
/// Get the 3-byte register value for this bitrate
|
||||||
fn to_bytes(self) -> [u8; 3] {
|
pub fn to_bytes(self) -> [u8; 3] {
|
||||||
let val = match self {
|
let val = match self {
|
||||||
Bitrate::Bps100 => 0x9C4000,
|
Bitrate::Bps100 => 0x9C4000,
|
||||||
Bitrate::Bps600 => 0x1A0AAA,
|
Bitrate::Bps600 => 0x1A0AAA,
|
||||||
@@ -32,6 +40,337 @@ impl Bitrate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Clone, Copy, defmt::Format)]
|
||||||
pub struct BpskConfig {
|
pub struct BpskConfig {
|
||||||
pub frequency: u32,
|
pub frequency: u32,
|
||||||
@@ -39,8 +378,10 @@ pub struct BpskConfig {
|
|||||||
pub pa: PaSelection,
|
pub pa: PaSelection,
|
||||||
pub power_dbm: i8,
|
pub power_dbm: i8,
|
||||||
pub ramp: RampTime,
|
pub ramp: RampTime,
|
||||||
|
pub packet: BpskPacket,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
impl Default for BpskConfig {
|
impl Default for BpskConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -49,17 +390,20 @@ impl Default for BpskConfig {
|
|||||||
pa: PaSelection::LowPower,
|
pa: PaSelection::LowPower,
|
||||||
power_dbm: 14,
|
power_dbm: 14,
|
||||||
ramp: RampTime::Us40,
|
ramp: RampTime::Us40,
|
||||||
|
packet: BpskPacket::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BPSK modulation - borrows a Radio, implements Configure + Transmit
|
/// BPSK modulation - borrows a Radio, implements Configure + Transmit
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
pub struct BpskRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
|
pub struct BpskRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
|
||||||
radio: &'a mut Radio<SPI, TX, RX, EN>,
|
radio: &'a mut Radio<SPI, TX, RX, EN>,
|
||||||
payload_len: u8,
|
payload_len: u8,
|
||||||
config: BpskConfig,
|
config: BpskConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
|
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
|
||||||
BpskRadio<'a, SPI, TX, RX, EN>
|
BpskRadio<'a, SPI, TX, RX, EN>
|
||||||
{
|
{
|
||||||
@@ -87,6 +431,7 @@ impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
|
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
|
||||||
for BpskRadio<'_, SPI, TX, RX, EN>
|
for BpskRadio<'_, SPI, TX, RX, EN>
|
||||||
{
|
{
|
||||||
@@ -119,20 +464,21 @@ impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Transmit
|
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Transmit
|
||||||
for BpskRadio<'_, SPI, TX, RX, EN>
|
for BpskRadio<'_, SPI, TX, RX, EN>
|
||||||
{
|
{
|
||||||
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError> {
|
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError> {
|
||||||
if data.len() > 255 {
|
let mut buf = [0u8; 255];
|
||||||
return Err(RadioError::PayloadTooLarge);
|
// Convert buffer to packet with chosen framing
|
||||||
}
|
let len = self.config.packet.to_bytes(data, &mut buf)?;
|
||||||
|
|
||||||
// Write payload to radio buffer
|
// Write payload to radio buffer
|
||||||
self.radio.set_buffer_base(0x00, 0x00).await?;
|
self.radio.set_buffer_base(0x00, 0x00).await?;
|
||||||
self.radio.write_buffer(0x00, data).await?;
|
self.radio.write_buffer(0x00, &buf[..len as usize]).await?;
|
||||||
|
|
||||||
// Update packet params with actual payload length
|
// Update packet params with actual payload length
|
||||||
self.update_payload_len(data.len() as u8).await?;
|
self.update_payload_len(len).await?;
|
||||||
|
|
||||||
// Clear any stale IRQ flags before starting TX
|
// Clear any stale IRQ flags before starting TX
|
||||||
self.radio.clear_irq(irq::ALL).await?;
|
self.radio.clear_irq(irq::ALL).await?;
|
||||||
|
|||||||
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 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 {
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
pub mod bpsk;
|
pub mod bpsk;
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
|
pub mod fsk;
|
||||||
|
#[cfg(feature = "hal")]
|
||||||
pub mod lora;
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/radio.rs
10
src/radio.rs
@@ -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)]
|
||||||
|
|||||||
Reference in New Issue
Block a user