diff --git a/embassy-lora/Cargo.toml b/embassy-lora/Cargo.toml new file mode 100644 index 000000000..af0f25d5a --- /dev/null +++ b/embassy-lora/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "embassy-lora" +version = "0.1.0" +authors = ["Ulf Lilleengen "] +edition = "2018" + +[lib] + +[features] +sx127x = [] +stm32wl = ["embassy-stm32", "embassy-stm32/subghz"] +time = [] + +defmt-trace = [] +defmt-debug = [] +defmt-info = [] +defmt-warn = [] +defmt-error = [] + +[dependencies] + +defmt = { version = "0.2.3", optional = true } +log = { version = "0.4.14", optional = true } + +embassy = { version = "0.1.0", path = "../embassy", default-features = false } +embassy-stm32 = { version = "0.1.0", path = "../embassy-stm32", default-features = false, optional = true } +embassy-hal-common = { version = "0.1.0", path = "../embassy-hal-common", default-features = false } +futures = { version = "0.3.17", default-features = false, features = [ "async-await" ] } +embedded-hal = { version = "0.2", features = ["unproven"] } +bit_field = { version = "0.10" } + +lorawan-device = { git = "https://github.com/lulf/rust-lorawan.git", rev = "a373d06fa8858d251bc70d5789cebcd9a638ec42", default-features = false, features = ["async"] } +lorawan-encoding = { git = "https://github.com/lulf/rust-lorawan.git", rev = "a373d06fa8858d251bc70d5789cebcd9a638ec42", default-features = false } \ No newline at end of file diff --git a/embassy-lora/src/fmt.rs b/embassy-lora/src/fmt.rs new file mode 100644 index 000000000..066970813 --- /dev/null +++ b/embassy-lora/src/fmt.rs @@ -0,0 +1,225 @@ +#![macro_use] +#![allow(unused_macros)] + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/embassy-lora/src/lib.rs b/embassy-lora/src/lib.rs new file mode 100644 index 000000000..b2da22090 --- /dev/null +++ b/embassy-lora/src/lib.rs @@ -0,0 +1,23 @@ +#![no_std] +#![feature(type_alias_impl_trait)] +#![feature(generic_associated_types)] +//! embassy-lora is a collection of async radio drivers that integrate with the lorawan-device +//! crate's async LoRaWAN MAC implementation. + +pub(crate) mod fmt; + +#[cfg(feature = "stm32wl")] +pub mod stm32wl; +#[cfg(feature = "sx127x")] +pub mod sx127x; + +/// A convenience timer to use with the LoRaWAN crate +pub struct LoraTimer; + +#[cfg(feature = "time")] +impl lorawan_device::async_device::radio::Timer for LoraTimer { + type DelayFuture<'m> = impl core::future::Future + 'm; + fn delay_ms<'m>(&'m mut self, millis: u64) -> Self::DelayFuture<'m> { + embassy::time::Timer::after(embassy::time::Duration::from_millis(millis)) + } +} diff --git a/embassy-lora/src/stm32wl/mod.rs b/embassy-lora/src/stm32wl/mod.rs new file mode 100644 index 000000000..8cac46f7a --- /dev/null +++ b/embassy-lora/src/stm32wl/mod.rs @@ -0,0 +1,368 @@ +//! A radio driver integration for the radio found on STM32WL family devices. +use core::future::Future; +use core::mem::MaybeUninit; +use embassy::channel::signal::Signal; +use embassy::interrupt::InterruptExt; +use embassy::util::Unborrow; +use embassy_hal_common::unborrow; +use embassy_stm32::{ + dma::NoDma, + gpio::{AnyPin, Output}, + interrupt::SUBGHZ_RADIO, + subghz::{ + CalibrateImage, CfgIrq, CodingRate, HeaderType, Irq, LoRaBandwidth, LoRaModParams, + LoRaPacketParams, LoRaSyncWord, Ocp, PaConfig, PaSel, PacketType, RampTime, RegMode, + RfFreq, SpreadingFactor as SF, StandbyClk, Status, SubGhz, TcxoMode, TcxoTrim, Timeout, + TxParams, + }, +}; +use embedded_hal::digital::v2::OutputPin; +use lorawan_device::async_device::{ + radio::{Bandwidth, PhyRxTx, RfConfig, RxQuality, SpreadingFactor, TxConfig}, + Timings, +}; + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + Idle, + Txing, + Rxing, +} + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct RadioError; + +static IRQ: Signal<(Status, u16)> = Signal::new(); + +struct StateInner<'a> { + radio: SubGhz<'a, NoDma, NoDma>, + switch: RadioSwitch<'a>, +} + +/// External state storage for the radio state +pub struct SubGhzState<'a>(MaybeUninit>); +impl<'a> SubGhzState<'a> { + pub const fn new() -> Self { + Self(MaybeUninit::uninit()) + } +} + +/// The radio peripheral keeping the radio state and owning the radio IRQ. +pub struct SubGhzRadio<'a> { + state: *mut StateInner<'a>, + _irq: SUBGHZ_RADIO, +} + +impl<'a> SubGhzRadio<'a> { + /// Create a new instance of a SubGhz radio for LoRaWAN. + /// + /// # Safety + /// Do not leak self or futures + pub unsafe fn new( + state: &'a mut SubGhzState<'a>, + radio: SubGhz<'a, NoDma, NoDma>, + switch: RadioSwitch<'a>, + irq: impl Unborrow, + ) -> Self { + unborrow!(irq); + + let mut inner = StateInner { radio, switch }; + inner.radio.reset(); + + let state_ptr = state.0.as_mut_ptr(); + state_ptr.write(inner); + + irq.disable(); + irq.set_handler(|p| { + // This is safe because we only get interrupts when configured for, so + // the radio will be awaiting on the signal at this point. If not, the ISR will + // anyway only adjust the state in the IRQ signal state. + let state = unsafe { &mut *(p as *mut StateInner<'a>) }; + state.on_interrupt(); + }); + irq.set_handler_context(state_ptr as *mut ()); + irq.enable(); + + Self { + state: state_ptr, + _irq: irq, + } + } +} + +impl<'a> StateInner<'a> { + /// Configure radio settings in preparation for TX or RX + pub(crate) fn configure(&mut self) -> Result<(), RadioError> { + trace!("Configuring STM32WL SUBGHZ radio"); + self.radio.set_standby(StandbyClk::Rc)?; + let tcxo_mode = TcxoMode::new() + .set_txco_trim(TcxoTrim::Volts1pt7) + .set_timeout(Timeout::from_duration_sat( + core::time::Duration::from_millis(40), + )); + + self.radio.set_tcxo_mode(&tcxo_mode)?; + self.radio.set_regulator_mode(RegMode::Ldo)?; + + self.radio.calibrate_image(CalibrateImage::ISM_863_870)?; + + self.radio.set_buffer_base_address(0, 0)?; + + self.radio.set_pa_config( + &PaConfig::new() + .set_pa_duty_cycle(0x1) + .set_hp_max(0x0) + .set_pa(PaSel::Lp), + )?; + + self.radio.set_pa_ocp(Ocp::Max140m)?; + + // let tx_params = TxParams::LP_14.set_ramp_time(RampTime::Micros40); + self.radio.set_tx_params( + &TxParams::new() + .set_ramp_time(RampTime::Micros40) + .set_power(0x0A), + )?; + + self.radio.set_packet_type(PacketType::LoRa)?; + self.radio.set_lora_sync_word(LoRaSyncWord::Public)?; + trace!("Done initializing STM32WL SUBGHZ radio"); + Ok(()) + } + + /// Perform a transmission with the given parameters and payload. Returns any time adjustements needed form + /// the upcoming RX window start. + async fn do_tx(&mut self, config: TxConfig, buf: &[u8]) -> Result { + //trace!("TX Request: {}", config); + trace!("TX START"); + self.switch.set_tx_lp(); + self.configure()?; + + self.radio + .set_rf_frequency(&RfFreq::from_frequency(config.rf.frequency))?; + + let mod_params = LoRaModParams::new() + .set_sf(convert_spreading_factor(config.rf.spreading_factor)) + .set_bw(convert_bandwidth(config.rf.bandwidth)) + .set_cr(CodingRate::Cr45) + .set_ldro_en(true); + self.radio.set_lora_mod_params(&mod_params)?; + + let packet_params = LoRaPacketParams::new() + .set_preamble_len(8) + .set_header_type(HeaderType::Variable) + .set_payload_len(buf.len() as u8) + .set_crc_en(true) + .set_invert_iq(false); + + self.radio.set_lora_packet_params(&packet_params)?; + + let irq_cfg = CfgIrq::new() + .irq_enable_all(Irq::TxDone) + .irq_enable_all(Irq::RxDone) + .irq_enable_all(Irq::Timeout); + self.radio.set_irq_cfg(&irq_cfg)?; + + self.radio.set_buffer_base_address(0, 0)?; + self.radio.write_buffer(0, buf)?; + + self.radio.set_tx(Timeout::DISABLED)?; + + loop { + let (_status, irq_status) = IRQ.wait().await; + IRQ.reset(); + + if irq_status & Irq::TxDone.mask() != 0 { + let stats = self.radio.lora_stats()?; + let (status, error_mask) = self.radio.op_error()?; + trace!( + "TX done. Stats: {:?}. OP error: {:?}, mask {:?}", + stats, + status, + error_mask + ); + + return Ok(0); + } else if irq_status & Irq::Timeout.mask() != 0 { + trace!("TX timeout"); + return Err(RadioError); + } + } + } + + /// Perform a radio receive operation with the radio config and receive buffer. The receive buffer must + /// be able to hold a single LoRaWAN packet. + async fn do_rx( + &mut self, + config: RfConfig, + buf: &mut [u8], + ) -> Result<(usize, RxQuality), RadioError> { + assert!(buf.len() >= 255); + trace!("RX START"); + // trace!("Starting RX: {}", config); + self.switch.set_rx(); + self.configure()?; + + self.radio + .set_rf_frequency(&RfFreq::from_frequency(config.frequency))?; + + let mod_params = LoRaModParams::new() + .set_sf(convert_spreading_factor(config.spreading_factor)) + .set_bw(convert_bandwidth(config.bandwidth)) + .set_cr(CodingRate::Cr45) + .set_ldro_en(true); + self.radio.set_lora_mod_params(&mod_params)?; + + let packet_params = LoRaPacketParams::new() + .set_preamble_len(8) + .set_header_type(HeaderType::Variable) + .set_payload_len(0xFF) + .set_crc_en(true) + .set_invert_iq(true); + self.radio.set_lora_packet_params(&packet_params)?; + + let irq_cfg = CfgIrq::new() + .irq_enable_all(Irq::RxDone) + .irq_enable_all(Irq::PreambleDetected) + .irq_enable_all(Irq::HeaderErr) + .irq_enable_all(Irq::Timeout) + .irq_enable_all(Irq::Err); + self.radio.set_irq_cfg(&irq_cfg)?; + + self.radio.set_rx(Timeout::DISABLED)?; + trace!("RX started"); + + loop { + let (status, irq_status) = IRQ.wait().await; + IRQ.reset(); + trace!("RX IRQ {:?}, {:?}", status, irq_status); + if irq_status & Irq::RxDone.mask() != 0 { + let (status, len, ptr) = self.radio.rx_buffer_status()?; + + let packet_status = self.radio.lora_packet_status()?; + let rssi = packet_status.rssi_pkt().to_integer(); + let snr = packet_status.snr_pkt().to_integer(); + trace!( + "RX done. Received {} bytes. RX status: {:?}. Pkt status: {:?}", + len, + status.cmd(), + packet_status, + ); + self.radio.read_buffer(ptr, &mut buf[..len as usize])?; + self.radio.set_standby(StandbyClk::Rc)?; + return Ok((len as usize, RxQuality::new(rssi, snr as i8))); + } else if irq_status & (Irq::Timeout.mask() | Irq::TxDone.mask()) != 0 { + return Err(RadioError); + } + } + } + + /// Read interrupt status and store in global signal + fn on_interrupt(&mut self) { + let (status, irq_status) = self.radio.irq_status().expect("error getting irq status"); + self.radio + .clear_irq_status(irq_status) + .expect("error clearing irq status"); + if irq_status & Irq::PreambleDetected.mask() != 0 { + trace!("Preamble detected, ignoring"); + } else { + IRQ.signal((status, irq_status)); + } + } +} + +impl PhyRxTx for SubGhzRadio<'static> { + type PhyError = RadioError; + + type TxFuture<'m> = impl Future> + 'm; + fn tx<'m>(&'m mut self, config: TxConfig, buf: &'m [u8]) -> Self::TxFuture<'m> { + async move { + let inner = unsafe { &mut *self.state }; + inner.do_tx(config, buf).await + } + } + + type RxFuture<'m> = impl Future> + 'm; + fn rx<'m>(&'m mut self, config: RfConfig, buf: &'m mut [u8]) -> Self::RxFuture<'m> { + async move { + let inner = unsafe { &mut *self.state }; + inner.do_rx(config, buf).await + } + } +} + +impl<'a> From for RadioError { + fn from(_: embassy_stm32::spi::Error) -> Self { + RadioError + } +} + +impl<'a> Timings for SubGhzRadio<'a> { + fn get_rx_window_offset_ms(&self) -> i32 { + -200 + } + fn get_rx_window_duration_ms(&self) -> u32 { + 800 + } +} + +/// Represents the radio switch found on STM32WL based boards, used to control the radio for transmission or reception. +pub struct RadioSwitch<'a> { + ctrl1: Output<'a, AnyPin>, + ctrl2: Output<'a, AnyPin>, + ctrl3: Output<'a, AnyPin>, +} + +impl<'a> RadioSwitch<'a> { + pub fn new( + ctrl1: Output<'a, AnyPin>, + ctrl2: Output<'a, AnyPin>, + ctrl3: Output<'a, AnyPin>, + ) -> Self { + Self { + ctrl1, + ctrl2, + ctrl3, + } + } + + pub(crate) fn set_rx(&mut self) { + self.ctrl1.set_high().unwrap(); + self.ctrl2.set_low().unwrap(); + self.ctrl3.set_high().unwrap(); + } + + pub(crate) fn set_tx_lp(&mut self) { + self.ctrl1.set_high().unwrap(); + self.ctrl2.set_high().unwrap(); + self.ctrl3.set_high().unwrap(); + } + + #[allow(dead_code)] + pub(crate) fn set_tx_hp(&mut self) { + self.ctrl2.set_high().unwrap(); + self.ctrl1.set_low().unwrap(); + self.ctrl3.set_high().unwrap(); + } +} + +fn convert_spreading_factor(sf: SpreadingFactor) -> SF { + match sf { + SpreadingFactor::_7 => SF::Sf7, + SpreadingFactor::_8 => SF::Sf8, + SpreadingFactor::_9 => SF::Sf9, + SpreadingFactor::_10 => SF::Sf10, + SpreadingFactor::_11 => SF::Sf11, + SpreadingFactor::_12 => SF::Sf12, + } +} + +fn convert_bandwidth(bw: Bandwidth) -> LoRaBandwidth { + match bw { + Bandwidth::_125KHz => LoRaBandwidth::Bw125, + Bandwidth::_250KHz => LoRaBandwidth::Bw250, + Bandwidth::_500KHz => LoRaBandwidth::Bw500, + } +} diff --git a/embassy-lora/src/sx127x/mod.rs b/embassy-lora/src/sx127x/mod.rs new file mode 100644 index 000000000..a9736b85f --- /dev/null +++ b/embassy-lora/src/sx127x/mod.rs @@ -0,0 +1,201 @@ +use core::future::Future; +use embassy::traits::gpio::WaitForRisingEdge; +use embedded_hal::blocking::delay::DelayMs; +use embedded_hal::blocking::spi::{Transfer, Write}; +use embedded_hal::digital::v2::OutputPin; +use lorawan_device::async_device::{ + radio::{Bandwidth, PhyRxTx, RfConfig, RxQuality, SpreadingFactor, TxConfig}, + Timings, +}; + +mod sx127x_lora; +use sx127x_lora::{Error as RadioError, LoRa, RadioMode, IRQ}; + +/// Trait representing a radio switch for boards using the Sx127x radio. One some +/// boards, this will be a dummy implementation that does nothing. +pub trait RadioSwitch { + fn set_tx(&mut self); + fn set_rx(&mut self); +} + +/// Semtech Sx127x radio peripheral +pub struct Sx127xRadio +where + SPI: Transfer + Write + 'static, + E: 'static, + CS: OutputPin + 'static, + RESET: OutputPin + 'static, + I: WaitForRisingEdge + 'static, + RFS: RadioSwitch + 'static, +{ + radio: LoRa, + rfs: RFS, + irq: I, +} + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + Idle, + Txing, + Rxing, +} + +impl Sx127xRadio +where + SPI: Transfer + Write + 'static, + CS: OutputPin + 'static, + RESET: OutputPin + 'static, + I: WaitForRisingEdge + 'static, + RFS: RadioSwitch + 'static, +{ + pub fn new>( + spi: SPI, + cs: CS, + reset: RESET, + irq: I, + rfs: RFS, + d: &mut D, + ) -> Result> { + let mut radio = LoRa::new(spi, cs, reset); + radio.reset(d)?; + Ok(Self { radio, irq, rfs }) + } +} + +impl Timings for Sx127xRadio +where + SPI: Transfer + Write + 'static, + CS: OutputPin + 'static, + RESET: OutputPin + 'static, + I: WaitForRisingEdge + 'static, + RFS: RadioSwitch + 'static, +{ + fn get_rx_window_offset_ms(&self) -> i32 { + -500 + } + fn get_rx_window_duration_ms(&self) -> u32 { + 800 + } +} + +impl PhyRxTx for Sx127xRadio +where + SPI: Transfer + Write + 'static, + CS: OutputPin + 'static, + E: 'static, + RESET: OutputPin + 'static, + I: WaitForRisingEdge + 'static, + RFS: RadioSwitch + 'static, +{ + type PhyError = Sx127xError; + + #[rustfmt::skip] + type TxFuture<'m> where SPI: 'm, CS: 'm, RESET: 'm, E: 'm, I: 'm, RFS: 'm = impl Future> + 'm; + + fn tx<'m>(&'m mut self, config: TxConfig, buf: &'m [u8]) -> Self::TxFuture<'m> { + trace!("TX START"); + async move { + self.rfs.set_tx(); + self.radio.set_tx_power(14, 0)?; + self.radio.set_frequency(config.rf.frequency)?; + // TODO: Modify radio to support other coding rates + self.radio.set_coding_rate_4(5)?; + self.radio + .set_signal_bandwidth(bandwidth_to_i64(config.rf.bandwidth))?; + self.radio + .set_spreading_factor(spreading_factor_to_u8(config.rf.spreading_factor))?; + + self.radio.set_preamble_length(8)?; + self.radio.set_lora_pa_ramp()?; + self.radio.set_lora_sync_word()?; + self.radio.set_invert_iq(false)?; + self.radio.set_crc(true)?; + + self.radio.set_dio0_tx_done()?; + self.radio.transmit_payload(buf)?; + + loop { + self.irq.wait_for_rising_edge().await; + self.radio.set_mode(RadioMode::Stdby).ok().unwrap(); + let irq = self.radio.clear_irq().ok().unwrap(); + if (irq & IRQ::IrqTxDoneMask.addr()) != 0 { + trace!("TX DONE"); + return Ok(0); + } + } + } + } + + #[rustfmt::skip] + type RxFuture<'m> where SPI: 'm, CS: 'm, RESET: 'm, E: 'm, I: 'm, RFS: 'm = impl Future> + 'm; + + fn rx<'m>(&'m mut self, config: RfConfig, buf: &'m mut [u8]) -> Self::RxFuture<'m> { + trace!("RX START"); + async move { + self.rfs.set_rx(); + self.radio.reset_payload_length()?; + self.radio.set_frequency(config.frequency)?; + // TODO: Modify radio to support other coding rates + self.radio.set_coding_rate_4(5)?; + self.radio + .set_signal_bandwidth(bandwidth_to_i64(config.bandwidth))?; + self.radio + .set_spreading_factor(spreading_factor_to_u8(config.spreading_factor))?; + + self.radio.set_preamble_length(8)?; + self.radio.set_lora_sync_word()?; + self.radio.set_invert_iq(true)?; + self.radio.set_crc(true)?; + + self.radio.set_dio0_rx_done()?; + self.radio.set_mode(RadioMode::RxContinuous)?; + + loop { + self.irq.wait_for_rising_edge().await; + self.radio.set_mode(RadioMode::Stdby).ok().unwrap(); + let irq = self.radio.clear_irq().ok().unwrap(); + if (irq & IRQ::IrqRxDoneMask.addr()) != 0 { + let rssi = self.radio.get_packet_rssi().unwrap_or(0) as i16; + let snr = self.radio.get_packet_snr().unwrap_or(0.0) as i8; + let response = if let Ok(size) = self.radio.read_packet_size() { + self.radio.read_packet(buf)?; + Ok((size, RxQuality::new(rssi, snr))) + } else { + Ok((0, RxQuality::new(rssi, snr))) + }; + trace!("RX DONE"); + return response; + } + } + } + } +} + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Sx127xError; + +impl From> for Sx127xError { + fn from(_: sx127x_lora::Error) -> Self { + Sx127xError + } +} + +fn spreading_factor_to_u8(sf: SpreadingFactor) -> u8 { + match sf { + SpreadingFactor::_7 => 7, + SpreadingFactor::_8 => 8, + SpreadingFactor::_9 => 9, + SpreadingFactor::_10 => 10, + SpreadingFactor::_11 => 11, + SpreadingFactor::_12 => 12, + } +} + +fn bandwidth_to_i64(bw: Bandwidth) -> i64 { + match bw { + Bandwidth::_125KHz => 125_000, + Bandwidth::_250KHz => 250_000, + Bandwidth::_500KHz => 500_000, + } +} diff --git a/embassy-lora/src/sx127x/sx127x_lora/mod.rs b/embassy-lora/src/sx127x/sx127x_lora/mod.rs new file mode 100644 index 000000000..c541815fe --- /dev/null +++ b/embassy-lora/src/sx127x/sx127x_lora/mod.rs @@ -0,0 +1,593 @@ +// Copyright Charles Wade (https://github.com/mr-glt/sx127x_lora). Licensed under the Apache 2.0 +// license +// +// Modifications made to make the driver work with the rust-lorawan link layer. + +#![allow(dead_code)] + +use bit_field::BitField; +use embedded_hal::blocking::{ + delay::DelayMs, + spi::{Transfer, Write}, +}; +use embedded_hal::digital::v2::OutputPin; +use embedded_hal::spi::{Mode, Phase, Polarity}; + +mod register; +use self::register::PaConfig; +use self::register::Register; +pub use self::register::IRQ; + +/// Provides the necessary SPI mode configuration for the radio +pub const MODE: Mode = Mode { + phase: Phase::CaptureOnSecondTransition, + polarity: Polarity::IdleHigh, +}; + +/// Provides high-level access to Semtech SX1276/77/78/79 based boards connected to a Raspberry Pi +pub struct LoRa { + spi: SPI, + cs: CS, + reset: RESET, + pub explicit_header: bool, + pub mode: RadioMode, +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + Uninformative, + VersionMismatch(u8), + CS(CS), + Reset(RESET), + SPI(SPI), + Transmitting, +} + +use super::sx127x_lora::register::{FskDataModulationShaping, FskRampUpRamDown}; +use Error::*; + +#[cfg(not(feature = "version_0x09"))] +const VERSION_CHECK: u8 = 0x12; + +#[cfg(feature = "version_0x09")] +const VERSION_CHECK: u8 = 0x09; + +impl LoRa +where + SPI: Transfer + Write, + CS: OutputPin, + RESET: OutputPin, +{ + /// Builds and returns a new instance of the radio. Only one instance of the radio should exist at a time. + /// This also preforms a hardware reset of the module and then puts it in standby. + pub fn new(spi: SPI, cs: CS, reset: RESET) -> Self { + Self { + spi, + cs, + reset, + explicit_header: true, + mode: RadioMode::Sleep, + } + } + + pub fn reset>( + &mut self, + d: &mut D, + ) -> Result<(), Error> { + self.reset.set_low().map_err(Reset)?; + d.delay_ms(10_u32); + self.reset.set_high().map_err(Reset)?; + d.delay_ms(10_u32); + let version = self.read_register(Register::RegVersion.addr())?; + if version == VERSION_CHECK { + self.set_mode(RadioMode::Sleep)?; + self.write_register(Register::RegFifoTxBaseAddr.addr(), 0)?; + self.write_register(Register::RegFifoRxBaseAddr.addr(), 0)?; + let lna = self.read_register(Register::RegLna.addr())?; + self.write_register(Register::RegLna.addr(), lna | 0x03)?; + self.write_register(Register::RegModemConfig3.addr(), 0x04)?; + self.set_tcxo(true)?; + self.set_mode(RadioMode::Stdby)?; + self.cs.set_high().map_err(CS)?; + Ok(()) + } else { + Err(Error::VersionMismatch(version)) + } + } + + /// Transmits up to 255 bytes of data. To avoid the use of an allocator, this takes a fixed 255 u8 + /// array and a payload size and returns the number of bytes sent if successful. + pub fn transmit_payload_busy( + &mut self, + buffer: [u8; 255], + payload_size: usize, + ) -> Result> { + if self.transmitting()? { + Err(Transmitting) + } else { + self.set_mode(RadioMode::Stdby)?; + if self.explicit_header { + self.set_explicit_header_mode()?; + } else { + self.set_implicit_header_mode()?; + } + + self.write_register(Register::RegIrqFlags.addr(), 0)?; + self.write_register(Register::RegFifoAddrPtr.addr(), 0)?; + self.write_register(Register::RegPayloadLength.addr(), 0)?; + for byte in buffer.iter().take(payload_size) { + self.write_register(Register::RegFifo.addr(), *byte)?; + } + self.write_register(Register::RegPayloadLength.addr(), payload_size as u8)?; + self.set_mode(RadioMode::Tx)?; + while self.transmitting()? {} + Ok(payload_size) + } + } + + pub fn set_dio0_tx_done(&mut self) -> Result<(), Error> { + self.write_register(Register::RegIrqFlagsMask.addr(), 0b1111_0111)?; + let mapping = self.read_register(Register::RegDioMapping1.addr())?; + self.write_register(Register::RegDioMapping1.addr(), (mapping & 0x3F) | 0x40) + } + + pub fn set_dio0_rx_done(&mut self) -> Result<(), Error> { + self.write_register(Register::RegIrqFlagsMask.addr(), 0b0001_1111)?; + let mapping = self.read_register(Register::RegDioMapping1.addr())?; + self.write_register(Register::RegDioMapping1.addr(), mapping & 0x3F) + } + + pub fn transmit_payload( + &mut self, + buffer: &[u8], + ) -> Result<(), Error> { + assert!(buffer.len() < 255); + if self.transmitting()? { + Err(Transmitting) + } else { + self.set_mode(RadioMode::Stdby)?; + if self.explicit_header { + self.set_explicit_header_mode()?; + } else { + self.set_implicit_header_mode()?; + } + + self.write_register(Register::RegIrqFlags.addr(), 0)?; + self.write_register(Register::RegFifoAddrPtr.addr(), 0)?; + self.write_register(Register::RegPayloadLength.addr(), 0)?; + for byte in buffer.iter() { + self.write_register(Register::RegFifo.addr(), *byte)?; + } + self.write_register(Register::RegPayloadLength.addr(), buffer.len() as u8)?; + self.set_mode(RadioMode::Tx)?; + Ok(()) + } + } + + pub fn packet_ready(&mut self) -> Result> { + Ok(self.read_register(Register::RegIrqFlags.addr())?.get_bit(6)) + } + + pub fn irq_flags_mask(&mut self) -> Result> { + Ok(self.read_register(Register::RegIrqFlagsMask.addr())? as u8) + } + + pub fn irq_flags(&mut self) -> Result> { + Ok(self.read_register(Register::RegIrqFlags.addr())? as u8) + } + + pub fn read_packet_size(&mut self) -> Result> { + let size = self.read_register(Register::RegRxNbBytes.addr())?; + Ok(size as usize) + } + + /// Returns the contents of the fifo as a fixed 255 u8 array. This should only be called is there is a + /// new packet ready to be read. + pub fn read_packet( + &mut self, + buffer: &mut [u8], + ) -> Result<(), Error> { + self.clear_irq()?; + let size = self.read_register(Register::RegRxNbBytes.addr())?; + assert!(size as usize <= buffer.len()); + let fifo_addr = self.read_register(Register::RegFifoRxCurrentAddr.addr())?; + self.write_register(Register::RegFifoAddrPtr.addr(), fifo_addr)?; + for i in 0..size { + let byte = self.read_register(Register::RegFifo.addr())?; + buffer[i as usize] = byte; + } + self.write_register(Register::RegFifoAddrPtr.addr(), 0)?; + Ok(()) + } + + /// Returns true if the radio is currently transmitting a packet. + pub fn transmitting(&mut self) -> Result> { + if (self.read_register(Register::RegOpMode.addr())? & RadioMode::Tx.addr()) + == RadioMode::Tx.addr() + { + Ok(true) + } else { + if (self.read_register(Register::RegIrqFlags.addr())? & IRQ::IrqTxDoneMask.addr()) == 1 + { + self.write_register(Register::RegIrqFlags.addr(), IRQ::IrqTxDoneMask.addr())?; + } + Ok(false) + } + } + + /// Clears the radio's IRQ registers. + pub fn clear_irq(&mut self) -> Result> { + let irq_flags = self.read_register(Register::RegIrqFlags.addr())?; + self.write_register(Register::RegIrqFlags.addr(), 0xFF)?; + Ok(irq_flags) + } + + /// Sets the transmit power and pin. Levels can range from 0-14 when the output + /// pin = 0(RFO), and form 0-20 when output pin = 1(PaBoost). Power is in dB. + /// Default value is `17`. + pub fn set_tx_power( + &mut self, + mut level: i32, + output_pin: u8, + ) -> Result<(), Error> { + if PaConfig::PaOutputRfoPin.addr() == output_pin { + // RFO + if level < 0 { + level = 0; + } else if level > 14 { + level = 14; + } + self.write_register(Register::RegPaConfig.addr(), (0x70 | level) as u8) + } else { + // PA BOOST + if level > 17 { + if level > 20 { + level = 20; + } + // subtract 3 from level, so 18 - 20 maps to 15 - 17 + level -= 3; + + // High Power +20 dBm Operation (Semtech SX1276/77/78/79 5.4.3.) + self.write_register(Register::RegPaDac.addr(), 0x87)?; + self.set_ocp(140)?; + } else { + if level < 2 { + level = 2; + } + //Default value PA_HF/LF or +17dBm + self.write_register(Register::RegPaDac.addr(), 0x84)?; + self.set_ocp(100)?; + } + level -= 2; + self.write_register( + Register::RegPaConfig.addr(), + PaConfig::PaBoost.addr() | level as u8, + ) + } + } + + pub fn get_modem_stat(&mut self) -> Result> { + Ok(self.read_register(Register::RegModemStat.addr())? as u8) + } + + /// Sets the over current protection on the radio(mA). + pub fn set_ocp(&mut self, ma: u8) -> Result<(), Error> { + let mut ocp_trim: u8 = 27; + + if ma <= 120 { + ocp_trim = (ma - 45) / 5; + } else if ma <= 240 { + ocp_trim = (ma + 30) / 10; + } + self.write_register(Register::RegOcp.addr(), 0x20 | (0x1F & ocp_trim)) + } + + /// Sets the state of the radio. Default mode after initiation is `Standby`. + pub fn set_mode(&mut self, mode: RadioMode) -> Result<(), Error> { + if self.explicit_header { + self.set_explicit_header_mode()?; + } else { + self.set_implicit_header_mode()?; + } + self.write_register( + Register::RegOpMode.addr(), + RadioMode::LongRangeMode.addr() | mode.addr(), + )?; + + self.mode = mode; + Ok(()) + } + + pub fn reset_payload_length(&mut self) -> Result<(), Error> { + self.write_register(Register::RegPayloadLength.addr(), 0xFF) + } + + /// Sets the frequency of the radio. Values are in megahertz. + /// I.E. 915 MHz must be used for North America. Check regulation for your area. + pub fn set_frequency(&mut self, freq: u32) -> Result<(), Error> { + const FREQ_STEP: f64 = 61.03515625; + // calculate register values + let frf = (freq as f64 / FREQ_STEP) as u32; + // write registers + self.write_register( + Register::RegFrfMsb.addr(), + ((frf & 0x00FF_0000) >> 16) as u8, + )?; + self.write_register(Register::RegFrfMid.addr(), ((frf & 0x0000_FF00) >> 8) as u8)?; + self.write_register(Register::RegFrfLsb.addr(), (frf & 0x0000_00FF) as u8) + } + + /// Sets the radio to use an explicit header. Default state is `ON`. + fn set_explicit_header_mode(&mut self) -> Result<(), Error> { + let reg_modem_config_1 = self.read_register(Register::RegModemConfig1.addr())?; + self.write_register(Register::RegModemConfig1.addr(), reg_modem_config_1 & 0xfe)?; + self.explicit_header = true; + Ok(()) + } + + /// Sets the radio to use an implicit header. Default state is `OFF`. + fn set_implicit_header_mode(&mut self) -> Result<(), Error> { + let reg_modem_config_1 = self.read_register(Register::RegModemConfig1.addr())?; + self.write_register(Register::RegModemConfig1.addr(), reg_modem_config_1 & 0x01)?; + self.explicit_header = false; + Ok(()) + } + + /// Sets the spreading factor of the radio. Supported values are between 6 and 12. + /// If a spreading factor of 6 is set, implicit header mode must be used to transmit + /// and receive packets. Default value is `7`. + pub fn set_spreading_factor( + &mut self, + mut sf: u8, + ) -> Result<(), Error> { + if sf < 6 { + sf = 6; + } else if sf > 12 { + sf = 12; + } + + if sf == 6 { + self.write_register(Register::RegDetectionOptimize.addr(), 0xc5)?; + self.write_register(Register::RegDetectionThreshold.addr(), 0x0c)?; + } else { + self.write_register(Register::RegDetectionOptimize.addr(), 0xc3)?; + self.write_register(Register::RegDetectionThreshold.addr(), 0x0a)?; + } + let modem_config_2 = self.read_register(Register::RegModemConfig2.addr())?; + self.write_register( + Register::RegModemConfig2.addr(), + (modem_config_2 & 0x0f) | ((sf << 4) & 0xf0), + )?; + self.set_ldo_flag()?; + + self.write_register(Register::RegSymbTimeoutLsb.addr(), 0x05)?; + + Ok(()) + } + + pub fn set_tcxo(&mut self, external: bool) -> Result<(), Error> { + if external { + self.write_register(Register::RegTcxo.addr(), 0x10) + } else { + self.write_register(Register::RegTcxo.addr(), 0x00) + } + } + + /// Sets the signal bandwidth of the radio. Supported values are: `7800 Hz`, `10400 Hz`, + /// `15600 Hz`, `20800 Hz`, `31250 Hz`,`41700 Hz` ,`62500 Hz`,`125000 Hz` and `250000 Hz` + /// Default value is `125000 Hz` + pub fn set_signal_bandwidth( + &mut self, + sbw: i64, + ) -> Result<(), Error> { + let bw: i64 = match sbw { + 7_800 => 0, + 10_400 => 1, + 15_600 => 2, + 20_800 => 3, + 31_250 => 4, + 41_700 => 5, + 62_500 => 6, + 125_000 => 7, + 250_000 => 8, + _ => 9, + }; + let modem_config_1 = self.read_register(Register::RegModemConfig1.addr())?; + self.write_register( + Register::RegModemConfig1.addr(), + (modem_config_1 & 0x0f) | ((bw << 4) as u8), + )?; + self.set_ldo_flag()?; + Ok(()) + } + + /// Sets the coding rate of the radio with the numerator fixed at 4. Supported values + /// are between `5` and `8`, these correspond to coding rates of `4/5` and `4/8`. + /// Default value is `5`. + pub fn set_coding_rate_4( + &mut self, + mut denominator: u8, + ) -> Result<(), Error> { + if denominator < 5 { + denominator = 5; + } else if denominator > 8 { + denominator = 8; + } + let cr = denominator - 4; + let modem_config_1 = self.read_register(Register::RegModemConfig1.addr())?; + self.write_register( + Register::RegModemConfig1.addr(), + (modem_config_1 & 0xf1) | (cr << 1), + ) + } + + /// Sets the preamble length of the radio. Values are between 6 and 65535. + /// Default value is `8`. + pub fn set_preamble_length( + &mut self, + length: i64, + ) -> Result<(), Error> { + self.write_register(Register::RegPreambleMsb.addr(), (length >> 8) as u8)?; + self.write_register(Register::RegPreambleLsb.addr(), length as u8) + } + + /// Enables are disables the radio's CRC check. Default value is `false`. + pub fn set_crc(&mut self, value: bool) -> Result<(), Error> { + let modem_config_2 = self.read_register(Register::RegModemConfig2.addr())?; + if value { + self.write_register(Register::RegModemConfig2.addr(), modem_config_2 | 0x04) + } else { + self.write_register(Register::RegModemConfig2.addr(), modem_config_2 & 0xfb) + } + } + + /// Inverts the radio's IQ signals. Default value is `false`. + pub fn set_invert_iq(&mut self, value: bool) -> Result<(), Error> { + if value { + self.write_register(Register::RegInvertiq.addr(), 0x66)?; + self.write_register(Register::RegInvertiq2.addr(), 0x19) + } else { + self.write_register(Register::RegInvertiq.addr(), 0x27)?; + self.write_register(Register::RegInvertiq2.addr(), 0x1d) + } + } + + /// Returns the spreading factor of the radio. + pub fn get_spreading_factor(&mut self) -> Result> { + Ok(self.read_register(Register::RegModemConfig2.addr())? >> 4) + } + + /// Returns the signal bandwidth of the radio. + pub fn get_signal_bandwidth(&mut self) -> Result> { + let bw = self.read_register(Register::RegModemConfig1.addr())? >> 4; + let bw = match bw { + 0 => 7_800, + 1 => 10_400, + 2 => 15_600, + 3 => 20_800, + 4 => 31_250, + 5 => 41_700, + 6 => 62_500, + 7 => 125_000, + 8 => 250_000, + 9 => 500_000, + _ => -1, + }; + Ok(bw) + } + + /// Returns the RSSI of the last received packet. + pub fn get_packet_rssi(&mut self) -> Result> { + Ok(i32::from(self.read_register(Register::RegPktRssiValue.addr())?) - 157) + } + + /// Returns the signal to noise radio of the the last received packet. + pub fn get_packet_snr(&mut self) -> Result> { + Ok(f64::from( + self.read_register(Register::RegPktSnrValue.addr())?, + )) + } + + /// Returns the frequency error of the last received packet in Hz. + pub fn get_packet_frequency_error(&mut self) -> Result> { + let mut freq_error: i32; + freq_error = i32::from(self.read_register(Register::RegFreqErrorMsb.addr())? & 0x7); + freq_error <<= 8i64; + freq_error += i32::from(self.read_register(Register::RegFreqErrorMid.addr())?); + freq_error <<= 8i64; + freq_error += i32::from(self.read_register(Register::RegFreqErrorLsb.addr())?); + + let f_xtal = 32_000_000; // FXOSC: crystal oscillator (XTAL) frequency (2.5. Chip Specification, p. 14) + let f_error = ((f64::from(freq_error) * (1i64 << 24) as f64) / f64::from(f_xtal)) + * (self.get_signal_bandwidth()? as f64 / 500_000.0f64); // p. 37 + Ok(f_error as i64) + } + + fn set_ldo_flag(&mut self) -> Result<(), Error> { + let sw = self.get_signal_bandwidth()?; + // Section 4.1.1.5 + let symbol_duration = 1000 / (sw / ((1_i64) << self.get_spreading_factor()?)); + + // Section 4.1.1.6 + let ldo_on = symbol_duration > 16; + + let mut config_3 = self.read_register(Register::RegModemConfig3.addr())?; + config_3.set_bit(3, ldo_on); + //config_3.set_bit(2, true); + self.write_register(Register::RegModemConfig3.addr(), config_3) + } + + fn read_register(&mut self, reg: u8) -> Result> { + self.cs.set_low().map_err(CS)?; + + let mut buffer = [reg & 0x7f, 0]; + let transfer = self.spi.transfer(&mut buffer).map_err(SPI)?; + self.cs.set_high().map_err(CS)?; + Ok(transfer[1]) + } + + fn write_register( + &mut self, + reg: u8, + byte: u8, + ) -> Result<(), Error> { + self.cs.set_low().map_err(CS)?; + + let buffer = [reg | 0x80, byte]; + self.spi.write(&buffer).map_err(SPI)?; + self.cs.set_high().map_err(CS)?; + Ok(()) + } + + pub fn put_in_fsk_mode(&mut self) -> Result<(), Error> { + // Put in FSK mode + let mut op_mode = 0; + op_mode + .set_bit(7, false) // FSK mode + .set_bits(5..6, 0x00) // FSK modulation + .set_bit(3, false) //Low freq registers + .set_bits(0..2, 0b011); // Mode + + self.write_register(Register::RegOpMode as u8, op_mode) + } + + pub fn set_fsk_pa_ramp( + &mut self, + modulation_shaping: FskDataModulationShaping, + ramp: FskRampUpRamDown, + ) -> Result<(), Error> { + let mut pa_ramp = 0; + pa_ramp + .set_bits(5..6, modulation_shaping as u8) + .set_bits(0..3, ramp as u8); + + self.write_register(Register::RegPaRamp as u8, pa_ramp) + } + + pub fn set_lora_pa_ramp(&mut self) -> Result<(), Error> { + self.write_register(Register::RegPaRamp as u8, 0b1000) + } + + pub fn set_lora_sync_word(&mut self) -> Result<(), Error> { + self.write_register(Register::RegSyncWord as u8, 0x34) + } +} +/// Modes of the radio and their corresponding register values. +#[derive(Clone, Copy)] +pub enum RadioMode { + LongRangeMode = 0x80, + Sleep = 0x00, + Stdby = 0x01, + Tx = 0x03, + RxContinuous = 0x05, + RxSingle = 0x06, +} + +impl RadioMode { + /// Returns the address of the mode. + pub fn addr(self) -> u8 { + self as u8 + } +} diff --git a/embassy-lora/src/sx127x/sx127x_lora/register.rs b/embassy-lora/src/sx127x/sx127x_lora/register.rs new file mode 100644 index 000000000..2445e21b1 --- /dev/null +++ b/embassy-lora/src/sx127x/sx127x_lora/register.rs @@ -0,0 +1,107 @@ +// Copyright Charles Wade (https://github.com/mr-glt/sx127x_lora). Licensed under the Apache 2.0 +// license +// +// Modifications made to make the driver work with the rust-lorawan link layer. +#![allow(dead_code, clippy::enum_variant_names)] + +#[derive(Clone, Copy)] +pub enum Register { + RegFifo = 0x00, + RegOpMode = 0x01, + RegFrfMsb = 0x06, + RegFrfMid = 0x07, + RegFrfLsb = 0x08, + RegPaConfig = 0x09, + RegPaRamp = 0x0a, + RegOcp = 0x0b, + RegLna = 0x0c, + RegFifoAddrPtr = 0x0d, + RegFifoTxBaseAddr = 0x0e, + RegFifoRxBaseAddr = 0x0f, + RegFifoRxCurrentAddr = 0x10, + RegIrqFlagsMask = 0x11, + RegIrqFlags = 0x12, + RegRxNbBytes = 0x13, + RegPktSnrValue = 0x19, + RegModemStat = 0x18, + RegPktRssiValue = 0x1a, + RegModemConfig1 = 0x1d, + RegModemConfig2 = 0x1e, + RegSymbTimeoutLsb = 0x1f, + RegPreambleMsb = 0x20, + RegPreambleLsb = 0x21, + RegPayloadLength = 0x22, + RegMaxPayloadLength = 0x23, + RegModemConfig3 = 0x26, + RegFreqErrorMsb = 0x28, + RegFreqErrorMid = 0x29, + RegFreqErrorLsb = 0x2a, + RegRssiWideband = 0x2c, + RegDetectionOptimize = 0x31, + RegInvertiq = 0x33, + RegDetectionThreshold = 0x37, + RegSyncWord = 0x39, + RegInvertiq2 = 0x3b, + RegDioMapping1 = 0x40, + RegVersion = 0x42, + RegTcxo = 0x4b, + RegPaDac = 0x4d, +} +#[derive(Clone, Copy)] +pub enum PaConfig { + PaBoost = 0x80, + PaOutputRfoPin = 0, +} + +#[derive(Clone, Copy)] +pub enum IRQ { + IrqTxDoneMask = 0x08, + IrqPayloadCrcErrorMask = 0x20, + IrqRxDoneMask = 0x40, +} + +impl Register { + pub fn addr(self) -> u8 { + self as u8 + } +} + +impl PaConfig { + pub fn addr(self) -> u8 { + self as u8 + } +} + +impl IRQ { + pub fn addr(self) -> u8 { + self as u8 + } +} + +#[derive(Clone, Copy)] +pub enum FskDataModulationShaping { + None = 1, + GaussianBt1d0 = 2, + GaussianBt0d5 = 10, + GaussianBt0d3 = 11, +} + +#[derive(Clone, Copy)] +pub enum FskRampUpRamDown { + _3d4ms = 0b000, + _2ms = 0b0001, + _1ms = 0b0010, + _500us = 0b0011, + _250us = 0b0100, + _125us = 0b0101, + _100us = 0b0110, + _62us = 0b0111, + _50us = 0b1000, + _40us = 0b1001, + _31us = 0b1010, + _25us = 0b1011, + _20us = 0b1100, + _15us = 0b1101, + _12us = 0b1110, + _10us = 0b1111, +} diff --git a/examples/stm32l0/Cargo.toml b/examples/stm32l0/Cargo.toml index 7dfcdb0fe..371ac68cc 100644 --- a/examples/stm32l0/Cargo.toml +++ b/examples/stm32l0/Cargo.toml @@ -23,6 +23,10 @@ embassy-stm32 = { version = "0.1.0", path = "../../embassy-stm32", features = [" embassy-hal-common = {version = "0.1.0", path = "../../embassy-hal-common" } embassy-macros = { path = "../../embassy-macros" } +embassy-lora = { version = "0.1.0", path = "../../embassy-lora", features = ["sx127x", "time"] } +lorawan-device = { git = "https://github.com/lulf/rust-lorawan.git", rev = "a373d06fa8858d251bc70d5789cebcd9a638ec42", default-features = false, features = ["async"] } +lorawan-encoding = { git = "https://github.com/lulf/rust-lorawan.git", rev = "a373d06fa8858d251bc70d5789cebcd9a638ec42", default-features = false, features = ["default-crypto"] } + defmt = "0.2.3" defmt-rtt = "0.2.0" diff --git a/examples/stm32l0/src/bin/lorawan.rs b/examples/stm32l0/src/bin/lorawan.rs new file mode 100644 index 000000000..5ca69f9a7 --- /dev/null +++ b/examples/stm32l0/src/bin/lorawan.rs @@ -0,0 +1,104 @@ +//! This example runs on the STM32 LoRa Discovery board which has a builtin Semtech Sx127x radio +#![no_std] +#![no_main] +#![macro_use] +#![allow(dead_code)] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +#[path = "../example_common.rs"] +mod example_common; + +use embassy_lora::{sx127x::*, LoraTimer}; +use embassy_stm32::{ + dbgmcu::Dbgmcu, + dma::NoDma, + exti::ExtiInput, + gpio::{Input, Level, Output, Pull, Speed}, + rcc, + rng::Rng, + spi, + time::U32Ext, + Peripherals, +}; +use lorawan_device::async_device::{region, Device, JoinMode}; +use lorawan_encoding::default_crypto::DefaultFactory as Crypto; + +fn config() -> embassy_stm32::Config { + let mut config = embassy_stm32::Config::default(); + config.rcc = config.rcc.clock_src(embassy_stm32::rcc::ClockSrc::HSI16); + config +} + +#[embassy::main(config = "config()")] +async fn main(_spawner: embassy::executor::Spawner, mut p: Peripherals) { + unsafe { + Dbgmcu::enable_all(); + } + + let mut rcc = rcc::Rcc::new(p.RCC); + let _ = rcc.enable_hsi48(&mut p.SYSCFG, p.CRS); + + // SPI for sx127x + let spi = spi::Spi::new( + p.SPI1, + p.PB3, + p.PA7, + p.PA6, + NoDma, + NoDma, + 200_000.hz(), + spi::Config::default(), + ); + + let cs = Output::new(p.PA15, Level::High, Speed::Low); + let reset = Output::new(p.PC0, Level::High, Speed::Low); + let _ = Input::new(p.PB1, Pull::None); + + let ready = Input::new(p.PB4, Pull::Up); + let ready_pin = ExtiInput::new(ready, p.EXTI4); + + let radio = Sx127xRadio::new( + spi, + cs, + reset, + ready_pin, + DummySwitch, + &mut embassy::time::Delay, + ) + .unwrap(); + + let region = region::EU868::default().into(); + let mut radio_buffer = [0; 256]; + let mut device: Device<'_, _, Crypto, _, _> = Device::new( + region, + radio, + LoraTimer, + Rng::new(p.RNG), + &mut radio_buffer[..], + ); + + defmt::info!("Joining LoRaWAN network"); + + // TODO: Adjust the EUI and Keys according to your network credentials + device + .join(&JoinMode::OTAA { + deveui: [0, 0, 0, 0, 0, 0, 0, 0], + appeui: [0, 0, 0, 0, 0, 0, 0, 0], + appkey: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }) + .await + .ok() + .unwrap(); + defmt::info!("LoRaWAN network joined"); + + defmt::info!("Sending 'PING'"); + device.send(b"PING", 1, false).await.ok().unwrap(); + defmt::info!("Message sent!"); +} + +pub struct DummySwitch; +impl RadioSwitch for DummySwitch { + fn set_rx(&mut self) {} + fn set_tx(&mut self) {} +} diff --git a/examples/stm32wl55/Cargo.toml b/examples/stm32wl55/Cargo.toml index d0c727862..4688bdad1 100644 --- a/examples/stm32wl55/Cargo.toml +++ b/examples/stm32wl55/Cargo.toml @@ -19,8 +19,12 @@ defmt-error = [] [dependencies] embassy = { version = "0.1.0", path = "../../embassy", features = ["defmt", "defmt-trace"] } embassy-traits = { version = "0.1.0", path = "../../embassy-traits", features = ["defmt"] } -embassy-stm32 = { version = "0.1.0", path = "../../embassy-stm32", features = ["defmt", "defmt-trace", "stm32wl55jc_cm4", "time-driver-tim2", "memory-x", "subghz"] } +embassy-stm32 = { version = "0.1.0", path = "../../embassy-stm32", features = ["defmt", "defmt-trace", "stm32wl55jc_cm4", "time-driver-tim2", "memory-x", "subghz", "unstable-pac"] } embassy-hal-common = {version = "0.1.0", path = "../../embassy-hal-common" } +embassy-lora = { version = "0.1.0", path = "../../embassy-lora", features = ["stm32wl", "time"] } + +lorawan-device = { git = "https://github.com/lulf/rust-lorawan.git", rev = "a373d06fa8858d251bc70d5789cebcd9a638ec42", default-features = false, features = ["async"] } +lorawan-encoding = { git = "https://github.com/lulf/rust-lorawan.git", rev = "a373d06fa8858d251bc70d5789cebcd9a638ec42", default-features = false, features = ["default-crypto"] } defmt = "0.2.3" defmt-rtt = "0.2.0" diff --git a/examples/stm32wl55/src/bin/lorawan.rs b/examples/stm32wl55/src/bin/lorawan.rs new file mode 100644 index 000000000..155905ae7 --- /dev/null +++ b/examples/stm32wl55/src/bin/lorawan.rs @@ -0,0 +1,79 @@ +#![no_std] +#![no_main] +#![macro_use] +#![allow(dead_code)] +#![feature(generic_associated_types)] +#![feature(type_alias_impl_trait)] + +#[path = "../example_common.rs"] +mod example_common; + +use embassy_lora::{stm32wl::*, LoraTimer}; +use embassy_stm32::{ + dbgmcu::Dbgmcu, + dma::NoDma, + gpio::{Level, Output, Pin, Speed}, + interrupt, pac, rcc, + rng::Rng, + subghz::*, + Peripherals, +}; +use lorawan_device::async_device::{region, Device, JoinMode}; +use lorawan_encoding::default_crypto::DefaultFactory as Crypto; + +fn config() -> embassy_stm32::Config { + let mut config = embassy_stm32::Config::default(); + config.rcc = config.rcc.clock_src(embassy_stm32::rcc::ClockSrc::HSI16); + config +} + +#[embassy::main(config = "config()")] +async fn main(_spawner: embassy::executor::Spawner, p: Peripherals) { + unsafe { + Dbgmcu::enable_all(); + let mut rcc = rcc::Rcc::new(p.RCC); + rcc.enable_lsi(); + pac::RCC.ccipr().modify(|w| { + w.set_rngsel(0b01); + }); + } + + let ctrl1 = Output::new(p.PC3.degrade(), Level::High, Speed::High); + let ctrl2 = Output::new(p.PC4.degrade(), Level::High, Speed::High); + let ctrl3 = Output::new(p.PC5.degrade(), Level::High, Speed::High); + let rfs = RadioSwitch::new(ctrl1, ctrl2, ctrl3); + + let radio = SubGhz::new(p.SUBGHZSPI, p.PA5, p.PA7, p.PA6, NoDma, NoDma); + + let irq = interrupt::take!(SUBGHZ_RADIO); + static mut RADIO_STATE: SubGhzState<'static> = SubGhzState::new(); + let radio = unsafe { SubGhzRadio::new(&mut RADIO_STATE, radio, rfs, irq) }; + + let region = region::EU868::default().into(); + let mut radio_buffer = [0; 256]; + let mut device: Device<'_, _, Crypto, _, _> = Device::new( + region, + radio, + LoraTimer, + Rng::new(p.RNG), + &mut radio_buffer[..], + ); + + defmt::info!("Joining LoRaWAN network"); + + // TODO: Adjust the EUI and Keys according to your network credentials + device + .join(&JoinMode::OTAA { + deveui: [0, 0, 0, 0, 0, 0, 0, 0], + appeui: [0, 0, 0, 0, 0, 0, 0, 0], + appkey: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }) + .await + .ok() + .unwrap(); + defmt::info!("LoRaWAN network joined"); + + defmt::info!("Sending 'PING'"); + device.send(b"PING", 1, false).await.ok().unwrap(); + defmt::info!("Message sent!"); +}