refractor into proper driver with abstractions and Radio struct, LoRa TX and RX working

This commit is contained in:
2026-02-26 00:14:05 +01:00
parent bac6567fe3
commit 15d13bdad7
9 changed files with 1472 additions and 136 deletions

View File

@@ -5,4 +5,4 @@ runner = "probe-rs run --chip STM32WLE5JC"
target = "thumbv7em-none-eabi"
[env]
DEFMT_LOG = "trace"
DEFMT_LOG = "debug"

19
src/error.rs Normal file
View File

@@ -0,0 +1,19 @@
#[derive(Debug, Clone, Copy, defmt::Format)]
pub enum RadioError {
/// SPI comms failed
Spi,
/// Radio busy (polling rfbusys)
Busy,
/// Timeout of tx or rx
Timeout,
/// Rx packet had bad CRC
CrcInvalid,
/// Header was invalid
HeaderInvalid,
/// Payload too large
PayloadTooLarge,
/// Invalid configuration (e.g. power out of range for selected PA)
InvalidConfig,
/// Command not allowed in the current radio state
InvalidState,
}

10
src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
#![no_std]
#![allow(async_fn_in_trait)]
pub mod error;
pub mod modulations;
pub mod radio;
pub mod spi;
pub mod traits;
pub use error::RadioError;

View File

@@ -1,67 +1,24 @@
#![no_std]
#![no_main]
use defmt::{debug, trace};
use defmt::{error, info, warn};
use embassy_executor::Spawner;
use embassy_stm32::{Config, gpio::{Level, Output, Speed}, pac, rcc::{MSIRange, Sysclk, mux}, spi::Spi};
use embassy_stm32::{
Config,
gpio::{Level, Output, Speed},
rcc::{MSIRange, Sysclk, mux},
spi::Spi,
};
use embassy_time::{Duration, Timer};
use embedded_hal_async::spi::{ErrorType, Operation, SpiBus, SpiDevice};
use stm32wle5jc_radio::{
RadioError,
modulations::lora::{Bandwidth, LoraConfig, LoraRadio, SpreadingFactor},
radio::{PaSelection, Radio},
spi::SubGhzSpiDevice,
traits::{Configure, Receive, Transmit},
};
use {defmt_rtt as _, panic_probe as _};
/// Wrapper for the sub-GHz SPI device
struct SubGhzSpiDevice<T>(T);
impl<T: SpiBus> ErrorType for SubGhzSpiDevice<T> {
type Error = T::Error;
}
/// This works as a translation layer between normal SPI transactions and sub-GHz device SPI
/// transactions. Everything above this layer sees it like a normal SPI device!
impl<T: SpiBus> SpiDevice for SubGhzSpiDevice<T> {
/// Perform a transaction on the sub-GHz device
async fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), Self::Error> {
// Pull NSS low to allow SPI comms
pac::PWR.subghzspicr().modify(|w| w.set_nss(false));
trace!("NSS low");
for operation in operations {
match operation {
Operation::Read(buf) => {
self.0.read(buf).await?;
trace!("Read {:x}", buf);
},
Operation::Write(buf) => {
self.0.write(buf).await?;
trace!("Wrote {:x}", buf);
},
Operation::Transfer(read, write) => {
self.0.transfer(read, write).await?;
trace!("Read {:x} wrote {:x}", read, write);
},
Operation::TransferInPlace(buf) => {
self.0.transfer_in_place(buf).await?;
trace!("Read+wrote {:x}", buf);
},
Operation::DelayNs(_) => {}
}
}
// Pull NSS high
pac::PWR.subghzspicr().modify(|w| w.set_nss(true));
trace!("NSS high");
// Poll BUSY flag until it's done
while pac::PWR.sr2().read().rfbusys() {}
trace!("BUSY flag clear");
Ok(())
}
}
async fn reset_radio() {
debug!("Resetting the radio");
pac::RCC.csr().modify(|w| w.set_rfrst(true));
pac::RCC.csr().modify(|w| w.set_rfrst(false));
Timer::after_millis(1).await;
debug!("Radio reset finished");
}
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let mut config = Config::default();
@@ -73,87 +30,38 @@ async fn main(_spawner: Spawner) {
}
let p = embassy_stm32::init(config);
let mut spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
reset_radio().await;
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();
debug!("Writing SetStandby");
let _ = spi.write(&[0x80, 0x00]).await;
debug!("Radio in standby!");
let mut lora = LoraRadio::new(&mut radio);
lora.configure(&LoraConfig {
frequency: 868_100_000,
sf: SpreadingFactor::SF9,
bw: Bandwidth::Bw7_8kHz,
pa: PaSelection::HighPower,
power_dbm: 22,
..Default::default()
})
.await
.unwrap();
// SetDIO3AsTCXOCtrl - 1.7V, 5ms timeout
debug!("SetDIO3AsTCXOCtrl");
let _ = spi.write(&[0x97, 0x01, 0x00, 0x01, 0x45]).await;
// Calibrate - all blocks (RC64k, RC13M, PLL, ADC pulse, ADC bulk N, ADC bulk P, image)
debug!("Calibrate");
let _ = spi.write(&[0x89, 0x7F]).await;
// CalibrateImage - for 863-870 MHz band
debug!("CalibrateImage");
let _ = spi.write(&[0x98, 0xD7, 0xDB]).await;
// SetBufferBaseAddress
debug!("SetBufferBaseAddress");
let _ = spi.write(&[0x8f, 0x00, 0x00]).await;
// WriteBuffer (max 255 bytes, wraps around after that)
debug!("WriteBuffer");
let _ = spi.write(&[0x0e, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05]).await;
// SetPacketType to LoRa
debug!("LoRa SetPacketType");
let _ = spi.write(&[0x8a, 0x01]).await;
// SetPacketParam - 8 preamble length, explicit header, 5-byte payload,
// CRC enabled, standard IQ setup
debug!("SetPacketParam");
let _ = spi.write(&[0x8c, 0x00, 0x08, 0x00, 0x05, 0x01, 0x00]).await;
// Probably redundant, but:
// Defining LoRa sync word (public network) with WriteRegister to SUBGHZ_LSYNCR (0x740)
debug!("WriteRegister to SUBGHZ_LSYNCR");
let _ = spi.write(&[0x0D, 0x07, 0x40, 0x14, 0x24]).await;
// SetRfFrequency: rffreq = (rf_frequency * 2^25) / f_xtal
// (868_100_000 * 33_554_432) / 32_000_000 = 910_268_825
// hex(910_268_825) = 0x36419999
debug!("SetRfFrequency");
let _ = spi.write(&[0x86, 0x36, 0x41, 0x99, 0x99]).await;
// SetPaConfig - +14dBm for SX1262
debug!("SetPaConfig");
let _ = spi.write(&[0x95, 0x02, 0x02, 0x00, 0x01]).await;
// SetTxParams - +22dBm (as written in the docs), 40µs ramp time
debug!("SetTxParams");
let _ = spi.write(&[0x8e, 0x16, 0x02]).await;
// SetModulationParams - sf12, 15.63kHz bandwidth, CR 4/5, ldro off
debug!("SetModulationParams");
let _ = spi.write(&[0x8b, 0x0c, 0x01, 0x01, 0x00]).await;
// SetDioIrqParams - set TxDone and Timeout IRQs
debug!("SetDioIrqParams");
let _ = spi.write(&[0x08,
0b00000000, 0b00000001, // IrqMask: bit 0 (TxDone)
0b00000000, 0b00000001, // DIO1Mask: same as IrqMask
0b00000000, 0b00000000, // DIO2Mask: none
0b00000000, 0b00000000, // DIO3Mask: none
]).await;
// Enable RF switch before tx
let _ctrl1 = Output::new(p.PC4, Level::High, Speed::High); // TX
let _ctrl2 = Output::new(p.PC5, Level::Low, Speed::High); // RX
let _ctrl3 = Output::new(p.PC3, Level::High, Speed::High); // EN
// SetTx - no timeout
debug!("SetTx - transmitting now");
let _ = spi.write(&[0x83, 0x00, 0x00, 0x00]).await;
// GetIrqStatus loop to poll when the command finishes
loop {
debug!("GetIrqStatus");
// Send 0x12 opcode + 4 nops (extra NOP because SPI is full-duplex, response is shifted)
let mut buf = [0x12, 0x00, 0x00, 0x00, 0x00];
let _ = spi.transfer_in_place(&mut buf).await;
trace!("IRQ raw: {:x}", buf);
// buf[0] = garbage, buf[1] = status, buf[2] = NOP, buf[3-4] = IrqStatus
if buf[3] & 0x01 != 0 {
debug!("GetIrqStatus - tx done");
break;
info!("sending stuffs");
match lora.tx(b"hiiiiiII!").await {
Ok(_) => info!("yay :3"),
Err(e) => error!("tx error: {:?}", e),
}
Timer::after_millis(1).await;
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),
}
// ClearIrqStatus - all IRQs
debug!("ClearIrqStatus");
let _ = spi.write(&[0x02, 0xff, 0xff]).await;
loop {
Timer::after(Duration::from_secs(1)).await;

275
src/modulations/lora.rs Normal file
View File

@@ -0,0 +1,275 @@
use defmt::{debug, trace};
use embedded_hal::digital::OutputPin;
use embedded_hal_async::spi::SpiDevice;
use crate::error::RadioError;
use crate::radio::{PacketType, PaSelection, Radio, RampTime, irq};
use crate::traits::{Configure, Receive, Transmit};
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum SpreadingFactor {
SF5 = 0x05,
SF6 = 0x06,
SF7 = 0x07,
SF8 = 0x08,
SF9 = 0x09,
SF10 = 0x0a,
SF11 = 0x0b,
SF12 = 0x0c,
}
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum Bandwidth {
Bw7_8kHz = 0x00,
Bw10_42kHz = 0x08,
Bw15_63kHz = 0x01,
Bw20_83kHz = 0x09,
Bw31_25kHz = 0x02,
Bw41_67kHz = 0x0a,
Bw62_50kHz = 0x03,
Bw125kHz = 0x04,
Bw250kHz = 0x05,
Bw500kHz = 0x06,
}
#[derive(Clone, Copy, defmt::Format)]
#[repr(u8)]
pub enum CodingRate {
/// No forward error correction coding
Cr44 = 0x00,
Cr45 = 0x01,
Cr46 = 0x02,
Cr47 = 0x03,
Cr48 = 0x04,
}
#[derive(Clone, Copy, defmt::Format)]
pub struct LoraConfig {
pub frequency: u32,
pub sf: SpreadingFactor,
pub bw: Bandwidth,
pub cr: CodingRate,
pub ldro: bool,
pub preamble_len: u16,
pub explicit_header: bool,
pub crc_on: bool,
pub iq_inverted: bool,
pub sync_word: u16,
pub pa: PaSelection,
pub power_dbm: i8,
pub ramp: RampTime,
}
impl Default for LoraConfig {
fn default() -> Self {
Self {
frequency: 868_100_000,
sf: SpreadingFactor::SF7,
bw: Bandwidth::Bw125kHz,
cr: CodingRate::Cr45,
ldro: false,
preamble_len: 8,
explicit_header: true,
crc_on: true,
iq_inverted: false,
sync_word: 0x1424, // private LoRa network
pa: PaSelection::LowPower,
power_dbm: 14,
ramp: RampTime::Us40,
}
}
}
/// LoRa modulation - borrows a Radio, implements Configure + Transmit + Receive
pub struct LoraRadio<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> {
radio: &'a mut Radio<SPI, TX, RX, EN>,
payload_len: u8,
config: LoraConfig,
}
impl<'a, SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin>
LoraRadio<'a, SPI, TX, RX, EN>
{
pub fn new(radio: &'a mut Radio<SPI, TX, RX, EN>) -> Self {
Self {
radio,
payload_len: 0,
config: LoraConfig::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(&[
(self.config.preamble_len >> 8) as u8,
self.config.preamble_len as u8,
!self.config.explicit_header as u8,
payload_len,
self.config.crc_on as u8,
self.config.iq_inverted as u8,
])
.await
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Configure
for LoraRadio<'_, SPI, TX, RX, EN>
{
type Config = LoraConfig;
async fn configure(&mut self, config: &LoraConfig) -> Result<(), RadioError> {
self.config = *config;
// Select LoRa packet type
self.radio.set_packet_type(PacketType::LoRa).await?;
// Calibrate image for this frequency band
let band = Radio::<SPI, TX, RX, EN>::image_cal_for_freq(config.frequency);
self.radio.calibrate_image(band).await?;
// RF frequency
self.radio.set_rf_frequency(config.frequency).await?;
// Modulation: SF, BW, CR, LDRO
self.radio
.set_modulation_params(&[
config.sf as u8,
config.bw as u8,
config.cr as u8,
config.ldro as u8,
])
.await?;
// Packet params (payload length 0 for now, updated per tx/rx)
self.send_packet_params(0).await?;
self.payload_len = 0;
// Fix IQ polarity for non-inverted IQ (set bit 2 of register 0x0736)
if !config.iq_inverted {
let mut iqpol = [0u8; 1];
self.radio.read_register(0x0736, &mut iqpol).await?;
trace!("Got data {:x}", iqpol);
self.radio.write_register(0x0736, &[iqpol[0] | 0x04]).await?;
}
// Sync word at SUBGHZ_LSYNCR (0x0740)
self.radio.set_lora_sync_word(config.sync_word).await?;
// PA config + TX power (uses optimal settings from the datasheet)
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 LoraRadio<'_, 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, 0x80).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(())
}
}
impl<SPI: SpiDevice, TX: OutputPin, RX: OutputPin, EN: OutputPin> Receive
for LoraRadio<'_, 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 (both at 0, same as in lora-rs)
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::HEADER_ERR)
.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 (0x94 = normal, 0x96 = boosted)
self.radio.write_register(0x08AC, &[0x94]).await?;
// Convert ms to 15.625µs steps (ms * 64), 0 = single mode, 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 | irq::HEADER_ERR)
.await?;
// Check what happened
if status & irq::TIMEOUT != 0 {
return Err(RadioError::Timeout);
}
if status & irq::CRC_ERR != 0 {
return Err(RadioError::CrcInvalid);
}
if status & irq::HEADER_ERR != 0 {
return Err(RadioError::HeaderInvalid);
}
// 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)
}
}

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

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

1049
src/radio.rs Normal file

File diff suppressed because it is too large Load Diff

54
src/spi.rs Normal file
View File

@@ -0,0 +1,54 @@
use defmt::trace;
use embassy_stm32::pac;
use embedded_hal_async::spi::{ErrorType, Operation, SpiBus, SpiDevice};
/// Wrapper for the sub-GHz SPI device
pub struct SubGhzSpiDevice<T>(pub T);
impl<T: SpiBus> ErrorType for SubGhzSpiDevice<T> {
type Error = T::Error;
}
/// This works as a translation layer between normal SPI transactions and sub-GHz device SPI
/// transactions. Everything above this layer sees it like a normal SPI device!
impl<T: SpiBus> SpiDevice for SubGhzSpiDevice<T> {
/// Perform a transaction on the sub-GHz device
async fn transaction(
&mut self,
operations: &mut [Operation<'_, u8>],
) -> Result<(), Self::Error> {
// Pull NSS low to allow SPI comms
pac::PWR.subghzspicr().modify(|w| w.set_nss(false));
trace!("NSS low");
for operation in operations {
match operation {
Operation::Read(buf) => {
self.0.read(buf).await?;
trace!("Read {:x}", buf);
}
Operation::Write(buf) => {
self.0.write(buf).await?;
trace!("Wrote {:x}", buf);
}
Operation::Transfer(read, write) => {
self.0.transfer(read, write).await?;
trace!("Read {:x} wrote {:x}", read, write);
}
Operation::TransferInPlace(buf) => {
self.0.transfer_in_place(buf).await?;
trace!("Read+wrote {:x}", buf);
}
Operation::DelayNs(_) => {}
}
}
// Pull NSS high
pac::PWR.subghzspicr().modify(|w| w.set_nss(true));
trace!("NSS high");
// Small delay for the radio to assert BUSY after NSS goes high
cortex_m::asm::delay(500);
// Poll BUSY flag until it's done
while pac::PWR.sr2().read().rfbusys() {}
trace!("BUSY flag clear");
Ok(())
}
}

20
src/traits.rs Normal file
View File

@@ -0,0 +1,20 @@
use crate::error::RadioError;
/// Can configure the radio
pub trait Configure {
/// Each modulation has its own `Config` struct
type Config;
async fn configure(&mut self, config: &Self::Config) -> Result<(), RadioError>;
}
/// Can send data
pub trait Transmit {
async fn tx(&mut self, data: &[u8]) -> Result<(), RadioError>;
}
/// Can receive data
pub trait Receive {
/// Returns the number of bytes received
async fn rx(&mut self, buf: &mut [u8], timeout_ms: u32) -> Result<usize, RadioError>;
}