refractor into proper driver with abstractions and Radio struct, LoRa TX and RX working
This commit is contained in:
@@ -5,4 +5,4 @@ runner = "probe-rs run --chip STM32WLE5JC"
|
|||||||
target = "thumbv7em-none-eabi"
|
target = "thumbv7em-none-eabi"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
DEFMT_LOG = "trace"
|
DEFMT_LOG = "debug"
|
||||||
|
|||||||
19
src/error.rs
Normal file
19
src/error.rs
Normal 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
10
src/lib.rs
Normal 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;
|
||||||
174
src/main.rs
174
src/main.rs
@@ -1,67 +1,24 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use defmt::{debug, trace};
|
use defmt::{error, info, warn};
|
||||||
use embassy_executor::Spawner;
|
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 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 _};
|
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]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(_spawner: Spawner) {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
@@ -73,87 +30,38 @@ async fn main(_spawner: Spawner) {
|
|||||||
}
|
}
|
||||||
let p = embassy_stm32::init(config);
|
let p = embassy_stm32::init(config);
|
||||||
|
|
||||||
let mut spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
|
let spi = SubGhzSpiDevice(Spi::new_subghz(p.SUBGHZSPI, p.DMA1_CH1, p.DMA1_CH2));
|
||||||
reset_radio().await;
|
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 mut lora = LoraRadio::new(&mut radio);
|
||||||
let _ = spi.write(&[0x80, 0x00]).await;
|
lora.configure(&LoraConfig {
|
||||||
debug!("Radio in standby!");
|
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
|
info!("sending stuffs");
|
||||||
debug!("SetDIO3AsTCXOCtrl");
|
match lora.tx(b"hiiiiiII!").await {
|
||||||
let _ = spi.write(&[0x97, 0x01, 0x00, 0x01, 0x45]).await;
|
Ok(_) => info!("yay :3"),
|
||||||
// Calibrate - all blocks (RC64k, RC13M, PLL, ADC pulse, ADC bulk N, ADC bulk P, image)
|
Err(e) => error!("tx error: {:?}", e),
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
loop {
|
||||||
Timer::after(Duration::from_secs(1)).await;
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|||||||
275
src/modulations/lora.rs
Normal file
275
src/modulations/lora.rs
Normal 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
1
src/modulations/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod lora;
|
||||||
1049
src/radio.rs
Normal file
1049
src/radio.rs
Normal file
File diff suppressed because it is too large
Load Diff
54
src/spi.rs
Normal file
54
src/spi.rs
Normal 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
20
src/traits.rs
Normal 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>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user