implement BPSK modulation (tx-only with arbitrary freq setting)

This commit is contained in:
2026-02-26 01:46:49 +01:00
parent 15d13bdad7
commit d624b92704
3 changed files with 163 additions and 18 deletions

View File

@@ -1,7 +1,7 @@
#![no_std]
#![no_main]
use defmt::{error, info, warn};
use defmt::{error, info};
use embassy_executor::Spawner;
use embassy_stm32::{
Config,
@@ -11,11 +11,12 @@ use embassy_stm32::{
};
use embassy_time::{Duration, Timer};
use stm32wle5jc_radio::{
RadioError,
modulations::lora::{Bandwidth, LoraConfig, LoraRadio, SpreadingFactor},
modulations::
bpsk::{Bitrate, BpskConfig, BpskRadio}
,
radio::{PaSelection, Radio},
spi::SubGhzSpiDevice,
traits::{Configure, Receive, Transmit},
traits::{Configure, Transmit},
};
use {defmt_rtt as _, panic_probe as _};
@@ -37,32 +38,23 @@ async fn main(_spawner: Spawner) {
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 {
let mut bpsk = BpskRadio::new(&mut radio);
bpsk.configure(&BpskConfig {
frequency: 868_100_000,
sf: SpreadingFactor::SF9,
bw: Bandwidth::Bw7_8kHz,
bitrate: Bitrate::Bps600,
pa: PaSelection::HighPower,
power_dbm: 22,
power_dbm: 17,
..Default::default()
})
.await
.unwrap();
info!("sending stuffs");
match lora.tx(b"hiiiiiII!").await {
match bpsk.tx(b"hiiiiiII!").await {
Ok(_) => info!("yay :3"),
Err(e) => error!("tx error: {:?}", e),
}
info!("waiting for packet...");
let mut buf = [0u8; 255];
match lora.rx(&mut buf, 1_000).await {
Ok(len) => info!("rx {} bytes: {:x}", len, &buf[..len]),
Err(RadioError::Timeout) => warn!("no packet received (timeout)"),
Err(e) => error!("rx error: {:?}", e),
}
loop {
Timer::after(Duration::from_secs(1)).await;
}

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

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

View File

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