From 7ad6280e6575fc400f84caa5c26eb3ba9770877f Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 31 Aug 2021 14:32:48 +0200 Subject: [PATCH] Add HAL for SubGhz peripheral for STM32 WL series Based on the HAL from stm32wl, the peripheral driver has been modified to fit into embassy, using the embassy APIs, providing operation of the radio peripheral. The initial version does not offer any async APIs, but the example shows how the radio IRQ can be used to perform async TX of the radio. --- embassy-hal-common/Cargo.toml | 1 + embassy-hal-common/src/lib.rs | 1 + embassy-hal-common/src/ratio.rs | 128 ++ embassy-stm32/Cargo.toml | 1 + embassy-stm32/src/lib.rs | 3 + embassy-stm32/src/pwr/mod.rs | 1 + embassy-stm32/src/rcc/wl5x/mod.rs | 16 +- embassy-stm32/src/spi/mod.rs | 1 + embassy-stm32/src/spi/v2.rs | 3 +- embassy-stm32/src/subghz/bit_sync.rs | 160 ++ embassy-stm32/src/subghz/cad_params.rs | 230 +++ embassy-stm32/src/subghz/calibrate.rs | 122 ++ embassy-stm32/src/subghz/fallback_mode.rs | 37 + embassy-stm32/src/subghz/hse_trim.rs | 107 ++ embassy-stm32/src/subghz/irq.rs | 292 ++++ embassy-stm32/src/subghz/lora_sync_word.rs | 20 + embassy-stm32/src/subghz/mod.rs | 1679 +++++++++++++++++++ embassy-stm32/src/subghz/mod_params.rs | 996 +++++++++++ embassy-stm32/src/subghz/ocp.rs | 14 + embassy-stm32/src/subghz/op_error.rs | 48 + embassy-stm32/src/subghz/pa_config.rs | 161 ++ embassy-stm32/src/subghz/packet_params.rs | 537 ++++++ embassy-stm32/src/subghz/packet_status.rs | 279 +++ embassy-stm32/src/subghz/packet_type.rs | 44 + embassy-stm32/src/subghz/pkt_ctrl.rs | 247 +++ embassy-stm32/src/subghz/pmode.rs | 27 + embassy-stm32/src/subghz/pwr_ctrl.rs | 160 ++ embassy-stm32/src/subghz/reg_mode.rs | 18 + embassy-stm32/src/subghz/rf_frequency.rs | 138 ++ embassy-stm32/src/subghz/rx_timeout_stop.rs | 21 + embassy-stm32/src/subghz/sleep_cfg.rs | 109 ++ embassy-stm32/src/subghz/smps.rs | 45 + embassy-stm32/src/subghz/standby_clk.rs | 20 + embassy-stm32/src/subghz/stats.rs | 184 ++ embassy-stm32/src/subghz/status.rs | 202 +++ embassy-stm32/src/subghz/tcxo_mode.rs | 170 ++ embassy-stm32/src/subghz/timeout.rs | 469 ++++++ embassy-stm32/src/subghz/tx_params.rs | 166 ++ embassy-stm32/src/subghz/value_error.rs | 129 ++ examples/stm32wl55/Cargo.toml | 2 +- examples/stm32wl55/src/bin/subghz.rs | 129 ++ stm32-data | 2 +- 42 files changed, 7110 insertions(+), 9 deletions(-) create mode 100644 embassy-hal-common/src/ratio.rs create mode 100644 embassy-stm32/src/subghz/bit_sync.rs create mode 100644 embassy-stm32/src/subghz/cad_params.rs create mode 100644 embassy-stm32/src/subghz/calibrate.rs create mode 100644 embassy-stm32/src/subghz/fallback_mode.rs create mode 100644 embassy-stm32/src/subghz/hse_trim.rs create mode 100644 embassy-stm32/src/subghz/irq.rs create mode 100644 embassy-stm32/src/subghz/lora_sync_word.rs create mode 100644 embassy-stm32/src/subghz/mod.rs create mode 100644 embassy-stm32/src/subghz/mod_params.rs create mode 100644 embassy-stm32/src/subghz/ocp.rs create mode 100644 embassy-stm32/src/subghz/op_error.rs create mode 100644 embassy-stm32/src/subghz/pa_config.rs create mode 100644 embassy-stm32/src/subghz/packet_params.rs create mode 100644 embassy-stm32/src/subghz/packet_status.rs create mode 100644 embassy-stm32/src/subghz/packet_type.rs create mode 100644 embassy-stm32/src/subghz/pkt_ctrl.rs create mode 100644 embassy-stm32/src/subghz/pmode.rs create mode 100644 embassy-stm32/src/subghz/pwr_ctrl.rs create mode 100644 embassy-stm32/src/subghz/reg_mode.rs create mode 100644 embassy-stm32/src/subghz/rf_frequency.rs create mode 100644 embassy-stm32/src/subghz/rx_timeout_stop.rs create mode 100644 embassy-stm32/src/subghz/sleep_cfg.rs create mode 100644 embassy-stm32/src/subghz/smps.rs create mode 100644 embassy-stm32/src/subghz/standby_clk.rs create mode 100644 embassy-stm32/src/subghz/stats.rs create mode 100644 embassy-stm32/src/subghz/status.rs create mode 100644 embassy-stm32/src/subghz/tcxo_mode.rs create mode 100644 embassy-stm32/src/subghz/timeout.rs create mode 100644 embassy-stm32/src/subghz/tx_params.rs create mode 100644 embassy-stm32/src/subghz/value_error.rs create mode 100644 examples/stm32wl55/src/bin/subghz.rs diff --git a/embassy-hal-common/Cargo.toml b/embassy-hal-common/Cargo.toml index 4db536de..0e28085f 100644 --- a/embassy-hal-common/Cargo.toml +++ b/embassy-hal-common/Cargo.toml @@ -18,3 +18,4 @@ defmt = { version = "0.2.0", optional = true } log = { version = "0.4.11", optional = true } cortex-m = "0.7.1" usb-device = "0.2.7" +num-traits = { version = "0.2.14", default-features = false } diff --git a/embassy-hal-common/src/lib.rs b/embassy-hal-common/src/lib.rs index b62ae8b9..d2f6daba 100644 --- a/embassy-hal-common/src/lib.rs +++ b/embassy-hal-common/src/lib.rs @@ -8,6 +8,7 @@ mod macros; pub mod peripheral; pub mod ring_buffer; pub mod usb; +pub mod ratio; /// Low power blocking wait loop using WFE/SEV. pub fn low_power_wait_until(mut condition: impl FnMut() -> bool) { diff --git a/embassy-hal-common/src/ratio.rs b/embassy-hal-common/src/ratio.rs new file mode 100644 index 00000000..ce7e4b1b --- /dev/null +++ b/embassy-hal-common/src/ratio.rs @@ -0,0 +1,128 @@ +use core::ops::{Add, Div, Mul}; +use num_traits::{CheckedAdd, CheckedDiv, CheckedMul}; + +/// Represents the ratio between two numbers. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Ratio { + /// Numerator. + numer: T, + /// Denominator. + denom: T, +} + +impl Ratio { + /// Creates a new `Ratio`. + #[inline(always)] + pub const fn new_raw(numer: T, denom: T) -> Ratio { + Ratio { numer, denom } + } + + /// Gets an immutable reference to the numerator. + #[inline(always)] + pub const fn numer(&self) -> &T { + &self.numer + } + + /// Gets an immutable reference to the denominator. + #[inline(always)] + pub const fn denom(&self) -> &T { + &self.denom + } +} + +impl Ratio { + /// Converts to an integer, rounding towards zero. + #[inline(always)] + pub fn to_integer(&self) -> T { + unwrap!(self.numer().checked_div(self.denom())) + } +} + +impl Div for Ratio { + type Output = Self; + + #[inline(always)] + fn div(mut self, rhs: T) -> Self::Output { + self.denom = unwrap!(self.denom().checked_mul(&rhs)); + self + } +} + +impl Mul for Ratio { + type Output = Self; + + #[inline(always)] + fn mul(mut self, rhs: T) -> Self::Output { + self.numer = unwrap!(self.numer().checked_mul(&rhs)); + self + } +} + +impl Add for Ratio { + type Output = Self; + + #[inline(always)] + fn add(mut self, rhs: T) -> Self::Output { + self.numer = unwrap!(unwrap!(self.denom().checked_mul(&rhs)).checked_add(self.numer())); + self + } +} + +macro_rules! impl_from_for_float { + ($from:ident) => { + impl From> for f32 { + #[inline(always)] + fn from(r: Ratio<$from>) -> Self { + (r.numer as f32) / (r.denom as f32) + } + } + + impl From> for f64 { + #[inline(always)] + fn from(r: Ratio<$from>) -> Self { + (r.numer as f64) / (r.denom as f64) + } + } + }; +} + +impl_from_for_float!(u8); +impl_from_for_float!(u16); +impl_from_for_float!(u32); +impl_from_for_float!(u64); +impl_from_for_float!(u128); +impl_from_for_float!(i8); +impl_from_for_float!(i16); +impl_from_for_float!(i32); +impl_from_for_float!(i64); +impl_from_for_float!(i128); + +impl core::fmt::Display for Ratio { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::write!(f, "{} / {}", self.numer(), self.denom()) + } +} + +#[cfg(test)] +mod tests { + use super::Ratio; + + #[test] + fn basics() { + let mut r = Ratio::new_raw(1, 2) + 2; + assert_eq!(*r.numer(), 5); + assert_eq!(*r.denom(), 2); + assert_eq!(r.to_integer(), 2); + + r = r * 2; + assert_eq!(*r.numer(), 10); + assert_eq!(*r.denom(), 2); + assert_eq!(r.to_integer(), 5); + + r = r / 2; + assert_eq!(*r.numer(), 10); + assert_eq!(*r.denom(), 4); + assert_eq!(r.to_integer(), 2); + } +} diff --git a/embassy-stm32/Cargo.toml b/embassy-stm32/Cargo.toml index f3b2e0e4..325e128d 100644 --- a/embassy-stm32/Cargo.toml +++ b/embassy-stm32/Cargo.toml @@ -44,6 +44,7 @@ defmt-error = [ ] sdmmc-rs = ["embedded-sdmmc"] net = ["embassy-net", "vcell"] memory-x = ["stm32-metapac/memory-x"] +subghz = [] # Features starting with `_` are for internal use only. They're not intended # to be enabled by other crates, and are not covered by semver guarantees. diff --git a/embassy-stm32/src/lib.rs b/embassy-stm32/src/lib.rs index 503d10f5..e0e77a59 100644 --- a/embassy-stm32/src/lib.rs +++ b/embassy-stm32/src/lib.rs @@ -51,6 +51,9 @@ pub mod spi; #[cfg(usart)] pub mod usart; +#[cfg(feature = "subghz")] +pub mod subghz; + // This must go last, so that it sees all the impl_foo! macros defined earlier. mod generated { diff --git a/embassy-stm32/src/pwr/mod.rs b/embassy-stm32/src/pwr/mod.rs index 1bb104bd..bc167b01 100644 --- a/embassy-stm32/src/pwr/mod.rs +++ b/embassy-stm32/src/pwr/mod.rs @@ -1,5 +1,6 @@ #[cfg_attr(any(pwr_h7, pwr_h7smps), path = "h7.rs")] #[cfg_attr(pwr_f4, path = "f4.rs")] +#[cfg_attr(pwr_wl5, path = "wl5.rs")] mod _version; pub use _version::*; diff --git a/embassy-stm32/src/rcc/wl5x/mod.rs b/embassy-stm32/src/rcc/wl5x/mod.rs index e1e001c7..8ed0cb95 100644 --- a/embassy-stm32/src/rcc/wl5x/mod.rs +++ b/embassy-stm32/src/rcc/wl5x/mod.rs @@ -2,7 +2,6 @@ pub use super::types::*; use crate::pac; use crate::peripherals::{self, RCC}; use crate::rcc::{get_freqs, set_freqs, Clocks}; -use crate::time::Hertz; use crate::time::U32Ext; use core::marker::PhantomData; use embassy::util::Unborrow; @@ -16,10 +15,12 @@ use embassy_hal_common::unborrow; /// HSI speed pub const HSI_FREQ: u32 = 16_000_000; +pub const HSE32_FREQ: u32 = 32_000_000; + /// System clock mux source #[derive(Clone, Copy)] pub enum ClockSrc { - HSE(Hertz), + HSE32, HSI16, } @@ -137,14 +138,17 @@ impl RccExt for RCC { (HSI_FREQ, 0x01) } - ClockSrc::HSE(freq) => { - // Enable HSE + ClockSrc::HSE32 => { + // Enable HSE32 unsafe { - rcc.cr().write(|w| w.set_hseon(true)); + rcc.cr().write(|w| { + w.set_hsebyppwr(true); + w.set_hseon(true); + }); while !rcc.cr().read().hserdy() {} } - (freq.0, 0x02) + (HSE32_FREQ, 0x02) } }; diff --git a/embassy-stm32/src/spi/mod.rs b/embassy-stm32/src/spi/mod.rs index 9bb5a729..6249de84 100644 --- a/embassy-stm32/src/spi/mod.rs +++ b/embassy-stm32/src/spi/mod.rs @@ -9,6 +9,7 @@ pub use _version::*; use crate::gpio::Pin; +#[derive(Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { Framing, diff --git a/embassy-stm32/src/spi/v2.rs b/embassy-stm32/src/spi/v2.rs index 496d100f..9df71ef2 100644 --- a/embassy-stm32/src/spi/v2.rs +++ b/embassy-stm32/src/spi/v2.rs @@ -71,7 +71,8 @@ impl<'d, T: Instance, Tx, Rx> Spi<'d, T, Tx, Rx> { let miso = miso.degrade(); let pclk = T::frequency(); - let br = Self::compute_baud_rate(pclk, freq.into()); + let freq = freq.into(); + let br = Self::compute_baud_rate(pclk, freq); unsafe { T::enable(); diff --git a/embassy-stm32/src/subghz/bit_sync.rs b/embassy-stm32/src/subghz/bit_sync.rs new file mode 100644 index 00000000..86b6c48f --- /dev/null +++ b/embassy-stm32/src/subghz/bit_sync.rs @@ -0,0 +1,160 @@ +/// Bit synchronization. +/// +/// This must be cleared to `0x00` (the reset value) when using packet types +/// other than LoRa. +/// +/// Argument of [`set_bit_sync`](crate::subghz::SubGhz::set_bit_sync). +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct BitSync { + val: u8, +} + +impl BitSync { + /// Bit synchronization register reset value. + pub const RESET: BitSync = BitSync { val: 0x00 }; + + /// Create a new [`BitSync`] structure from a raw value. + /// + /// Reserved bits will be masked. + pub const fn from_raw(raw: u8) -> Self { + Self { val: raw & 0x70 } + } + + /// Get the raw value of the [`BitSync`] register. + pub const fn as_bits(&self) -> u8 { + self.val + } + + /// LoRa simple bit synchronization enable. + /// + /// # Example + /// + /// Enable simple bit synchronization. + /// + /// ``` + /// use stm32wl_hal::subghz::BitSync; + /// + /// const BIT_SYNC: BitSync = BitSync::RESET.set_simple_bit_sync_en(true); + /// # assert_eq!(u8::from(BIT_SYNC), 0x40u8); + /// ``` + #[must_use = "set_simple_bit_sync_en returns a modified BitSync"] + pub const fn set_simple_bit_sync_en(mut self, en: bool) -> BitSync { + if en { + self.val |= 1 << 6; + } else { + self.val &= !(1 << 6); + } + self + } + + /// Returns `true` if simple bit synchronization is enabled. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::BitSync; + /// + /// let bs: BitSync = BitSync::RESET; + /// assert_eq!(bs.simple_bit_sync_en(), false); + /// let bs: BitSync = bs.set_simple_bit_sync_en(true); + /// assert_eq!(bs.simple_bit_sync_en(), true); + /// let bs: BitSync = bs.set_simple_bit_sync_en(false); + /// assert_eq!(bs.simple_bit_sync_en(), false); + /// ``` + pub const fn simple_bit_sync_en(&self) -> bool { + self.val & (1 << 6) != 0 + } + + /// LoRa RX data inversion. + /// + /// # Example + /// + /// Invert receive data. + /// + /// ``` + /// use stm32wl_hal::subghz::BitSync; + /// + /// const BIT_SYNC: BitSync = BitSync::RESET.set_rx_data_inv(true); + /// # assert_eq!(u8::from(BIT_SYNC), 0x20u8); + /// ``` + #[must_use = "set_rx_data_inv returns a modified BitSync"] + pub const fn set_rx_data_inv(mut self, inv: bool) -> BitSync { + if inv { + self.val |= 1 << 5; + } else { + self.val &= !(1 << 5); + } + self + } + + /// Returns `true` if LoRa RX data is inverted. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::BitSync; + /// + /// let bs: BitSync = BitSync::RESET; + /// assert_eq!(bs.rx_data_inv(), false); + /// let bs: BitSync = bs.set_rx_data_inv(true); + /// assert_eq!(bs.rx_data_inv(), true); + /// let bs: BitSync = bs.set_rx_data_inv(false); + /// assert_eq!(bs.rx_data_inv(), false); + /// ``` + pub const fn rx_data_inv(&self) -> bool { + self.val & (1 << 5) != 0 + } + + /// LoRa normal bit synchronization enable. + /// + /// # Example + /// + /// Enable normal bit synchronization. + /// + /// ``` + /// use stm32wl_hal::subghz::BitSync; + /// + /// const BIT_SYNC: BitSync = BitSync::RESET.set_norm_bit_sync_en(true); + /// # assert_eq!(u8::from(BIT_SYNC), 0x10u8); + /// ``` + #[must_use = "set_norm_bit_sync_en returns a modified BitSync"] + pub const fn set_norm_bit_sync_en(mut self, en: bool) -> BitSync { + if en { + self.val |= 1 << 4; + } else { + self.val &= !(1 << 4); + } + self + } + + /// Returns `true` if normal bit synchronization is enabled. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::BitSync; + /// + /// let bs: BitSync = BitSync::RESET; + /// assert_eq!(bs.norm_bit_sync_en(), false); + /// let bs: BitSync = bs.set_norm_bit_sync_en(true); + /// assert_eq!(bs.norm_bit_sync_en(), true); + /// let bs: BitSync = bs.set_norm_bit_sync_en(false); + /// assert_eq!(bs.norm_bit_sync_en(), false); + /// ``` + pub const fn norm_bit_sync_en(&self) -> bool { + self.val & (1 << 4) != 0 + } +} + +impl From for u8 { + fn from(bs: BitSync) -> Self { + bs.val + } +} + +impl Default for BitSync { + fn default() -> Self { + Self::RESET + } +} diff --git a/embassy-stm32/src/subghz/cad_params.rs b/embassy-stm32/src/subghz/cad_params.rs new file mode 100644 index 00000000..fc887a24 --- /dev/null +++ b/embassy-stm32/src/subghz/cad_params.rs @@ -0,0 +1,230 @@ +use crate::subghz::timeout::Timeout; + +/// Number of symbols used for channel activity detection scans. +/// +/// Argument of [`CadParams::set_num_symbol`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum NbCadSymbol { + /// 1 symbol. + S1 = 0x0, + /// 2 symbols. + S2 = 0x1, + /// 4 symbols. + S4 = 0x2, + /// 8 symbols. + S8 = 0x3, + /// 16 symbols. + S16 = 0x4, +} + +/// Mode to enter after a channel activity detection scan is finished. +/// +/// Argument of [`CadParams::set_exit_mode`]. +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum ExitMode { + /// Standby with RC 13 MHz mode entry after CAD. + Standby = 0, + /// Standby with RC 13 MHz mode after CAD if no LoRa symbol is detected + /// during the CAD scan. + /// If a LoRa symbol is detected, the sub-GHz radio stays in RX mode + /// until a packet is received or until the CAD timeout is reached. + StandbyLoRa = 1, +} + +/// Channel activity detection (CAD) parameters. +/// +/// Argument of [`set_cad_params`]. +/// +/// # Recommended CAD settings +/// +/// This is taken directly from the datasheet. +/// +/// "The correct values selected in the table below must be carefully tested to +/// ensure a good detection at sensitivity level and to limit the number of +/// false detections" +/// +/// | SF (Spreading Factor) | [`set_det_peak`] | [`set_det_min`] | +/// |-----------------------|------------------|-----------------| +/// | 5 | 0x18 | 0x10 | +/// | 6 | 0x19 | 0x10 | +/// | 7 | 0x20 | 0x10 | +/// | 8 | 0x21 | 0x10 | +/// | 9 | 0x22 | 0x10 | +/// | 10 | 0x23 | 0x10 | +/// | 11 | 0x24 | 0x10 | +/// | 12 | 0x25 | 0x10 | +/// +/// [`set_cad_params`]: crate::subghz::SubGhz::set_cad_params +/// [`set_det_peak`]: crate::subghz::CadParams::set_det_peak +/// [`set_det_min`]: crate::subghz::CadParams::set_det_min +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CadParams { + buf: [u8; 8], +} + +impl CadParams { + /// Create a new `CadParams`. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::CadParams; + /// + /// const CAD_PARAMS: CadParams = CadParams::new(); + /// assert_eq!(CAD_PARAMS, CadParams::default()); + /// ``` + pub const fn new() -> CadParams { + CadParams { + buf: [super::OpCode::SetCadParams as u8, 0, 0, 0, 0, 0, 0, 0], + } + .set_num_symbol(NbCadSymbol::S1) + .set_det_peak(0x18) + .set_det_min(0x10) + .set_exit_mode(ExitMode::Standby) + } + + /// Number of symbols used for a CAD scan. + /// + /// # Example + /// + /// Set the number of symbols to 4. + /// + /// ``` + /// use stm32wl_hal::subghz::{CadParams, NbCadSymbol}; + /// + /// const CAD_PARAMS: CadParams = CadParams::new().set_num_symbol(NbCadSymbol::S4); + /// # assert_eq!(CAD_PARAMS.as_slice()[1], 0x2); + /// ``` + #[must_use = "set_num_symbol returns a modified CadParams"] + pub const fn set_num_symbol(mut self, nb: NbCadSymbol) -> CadParams { + self.buf[1] = nb as u8; + self + } + + /// Used with [`set_det_min`] to correlate the LoRa symbol. + /// + /// See the table in [`CadParams`] docs for recommended values. + /// + /// # Example + /// + /// Setting the recommended value for a spreading factor of 7. + /// + /// ``` + /// use stm32wl_hal::subghz::CadParams; + /// + /// const CAD_PARAMS: CadParams = CadParams::new().set_det_peak(0x20).set_det_min(0x10); + /// # assert_eq!(CAD_PARAMS.as_slice()[2], 0x20); + /// # assert_eq!(CAD_PARAMS.as_slice()[3], 0x10); + /// ``` + /// + /// [`set_det_min`]: crate::subghz::CadParams::set_det_min + #[must_use = "set_det_peak returns a modified CadParams"] + pub const fn set_det_peak(mut self, peak: u8) -> CadParams { + self.buf[2] = peak; + self + } + + /// Used with [`set_det_peak`] to correlate the LoRa symbol. + /// + /// See the table in [`CadParams`] docs for recommended values. + /// + /// # Example + /// + /// Setting the recommended value for a spreading factor of 6. + /// + /// ``` + /// use stm32wl_hal::subghz::CadParams; + /// + /// const CAD_PARAMS: CadParams = CadParams::new().set_det_peak(0x18).set_det_min(0x10); + /// # assert_eq!(CAD_PARAMS.as_slice()[2], 0x18); + /// # assert_eq!(CAD_PARAMS.as_slice()[3], 0x10); + /// ``` + /// + /// [`set_det_peak`]: crate::subghz::CadParams::set_det_peak + #[must_use = "set_det_min returns a modified CadParams"] + pub const fn set_det_min(mut self, min: u8) -> CadParams { + self.buf[3] = min; + self + } + + /// Mode to enter after a channel activity detection scan is finished. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CadParams, ExitMode}; + /// + /// const CAD_PARAMS: CadParams = CadParams::new().set_exit_mode(ExitMode::Standby); + /// # assert_eq!(CAD_PARAMS.as_slice()[4], 0x00); + /// # assert_eq!(CAD_PARAMS.set_exit_mode(ExitMode::StandbyLoRa).as_slice()[4], 0x01); + /// ``` + #[must_use = "set_exit_mode returns a modified CadParams"] + pub const fn set_exit_mode(mut self, mode: ExitMode) -> CadParams { + self.buf[4] = mode as u8; + self + } + + /// Set the timeout. + /// + /// This is only used with [`ExitMode::StandbyLoRa`]. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CadParams, ExitMode, Timeout}; + /// + /// const TIMEOUT: Timeout = Timeout::from_raw(0x123456); + /// const CAD_PARAMS: CadParams = CadParams::new() + /// .set_exit_mode(ExitMode::StandbyLoRa) + /// .set_timeout(TIMEOUT); + /// # assert_eq!(CAD_PARAMS.as_slice()[4], 0x01); + /// # assert_eq!(CAD_PARAMS.as_slice()[5], 0x12); + /// # assert_eq!(CAD_PARAMS.as_slice()[6], 0x34); + /// # assert_eq!(CAD_PARAMS.as_slice()[7], 0x56); + /// ``` + #[must_use = "set_timeout returns a modified CadParams"] + pub const fn set_timeout(mut self, to: Timeout) -> CadParams { + let to_bytes: [u8; 3] = to.as_bytes(); + self.buf[5] = to_bytes[0]; + self.buf[6] = to_bytes[1]; + self.buf[7] = to_bytes[2]; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CadParams, ExitMode, NbCadSymbol, Timeout}; + /// + /// const TIMEOUT: Timeout = Timeout::from_raw(0x123456); + /// const CAD_PARAMS: CadParams = CadParams::new() + /// .set_num_symbol(NbCadSymbol::S4) + /// .set_det_peak(0x18) + /// .set_det_min(0x10) + /// .set_exit_mode(ExitMode::StandbyLoRa) + /// .set_timeout(TIMEOUT); + /// + /// assert_eq!( + /// CAD_PARAMS.as_slice(), + /// &[0x88, 0x02, 0x18, 0x10, 0x01, 0x12, 0x34, 0x56] + /// ); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for CadParams { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32/src/subghz/calibrate.rs b/embassy-stm32/src/subghz/calibrate.rs new file mode 100644 index 00000000..dc8c8069 --- /dev/null +++ b/embassy-stm32/src/subghz/calibrate.rs @@ -0,0 +1,122 @@ +/// Image calibration. +/// +/// Argument of [`calibrate_image`]. +/// +/// [`calibrate_image`]: crate::subghz::SubGhz::calibrate_image +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CalibrateImage(pub(crate) u8, pub(crate) u8); + +impl CalibrateImage { + /// Image calibration for the 430 - 440 MHz ISM band. + pub const ISM_430_440: CalibrateImage = CalibrateImage(0x6B, 0x6F); + + /// Image calibration for the 470 - 510 MHz ISM band. + pub const ISM_470_510: CalibrateImage = CalibrateImage(0x75, 0x81); + + /// Image calibration for the 779 - 787 MHz ISM band. + pub const ISM_779_787: CalibrateImage = CalibrateImage(0xC1, 0xC5); + + /// Image calibration for the 863 - 870 MHz ISM band. + pub const ISM_863_870: CalibrateImage = CalibrateImage(0xD7, 0xDB); + + /// Image calibration for the 902 - 928 MHz ISM band. + pub const ISM_902_928: CalibrateImage = CalibrateImage(0xE1, 0xE9); + + /// Create a new `CalibrateImage` structure from raw values. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::CalibrateImage; + /// + /// const CAL: CalibrateImage = CalibrateImage::new(0xE1, 0xE9); + /// assert_eq!(CAL, CalibrateImage::ISM_902_928); + /// ``` + pub const fn new(f1: u8, f2: u8) -> CalibrateImage { + CalibrateImage(f1, f2) + } + + /// Create a new `CalibrateImage` structure from two frequencies. + /// + /// # Arguments + /// + /// The units for `freq1` and `freq2` are in MHz. + /// + /// # Panics + /// + /// * Panics if `freq1` is less than `freq2`. + /// * Panics if `freq1` or `freq2` is not a multiple of 4MHz. + /// * Panics if `freq1` or `freq2` is greater than `1020`. + /// + /// # Example + /// + /// Create an image calibration for the 430 - 440 MHz ISM band. + /// + /// ``` + /// use stm32wl_hal::subghz::CalibrateImage; + /// + /// let cal: CalibrateImage = CalibrateImage::from_freq(428, 444); + /// assert_eq!(cal, CalibrateImage::ISM_430_440); + /// ``` + pub fn from_freq(freq1: u16, freq2: u16) -> CalibrateImage { + assert!(freq2 >= freq1); + assert_eq!(freq1 % 4, 0); + assert_eq!(freq2 % 4, 0); + assert!(freq1 <= 1020); + assert!(freq2 <= 1020); + CalibrateImage((freq1 / 4) as u8, (freq2 / 4) as u8) + } +} + +impl Default for CalibrateImage { + fn default() -> Self { + CalibrateImage::new(0xE1, 0xE9) + } +} + +/// Block calibration. +/// +/// Argument of [`calibrate`]. +/// +/// [`calibrate`]: crate::subghz::SubGhz::calibrate +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum Calibrate { + /// Image calibration + Image = 1 << 6, + /// RF-ADC bulk P calibration + AdcBulkP = 1 << 5, + /// RF-ADC bulk N calibration + AdcBulkN = 1 << 4, + /// RF-ADC pulse calibration + AdcPulse = 1 << 3, + /// RF-PLL calibration + Pll = 1 << 2, + /// Sub-GHz radio RC 13 MHz calibration + Rc13M = 1 << 1, + /// Sub-GHz radio RC 64 kHz calibration + Rc64K = 1, +} + +impl Calibrate { + /// Get the bitmask for the block calibration. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Calibrate; + /// + /// assert_eq!(Calibrate::Image.mask(), 0b0100_0000); + /// assert_eq!(Calibrate::AdcBulkP.mask(), 0b0010_0000); + /// assert_eq!(Calibrate::AdcBulkN.mask(), 0b0001_0000); + /// assert_eq!(Calibrate::AdcPulse.mask(), 0b0000_1000); + /// assert_eq!(Calibrate::Pll.mask(), 0b0000_0100); + /// assert_eq!(Calibrate::Rc13M.mask(), 0b0000_0010); + /// assert_eq!(Calibrate::Rc64K.mask(), 0b0000_0001); + /// ``` + pub const fn mask(self) -> u8 { + self as u8 + } +} diff --git a/embassy-stm32/src/subghz/fallback_mode.rs b/embassy-stm32/src/subghz/fallback_mode.rs new file mode 100644 index 00000000..bc7204da --- /dev/null +++ b/embassy-stm32/src/subghz/fallback_mode.rs @@ -0,0 +1,37 @@ +/// Fallback mode after successful packet transmission or packet reception. +/// +/// Argument of [`set_tx_rx_fallback_mode`]. +/// +/// [`set_tx_rx_fallback_mode`]: crate::subghz::SubGhz::set_tx_rx_fallback_mode. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum FallbackMode { + /// Standby mode entry. + Standby = 0x20, + /// Standby with HSE32 enabled. + StandbyHse = 0x30, + /// Frequency synthesizer entry. + Fs = 0x40, +} + +impl From for u8 { + fn from(fm: FallbackMode) -> Self { + fm as u8 + } +} + +impl Default for FallbackMode { + /// Default fallback mode after power-on reset. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FallbackMode; + /// + /// assert_eq!(FallbackMode::default(), FallbackMode::Standby); + /// ``` + fn default() -> Self { + FallbackMode::Standby + } +} diff --git a/embassy-stm32/src/subghz/hse_trim.rs b/embassy-stm32/src/subghz/hse_trim.rs new file mode 100644 index 00000000..101baa86 --- /dev/null +++ b/embassy-stm32/src/subghz/hse_trim.rs @@ -0,0 +1,107 @@ +use crate::subghz::value_error::ValueError; + +/// HSE32 load capacitor trimming. +/// +/// Argument of [`set_hse_in_trim`] and [`set_hse_out_trim`]. +/// +/// [`set_hse_in_trim`]: crate::subghz::SubGhz::set_hse_in_trim +/// [`set_hse_out_trim`]: crate::subghz::SubGhz::set_hse_out_trim +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct HseTrim { + val: u8, +} + +impl HseTrim { + /// Maximum capacitor value, ~33.4 pF + pub const MAX: HseTrim = HseTrim::from_raw(0x2F); + + /// Minimum capacitor value, ~11.3 pF + pub const MIN: HseTrim = HseTrim::from_raw(0x00); + + /// Power-on-reset capacitor value, ~20.3 pF + /// + /// This is the same as `default`. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::HseTrim; + /// + /// assert_eq!(HseTrim::POR, HseTrim::default()); + /// ``` + pub const POR: HseTrim = HseTrim::from_raw(0x12); + + /// Create a new [`HseTrim`] structure from a raw value. + /// + /// Values greater than the maximum of `0x2F` will be set to the maximum. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::HseTrim; + /// + /// assert_eq!(HseTrim::from_raw(0xFF), HseTrim::MAX); + /// assert_eq!(HseTrim::from_raw(0x2F), HseTrim::MAX); + /// assert_eq!(HseTrim::from_raw(0x00), HseTrim::MIN); + /// ``` + pub const fn from_raw(raw: u8) -> HseTrim { + if raw > 0x2F { + HseTrim { val: 0x2F } + } else { + HseTrim { val: raw } + } + } + + /// Create a HSE trim value from farads. + /// + /// Values greater than the maximum of 33.4 pF will be set to the maximum. + /// Values less than the minimum of 11.3 pF will be set to the minimum. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::HseTrim; + /// + /// assert!(HseTrim::from_farads(1.0).is_err()); + /// assert!(HseTrim::from_farads(1e-12).is_err()); + /// assert_eq!(HseTrim::from_farads(20.2e-12), Ok(HseTrim::default())); + /// ``` + pub fn from_farads(farads: f32) -> Result> { + const MAX: f32 = 33.4E-12; + const MIN: f32 = 11.3E-12; + if farads > MAX { + Err(ValueError::too_high(farads, MAX)) + } else if farads < MIN { + Err(ValueError::too_low(farads, MIN)) + } else { + Ok(HseTrim::from_raw(((farads - 11.3e-12) / 0.47e-12) as u8)) + } + } + + /// Get the capacitance as farads. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::HseTrim; + /// + /// assert_eq!((HseTrim::MAX.as_farads() * 10e11) as u8, 33); + /// assert_eq!((HseTrim::MIN.as_farads() * 10e11) as u8, 11); + /// ``` + pub fn as_farads(&self) -> f32 { + (self.val as f32) * 0.47E-12 + 11.3E-12 + } +} + +impl From for u8 { + fn from(ht: HseTrim) -> Self { + ht.val + } +} + +impl Default for HseTrim { + fn default() -> Self { + Self::POR + } +} diff --git a/embassy-stm32/src/subghz/irq.rs b/embassy-stm32/src/subghz/irq.rs new file mode 100644 index 00000000..b113095a --- /dev/null +++ b/embassy-stm32/src/subghz/irq.rs @@ -0,0 +1,292 @@ +/// Interrupt lines. +/// +/// Argument of [`CfgIrq::irq_enable`] and [`CfgIrq::irq_disable`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum IrqLine { + /// Global interrupt. + Global, + /// Interrupt line 1. + /// + /// This will output to the [`RfIrq0`](crate::gpio::RfIrq0) pin. + Line1, + /// Interrupt line 2. + /// + /// This will output to the [`RfIrq1`](crate::gpio::RfIrq1) pin. + Line2, + /// Interrupt line 3. + /// + /// This will output to the [`RfIrq2`](crate::gpio::RfIrq2) pin. + Line3, +} + +impl IrqLine { + pub(super) const fn offset(&self) -> usize { + match self { + IrqLine::Global => 1, + IrqLine::Line1 => 3, + IrqLine::Line2 => 5, + IrqLine::Line3 => 7, + } + } +} + +/// IRQ bit mapping +/// +/// See table 37 "IRQ bit mapping and definition" in the reference manual for +/// more information. +#[repr(u16)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Irq { + /// Packet transmission finished. + /// + /// * Packet type: LoRa and GFSK + /// * Operation: TX + TxDone = (1 << 0), + /// Packet reception finished. + /// + /// * Packet type: LoRa and GFSK + /// * Operation: RX + RxDone = (1 << 1), + /// Preamble detected. + /// + /// * Packet type: LoRa and GFSK + /// * Operation: RX + PreambleDetected = (1 << 2), + /// Synchronization word valid. + /// + /// * Packet type: GFSK + /// * Operation: RX + SyncDetected = (1 << 3), + /// Header valid. + /// + /// * Packet type: LoRa + /// * Operation: RX + HeaderValid = (1 << 4), + /// Header CRC error. + /// + /// * Packet type: LoRa + /// * Operation: RX + HeaderErr = (1 << 5), + /// Dual meaning error. + /// + /// For GFSK RX this indicates a preamble, syncword, address, CRC, or length + /// error. + /// + /// For LoRa RX this indicates a CRC error. + Err = (1 << 6), + /// Channel activity detection finished. + /// + /// * Packet type: LoRa + /// * Operation: CAD + CadDone = (1 << 7), + /// Channel activity detected. + /// + /// * Packet type: LoRa + /// * Operation: CAD + CadDetected = (1 << 8), + /// RX or TX timeout. + /// + /// * Packet type: LoRa and GFSK + /// * Operation: RX and TX + Timeout = (1 << 9), +} + +impl Irq { + /// Get the bitmask for an IRQ. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Irq; + /// + /// assert_eq!(Irq::TxDone.mask(), 0x0001); + /// assert_eq!(Irq::Timeout.mask(), 0x0200); + /// ``` + pub const fn mask(self) -> u16 { + self as u16 + } +} + +/// Argument for [`set_irq_cfg`]. +/// +/// [`set_irq_cfg`]: crate::subghz::SubGhz::set_irq_cfg +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CfgIrq { + buf: [u8; 9], +} + +impl CfgIrq { + /// Create a new `CfgIrq`. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// The default value has all interrupts disabled on all lines. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::CfgIrq; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new(); + /// ``` + pub const fn new() -> CfgIrq { + CfgIrq { + buf: [ + super::OpCode::CfgDioIrq as u8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ], + } + } + + /// Enable an interrupt. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CfgIrq, Irq, IrqLine}; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new() + /// .irq_enable(IrqLine::Global, Irq::TxDone) + /// .irq_enable(IrqLine::Global, Irq::Timeout); + /// # assert_eq!(IRQ_CFG.as_slice()[1], 0x02); + /// # assert_eq!(IRQ_CFG.as_slice()[2], 0x01); + /// # assert_eq!(IRQ_CFG.as_slice()[3], 0x00); + /// ``` + #[must_use = "irq_enable returns a modified CfgIrq"] + pub const fn irq_enable(mut self, line: IrqLine, irq: Irq) -> CfgIrq { + let mask: u16 = irq as u16; + let offset: usize = line.offset(); + self.buf[offset] |= ((mask >> 8) & 0xFF) as u8; + self.buf[offset + 1] |= (mask & 0xFF) as u8; + self + } + + /// Enable an interrupt on all lines. + /// + /// As far as I can tell with empirical testing all IRQ lines need to be + /// enabled for the internal interrupt to be pending in the NVIC. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CfgIrq, Irq}; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new() + /// .irq_enable_all(Irq::TxDone) + /// .irq_enable_all(Irq::Timeout); + /// # assert_eq!(IRQ_CFG.as_slice()[1], 0x02); + /// # assert_eq!(IRQ_CFG.as_slice()[2], 0x01); + /// # assert_eq!(IRQ_CFG.as_slice()[3], 0x02); + /// # assert_eq!(IRQ_CFG.as_slice()[4], 0x01); + /// # assert_eq!(IRQ_CFG.as_slice()[5], 0x02); + /// # assert_eq!(IRQ_CFG.as_slice()[6], 0x01); + /// # assert_eq!(IRQ_CFG.as_slice()[7], 0x02); + /// # assert_eq!(IRQ_CFG.as_slice()[8], 0x01); + /// ``` + #[must_use = "irq_enable_all returns a modified CfgIrq"] + pub const fn irq_enable_all(mut self, irq: Irq) -> CfgIrq { + let mask: [u8; 2] = irq.mask().to_be_bytes(); + + self.buf[1] |= mask[0]; + self.buf[2] |= mask[1]; + self.buf[3] |= mask[0]; + self.buf[4] |= mask[1]; + self.buf[5] |= mask[0]; + self.buf[6] |= mask[1]; + self.buf[7] |= mask[0]; + self.buf[8] |= mask[1]; + + self + } + + /// Disable an interrupt. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CfgIrq, Irq, IrqLine}; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new() + /// .irq_enable(IrqLine::Global, Irq::TxDone) + /// .irq_enable(IrqLine::Global, Irq::Timeout) + /// .irq_disable(IrqLine::Global, Irq::TxDone) + /// .irq_disable(IrqLine::Global, Irq::Timeout); + /// # assert_eq!(IRQ_CFG.as_slice()[1], 0x00); + /// # assert_eq!(IRQ_CFG.as_slice()[2], 0x00); + /// # assert_eq!(IRQ_CFG.as_slice()[3], 0x00); + /// ``` + #[must_use = "irq_disable returns a modified CfgIrq"] + pub const fn irq_disable(mut self, line: IrqLine, irq: Irq) -> CfgIrq { + let mask: u16 = !(irq as u16); + let offset: usize = line.offset(); + self.buf[offset] &= ((mask >> 8) & 0xFF) as u8; + self.buf[offset + 1] &= (mask & 0xFF) as u8; + self + } + + /// Disable an interrupt on all lines. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CfgIrq, Irq}; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new() + /// .irq_enable_all(Irq::TxDone) + /// .irq_enable_all(Irq::Timeout) + /// .irq_disable_all(Irq::TxDone) + /// .irq_disable_all(Irq::Timeout); + /// # assert_eq!(IRQ_CFG, CfgIrq::new()); + /// ``` + #[must_use = "irq_disable_all returns a modified CfgIrq"] + pub const fn irq_disable_all(mut self, irq: Irq) -> CfgIrq { + let mask: [u8; 2] = (!irq.mask()).to_be_bytes(); + + self.buf[1] &= mask[0]; + self.buf[2] &= mask[1]; + self.buf[3] &= mask[0]; + self.buf[4] &= mask[1]; + self.buf[5] &= mask[0]; + self.buf[6] &= mask[1]; + self.buf[7] &= mask[0]; + self.buf[8] &= mask[1]; + + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CfgIrq, Irq}; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new() + /// .irq_enable_all(Irq::TxDone) + /// .irq_enable_all(Irq::Timeout); + /// + /// assert_eq!( + /// IRQ_CFG.as_slice(), + /// &[0x08, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01] + /// ); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for CfgIrq { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32/src/subghz/lora_sync_word.rs b/embassy-stm32/src/subghz/lora_sync_word.rs new file mode 100644 index 00000000..2c163104 --- /dev/null +++ b/embassy-stm32/src/subghz/lora_sync_word.rs @@ -0,0 +1,20 @@ +/// LoRa synchronization word. +/// +/// Argument of [`set_lora_sync_word`][crate::subghz::SubGhz::set_lora_sync_word]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum LoRaSyncWord { + /// LoRa private network. + Private, + /// LoRa public network. + Public, +} + +impl LoRaSyncWord { + pub(crate) const fn bytes(self) -> [u8; 2] { + match self { + LoRaSyncWord::Private => [0x14, 0x24], + LoRaSyncWord::Public => [0x34, 0x44], + } + } +} diff --git a/embassy-stm32/src/subghz/mod.rs b/embassy-stm32/src/subghz/mod.rs new file mode 100644 index 00000000..b1ed078f --- /dev/null +++ b/embassy-stm32/src/subghz/mod.rs @@ -0,0 +1,1679 @@ +//! Sub-GHz radio operating in the 150 - 960 MHz ISM band +//! +//! ## LoRa user notice +//! +//! The Sub-GHz radio may have an undocumented erratum, see this ST community +//! post for more information: [link] +//! +//! [link]: https://community.st.com/s/question/0D53W00000hR8kpSAC/stm32wl55-erratum-clairification +//! +//! NOTE: This HAL is based on https://github.com/newAM/stm32wl-hal, but adopted for use with the stm32-metapac +//! and SPI HALs. + +mod bit_sync; +mod cad_params; +mod calibrate; +mod fallback_mode; +mod hse_trim; +mod irq; +mod lora_sync_word; +mod mod_params; +mod ocp; +mod op_error; +mod pa_config; +mod packet_params; +mod packet_status; +mod packet_type; +mod pkt_ctrl; +mod pmode; +mod pwr_ctrl; +mod reg_mode; +mod rf_frequency; +mod rx_timeout_stop; +mod sleep_cfg; +mod smps; +mod standby_clk; +mod stats; +mod status; +mod tcxo_mode; +mod timeout; +mod tx_params; +mod value_error; + +pub use bit_sync::BitSync; +pub use cad_params::{CadParams, ExitMode, NbCadSymbol}; +pub use calibrate::{Calibrate, CalibrateImage}; +pub use fallback_mode::FallbackMode; +pub use hse_trim::HseTrim; +pub use irq::{CfgIrq, Irq, IrqLine}; +pub use lora_sync_word::LoRaSyncWord; +pub use mod_params::BpskModParams; +pub use mod_params::{CodingRate, LoRaBandwidth, LoRaModParams, SpreadingFactor}; +pub use mod_params::{FskBandwidth, FskBitrate, FskFdev, FskModParams, FskPulseShape}; +pub use ocp::Ocp; +pub use op_error::OpError; +pub use pa_config::{PaConfig, PaSel}; +pub use packet_params::{ + AddrComp, BpskPacketParams, CrcType, GenericPacketParams, HeaderType, LoRaPacketParams, + PreambleDetection, +}; +pub use packet_status::{FskPacketStatus, LoRaPacketStatus}; +pub use packet_type::PacketType; +pub use pkt_ctrl::{InfSeqSel, PktCtrl}; +pub use pmode::PMode; +pub use pwr_ctrl::{CurrentLim, PwrCtrl}; +pub use reg_mode::RegMode; +pub use rf_frequency::RfFreq; +pub use rx_timeout_stop::RxTimeoutStop; +pub use sleep_cfg::{SleepCfg, Startup}; +pub use smps::SmpsDrv; +pub use standby_clk::StandbyClk; +pub use stats::{FskStats, LoRaStats, Stats}; +pub use status::{CmdStatus, Status, StatusMode}; +pub use tcxo_mode::{TcxoMode, TcxoTrim}; +pub use timeout::Timeout; +pub use tx_params::{RampTime, TxParams}; +pub use value_error::ValueError; + +use embassy_hal_common::ratio::Ratio; + +use crate::{ + dma::NoDma, + pac, + peripherals::SUBGHZSPI, + rcc::sealed::RccPeripheral, + spi::{ByteOrder, Config as SpiConfig, MisoPin, MosiPin, SckPin, Spi}, + time::Hertz, +}; +use embassy::util::Unborrow; +use embedded_hal::{ + blocking::spi::{Transfer, Write}, + spi::MODE_0, +}; + +/// Passthrough for SPI errors (for now) +pub type Error = crate::spi::Error; + +struct Nss { + _priv: (), +} + +impl Nss { + pub fn new() -> Nss { + Self::clear(); + Nss { _priv: () } + } + + /// Clear NSS, enabling SPI transactions + #[inline(always)] + fn clear() { + let pwr = pac::PWR; + unsafe { + pwr.subghzspicr() + .modify(|w| w.set_nss(pac::pwr::vals::Nss::LOW)); + } + } + + /// Set NSS, disabling SPI transactions + #[inline(always)] + fn set() { + let pwr = pac::PWR; + unsafe { + pwr.subghzspicr() + .modify(|w| w.set_nss(pac::pwr::vals::Nss::HIGH)); + } + } +} + +impl Drop for Nss { + fn drop(&mut self) { + Self::set() + } +} + +/// Wakeup the radio from sleep mode. +/// +/// # Safety +/// +/// 1. This must not be called when the SubGHz radio is in use. +/// 2. This must not be called when the SubGHz SPI bus is in use. +/// +/// # Example +/// +/// See [`SubGhz::set_sleep`] +#[inline] +unsafe fn wakeup() { + Nss::clear(); + // RM0453 rev 2 page 171 section 5.7.2 "Sleep mode" + // on a firmware request via the sub-GHz radio SPI NSS signal + // (keeping sub-GHz radio SPI NSS low for at least 20 μs) + // + // I have found this to be a more reliable mechanism for ensuring NSS is + // pulled low for long enough to wake the radio. + while rfbusys() {} + Nss::set(); +} + +/// Returns `true` if the radio is busy. +/// +/// This may not be set immediately after NSS going low. +/// +/// See RM0461 Rev 4 section 5.3 page 181 "Radio busy management" for more +/// details. +#[inline] +fn rfbusys() -> bool { + // safety: atmoic read with no side-effects + //unsafe { (*pac::PWR::ptr()).sr2.read().rfbusys().is_busy() } + let pwr = pac::PWR; + unsafe { pwr.sr2().read().rfbusys() == pac::pwr::vals::Rfbusys::BUSY } +} + +/// Returns `true` if the radio is busy or NSS is low. +/// +/// See RM0461 Rev 4 section 5.3 page 181 "Radio busy management" for more +/// details. +#[inline] +fn rfbusyms() -> bool { + let pwr = pac::PWR; + unsafe { pwr.sr2().read().rfbusyms() == pac::pwr::vals::Rfbusyms::BUSY } +} + +/// Sub-GHz radio peripheral +pub struct SubGhz<'d, Tx, Rx> { + spi: Spi<'d, SUBGHZSPI, Tx, Rx>, +} + +impl<'d, Tx, Rx> SubGhz<'d, Tx, Rx> { + fn pulse_radio_reset() { + let rcc = pac::RCC; + unsafe { + rcc.csr().modify(|w| w.set_rfrst(true)); + rcc.csr().modify(|w| w.set_rfrst(false)); + } + } + + // TODO: This should be replaced with async handling based on IRQ + fn poll_not_busy(&self) { + let mut count: u32 = 1_000_000; + while rfbusys() { + count -= 1; + if count == 0 { + let pwr = pac::PWR; + unsafe { + panic!( + "rfbusys timeout pwr.sr2=0x{:X} pwr.subghzspicr=0x{:X} pwr.cr1=0x{:X}", + pwr.sr2().read().0, + pwr.subghzspicr().read().0, + pwr.cr1().read().0 + ); + } + } + } + } + + /// Create a new sub-GHz radio driver from a peripheral. + /// + /// This will reset the radio and the SPI bus, and enable the peripheral + /// clock. + pub fn new( + peri: impl Unborrow + 'd, + sck: impl Unborrow>, + mosi: impl Unborrow>, + miso: impl Unborrow>, + txdma: impl Unborrow, + rxdma: impl Unborrow, + ) -> Self { + Self::pulse_radio_reset(); + + // see RM0453 rev 1 section 7.2.13 page 291 + // The SUBGHZSPI_SCK frequency is obtained by PCLK3 divided by two. + // The SUBGHZSPI_SCK clock maximum speed must not exceed 16 MHz. + let clk = Hertz(core::cmp::min(SUBGHZSPI::frequency().0 / 2, 16_000_000)); + let mut config = SpiConfig::default(); + config.mode = MODE_0; + config.byte_order = ByteOrder::MsbFirst; + let spi = Spi::new(peri, sck, mosi, miso, txdma, rxdma, clk, config); + + unsafe { wakeup() }; + + SubGhz { spi } + } +} + +impl<'d> SubGhz<'d, NoDma, NoDma> { + fn read(&mut self, opcode: OpCode, data: &mut [u8]) -> Result<(), Error> { + self.poll_not_busy(); + { + let _nss: Nss = Nss::new(); + self.spi.write(&[opcode as u8])?; + self.spi.transfer(data)?; + } + self.poll_not_busy(); + Ok(()) + } + + /// Read one byte from the sub-Ghz radio. + fn read_1(&mut self, opcode: OpCode) -> Result { + let mut buf: [u8; 1] = [0; 1]; + self.read(opcode, &mut buf)?; + Ok(buf[0]) + } + + /// Read a fixed number of bytes from the sub-Ghz radio. + fn read_n(&mut self, opcode: OpCode) -> Result<[u8; N], Error> { + let mut buf: [u8; N] = [0; N]; + self.read(opcode, &mut buf)?; + Ok(buf) + } + + fn write(&mut self, data: &[u8]) -> Result<(), Error> { + self.poll_not_busy(); + { + let _nss: Nss = Nss::new(); + self.spi.write(data)?; + } + self.poll_not_busy(); + Ok(()) + } + + pub fn write_buffer(&mut self, offset: u8, data: &[u8]) -> Result<(), Error> { + self.poll_not_busy(); + { + let _nss: Nss = Nss::new(); + self.spi.write(&[OpCode::WriteBuffer as u8, offset])?; + self.spi.write(data)?; + } + self.poll_not_busy(); + + Ok(()) + } + + /// Read the radio buffer at the given offset. + /// + /// The offset and length of a received packet is provided by + /// [`rx_buffer_status`](Self::rx_buffer_status). + pub fn read_buffer(&mut self, offset: u8, buf: &mut [u8]) -> Result { + let mut status_buf: [u8; 1] = [0]; + + self.poll_not_busy(); + { + let _nss: Nss = Nss::new(); + self.spi.write(&[OpCode::ReadBuffer as u8, offset])?; + self.spi.transfer(&mut status_buf)?; + self.spi.transfer(buf)?; + } + self.poll_not_busy(); + + Ok(status_buf[0].into()) + } +} + +// helper to pack register writes into a single buffer to avoid multiple DMA +// transfers +macro_rules! wr_reg { + [$reg:ident, $($data:expr),+] => { + &[ + OpCode::WriteRegister as u8, + Register::$reg.address().to_be_bytes()[0], + Register::$reg.address().to_be_bytes()[1], + $($data),+ + ] + }; +} + +// 5.8.2 +/// Register access +impl<'d> SubGhz<'d, NoDma, NoDma> { + // register write with variable length data + fn write_register(&mut self, register: Register, data: &[u8]) -> Result<(), Error> { + let addr: [u8; 2] = register.address().to_be_bytes(); + + self.poll_not_busy(); + { + let _nss: Nss = Nss::new(); + self.spi + .write(&[OpCode::WriteRegister as u8, addr[0], addr[1]])?; + self.spi.write(data)?; + } + self.poll_not_busy(); + + Ok(()) + } + + /// Set the LoRa bit synchronization. + pub fn set_bit_sync(&mut self, bs: BitSync) -> Result<(), Error> { + self.write(wr_reg![GBSYNC, bs.as_bits()]) + } + + /// Set the generic packet control register. + pub fn set_pkt_ctrl(&mut self, pkt_ctrl: PktCtrl) -> Result<(), Error> { + self.write(wr_reg![GPKTCTL1A, pkt_ctrl.as_bits()]) + } + + /// Set the initial value for generic packet whitening. + /// + /// This sets the first 8 bits, the 9th bit is set with + /// [`set_pkt_ctrl`](Self::set_pkt_ctrl). + pub fn set_init_whitening(&mut self, init: u8) -> Result<(), Error> { + self.write(wr_reg![GWHITEINIRL, init]) + } + + /// Set the initial value for generic packet CRC polynomial. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// sg.set_crc_polynomial(0x1D0F)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_crc_polynomial(&mut self, polynomial: u16) -> Result<(), Error> { + let bytes: [u8; 2] = polynomial.to_be_bytes(); + self.write(wr_reg![GCRCINIRH, bytes[0], bytes[1]]) + } + + /// Set the generic packet CRC polynomial. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// sg.set_initial_crc_polynomial(0x1021)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_initial_crc_polynomial(&mut self, polynomial: u16) -> Result<(), Error> { + let bytes: [u8; 2] = polynomial.to_be_bytes(); + self.write(wr_reg![GCRCPOLRH, bytes[0], bytes[1]]) + } + + /// Set the synchronization word registers. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// const SYNC_WORD: [u8; 8] = [0x79, 0x80, 0x0C, 0xC0, 0x29, 0x95, 0xF8, 0x4A]; + /// + /// sg.set_sync_word(&SYNC_WORD)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_sync_word(&mut self, sync_word: &[u8; 8]) -> Result<(), Error> { + self.write_register(Register::GSYNC7, sync_word) + } + + /// Set the LoRa synchronization word registers. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{LoRaSyncWord, PacketType}; + /// + /// sg.set_packet_type(PacketType::LoRa)?; + /// sg.set_lora_sync_word(LoRaSyncWord::Public)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_lora_sync_word(&mut self, sync_word: LoRaSyncWord) -> Result<(), Error> { + let bytes: [u8; 2] = sync_word.bytes(); + self.write(wr_reg![LSYNCH, bytes[0], bytes[1]]) + } + + /// Set the RX gain control. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::PMode; + /// + /// sg.set_rx_gain(PMode::Boost)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_rx_gain(&mut self, pmode: PMode) -> Result<(), Error> { + self.write(wr_reg![RXGAINC, pmode as u8]) + } + + /// Set the power amplifier over current protection. + /// + /// # Example + /// + /// Maximum 60mA for LP PA mode. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::Ocp; + /// + /// sg.set_pa_ocp(Ocp::Max60m)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// Maximum 60mA for HP PA mode. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::Ocp; + /// + /// sg.set_pa_ocp(Ocp::Max140m)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_pa_ocp(&mut self, ocp: Ocp) -> Result<(), Error> { + self.write(wr_reg![PAOCP, ocp as u8]) + } + + /// Set the HSE32 crystal OSC_IN load capaitor trimming. + /// + /// # Example + /// + /// Set the trim to the lowest value. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::HseTrim; + /// + /// sg.set_hse_in_trim(HseTrim::MIN)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_hse_in_trim(&mut self, trim: HseTrim) -> Result<(), Error> { + self.write(wr_reg![HSEINTRIM, trim.into()]) + } + + /// Set the HSE32 crystal OSC_OUT load capaitor trimming. + /// + /// # Example + /// + /// Set the trim to the lowest value. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::HseTrim; + /// + /// sg.set_hse_out_trim(HseTrim::MIN)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_hse_out_trim(&mut self, trim: HseTrim) -> Result<(), Error> { + self.write(wr_reg![HSEOUTTRIM, trim.into()]) + } + + /// Set the SMPS clock detection enabled. + /// + /// SMPS clock detection must be enabled fore enabling the SMPS. + pub fn set_smps_clock_det_en(&mut self, en: bool) -> Result<(), Error> { + self.write(wr_reg![SMPSC0, (en as u8) << 6]) + } + + /// Set the power current limiting. + pub fn set_pwr_ctrl(&mut self, pwr_ctrl: PwrCtrl) -> Result<(), Error> { + self.write(wr_reg![PC, pwr_ctrl.as_bits()]) + } + + /// Set the maximum SMPS drive capability. + pub fn set_smps_drv(&mut self, drv: SmpsDrv) -> Result<(), Error> { + self.write(wr_reg![SMPSC2, (drv as u8) << 1]) + } +} + +// 5.8.3 +/// Operating mode commands +impl<'d> SubGhz<'d, NoDma, NoDma> { + /// Put the radio into sleep mode. + /// + /// This command is only accepted in standby mode. + /// The cfg argument allows some optional functions to be maintained + /// in sleep mode. + /// + /// # Safety + /// + /// 1. After the `set_sleep` command, the sub-GHz radio NSS must not go low + /// for 500 μs. + /// No reason is provided, the reference manual (RM0453 rev 2) simply + /// says "you must". + /// 2. The radio cannot be used while in sleep mode. + /// 3. The radio must be woken up with [`wakeup`] before resuming use. + /// + /// # Example + /// + /// Put the radio into sleep mode. + /// + /// ```no_run + /// # let dp = unsafe { embassy_stm32::pac::Peripherals::steal() }; + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::{ + /// subghz::{wakeup, SleepCfg, StandbyClk}, + /// }; + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// unsafe { sg.set_sleep(SleepCfg::default())? }; + /// embassy::time::Timer::after(embassy::time::Duration::from_micros(500)).await; + /// unsafe { wakeup() }; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub unsafe fn set_sleep(&mut self, cfg: SleepCfg) -> Result<(), Error> { + self.write(&[OpCode::SetSleep as u8, u8::from(cfg)]) + } + + /// Put the radio into standby mode. + /// + /// # Examples + /// + /// Put the radio into standby mode using the RC 13MHz clock. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::StandbyClk; + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// Put the radio into standby mode using the HSE32 clock. + /// + /// ```no_run + /// # let mut dp = unsafe { embassy_stm32::pac::Peripherals::steal() }; + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::StandbyClk; + /// + /// dp.RCC + /// .cr + /// .modify(|_, w| w.hseon().enabled().hsebyppwr().vddtcxo()); + /// while dp.RCC.cr.read().hserdy().is_not_ready() {} + /// + /// sg.set_standby(StandbyClk::Hse)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_standby(&mut self, standby_clk: StandbyClk) -> Result<(), Error> { + self.write(&[OpCode::SetStandby as u8, u8::from(standby_clk)]) + } + + /// Put the subghz radio into frequency synthesis mode. + /// + /// The RF-PLL frequency must be set with [`set_rf_frequency`] before using + /// this command. + /// + /// Check the datasheet for more information, this is a test command but + /// I honestly do not see any use for it. Please update this description + /// if you know more than I do. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::RfFreq; + /// + /// sg.set_rf_frequency(&RfFreq::from_frequency(915_000_000))?; + /// sg.set_fs()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`set_rf_frequency`]: crate::subghz::SubGhz::set_rf_frequency + pub fn set_fs(&mut self) -> Result<(), Error> { + self.write(&[OpCode::SetFs.into()]) + } + + /// Set the sub-GHz radio in TX mode. + /// + /// # Example + /// + /// Transmit with no timeout. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::Timeout; + /// + /// sg.set_tx(Timeout::DISABLED)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_tx(&mut self, timeout: Timeout) -> Result<(), Error> { + let tobits: u32 = timeout.into_bits(); + self.write(&[ + OpCode::SetTx.into(), + (tobits >> 16) as u8, + (tobits >> 8) as u8, + tobits as u8, + ]) + } + + /// Set the sub-GHz radio in RX mode. + /// + /// # Example + /// + /// Receive with a 1 second timeout. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use core::time::Duration; + /// use embassy_stm32::subghz::Timeout; + /// + /// sg.set_rx(Timeout::from_duration_sat(Duration::from_secs(1)))?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_rx(&mut self, timeout: Timeout) -> Result<(), Error> { + let tobits: u32 = timeout.into_bits(); + self.write(&[ + OpCode::SetRx.into(), + (tobits >> 16) as u8, + (tobits >> 8) as u8, + tobits as u8, + ]) + } + + /// Allows selection of the receiver event which stops the RX timeout timer. + /// + /// # Example + /// + /// Set the RX timeout timer to stop on preamble detection. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::RxTimeoutStop; + /// + /// sg.set_rx_timeout_stop(RxTimeoutStop::Preamble)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_rx_timeout_stop(&mut self, rx_timeout_stop: RxTimeoutStop) -> Result<(), Error> { + self.write(&[ + OpCode::SetStopRxTimerOnPreamble.into(), + rx_timeout_stop.into(), + ]) + } + + /// Put the radio in non-continuous RX mode. + /// + /// This command must be sent in Standby mode. + /// This command is only functional with FSK and LoRa packet type. + /// + /// The following steps are performed: + /// 1. Save sub-GHz radio configuration. + /// 2. Enter Receive mode and listen for a preamble for the specified `rx_period`. + /// 3. Upon the detection of a preamble, the `rx_period` timeout is stopped + /// and restarted with the value 2 x `rx_period` + `sleep_period`. + /// During this new period, the sub-GHz radio looks for the detection of + /// a synchronization word when in (G)FSK modulation mode, + /// or a header when in LoRa modulation mode. + /// 4. If no packet is received during the listen period defined by + /// 2 x `rx_period` + `sleep_period`, the sleep mode is entered for a + /// duration of `sleep_period`. At the end of the receive period, + /// the sub-GHz radio takes some time to save the context before starting + /// the sleep period. + /// 5. After the sleep period, a new listening period is automatically + /// started. The sub-GHz radio restores the sub-GHz radio configuration + /// and continuous with step 2. + /// + /// The listening mode is terminated in one of the following cases: + /// * if a packet is received during the listening period: the sub-GHz radio + /// issues a [`RxDone`] interrupt and enters standby mode. + /// * if [`set_standby`] is sent during the listening period or after the + /// sub-GHz has been requested to exit sleep mode by sub-GHz radio SPI NSS + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use core::time::Duration; + /// use embassy_stm32::subghz::{StandbyClk, Timeout}; + /// + /// const RX_PERIOD: Timeout = Timeout::from_duration_sat(Duration::from_millis(100)); + /// const SLEEP_PERIOD: Timeout = Timeout::from_duration_sat(Duration::from_secs(1)); + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// sg.set_rx_duty_cycle(RX_PERIOD, SLEEP_PERIOD)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`RxDone`]: crate::subghz::Irq::RxDone + /// [`set_rf_frequency`]: crate::subghz::SubGhz::set_rf_frequency + /// [`set_standby`]: crate::subghz::SubGhz::set_standby + pub fn set_rx_duty_cycle( + &mut self, + rx_period: Timeout, + sleep_period: Timeout, + ) -> Result<(), Error> { + let rx_period_bits: u32 = rx_period.into_bits(); + let sleep_period_bits: u32 = sleep_period.into_bits(); + self.write(&[ + OpCode::SetRxDutyCycle.into(), + (rx_period_bits >> 16) as u8, + (rx_period_bits >> 8) as u8, + rx_period_bits as u8, + (sleep_period_bits >> 16) as u8, + (sleep_period_bits >> 8) as u8, + sleep_period_bits as u8, + ]) + } + + /// Channel Activity Detection (CAD) with LoRa packets. + /// + /// The channel activity detection (CAD) is a specific LoRa operation mode, + /// where the sub-GHz radio searches for a LoRa radio signal. + /// After the search is completed, the Standby mode is automatically + /// entered, CAD is done and IRQ is generated. + /// When a LoRa radio signal is detected, the CAD detected IRQ is also + /// generated. + /// + /// The length of the search must be configured with [`set_cad_params`] + /// prior to calling `set_cad`. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use core::time::Duration; + /// use embassy_stm32::subghz::{CadParams, ExitMode, NbCadSymbol, StandbyClk, Timeout}; + /// + /// const RX_PERIOD: Timeout = Timeout::from_duration_sat(Duration::from_millis(100)); + /// const SLEEP_PERIOD: Timeout = Timeout::from_duration_sat(Duration::from_secs(1)); + /// const CAD_PARAMS: CadParams = CadParams::new() + /// .set_num_symbol(NbCadSymbol::S4) + /// .set_det_peak(0x18) + /// .set_det_min(0x10) + /// .set_exit_mode(ExitMode::Standby); + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// sg.set_cad_params(&CAD_PARAMS)?; + /// sg.set_cad()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`set_cad_params`]: crate::subghz::SubGhz::set_cad_params + pub fn set_cad(&mut self) -> Result<(), Error> { + self.write(&[OpCode::SetCad.into()]) + } + + /// Generate a continuous transmit tone at the RF-PLL frequency. + /// + /// The sub-GHz radio remains in continuous transmit tone mode until a mode + /// configuration command is received. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// sg.set_tx_continuous_wave()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_tx_continuous_wave(&mut self) -> Result<(), Error> { + self.write(&[OpCode::SetTxContinuousWave as u8]) + } + + /// Generate an infinite preamble at the RF-PLL frequency. + /// + /// The preamble is an alternating 0s and 1s sequence in generic (G)FSK and + /// (G)MSK modulations. + /// The preamble is symbol 0 in LoRa modulation. + /// The sub-GHz radio remains in infinite preamble mode until a mode + /// configuration command is received. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// sg.set_tx_continuous_preamble()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_tx_continuous_preamble(&mut self) -> Result<(), Error> { + self.write(&[OpCode::SetTxContinuousPreamble as u8]) + } +} + +// 5.8.4 +/// Radio configuration commands +impl<'d> SubGhz<'d, NoDma, NoDma> { + /// Set the packet type (modulation scheme). + /// + /// # Examples + /// + /// FSK (frequency shift keying): + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::PacketType; + /// + /// sg.set_packet_type(PacketType::Fsk)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// LoRa (long range): + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::PacketType; + /// + /// sg.set_packet_type(PacketType::LoRa)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// BPSK (binary phase shift keying): + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::PacketType; + /// + /// sg.set_packet_type(PacketType::Bpsk)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// MSK (minimum shift keying): + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::PacketType; + /// + /// sg.set_packet_type(PacketType::Msk)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_packet_type(&mut self, packet_type: PacketType) -> Result<(), Error> { + self.write(&[OpCode::SetPacketType as u8, packet_type as u8]) + } + + /// Get the packet type. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::PacketType; + /// + /// sg.set_packet_type(PacketType::LoRa)?; + /// assert_eq!(sg.packet_type()?, Ok(PacketType::LoRa)); + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn packet_type(&mut self) -> Result, Error> { + let pkt_type: [u8; 2] = self.read_n(OpCode::GetPacketType)?; + Ok(PacketType::from_raw(pkt_type[1])) + } + + /// Set the radio carrier frequency. + /// + /// # Example + /// + /// Set the frequency to 915MHz (Australia and North America). + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::RfFreq; + /// + /// sg.set_rf_frequency(&RfFreq::F915)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_rf_frequency(&mut self, freq: &RfFreq) -> Result<(), Error> { + self.write(freq.as_slice()) + } + + /// Set the transmit output power and the PA ramp-up time. + /// + /// # Example + /// + /// Set the output power to +10 dBm (low power mode) and a ramp up time of + /// 40 microseconds. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{PaConfig, PaSel, RampTime, TxParams}; + /// + /// const TX_PARAMS: TxParams = TxParams::new() + /// .set_ramp_time(RampTime::Micros40) + /// .set_power(0x0D); + /// const PA_CONFIG: PaConfig = PaConfig::new() + /// .set_pa(PaSel::Lp) + /// .set_pa_duty_cycle(0x1) + /// .set_hp_max(0x0); + /// + /// sg.set_pa_config(&PA_CONFIG)?; + /// sg.set_tx_params(&TX_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_tx_params(&mut self, params: &TxParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Power amplifier configuation. + /// + /// Used to customize the maximum output power and efficiency. + /// + /// # Example + /// + /// Set the output power to +22 dBm (high power mode) and a ramp up time of + /// 200 microseconds. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{PaConfig, PaSel, RampTime, TxParams}; + /// + /// const TX_PARAMS: TxParams = TxParams::new() + /// .set_ramp_time(RampTime::Micros200) + /// .set_power(0x16); + /// const PA_CONFIG: PaConfig = PaConfig::new() + /// .set_pa(PaSel::Hp) + /// .set_pa_duty_cycle(0x4) + /// .set_hp_max(0x7); + /// + /// sg.set_pa_config(&PA_CONFIG)?; + /// sg.set_tx_params(&TX_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_pa_config(&mut self, pa_config: &PaConfig) -> Result<(), Error> { + self.write(pa_config.as_slice()) + } + + /// Operating mode to enter after a successful packet transmission or + /// packet reception. + /// + /// # Example + /// + /// Set the fallback mode to standby mode. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::FallbackMode; + /// + /// sg.set_tx_rx_fallback_mode(FallbackMode::Standby)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_tx_rx_fallback_mode(&mut self, fm: FallbackMode) -> Result<(), Error> { + self.write(&[OpCode::SetTxRxFallbackMode as u8, fm as u8]) + } + + /// Set channel activity detection (CAD) parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use core::time::Duration; + /// use embassy_stm32::subghz::{CadParams, ExitMode, NbCadSymbol, StandbyClk, Timeout}; + /// + /// const RX_PERIOD: Timeout = Timeout::from_duration_sat(Duration::from_millis(100)); + /// const SLEEP_PERIOD: Timeout = Timeout::from_duration_sat(Duration::from_secs(1)); + /// const CAD_PARAMS: CadParams = CadParams::new() + /// .set_num_symbol(NbCadSymbol::S4) + /// .set_det_peak(0x18) + /// .set_det_min(0x10) + /// .set_exit_mode(ExitMode::Standby); + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// sg.set_cad_params(&CAD_PARAMS)?; + /// sg.set_cad()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_cad_params(&mut self, params: &CadParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the data buffer base address for the packet handling in TX and RX. + /// + /// There is a 256B TX buffer and a 256B RX buffer. + /// These buffers are not memory mapped, they are accessed via the + /// [`read_buffer`] and [`write_buffer`] methods. + /// + /// # Example + /// + /// Set the TX and RX buffer base to the start. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// sg.set_buffer_base_address(0, 0)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`read_buffer`]: SubGhz::read_buffer + /// [`write_buffer`]: SubGhz::write_buffer + pub fn set_buffer_base_address(&mut self, tx: u8, rx: u8) -> Result<(), Error> { + self.write(&[OpCode::SetBufferBaseAddress as u8, tx, rx]) + } + + /// Set the (G)FSK modulation parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{ + /// FskBandwidth, FskBitrate, FskFdev, FskModParams, FskPulseShape, PacketType, + /// }; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(32_000); + /// const PULSE_SHAPE: FskPulseShape = FskPulseShape::Bt03; + /// const BW: FskBandwidth = FskBandwidth::Bw9; + /// const FDEV: FskFdev = FskFdev::from_hertz(31_250); + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new() + /// .set_bitrate(BITRATE) + /// .set_pulse_shape(PULSE_SHAPE) + /// .set_bandwidth(BW) + /// .set_fdev(FDEV); + /// + /// sg.set_packet_type(PacketType::Fsk)?; + /// sg.set_fsk_mod_params(&MOD_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_fsk_mod_params(&mut self, params: &FskModParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the LoRa modulation parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{ + /// CodingRate, LoRaBandwidth, LoRaModParams, PacketType, SpreadingFactor, + /// }; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new() + /// .set_sf(SpreadingFactor::Sf7) + /// .set_bw(LoRaBandwidth::Bw125) + /// .set_cr(CodingRate::Cr45) + /// .set_ldro_en(false); + /// + /// sg.set_packet_type(PacketType::LoRa)?; + /// sg.set_lora_mod_params(&MOD_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_lora_mod_params(&mut self, params: &LoRaModParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the BPSK modulation parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{BpskModParams, FskBitrate, PacketType}; + /// + /// const MOD_PARAMS: BpskModParams = BpskModParams::new().set_bitrate(FskBitrate::from_bps(600)); + /// + /// sg.set_packet_type(PacketType::Bpsk)?; + /// sg.set_bpsk_mod_params(&MOD_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_bpsk_mod_params(&mut self, params: &BpskModParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the generic (FSK) packet parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{ + /// AddrComp, CrcType, GenericPacketParams, HeaderType, PacketType, PreambleDetection, + /// }; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new() + /// .set_preamble_len(8) + /// .set_preamble_detection(PreambleDetection::Disabled) + /// .set_sync_word_len(2) + /// .set_addr_comp(AddrComp::Disabled) + /// .set_header_type(HeaderType::Fixed) + /// .set_payload_len(128) + /// .set_crc_type(CrcType::Byte2) + /// .set_whitening_enable(true); + /// + /// sg.set_packet_type(PacketType::Fsk)?; + /// sg.set_packet_params(&PKT_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_packet_params(&mut self, params: &GenericPacketParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the BPSK packet parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{BpskPacketParams, PacketType}; + /// + /// sg.set_packet_type(PacketType::Bpsk)?; + /// sg.set_bpsk_packet_params(&BpskPacketParams::new().set_payload_len(64))?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_bpsk_packet_params(&mut self, params: &BpskPacketParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the LoRa packet parameters. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{HeaderType, LoRaPacketParams, PacketType}; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new() + /// .set_preamble_len(5 * 8) + /// .set_header_type(HeaderType::Fixed) + /// .set_payload_len(64) + /// .set_crc_en(true) + /// .set_invert_iq(true); + /// + /// sg.set_packet_type(PacketType::LoRa)?; + /// sg.set_lora_packet_params(&PKT_PARAMS)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_lora_packet_params(&mut self, params: &LoRaPacketParams) -> Result<(), Error> { + self.write(params.as_slice()) + } + + /// Set the number of LoRa symbols to be received before starting the + /// reception of a LoRa packet. + /// + /// Packet reception is started after `n` + 1 symbols are detected. + /// + /// # Example + /// + /// Start reception after a single LoRa word is detected + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// + /// // ... setup the radio for LoRa RX + /// + /// sg.set_lora_symb_timeout(0)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_lora_symb_timeout(&mut self, n: u8) -> Result<(), Error> { + self.write(&[OpCode::SetLoRaSymbTimeout.into(), n]) + } +} + +// 5.8.5 +/// Communication status and information commands +impl<'d> SubGhz<'d, NoDma, NoDma> { + /// Get the radio status. + /// + /// The hardware (or documentation) appears to have many bugs where this + /// will return reserved values. + /// See this thread in the ST community for details: [link] + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::Status; + /// + /// let status: Status = sg.status()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [link]: https://community.st.com/s/question/0D53W00000hR9GQSA0/stm32wl55-getstatus-command-returns-reserved-cmdstatus + pub fn status(&mut self) -> Result { + Ok(self.read_1(OpCode::GetStatus)?.into()) + } + + /// Get the RX buffer status. + /// + /// The return tuple is (status, payload_length, buffer_pointer). + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{CmdStatus, Timeout}; + /// + /// sg.set_rx(Timeout::DISABLED)?; + /// loop { + /// let (status, len, ptr) = sg.rx_buffer_status()?; + /// + /// if status.cmd() == Ok(CmdStatus::Avaliable) { + /// let mut buf: [u8; 256] = [0; 256]; + /// let data: &mut [u8] = &mut buf[..usize::from(len)]; + /// sg.read_buffer(ptr, data)?; + /// // ... do things with the data + /// break; + /// } + /// } + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn rx_buffer_status(&mut self) -> Result<(Status, u8, u8), Error> { + let data: [u8; 3] = self.read_n(OpCode::GetRxBufferStatus)?; + Ok((data[0].into(), data[1], data[2])) + } + + /// Returns information on the last received (G)FSK packet. + /// + /// # Example + /// + /// ```no_run + /// # use std::fmt::Write; + /// # let mut uart = String::new(); + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{CmdStatus, Timeout}; + /// + /// sg.set_rx(Timeout::DISABLED)?; + /// loop { + /// let pkt_status = sg.fsk_packet_status()?; + /// + /// if pkt_status.status().cmd() == Ok(CmdStatus::Avaliable) { + /// let rssi = pkt_status.rssi_avg(); + /// writeln!(&mut uart, "Avg RSSI: {} dBm", rssi); + /// break; + /// } + /// } + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn fsk_packet_status(&mut self) -> Result { + Ok(FskPacketStatus::from(self.read_n(OpCode::GetPacketStatus)?)) + } + + /// Returns information on the last received LoRa packet. + /// + /// # Example + /// + /// ```no_run + /// # use std::fmt::Write; + /// # let mut uart = String::new(); + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{CmdStatus, Timeout}; + /// + /// sg.set_rx(Timeout::DISABLED)?; + /// loop { + /// let pkt_status = sg.lora_packet_status()?; + /// + /// if pkt_status.status().cmd() == Ok(CmdStatus::Avaliable) { + /// let snr = pkt_status.snr_pkt(); + /// writeln!(&mut uart, "SNR: {} dB", snr); + /// break; + /// } + /// } + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn lora_packet_status(&mut self) -> Result { + Ok(LoRaPacketStatus::from( + self.read_n(OpCode::GetPacketStatus)?, + )) + } + + /// Get the instantaneous signal strength during packet reception. + /// + /// The units are in dbm. + /// + /// # Example + /// + /// Log the instantaneous signal strength to UART. + /// + /// ```no_run + /// # use std::fmt::Write; + /// # let mut uart = String::new(); + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{CmdStatus, Timeout}; + /// + /// sg.set_rx(Timeout::DISABLED)?; + /// let (_, rssi) = sg.rssi_inst()?; + /// writeln!(&mut uart, "RSSI: {} dBm", rssi); + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn rssi_inst(&mut self) -> Result<(Status, Ratio), Error> { + let data: [u8; 2] = self.read_n(OpCode::GetRssiInst)?; + let status: Status = data[0].into(); + let rssi: Ratio = Ratio::new_raw(i16::from(data[1]), -2); + + Ok((status, rssi)) + } + + /// (G)FSK packet stats. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{FskStats, Stats}; + /// + /// let stats: Stats = sg.fsk_stats()?; + /// // ... use stats + /// sg.reset_stats()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn fsk_stats(&mut self) -> Result, Error> { + let data: [u8; 7] = self.read_n(OpCode::GetStats)?; + Ok(Stats::from_raw_fsk(data)) + } + + /// LoRa packet stats. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{LoRaStats, Stats}; + /// + /// let stats: Stats = sg.lora_stats()?; + /// // ... use stats + /// sg.reset_stats()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn lora_stats(&mut self) -> Result, Error> { + let data: [u8; 7] = self.read_n(OpCode::GetStats)?; + Ok(Stats::from_raw_lora(data)) + } + + /// Reset the stats as reported in [`lora_stats`] and [`fsk_stats`]. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// + /// sg.reset_stats()?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`lora_stats`]: crate::subghz::SubGhz::lora_stats + /// [`fsk_stats`]: crate::subghz::SubGhz::fsk_stats + pub fn reset_stats(&mut self) -> Result<(), Error> { + const RESET_STATS: [u8; 7] = [0x00; 7]; + self.write(&RESET_STATS) + } +} + +// 5.8.6 +/// IRQ commands +impl<'d> SubGhz<'d, NoDma, NoDma> { + /// Set the interrupt configuration. + /// + /// # Example + /// + /// Enable TX and timeout interrupts. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{CfgIrq, Irq}; + /// + /// const IRQ_CFG: CfgIrq = CfgIrq::new() + /// .irq_enable_all(Irq::TxDone) + /// .irq_enable_all(Irq::Timeout); + /// sg.set_irq_cfg(&IRQ_CFG)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_irq_cfg(&mut self, cfg: &CfgIrq) -> Result<(), Error> { + self.write(cfg.as_slice()) + } + + /// Get the IRQ status. + /// + /// # Example + /// + /// Wait for TX to complete or timeout. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::Irq; + /// + /// loop { + /// let (_, irq_status) = sg.irq_status()?; + /// sg.clear_irq_status(irq_status)?; + /// if irq_status & Irq::TxDone.mask() != 0 { + /// // handle TX done + /// break; + /// } + /// if irq_status & Irq::Timeout.mask() != 0 { + /// // handle timeout + /// break; + /// } + /// } + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn irq_status(&mut self) -> Result<(Status, u16), Error> { + let data: [u8; 3] = self.read_n(OpCode::GetIrqStatus)?; + let irq_status: u16 = u16::from_be_bytes([data[1], data[2]]); + Ok((data[0].into(), irq_status)) + } + + /// Clear the IRQ status. + /// + /// # Example + /// + /// Clear the [`TxDone`] and [`RxDone`] interrupts. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::Irq; + /// + /// sg.clear_irq_status(Irq::TxDone.mask() | Irq::RxDone.mask())?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`TxDone`]: crate::subghz::Irq::TxDone + /// [`RxDone`]: crate::subghz::Irq::RxDone + pub fn clear_irq_status(&mut self, mask: u16) -> Result<(), Error> { + self.write(&[OpCode::ClrIrqStatus as u8, (mask >> 8) as u8, mask as u8]) + } +} + +// 5.8.7 +/// Miscellaneous commands +impl<'d> SubGhz<'d, NoDma, NoDma> { + /// Calibrate one or several blocks at any time when in standby mode. + /// + /// The blocks to calibrate are defined by `cal` argument. + /// When the calibration is ongoing, BUSY is set. + /// A falling edge on BUSY indicates the end of all enabled calibrations. + /// + /// This function will not poll for BUSY. + /// + /// # Example + /// + /// Calibrate the RC 13 MHz and PLL. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{Calibrate, StandbyClk, SubGhz}; + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// sg.calibrate(Calibrate::Rc13M.mask() | Calibrate::Pll.mask())?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn calibrate(&mut self, cal: u8) -> Result<(), Error> { + // bit 7 is reserved and must be kept at reset value. + self.write(&[OpCode::Calibrate as u8, cal & 0x7F]) + } + + /// Calibrate the image at the given frequencies. + /// + /// Requires the radio to be in standby mode. + /// + /// # Example + /// + /// Calibrate the image for the 430 - 440 MHz ISM band. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{CalibrateImage, StandbyClk}; + /// + /// sg.set_standby(StandbyClk::Rc)?; + /// sg.calibrate_image(CalibrateImage::ISM_430_440)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn calibrate_image(&mut self, cal: CalibrateImage) -> Result<(), Error> { + self.write(&[OpCode::CalibrateImage as u8, cal.0, cal.1]) + } + + /// Set the radio power supply. + /// + /// # Examples + /// + /// Use the linear dropout regulator (LDO): + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::RegMode; + /// + /// sg.set_regulator_mode(RegMode::Ldo)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// Use the switch mode power supply (SPMS): + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::RegMode; + /// + /// sg.set_regulator_mode(RegMode::Smps)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_regulator_mode(&mut self, reg_mode: RegMode) -> Result<(), Error> { + self.write(&[OpCode::SetRegulatorMode as u8, reg_mode as u8]) + } + + /// Get the radio operational errors. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::OpError; + /// + /// let (status, error_mask) = sg.op_error()?; + /// if error_mask & OpError::PllLockError.mask() != 0 { + /// // ... handle PLL lock error + /// } + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn op_error(&mut self) -> Result<(Status, u16), Error> { + let data: [u8; 3] = self.read_n(OpCode::GetError)?; + Ok((data[0].into(), u16::from_le_bytes([data[1], data[2]]))) + } + + /// Clear all errors as reported by [`op_error`]. + /// + /// # Example + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::OpError; + /// + /// let (status, error_mask) = sg.op_error()?; + /// // ignore all errors + /// if error_mask != 0 { + /// sg.clear_error()?; + /// } + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + /// + /// [`op_error`]: crate::subghz::SubGhz::op_error + pub fn clear_error(&mut self) -> Result<(), Error> { + self.write(&[OpCode::ClrError as u8, 0x00]) + } +} + +// 5.8.8 +/// Set TCXO mode command +impl<'d> SubGhz<'d, NoDma, NoDma> { + /// Set the TCXO trim and HSE32 ready timeout. + /// + /// # Example + /// + /// Setup the TCXO with 1.7V trim and a 10ms timeout. + /// + /// ```no_run + /// # let mut sg = embassy_stm32::subghz::SubGhz::new(p.SUBGHZSPI, ...); + /// use embassy_stm32::subghz::{TcxoMode, TcxoTrim, Timeout}; + /// + /// const TCXO_MODE: TcxoMode = TcxoMode::new() + /// .set_txco_trim(TcxoTrim::Volts1pt7) + /// .set_timeout(Timeout::from_millis_sat(10)); + /// sg.set_tcxo_mode(&TCXO_MODE)?; + /// # Ok::<(), embassy_stm32::subghz::Error>(()) + /// ``` + pub fn set_tcxo_mode(&mut self, tcxo_mode: &TcxoMode) -> Result<(), Error> { + self.write(tcxo_mode.as_slice()) + } +} + +/// sub-GHz radio opcodes. +/// +/// See Table 41 "Sub-GHz radio SPI commands overview" +#[repr(u8)] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub(crate) enum OpCode { + Calibrate = 0x89, + CalibrateImage = 0x98, + CfgDioIrq = 0x08, + ClrError = 0x07, + ClrIrqStatus = 0x02, + GetError = 0x17, + GetIrqStatus = 0x12, + GetPacketStatus = 0x14, + GetPacketType = 0x11, + GetRssiInst = 0x15, + GetRxBufferStatus = 0x13, + GetStats = 0x10, + GetStatus = 0xC0, + ReadBuffer = 0x1E, + RegRegister = 0x1D, + ResetStats = 0x00, + SetBufferBaseAddress = 0x8F, + SetCad = 0xC5, + SetCadParams = 0x88, + SetFs = 0xC1, + SetLoRaSymbTimeout = 0xA0, + SetModulationParams = 0x8B, + SetPacketParams = 0x8C, + SetPacketType = 0x8A, + SetPaConfig = 0x95, + SetRegulatorMode = 0x96, + SetRfFrequency = 0x86, + SetRx = 0x82, + SetRxDutyCycle = 0x94, + SetSleep = 0x84, + SetStandby = 0x80, + SetStopRxTimerOnPreamble = 0x9F, + SetTcxoMode = 0x97, + SetTx = 0x83, + SetTxContinuousPreamble = 0xD2, + SetTxContinuousWave = 0xD1, + SetTxParams = 0x8E, + SetTxRxFallbackMode = 0x93, + WriteBuffer = 0x0E, + WriteRegister = 0x0D, +} + +impl From for u8 { + fn from(opcode: OpCode) -> Self { + opcode as u8 + } +} + +#[repr(u16)] +#[allow(clippy::upper_case_acronyms)] +pub(crate) enum Register { + /// Generic bit synchronization. + GBSYNC = 0x06AC, + /// Generic packet control. + GPKTCTL1A = 0x06B8, + /// Generic whitening. + GWHITEINIRL = 0x06B9, + /// Generic CRC initial. + GCRCINIRH = 0x06BC, + /// Generic CRC polynomial. + GCRCPOLRH = 0x06BE, + /// Generic synchronization word 7. + GSYNC7 = 0x06C0, + /// LoRa synchronization word MSB. + LSYNCH = 0x0740, + /// LoRa synchronization word LSB. + #[allow(dead_code)] + LSYNCL = 0x0741, + /// Receiver gain control. + RXGAINC = 0x08AC, + /// PA over current protection. + PAOCP = 0x08E7, + /// HSE32 OSC_IN capacitor trim. + HSEINTRIM = 0x0911, + /// HSE32 OSC_OUT capacitor trim. + HSEOUTTRIM = 0x0912, + /// SMPS control 0. + SMPSC0 = 0x0916, + /// Power control. + PC = 0x091A, + /// SMPS control 2. + SMPSC2 = 0x0923, +} + +impl Register { + pub const fn address(self) -> u16 { + self as u16 + } +} diff --git a/embassy-stm32/src/subghz/mod_params.rs b/embassy-stm32/src/subghz/mod_params.rs new file mode 100644 index 00000000..3a5cb199 --- /dev/null +++ b/embassy-stm32/src/subghz/mod_params.rs @@ -0,0 +1,996 @@ +/// Bandwidth options for [`FskModParams`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum FskBandwidth { + /// 4.8 kHz double-sideband + Bw4 = 0x1F, + /// 5.8 kHz double-sideband + Bw5 = 0x17, + /// 7.3 kHz double-sideband + Bw7 = 0x0F, + /// 9.7 kHz double-sideband + Bw9 = 0x1E, + /// 11.7 kHz double-sideband + Bw11 = 0x16, + /// 14.6 kHz double-sideband + Bw14 = 0x0E, + /// 19.5 kHz double-sideband + Bw19 = 0x1D, + /// 23.4 kHz double-sideband + Bw23 = 0x15, + /// 29.3 kHz double-sideband + Bw29 = 0x0D, + /// 39.0 kHz double-sideband + Bw39 = 0x1C, + /// 46.9 kHz double-sideband + Bw46 = 0x14, + /// 58.6 kHz double-sideband + Bw58 = 0x0C, + /// 78.2 kHz double-sideband + Bw78 = 0x1B, + /// 93.8 kHz double-sideband + Bw93 = 0x13, + /// 117.3 kHz double-sideband + Bw117 = 0x0B, + /// 156.2 kHz double-sideband + Bw156 = 0x1A, + /// 187.2 kHz double-sideband + Bw187 = 0x12, + /// 234.3 kHz double-sideband + Bw234 = 0x0A, + /// 312.0 kHz double-sideband + Bw312 = 0x19, + /// 373.6 kHz double-sideband + Bw373 = 0x11, + /// 467.0 kHz double-sideband + Bw467 = 0x09, +} + +impl FskBandwidth { + /// Get the bandwidth in hertz. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskBandwidth; + /// + /// assert_eq!(FskBandwidth::Bw4.hertz(), 4_800); + /// assert_eq!(FskBandwidth::Bw5.hertz(), 5_800); + /// assert_eq!(FskBandwidth::Bw7.hertz(), 7_300); + /// assert_eq!(FskBandwidth::Bw9.hertz(), 9_700); + /// assert_eq!(FskBandwidth::Bw11.hertz(), 11_700); + /// assert_eq!(FskBandwidth::Bw14.hertz(), 14_600); + /// assert_eq!(FskBandwidth::Bw19.hertz(), 19_500); + /// assert_eq!(FskBandwidth::Bw23.hertz(), 23_400); + /// assert_eq!(FskBandwidth::Bw29.hertz(), 29_300); + /// assert_eq!(FskBandwidth::Bw39.hertz(), 39_000); + /// assert_eq!(FskBandwidth::Bw46.hertz(), 46_900); + /// assert_eq!(FskBandwidth::Bw58.hertz(), 58_600); + /// assert_eq!(FskBandwidth::Bw78.hertz(), 78_200); + /// assert_eq!(FskBandwidth::Bw93.hertz(), 93_800); + /// assert_eq!(FskBandwidth::Bw117.hertz(), 117_300); + /// assert_eq!(FskBandwidth::Bw156.hertz(), 156_200); + /// assert_eq!(FskBandwidth::Bw187.hertz(), 187_200); + /// assert_eq!(FskBandwidth::Bw234.hertz(), 234_300); + /// assert_eq!(FskBandwidth::Bw312.hertz(), 312_000); + /// assert_eq!(FskBandwidth::Bw373.hertz(), 373_600); + /// assert_eq!(FskBandwidth::Bw467.hertz(), 467_000); + /// ``` + pub const fn hertz(&self) -> u32 { + match self { + FskBandwidth::Bw4 => 4_800, + FskBandwidth::Bw5 => 5_800, + FskBandwidth::Bw7 => 7_300, + FskBandwidth::Bw9 => 9_700, + FskBandwidth::Bw11 => 11_700, + FskBandwidth::Bw14 => 14_600, + FskBandwidth::Bw19 => 19_500, + FskBandwidth::Bw23 => 23_400, + FskBandwidth::Bw29 => 29_300, + FskBandwidth::Bw39 => 39_000, + FskBandwidth::Bw46 => 46_900, + FskBandwidth::Bw58 => 58_600, + FskBandwidth::Bw78 => 78_200, + FskBandwidth::Bw93 => 93_800, + FskBandwidth::Bw117 => 117_300, + FskBandwidth::Bw156 => 156_200, + FskBandwidth::Bw187 => 187_200, + FskBandwidth::Bw234 => 234_300, + FskBandwidth::Bw312 => 312_000, + FskBandwidth::Bw373 => 373_600, + FskBandwidth::Bw467 => 467_000, + } + } + + /// Convert from a raw bit value. + /// + /// Invalid values will be returned in the `Err` variant of the result. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskBandwidth; + /// + /// assert_eq!(FskBandwidth::from_bits(0x1F), Ok(FskBandwidth::Bw4)); + /// assert_eq!(FskBandwidth::from_bits(0x17), Ok(FskBandwidth::Bw5)); + /// assert_eq!(FskBandwidth::from_bits(0x0F), Ok(FskBandwidth::Bw7)); + /// assert_eq!(FskBandwidth::from_bits(0x1E), Ok(FskBandwidth::Bw9)); + /// assert_eq!(FskBandwidth::from_bits(0x16), Ok(FskBandwidth::Bw11)); + /// assert_eq!(FskBandwidth::from_bits(0x0E), Ok(FskBandwidth::Bw14)); + /// assert_eq!(FskBandwidth::from_bits(0x1D), Ok(FskBandwidth::Bw19)); + /// assert_eq!(FskBandwidth::from_bits(0x15), Ok(FskBandwidth::Bw23)); + /// assert_eq!(FskBandwidth::from_bits(0x0D), Ok(FskBandwidth::Bw29)); + /// assert_eq!(FskBandwidth::from_bits(0x1C), Ok(FskBandwidth::Bw39)); + /// assert_eq!(FskBandwidth::from_bits(0x14), Ok(FskBandwidth::Bw46)); + /// assert_eq!(FskBandwidth::from_bits(0x0C), Ok(FskBandwidth::Bw58)); + /// assert_eq!(FskBandwidth::from_bits(0x1B), Ok(FskBandwidth::Bw78)); + /// assert_eq!(FskBandwidth::from_bits(0x13), Ok(FskBandwidth::Bw93)); + /// assert_eq!(FskBandwidth::from_bits(0x0B), Ok(FskBandwidth::Bw117)); + /// assert_eq!(FskBandwidth::from_bits(0x1A), Ok(FskBandwidth::Bw156)); + /// assert_eq!(FskBandwidth::from_bits(0x12), Ok(FskBandwidth::Bw187)); + /// assert_eq!(FskBandwidth::from_bits(0x0A), Ok(FskBandwidth::Bw234)); + /// assert_eq!(FskBandwidth::from_bits(0x19), Ok(FskBandwidth::Bw312)); + /// assert_eq!(FskBandwidth::from_bits(0x11), Ok(FskBandwidth::Bw373)); + /// assert_eq!(FskBandwidth::from_bits(0x09), Ok(FskBandwidth::Bw467)); + /// assert_eq!(FskBandwidth::from_bits(0x00), Err(0x00)); + /// ``` + pub const fn from_bits(bits: u8) -> Result { + match bits { + 0x1F => Ok(Self::Bw4), + 0x17 => Ok(Self::Bw5), + 0x0F => Ok(Self::Bw7), + 0x1E => Ok(Self::Bw9), + 0x16 => Ok(Self::Bw11), + 0x0E => Ok(Self::Bw14), + 0x1D => Ok(Self::Bw19), + 0x15 => Ok(Self::Bw23), + 0x0D => Ok(Self::Bw29), + 0x1C => Ok(Self::Bw39), + 0x14 => Ok(Self::Bw46), + 0x0C => Ok(Self::Bw58), + 0x1B => Ok(Self::Bw78), + 0x13 => Ok(Self::Bw93), + 0x0B => Ok(Self::Bw117), + 0x1A => Ok(Self::Bw156), + 0x12 => Ok(Self::Bw187), + 0x0A => Ok(Self::Bw234), + 0x19 => Ok(Self::Bw312), + 0x11 => Ok(Self::Bw373), + 0x09 => Ok(Self::Bw467), + x => Err(x), + } + } +} + +impl Ord for FskBandwidth { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.hertz().cmp(&other.hertz()) + } +} + +impl PartialOrd for FskBandwidth { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.hertz().cmp(&other.hertz())) + } +} + +/// Pulse shaping options for [`FskModParams`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum FskPulseShape { + /// No filtering applied. + None = 0b00, + /// Gaussian BT 0.3 + Bt03 = 0x08, + /// Gaussian BT 0.5 + Bt05 = 0x09, + /// Gaussian BT 0.7 + Bt07 = 0x0A, + /// Gaussian BT 1.0 + Bt10 = 0x0B, +} + +/// Bitrate argument for [`FskModParams::set_bitrate`] and +/// [`BpskModParams::set_bitrate`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct FskBitrate { + bits: u32, +} + +impl FskBitrate { + /// Create a new `FskBitrate` from a bitrate in bits per second. + /// + /// This the resulting value will be rounded down, and will saturate if + /// `bps` is outside of the theoretical limits. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskBitrate; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(9600); + /// assert_eq!(BITRATE.as_bps(), 9600); + /// ``` + pub const fn from_bps(bps: u32) -> Self { + const MAX: u32 = 0x00FF_FFFF; + if bps == 0 { + Self { bits: MAX } + } else { + let bits: u32 = 32 * 32_000_000 / bps; + if bits > MAX { + Self { bits: MAX } + } else { + Self { bits } + } + } + } + + /// Create a new `FskBitrate` from a raw bit value. + /// + /// bits = 32 × 32 MHz / bitrate + /// + /// **Note:** Only the first 24 bits of the `u32` are used, the `bits` + /// argument will be masked. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskBitrate; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_raw(0x7D00); + /// assert_eq!(BITRATE.as_bps(), 32_000); + /// ``` + pub const fn from_raw(bits: u32) -> Self { + Self { + bits: bits & 0x00FF_FFFF, + } + } + + /// Return the bitrate in bits per second, rounded down. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskBitrate; + /// + /// const BITS_PER_SEC: u32 = 9600; + /// const BITRATE: FskBitrate = FskBitrate::from_bps(BITS_PER_SEC); + /// assert_eq!(BITRATE.as_bps(), BITS_PER_SEC); + /// ``` + pub const fn as_bps(&self) -> u32 { + if self.bits == 0 { + 0 + } else { + 32 * 32_000_000 / self.bits + } + } + + pub(crate) const fn into_bits(self) -> u32 { + self.bits + } +} + +impl Ord for FskBitrate { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.as_bps().cmp(&other.as_bps()) + } +} + +impl PartialOrd for FskBitrate { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.as_bps().cmp(&other.as_bps())) + } +} + +/// Frequency deviation argument for [`FskModParams::set_fdev`] +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct FskFdev { + bits: u32, +} + +impl FskFdev { + /// Create a new `FskFdev` from a frequency deviation in hertz, rounded + /// down. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskFdev; + /// + /// const FDEV: FskFdev = FskFdev::from_hertz(31_250); + /// assert_eq!(FDEV.as_hertz(), 31_250); + /// ``` + pub const fn from_hertz(hz: u32) -> Self { + Self { + bits: ((hz as u64) * (1 << 25) / 32_000_000) as u32 & 0x00FF_FFFF, + } + } + + /// Create a new `FskFdev` from a raw bit value. + /// + /// bits = fdev × 225 / 32 MHz + /// + /// **Note:** Only the first 24 bits of the `u32` are used, the `bits` + /// argument will be masked. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskFdev; + /// + /// const FDEV: FskFdev = FskFdev::from_raw(0x8000); + /// assert_eq!(FDEV.as_hertz(), 31_250); + /// ``` + pub const fn from_raw(bits: u32) -> Self { + Self { + bits: bits & 0x00FF_FFFF, + } + } + + /// Return the frequency deviation in hertz, rounded down. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskFdev; + /// + /// const HERTZ: u32 = 31_250; + /// const FDEV: FskFdev = FskFdev::from_hertz(HERTZ); + /// assert_eq!(FDEV.as_hertz(), HERTZ); + /// ``` + pub const fn as_hertz(&self) -> u32 { + ((self.bits as u64) * 32_000_000 / (1 << 25)) as u32 + } + + pub(crate) const fn into_bits(self) -> u32 { + self.bits + } +} + +/// (G)FSK modulation paramters. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct FskModParams { + buf: [u8; 9], +} + +impl FskModParams { + /// Create a new `FskModParams` struct. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::FskModParams; + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new(); + /// ``` + pub const fn new() -> FskModParams { + FskModParams { + buf: [ + super::OpCode::SetModulationParams as u8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ], + } + .set_bitrate(FskBitrate::from_bps(50_000)) + .set_pulse_shape(FskPulseShape::None) + .set_bandwidth(FskBandwidth::Bw58) + .set_fdev(FskFdev::from_hertz(25_000)) + } + + /// Get the bitrate. + /// + /// # Example + /// + /// Setting the bitrate to 32,000 bits per second. + /// + /// ``` + /// use stm32wl_hal::subghz::{FskBitrate, FskModParams}; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(32_000); + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_bitrate(BITRATE); + /// assert_eq!(MOD_PARAMS.bitrate(), BITRATE); + /// ``` + pub const fn bitrate(&self) -> FskBitrate { + let raw: u32 = u32::from_be_bytes([0, self.buf[1], self.buf[2], self.buf[3]]); + FskBitrate::from_raw(raw) + } + + /// Set the bitrate. + /// + /// # Example + /// + /// Setting the bitrate to 32,000 bits per second. + /// + /// ``` + /// use stm32wl_hal::subghz::{FskBitrate, FskModParams}; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(32_000); + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_bitrate(BITRATE); + /// # assert_eq!(MOD_PARAMS.as_slice()[1], 0x00); + /// # assert_eq!(MOD_PARAMS.as_slice()[2], 0x7D); + /// # assert_eq!(MOD_PARAMS.as_slice()[3], 0x00); + /// ``` + #[must_use = "set_bitrate returns a modified FskModParams"] + pub const fn set_bitrate(mut self, bitrate: FskBitrate) -> FskModParams { + let bits: u32 = bitrate.into_bits(); + self.buf[1] = ((bits >> 16) & 0xFF) as u8; + self.buf[2] = ((bits >> 8) & 0xFF) as u8; + self.buf[3] = (bits & 0xFF) as u8; + self + } + + /// Set the pulse shaping. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskModParams, FskPulseShape}; + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_pulse_shape(FskPulseShape::Bt03); + /// # assert_eq!(MOD_PARAMS.as_slice()[4], 0x08); + /// ``` + #[must_use = "set_pulse_shape returns a modified FskModParams"] + pub const fn set_pulse_shape(mut self, shape: FskPulseShape) -> FskModParams { + self.buf[4] = shape as u8; + self + } + + /// Get the bandwidth. + /// + /// Values that do not correspond to a valid [`FskBandwidth`] will be + /// returned in the `Err` variant of the result. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskBandwidth, FskModParams}; + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_bandwidth(FskBandwidth::Bw9); + /// assert_eq!(MOD_PARAMS.bandwidth(), Ok(FskBandwidth::Bw9)); + /// ``` + pub const fn bandwidth(&self) -> Result { + FskBandwidth::from_bits(self.buf[5]) + } + + /// Set the bandwidth. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskBandwidth, FskModParams}; + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_bandwidth(FskBandwidth::Bw9); + /// # assert_eq!(MOD_PARAMS.as_slice()[5], 0x1E); + /// ``` + #[must_use = "set_pulse_shape returns a modified FskModParams"] + pub const fn set_bandwidth(mut self, bw: FskBandwidth) -> FskModParams { + self.buf[5] = bw as u8; + self + } + + /// Get the frequency deviation. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskFdev, FskModParams}; + /// + /// const FDEV: FskFdev = FskFdev::from_hertz(31_250); + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_fdev(FDEV); + /// assert_eq!(MOD_PARAMS.fdev(), FDEV); + /// ``` + pub const fn fdev(&self) -> FskFdev { + let raw: u32 = u32::from_be_bytes([0, self.buf[6], self.buf[7], self.buf[8]]); + FskFdev::from_raw(raw) + } + + /// Set the frequency deviation. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskFdev, FskModParams}; + /// + /// const FDEV: FskFdev = FskFdev::from_hertz(31_250); + /// const MOD_PARAMS: FskModParams = FskModParams::new().set_fdev(FDEV); + /// # assert_eq!(MOD_PARAMS.as_slice()[6], 0x00); + /// # assert_eq!(MOD_PARAMS.as_slice()[7], 0x80); + /// # assert_eq!(MOD_PARAMS.as_slice()[8], 0x00); + /// ``` + #[must_use = "set_fdev returns a modified FskModParams"] + pub const fn set_fdev(mut self, fdev: FskFdev) -> FskModParams { + let bits: u32 = fdev.into_bits(); + self.buf[6] = ((bits >> 16) & 0xFF) as u8; + self.buf[7] = ((bits >> 8) & 0xFF) as u8; + self.buf[8] = (bits & 0xFF) as u8; + self + } + /// Returns `true` if the modulation parameters are valid. + /// + /// The bandwidth must be chosen so that: + /// + /// [`FskBandwidth`] > [`FskBitrate`] + 2 × [`FskFdev`] + frequency error + /// + /// Where frequency error = 2 × HSE32FREQ error. + /// + /// The datasheet (DS13293 Rev 1) gives these requirements for the HSE32 + /// frequency tolerance: + /// + /// * Initial: ±10 ppm + /// * Over temperature (-20 to 70 °C): ±10 ppm + /// * Aging over 10 years: ±10 ppm + /// + /// # Example + /// + /// Checking valid parameters at compile-time + /// + /// ``` + /// extern crate static_assertions as sa; + /// use stm32wl_hal::subghz::{FskBandwidth, FskBitrate, FskFdev, FskModParams, FskPulseShape}; + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new() + /// .set_bitrate(FskBitrate::from_bps(20_000)) + /// .set_pulse_shape(FskPulseShape::Bt03) + /// .set_bandwidth(FskBandwidth::Bw58) + /// .set_fdev(FskFdev::from_hertz(10_000)); + /// + /// // 30 PPM is wost case (if the HSE32 crystal meets requirements) + /// sa::const_assert!(MOD_PARAMS.is_valid(30)); + /// ``` + #[must_use = "the return value indicates if the modulation parameters are valid"] + pub const fn is_valid(&self, ppm: u8) -> bool { + let bw: u32 = match self.bandwidth() { + Ok(bw) => bw.hertz(), + Err(_) => return false, + }; + let br: u32 = self.bitrate().as_bps(); + let fdev: u32 = self.fdev().as_hertz(); + let hse_err: u32 = 32 * (ppm as u32); + let freq_err: u32 = 2 * hse_err; + + bw > br + 2 * fdev + freq_err + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskBandwidth, FskBitrate, FskFdev, FskModParams, FskPulseShape}; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(20_000); + /// const PULSE_SHAPE: FskPulseShape = FskPulseShape::Bt03; + /// const BW: FskBandwidth = FskBandwidth::Bw58; + /// const FDEV: FskFdev = FskFdev::from_hertz(10_000); + /// + /// const MOD_PARAMS: FskModParams = FskModParams::new() + /// .set_bitrate(BITRATE) + /// .set_pulse_shape(PULSE_SHAPE) + /// .set_bandwidth(BW) + /// .set_fdev(FDEV); + /// + /// assert_eq!( + /// MOD_PARAMS.as_slice(), + /// &[0x8B, 0x00, 0xC8, 0x00, 0x08, 0x0C, 0x00, 0x28, 0xF5] + /// ); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for FskModParams { + fn default() -> Self { + Self::new() + } +} + +/// LoRa spreading factor. +/// +/// Argument of [`LoRaModParams::set_sf`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum SpreadingFactor { + /// Spreading factor 5. + Sf5 = 0x05, + /// Spreading factor 6. + Sf6 = 0x06, + /// Spreading factor 7. + Sf7 = 0x07, + /// Spreading factor 8. + Sf8 = 0x08, + /// Spreading factor 9. + Sf9 = 0x09, + /// Spreading factor 10. + Sf10 = 0xA0, + /// Spreading factor 11. + Sf11 = 0xB0, + /// Spreading factor 12. + Sf12 = 0xC0, +} + +impl From for u8 { + fn from(sf: SpreadingFactor) -> Self { + sf as u8 + } +} + +/// LoRa bandwidth. +/// +/// Argument of [`LoRaModParams::set_bw`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum LoRaBandwidth { + /// 7.81 kHz + Bw7 = 0x00, + /// 10.42 kHz + Bw10 = 0x08, + /// 15.63 kHz + Bw15 = 0x01, + /// 20.83 kHz + Bw20 = 0x09, + /// 31.25 kHz + Bw31 = 0x02, + /// 41.67 kHz + Bw41 = 0x0A, + /// 62.50 kHz + Bw62 = 0x03, + /// 125 kHz + Bw125 = 0x04, + /// 250 kHz + Bw250 = 0x05, + /// 500 kHz + Bw500 = 0x06, +} + +impl LoRaBandwidth { + /// Get the bandwidth in hertz. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaBandwidth; + /// + /// assert_eq!(LoRaBandwidth::Bw7.hertz(), 7_810); + /// assert_eq!(LoRaBandwidth::Bw10.hertz(), 10_420); + /// assert_eq!(LoRaBandwidth::Bw15.hertz(), 15_630); + /// assert_eq!(LoRaBandwidth::Bw20.hertz(), 20_830); + /// assert_eq!(LoRaBandwidth::Bw31.hertz(), 31_250); + /// assert_eq!(LoRaBandwidth::Bw41.hertz(), 41_670); + /// assert_eq!(LoRaBandwidth::Bw62.hertz(), 62_500); + /// assert_eq!(LoRaBandwidth::Bw125.hertz(), 125_000); + /// assert_eq!(LoRaBandwidth::Bw250.hertz(), 250_000); + /// assert_eq!(LoRaBandwidth::Bw500.hertz(), 500_000); + /// ``` + pub const fn hertz(&self) -> u32 { + match self { + LoRaBandwidth::Bw7 => 7_810, + LoRaBandwidth::Bw10 => 10_420, + LoRaBandwidth::Bw15 => 15_630, + LoRaBandwidth::Bw20 => 20_830, + LoRaBandwidth::Bw31 => 31_250, + LoRaBandwidth::Bw41 => 41_670, + LoRaBandwidth::Bw62 => 62_500, + LoRaBandwidth::Bw125 => 125_000, + LoRaBandwidth::Bw250 => 250_000, + LoRaBandwidth::Bw500 => 500_000, + } + } +} + +impl Ord for LoRaBandwidth { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.hertz().cmp(&other.hertz()) + } +} + +impl PartialOrd for LoRaBandwidth { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.hertz().cmp(&other.hertz())) + } +} + +/// LoRa forward error correction coding rate. +/// +/// Argument of [`LoRaModParams::set_cr`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum CodingRate { + /// No forward error correction coding rate 4/4 + Cr44 = 0x00, + /// Forward error correction coding rate 4/5 + Cr45 = 0x1, + /// Forward error correction coding rate 4/6 + Cr46 = 0x2, + /// Forward error correction coding rate 4/7 + Cr47 = 0x3, + /// Forward error correction coding rate 4/8 + Cr48 = 0x4, +} + +/// LoRa modulation paramters. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] + +pub struct LoRaModParams { + buf: [u8; 5], +} + +impl LoRaModParams { + /// Create a new `LoRaModParams` struct. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaModParams; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new(); + /// assert_eq!(MOD_PARAMS, LoRaModParams::default()); + /// ``` + pub const fn new() -> LoRaModParams { + LoRaModParams { + buf: [ + super::OpCode::SetModulationParams as u8, + 0x05, // valid spreading factor + 0x00, + 0x00, + 0x00, + ], + } + } + + /// Set the spreading factor. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{LoRaModParams, SpreadingFactor}; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new().set_sf(SpreadingFactor::Sf7); + /// # assert_eq!(MOD_PARAMS.as_slice(), &[0x8B, 0x07, 0x00, 0x00, 0x00]); + /// ``` + #[must_use = "set_sf returns a modified LoRaModParams"] + pub const fn set_sf(mut self, sf: SpreadingFactor) -> Self { + self.buf[1] = sf as u8; + self + } + + /// Set the bandwidth. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{LoRaBandwidth, LoRaModParams}; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new().set_bw(LoRaBandwidth::Bw125); + /// # assert_eq!(MOD_PARAMS.as_slice(), &[0x8B, 0x05, 0x04, 0x00, 0x00]); + /// ``` + #[must_use = "set_bw returns a modified LoRaModParams"] + pub const fn set_bw(mut self, bw: LoRaBandwidth) -> Self { + self.buf[2] = bw as u8; + self + } + + /// Set the forward error correction coding rate. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CodingRate, LoRaModParams}; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new().set_cr(CodingRate::Cr45); + /// # assert_eq!(MOD_PARAMS.as_slice(), &[0x8B, 0x05, 0x00, 0x01, 0x00]); + /// ``` + #[must_use = "set_cr returns a modified LoRaModParams"] + pub const fn set_cr(mut self, cr: CodingRate) -> Self { + self.buf[3] = cr as u8; + self + } + + /// Set low data rate optimization enable. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaModParams; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new().set_ldro_en(true); + /// # assert_eq!(MOD_PARAMS.as_slice(), &[0x8B, 0x05, 0x00, 0x00, 0x01]); + /// ``` + #[must_use = "set_ldro_en returns a modified LoRaModParams"] + pub const fn set_ldro_en(mut self, en: bool) -> Self { + self.buf[4] = en as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CodingRate, LoRaBandwidth, LoRaModParams, SpreadingFactor}; + /// + /// const MOD_PARAMS: LoRaModParams = LoRaModParams::new() + /// .set_sf(SpreadingFactor::Sf7) + /// .set_bw(LoRaBandwidth::Bw125) + /// .set_cr(CodingRate::Cr45) + /// .set_ldro_en(false); + /// + /// assert_eq!(MOD_PARAMS.as_slice(), &[0x8B, 0x07, 0x04, 0x01, 0x00]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for LoRaModParams { + fn default() -> Self { + Self::new() + } +} + +/// BPSK modulation paramters. +/// +/// **Note:** There is no method to set the pulse shape because there is only +/// one valid pulse shape (Gaussian BT 0.5). +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct BpskModParams { + buf: [u8; 5], +} + +impl BpskModParams { + /// Create a new `BpskModParams` struct. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::BpskModParams; + /// + /// const MOD_PARAMS: BpskModParams = BpskModParams::new(); + /// assert_eq!(MOD_PARAMS, BpskModParams::default()); + /// ``` + pub const fn new() -> BpskModParams { + const OPCODE: u8 = super::OpCode::SetModulationParams as u8; + BpskModParams { + buf: [OPCODE, 0x1A, 0x0A, 0xAA, 0x16], + } + } + + /// Set the bitrate. + /// + /// # Example + /// + /// Setting the bitrate to 600 bits per second. + /// + /// ``` + /// use stm32wl_hal::subghz::{BpskModParams, FskBitrate}; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(600); + /// const MOD_PARAMS: BpskModParams = BpskModParams::new().set_bitrate(BITRATE); + /// # assert_eq!(MOD_PARAMS.as_slice()[1], 0x1A); + /// # assert_eq!(MOD_PARAMS.as_slice()[2], 0x0A); + /// # assert_eq!(MOD_PARAMS.as_slice()[3], 0xAA); + /// ``` + #[must_use = "set_bitrate returns a modified BpskModParams"] + pub const fn set_bitrate(mut self, bitrate: FskBitrate) -> BpskModParams { + let bits: u32 = bitrate.into_bits(); + self.buf[1] = ((bits >> 16) & 0xFF) as u8; + self.buf[2] = ((bits >> 8) & 0xFF) as u8; + self.buf[3] = (bits & 0xFF) as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{BpskModParams, FskBitrate}; + /// + /// const BITRATE: FskBitrate = FskBitrate::from_bps(100); + /// const MOD_PARAMS: BpskModParams = BpskModParams::new().set_bitrate(BITRATE); + /// assert_eq!(MOD_PARAMS.as_slice(), [0x8B, 0x9C, 0x40, 0x00, 0x16]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for BpskModParams { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use super::{FskBandwidth, FskBitrate, FskFdev, LoRaBandwidth}; + + #[test] + fn fsk_bw_ord() { + assert!((FskBandwidth::Bw4 as u8) > (FskBandwidth::Bw5 as u8)); + assert!(FskBandwidth::Bw4 < FskBandwidth::Bw5); + assert!(FskBandwidth::Bw5 > FskBandwidth::Bw4); + } + + #[test] + fn lora_bw_ord() { + assert!((LoRaBandwidth::Bw10 as u8) > (LoRaBandwidth::Bw15 as u8)); + assert!(LoRaBandwidth::Bw10 < LoRaBandwidth::Bw15); + assert!(LoRaBandwidth::Bw15 > LoRaBandwidth::Bw10); + } + + #[test] + fn fsk_bitrate_ord() { + assert!(FskBitrate::from_bps(9600) > FskBitrate::from_bps(4800)); + assert!(FskBitrate::from_bps(4800) < FskBitrate::from_bps(9600)); + } + + #[test] + fn fsk_bitrate_as_bps_limits() { + const ZERO: FskBitrate = FskBitrate::from_raw(0); + const ONE: FskBitrate = FskBitrate::from_raw(1); + const MAX: FskBitrate = FskBitrate::from_raw(u32::MAX); + + assert_eq!(ZERO.as_bps(), 0); + assert_eq!(ONE.as_bps(), 1_024_000_000); + assert_eq!(MAX.as_bps(), 61); + } + + #[test] + fn fsk_bitrate_from_bps_limits() { + const ZERO: FskBitrate = FskBitrate::from_bps(0); + const ONE: FskBitrate = FskBitrate::from_bps(1); + const MAX: FskBitrate = FskBitrate::from_bps(u32::MAX); + + assert_eq!(ZERO.as_bps(), 61); + assert_eq!(ONE.as_bps(), 61); + assert_eq!(MAX.as_bps(), 0); + } + + #[test] + fn fsk_fdev_ord() { + assert!(FskFdev::from_hertz(30_000) > FskFdev::from_hertz(20_000)); + assert!(FskFdev::from_hertz(20_000) < FskFdev::from_hertz(30_000)); + } + + #[test] + fn fsk_fdev_as_hertz_limits() { + const ZERO: FskFdev = FskFdev::from_raw(0); + const ONE: FskFdev = FskFdev::from_raw(1); + const MAX: FskFdev = FskFdev::from_raw(u32::MAX); + + assert_eq!(ZERO.as_hertz(), 0); + assert_eq!(ONE.as_hertz(), 0); + assert_eq!(MAX.as_hertz(), 15_999_999); + } + + #[test] + fn fsk_fdev_from_hertz_limits() { + const ZERO: FskFdev = FskFdev::from_hertz(0); + const ONE: FskFdev = FskFdev::from_hertz(1); + const MAX: FskFdev = FskFdev::from_hertz(u32::MAX); + + assert_eq!(ZERO.as_hertz(), 0); + assert_eq!(ONE.as_hertz(), 0); + assert_eq!(MAX.as_hertz(), 6_967_294); + } +} diff --git a/embassy-stm32/src/subghz/ocp.rs b/embassy-stm32/src/subghz/ocp.rs new file mode 100644 index 00000000..88eea1a2 --- /dev/null +++ b/embassy-stm32/src/subghz/ocp.rs @@ -0,0 +1,14 @@ +/// Power amplifier over current protection. +/// +/// Used by [`set_pa_ocp`]. +/// +/// [`set_pa_ocp`]: crate::subghz::SubGhz::set_pa_ocp +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum Ocp { + /// Maximum 60mA current for LP PA mode. + Max60m = 0x18, + /// Maximum 140mA for HP PA mode. + Max140m = 0x38, +} diff --git a/embassy-stm32/src/subghz/op_error.rs b/embassy-stm32/src/subghz/op_error.rs new file mode 100644 index 00000000..35ebda8a --- /dev/null +++ b/embassy-stm32/src/subghz/op_error.rs @@ -0,0 +1,48 @@ +/// Operation Errors. +/// +/// Returned by [`op_error`]. +/// +/// [`op_error`]: crate::subghz::SubGhz::op_error +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum OpError { + /// PA ramping failed + PaRampError = 8, + /// RF-PLL locking failed + PllLockError = 6, + /// HSE32 clock startup failed + XoscStartError = 5, + /// Image calibration failed + ImageCalibrationError = 4, + /// RF-ADC calibration failed + AdcCalibrationError = 3, + /// RF-PLL calibration failed + PllCalibrationError = 2, + /// Sub-GHz radio RC 13 MHz oscillator + RC13MCalibrationError = 1, + /// Sub-GHz radio RC 64 kHz oscillator + RC64KCalibrationError = 0, +} + +impl OpError { + /// Get the bitmask for the error. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::OpError; + /// + /// assert_eq!(OpError::PaRampError.mask(), 0b1_0000_0000); + /// assert_eq!(OpError::PllLockError.mask(), 0b0_0100_0000); + /// assert_eq!(OpError::XoscStartError.mask(), 0b0_0010_0000); + /// assert_eq!(OpError::ImageCalibrationError.mask(), 0b0_0001_0000); + /// assert_eq!(OpError::AdcCalibrationError.mask(), 0b0_0000_1000); + /// assert_eq!(OpError::PllCalibrationError.mask(), 0b0_0000_0100); + /// assert_eq!(OpError::RC13MCalibrationError.mask(), 0b0_0000_0010); + /// assert_eq!(OpError::RC64KCalibrationError.mask(), 0b0_0000_0001); + /// ``` + pub const fn mask(self) -> u16 { + 1 << (self as u8) + } +} diff --git a/embassy-stm32/src/subghz/pa_config.rs b/embassy-stm32/src/subghz/pa_config.rs new file mode 100644 index 00000000..83c510aa --- /dev/null +++ b/embassy-stm32/src/subghz/pa_config.rs @@ -0,0 +1,161 @@ +/// Power amplifier configuration paramters. +/// +/// Argument of [`set_pa_config`]. +/// +/// [`set_pa_config`]: crate::subghz::SubGhz::set_pa_config +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PaConfig { + buf: [u8; 5], +} + +impl PaConfig { + /// Create a new `PaConfig` struct. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PaConfig; + /// + /// const PA_CONFIG: PaConfig = PaConfig::new(); + /// ``` + pub const fn new() -> PaConfig { + PaConfig { + buf: [super::OpCode::SetPaConfig as u8, 0x01, 0x00, 0x01, 0x01], + } + } + + /// Set the power amplifier duty cycle (conduit angle) control. + /// + /// **Note:** Only the first 3 bits of the `pa_duty_cycle` argument are used. + /// + /// Duty cycle = 0.2 + 0.04 × bits + /// + /// # Caution + /// + /// The following restrictions must be observed to avoid over-stress on the PA: + /// * LP PA mode with synthesis frequency > 400 MHz, PaDutyCycle must be < 0x7. + /// * LP PA mode with synthesis frequency < 400 MHz, PaDutyCycle must be < 0x4. + /// * HP PA mode, PaDutyCycle must be < 0x4 + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{PaConfig, PaSel}; + /// + /// const PA_CONFIG: PaConfig = PaConfig::new().set_pa(PaSel::Lp).set_pa_duty_cycle(0x4); + /// # assert_eq!(PA_CONFIG.as_slice()[1], 0x04); + /// ``` + #[must_use = "set_pa_duty_cycle returns a modified PaConfig"] + pub const fn set_pa_duty_cycle(mut self, pa_duty_cycle: u8) -> PaConfig { + self.buf[1] = pa_duty_cycle & 0b111; + self + } + + /// Set the high power amplifier output power. + /// + /// **Note:** Only the first 3 bits of the `hp_max` argument are used. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{PaConfig, PaSel}; + /// + /// const PA_CONFIG: PaConfig = PaConfig::new().set_pa(PaSel::Hp).set_hp_max(0x2); + /// # assert_eq!(PA_CONFIG.as_slice()[2], 0x02); + /// ``` + #[must_use = "set_hp_max returns a modified PaConfig"] + pub const fn set_hp_max(mut self, hp_max: u8) -> PaConfig { + self.buf[2] = hp_max & 0b111; + self + } + + /// Set the power amplifier to use, low or high power. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{PaConfig, PaSel}; + /// + /// const PA_CONFIG_HP: PaConfig = PaConfig::new().set_pa(PaSel::Hp); + /// const PA_CONFIG_LP: PaConfig = PaConfig::new().set_pa(PaSel::Lp); + /// # assert_eq!(PA_CONFIG_HP.as_slice()[3], 0x00); + /// # assert_eq!(PA_CONFIG_LP.as_slice()[3], 0x01); + /// ``` + #[must_use = "set_pa returns a modified PaConfig"] + pub const fn set_pa(mut self, pa: PaSel) -> PaConfig { + self.buf[3] = pa as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{PaConfig, PaSel}; + /// + /// const PA_CONFIG: PaConfig = PaConfig::new() + /// .set_pa(PaSel::Hp) + /// .set_pa_duty_cycle(0x2) + /// .set_hp_max(0x3); + /// + /// assert_eq!(PA_CONFIG.as_slice(), &[0x95, 0x2, 0x03, 0x00, 0x01]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for PaConfig { + fn default() -> Self { + Self::new() + } +} + +/// Power amplifier selection. +/// +/// Argument of [`PaConfig::set_pa`]. +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum PaSel { + /// High power amplifier. + Hp = 0b0, + /// Low power amplifier. + Lp = 0b1, +} + +impl PartialOrd for PaSel { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PaSel { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + match (self, other) { + (PaSel::Hp, PaSel::Hp) | (PaSel::Lp, PaSel::Lp) => core::cmp::Ordering::Equal, + (PaSel::Hp, PaSel::Lp) => core::cmp::Ordering::Greater, + (PaSel::Lp, PaSel::Hp) => core::cmp::Ordering::Less, + } + } +} + +impl Default for PaSel { + fn default() -> Self { + PaSel::Lp + } +} + +#[cfg(test)] +mod test { + use super::PaSel; + + #[test] + fn pa_sel_ord() { + assert!(PaSel::Lp < PaSel::Hp); + assert!(PaSel::Hp > PaSel::Lp); + } +} diff --git a/embassy-stm32/src/subghz/packet_params.rs b/embassy-stm32/src/subghz/packet_params.rs new file mode 100644 index 00000000..712dbaee --- /dev/null +++ b/embassy-stm32/src/subghz/packet_params.rs @@ -0,0 +1,537 @@ +/// Preamble detection length for [`GenericPacketParams`]. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PreambleDetection { + /// Preamble detection disabled. + Disabled = 0x0, + /// 8-bit preamble detection. + Bit8 = 0x4, + /// 16-bit preamble detection. + Bit16 = 0x5, + /// 24-bit preamble detection. + Bit24 = 0x6, + /// 32-bit preamble detection. + Bit32 = 0x7, +} + +/// Address comparison/filtering for [`GenericPacketParams`]. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AddrComp { + /// Address comparison/filtering disabled. + Disabled = 0x0, + /// Address comparison/filtering on node address. + Node = 0x1, + /// Address comparison/filtering on node and broadcast addresses. + Broadcast = 0x2, +} + +/// Packet header type. +/// +/// Argument of [`GenericPacketParams::set_header_type`] and +/// [`LoRaPacketParams::set_header_type`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum HeaderType { + /// Fixed; payload length and header field not added to packet. + Fixed, + /// Variable; payload length and header field added to packet. + Variable, +} + +impl HeaderType { + pub(crate) const fn to_bits_generic(self) -> u8 { + match self { + HeaderType::Fixed => 0, + HeaderType::Variable => 1, + } + } + + pub(crate) const fn to_bits_lora(self) -> u8 { + match self { + HeaderType::Fixed => 1, + HeaderType::Variable => 0, + } + } +} + +/// CRC type definition for [`GenericPacketParams`]. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CrcType { + /// 1-byte CRC. + Byte1 = 0x0, + /// CRC disabled. + Disabled = 0x1, + /// 2-byte CRC. + Byte2 = 0x2, + /// 1-byte inverted CRC. + Byte1Inverted = 0x4, + /// 2-byte inverted CRC. + Byte2Inverted = 0x6, +} + +/// Packet parameters for [`set_packet_params`]. +/// +/// [`set_packet_params`]: crate::subghz::SubGhz::set_packet_params +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct GenericPacketParams { + buf: [u8; 10], +} + +impl GenericPacketParams { + /// Create a new `GenericPacketParams`. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::GenericPacketParams; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new(); + /// assert_eq!(PKT_PARAMS, GenericPacketParams::default()); + /// ``` + pub const fn new() -> GenericPacketParams { + const OPCODE: u8 = super::OpCode::SetPacketParams as u8; + // const variable ensure the compile always optimizes the methods + const NEW: GenericPacketParams = GenericPacketParams { + buf: [OPCODE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + } + .set_preamble_len(1) + .set_preamble_detection(PreambleDetection::Disabled) + .set_sync_word_len(0) + .set_addr_comp(AddrComp::Disabled) + .set_header_type(HeaderType::Fixed) + .set_payload_len(1); + + NEW + } + + /// Preamble length in number of symbols. + /// + /// Values of zero are invalid, and will automatically be set to 1. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::GenericPacketParams; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new().set_preamble_len(0x1234); + /// # assert_eq!(PKT_PARAMS.as_slice()[1], 0x12); + /// # assert_eq!(PKT_PARAMS.as_slice()[2], 0x34); + /// ``` + #[must_use = "preamble_length returns a modified GenericPacketParams"] + pub const fn set_preamble_len(mut self, mut len: u16) -> GenericPacketParams { + if len == 0 { + len = 1 + } + self.buf[1] = ((len >> 8) & 0xFF) as u8; + self.buf[2] = (len & 0xFF) as u8; + self + } + + /// Preabmle detection length in number of bit symbols. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{GenericPacketParams, PreambleDetection}; + /// + /// const PKT_PARAMS: GenericPacketParams = + /// GenericPacketParams::new().set_preamble_detection(PreambleDetection::Bit8); + /// # assert_eq!(PKT_PARAMS.as_slice()[3], 0x4); + /// ``` + #[must_use = "set_preamble_detection returns a modified GenericPacketParams"] + pub const fn set_preamble_detection( + mut self, + pb_det: PreambleDetection, + ) -> GenericPacketParams { + self.buf[3] = pb_det as u8; + self + } + + /// Sync word length in number of bit symbols. + /// + /// Valid values are `0x00` - `0x40` for 0 to 64-bits respectively. + /// Values that exceed the maximum will saturate at `0x40`. + /// + /// # Example + /// + /// Set the sync word length to 4 bytes (16 bits). + /// + /// ``` + /// use stm32wl_hal::subghz::GenericPacketParams; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new().set_sync_word_len(16); + /// # assert_eq!(PKT_PARAMS.as_slice()[4], 0x10); + /// ``` + #[must_use = "set_sync_word_len returns a modified GenericPacketParams"] + pub const fn set_sync_word_len(mut self, len: u8) -> GenericPacketParams { + const MAX: u8 = 0x40; + if len > MAX { + self.buf[4] = MAX; + } else { + self.buf[4] = len; + } + self + } + + /// Address comparison/filtering. + /// + /// # Example + /// + /// Enable address on the node address. + /// + /// ``` + /// use stm32wl_hal::subghz::{AddrComp, GenericPacketParams}; + /// + /// const PKT_PARAMS: GenericPacketParams = + /// GenericPacketParams::new().set_addr_comp(AddrComp::Node); + /// # assert_eq!(PKT_PARAMS.as_slice()[5], 0x01); + /// ``` + #[must_use = "set_addr_comp returns a modified GenericPacketParams"] + pub const fn set_addr_comp(mut self, addr_comp: AddrComp) -> GenericPacketParams { + self.buf[5] = addr_comp as u8; + self + } + + /// Header type definition. + /// + /// **Note:** The reference manual calls this packet type, but that results + /// in a conflicting variable name for the modulation scheme, which the + /// reference manual also calls packet type. + /// + /// # Example + /// + /// Set the header type to a variable length. + /// + /// ``` + /// use stm32wl_hal::subghz::{GenericPacketParams, HeaderType}; + /// + /// const PKT_PARAMS: GenericPacketParams = + /// GenericPacketParams::new().set_header_type(HeaderType::Variable); + /// # assert_eq!(PKT_PARAMS.as_slice()[6], 0x01); + /// ``` + #[must_use = "set_header_type returns a modified GenericPacketParams"] + pub const fn set_header_type(mut self, header_type: HeaderType) -> GenericPacketParams { + self.buf[6] = header_type.to_bits_generic(); + self + } + + /// Set the payload length in bytes. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::GenericPacketParams; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new().set_payload_len(12); + /// # assert_eq!(PKT_PARAMS.as_slice()[7], 12); + /// ``` + #[must_use = "set_payload_len returns a modified GenericPacketParams"] + pub const fn set_payload_len(mut self, len: u8) -> GenericPacketParams { + self.buf[7] = len; + self + } + + /// CRC type definition. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CrcType, GenericPacketParams}; + /// + /// const PKT_PARAMS: GenericPacketParams = + /// GenericPacketParams::new().set_crc_type(CrcType::Byte2Inverted); + /// # assert_eq!(PKT_PARAMS.as_slice()[8], 0x6); + /// ``` + #[must_use = "set_payload_len returns a modified GenericPacketParams"] + pub const fn set_crc_type(mut self, crc_type: CrcType) -> GenericPacketParams { + self.buf[8] = crc_type as u8; + self + } + + /// Whitening enable. + /// + /// # Example + /// + /// Enable whitening. + /// + /// ``` + /// use stm32wl_hal::subghz::GenericPacketParams; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new().set_whitening_enable(true); + /// # assert_eq!(PKT_PARAMS.as_slice()[9], 1); + /// ``` + #[must_use = "set_whitening_enable returns a modified GenericPacketParams"] + pub const fn set_whitening_enable(mut self, en: bool) -> GenericPacketParams { + self.buf[9] = en as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{ + /// AddrComp, CrcType, GenericPacketParams, HeaderType, PreambleDetection, + /// }; + /// + /// const PKT_PARAMS: GenericPacketParams = GenericPacketParams::new() + /// .set_preamble_len(8) + /// .set_preamble_detection(PreambleDetection::Disabled) + /// .set_sync_word_len(2) + /// .set_addr_comp(AddrComp::Disabled) + /// .set_header_type(HeaderType::Fixed) + /// .set_payload_len(128) + /// .set_crc_type(CrcType::Byte2) + /// .set_whitening_enable(true); + /// + /// assert_eq!( + /// PKT_PARAMS.as_slice(), + /// &[0x8C, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x80, 0x02, 0x01] + /// ); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for GenericPacketParams { + fn default() -> Self { + Self::new() + } +} + +/// Packet parameters for [`set_lora_packet_params`]. +/// +/// [`set_lora_packet_params`]: crate::subghz::SubGhz::set_lora_packet_params +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LoRaPacketParams { + buf: [u8; 7], +} + +impl LoRaPacketParams { + /// Create a new `GenericPacketParams`. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaPacketParams; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new(); + /// assert_eq!(PKT_PARAMS, LoRaPacketParams::default()); + /// ``` + pub const fn new() -> LoRaPacketParams { + const OPCODE: u8 = super::OpCode::SetPacketParams as u8; + // const variable ensure the compile always optimizes the methods + const NEW: LoRaPacketParams = LoRaPacketParams { + buf: [OPCODE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + } + .set_preamble_len(1) + .set_header_type(HeaderType::Fixed) + .set_payload_len(1) + .set_crc_en(true) + .set_invert_iq(false); + + NEW + } + + /// Preamble length in number of symbols. + /// + /// Values of zero are invalid, and will automatically be set to 1. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaPacketParams; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new().set_preamble_len(0x1234); + /// # assert_eq!(PKT_PARAMS.as_slice()[1], 0x12); + /// # assert_eq!(PKT_PARAMS.as_slice()[2], 0x34); + /// ``` + #[must_use = "preamble_length returns a modified LoRaPacketParams"] + pub const fn set_preamble_len(mut self, mut len: u16) -> LoRaPacketParams { + if len == 0 { + len = 1 + } + self.buf[1] = ((len >> 8) & 0xFF) as u8; + self.buf[2] = (len & 0xFF) as u8; + self + } + + /// Header type (fixed or variable). + /// + /// # Example + /// + /// Set the payload type to a fixed length. + /// + /// ``` + /// use stm32wl_hal::subghz::{HeaderType, LoRaPacketParams}; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new().set_header_type(HeaderType::Fixed); + /// # assert_eq!(PKT_PARAMS.as_slice()[3], 0x01); + /// ``` + #[must_use = "set_header_type returns a modified LoRaPacketParams"] + pub const fn set_header_type(mut self, header_type: HeaderType) -> LoRaPacketParams { + self.buf[3] = header_type.to_bits_lora(); + self + } + + /// Set the payload length in bytes. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaPacketParams; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new().set_payload_len(12); + /// # assert_eq!(PKT_PARAMS.as_slice()[4], 12); + /// ``` + #[must_use = "set_payload_len returns a modified LoRaPacketParams"] + pub const fn set_payload_len(mut self, len: u8) -> LoRaPacketParams { + self.buf[4] = len; + self + } + + /// CRC enable. + /// + /// # Example + /// + /// Enable CRC. + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaPacketParams; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new().set_crc_en(true); + /// # assert_eq!(PKT_PARAMS.as_slice()[5], 0x1); + /// ``` + #[must_use = "set_crc_en returns a modified LoRaPacketParams"] + pub const fn set_crc_en(mut self, en: bool) -> LoRaPacketParams { + self.buf[5] = en as u8; + self + } + + /// IQ setup. + /// + /// # Example + /// + /// Use an inverted IQ setup. + /// + /// ``` + /// use stm32wl_hal::subghz::LoRaPacketParams; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new().set_invert_iq(true); + /// # assert_eq!(PKT_PARAMS.as_slice()[6], 0x1); + /// ``` + #[must_use = "set_invert_iq returns a modified LoRaPacketParams"] + pub const fn set_invert_iq(mut self, invert: bool) -> LoRaPacketParams { + self.buf[6] = invert as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{HeaderType, LoRaPacketParams}; + /// + /// const PKT_PARAMS: LoRaPacketParams = LoRaPacketParams::new() + /// .set_preamble_len(5 * 8) + /// .set_header_type(HeaderType::Fixed) + /// .set_payload_len(64) + /// .set_crc_en(true) + /// .set_invert_iq(true); + /// + /// assert_eq!( + /// PKT_PARAMS.as_slice(), + /// &[0x8C, 0x00, 0x28, 0x01, 0x40, 0x01, 0x01] + /// ); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for LoRaPacketParams { + fn default() -> Self { + Self::new() + } +} + +/// Packet parameters for [`set_lora_packet_params`]. +/// +/// [`set_lora_packet_params`]: crate::subghz::SubGhz::set_lora_packet_params +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct BpskPacketParams { + buf: [u8; 2], +} + +impl BpskPacketParams { + /// Create a new `BpskPacketParams`. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::BpskPacketParams; + /// + /// const PKT_PARAMS: BpskPacketParams = BpskPacketParams::new(); + /// assert_eq!(PKT_PARAMS, BpskPacketParams::default()); + /// ``` + pub const fn new() -> BpskPacketParams { + BpskPacketParams { + buf: [super::OpCode::SetPacketParams as u8, 0x00], + } + } + + /// Set the payload length in bytes. + /// + /// The length includes preamble, sync word, device ID, and CRC. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::BpskPacketParams; + /// + /// const PKT_PARAMS: BpskPacketParams = BpskPacketParams::new().set_payload_len(12); + /// # assert_eq!(PKT_PARAMS.as_slice()[1], 12); + /// ``` + #[must_use = "set_payload_len returns a modified BpskPacketParams"] + pub const fn set_payload_len(mut self, len: u8) -> BpskPacketParams { + self.buf[1] = len; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{BpskPacketParams, HeaderType}; + /// + /// const PKT_PARAMS: BpskPacketParams = BpskPacketParams::new().set_payload_len(24); + /// + /// assert_eq!(PKT_PARAMS.as_slice(), &[0x8C, 24]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for BpskPacketParams { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32/src/subghz/packet_status.rs b/embassy-stm32/src/subghz/packet_status.rs new file mode 100644 index 00000000..c5316dc5 --- /dev/null +++ b/embassy-stm32/src/subghz/packet_status.rs @@ -0,0 +1,279 @@ +use embassy_hal_common::ratio::Ratio; + +use crate::subghz::status::Status; + +/// (G)FSK packet status. +/// +/// Returned by [`fsk_packet_status`]. +/// +/// [`fsk_packet_status`]: crate::subghz::SubGhz::fsk_packet_status +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FskPacketStatus { + buf: [u8; 4], +} + +impl From<[u8; 4]> for FskPacketStatus { + fn from(buf: [u8; 4]) -> Self { + FskPacketStatus { buf } + } +} + +impl FskPacketStatus { + /// Get the status. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CmdStatus, FskPacketStatus, Status, StatusMode}; + /// + /// let example_data_from_radio: [u8; 4] = [0x54, 0, 0, 0]; + /// let pkt_status: FskPacketStatus = FskPacketStatus::from(example_data_from_radio); + /// let status: Status = pkt_status.status(); + /// assert_eq!(status.mode(), Ok(StatusMode::Rx)); + /// assert_eq!(status.cmd(), Ok(CmdStatus::Avaliable)); + /// ``` + pub const fn status(&self) -> Status { + Status::from_raw(self.buf[0]) + } + + /// Returns `true` if a preabmle error occured. + pub const fn preamble_error(&self) -> bool { + (self.buf[1] & (1 << 7)) != 0 + } + + /// Returns `true` if a synchronization error occured. + pub const fn sync_err(&self) -> bool { + (self.buf[1] & (1 << 6)) != 0 + } + + /// Returns `true` if an address error occured. + pub const fn adrs_err(&self) -> bool { + (self.buf[1] & (1 << 5)) != 0 + } + + /// Returns `true` if an crc error occured. + pub const fn crc_err(&self) -> bool { + (self.buf[1] & (1 << 4)) != 0 + } + + /// Returns `true` if a length error occured. + pub const fn length_err(&self) -> bool { + (self.buf[1] & (1 << 3)) != 0 + } + + /// Returns `true` if an abort error occured. + pub const fn abort_err(&self) -> bool { + (self.buf[1] & (1 << 2)) != 0 + } + + /// Returns `true` if a packet is received. + pub const fn pkt_received(&self) -> bool { + (self.buf[1] & (1 << 1)) != 0 + } + + /// Returns `true` when a packet has been sent. + pub const fn pkt_sent(&self) -> bool { + (self.buf[1] & 1) != 0 + } + + /// RSSI level when the synchronization address is detected. + /// + /// Units are in dBm. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::{subghz::FskPacketStatus, Ratio}; + /// + /// let example_data_from_radio: [u8; 4] = [0, 0, 80, 0]; + /// let pkt_status: FskPacketStatus = FskPacketStatus::from(example_data_from_radio); + /// assert_eq!(pkt_status.rssi_sync().to_integer(), -40); + /// ``` + pub fn rssi_sync(&self) -> Ratio { + Ratio::new_raw(i16::from(self.buf[2]), -2) + } + + /// Return the RSSI level over the received packet. + /// + /// Units are in dBm. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::{subghz::FskPacketStatus, Ratio}; + /// + /// let example_data_from_radio: [u8; 4] = [0, 0, 0, 100]; + /// let pkt_status: FskPacketStatus = FskPacketStatus::from(example_data_from_radio); + /// assert_eq!(pkt_status.rssi_avg().to_integer(), -50); + /// ``` + pub fn rssi_avg(&self) -> Ratio { + Ratio::new_raw(i16::from(self.buf[3]), -2) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for FskPacketStatus { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + r#"FskPacketStatus {{ + status: {}, + preamble_error: {}, + sync_err: {}, + adrs_err: {}, + crc_err: {}, + length_err: {}, + abort_err: {}, + pkt_received: {}, + pkt_sent: {}, + rssi_sync: {}, + rssi_avg: {}, +}}"#, + self.status(), + self.preamble_error(), + self.sync_err(), + self.adrs_err(), + self.crc_err(), + self.length_err(), + self.abort_err(), + self.pkt_received(), + self.pkt_sent(), + self.rssi_sync().to_integer(), + self.rssi_avg().to_integer() + ) + } +} + +impl core::fmt::Display for FskPacketStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FskPacketStatus") + .field("status", &self.status()) + .field("preamble_error", &self.preamble_error()) + .field("sync_err", &self.sync_err()) + .field("adrs_err", &self.adrs_err()) + .field("crc_err", &self.crc_err()) + .field("length_err", &self.length_err()) + .field("abort_err", &self.abort_err()) + .field("pkt_received", &self.pkt_received()) + .field("pkt_sent", &self.pkt_sent()) + .field("rssi_sync", &self.rssi_sync().to_integer()) + .field("rssi_avg", &self.rssi_avg().to_integer()) + .finish() + } +} + +/// (G)FSK packet status. +/// +/// Returned by [`lora_packet_status`]. +/// +/// [`lora_packet_status`]: crate::subghz::SubGhz::lora_packet_status +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LoRaPacketStatus { + buf: [u8; 4], +} + +impl From<[u8; 4]> for LoRaPacketStatus { + fn from(buf: [u8; 4]) -> Self { + LoRaPacketStatus { buf } + } +} + +impl LoRaPacketStatus { + /// Get the status. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CmdStatus, LoRaPacketStatus, Status, StatusMode}; + /// + /// let example_data_from_radio: [u8; 4] = [0x54, 0, 0, 0]; + /// let pkt_status: LoRaPacketStatus = LoRaPacketStatus::from(example_data_from_radio); + /// let status: Status = pkt_status.status(); + /// assert_eq!(status.mode(), Ok(StatusMode::Rx)); + /// assert_eq!(status.cmd(), Ok(CmdStatus::Avaliable)); + /// ``` + pub const fn status(&self) -> Status { + Status::from_raw(self.buf[0]) + } + + /// Average RSSI level over the received packet. + /// + /// Units are in dBm. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::{subghz::LoRaPacketStatus, Ratio}; + /// + /// let example_data_from_radio: [u8; 4] = [0, 80, 0, 0]; + /// let pkt_status: LoRaPacketStatus = LoRaPacketStatus::from(example_data_from_radio); + /// assert_eq!(pkt_status.rssi_pkt().to_integer(), -40); + /// ``` + pub fn rssi_pkt(&self) -> Ratio { + Ratio::new_raw(i16::from(self.buf[1]), -2) + } + + /// Estimation of SNR over the received packet. + /// + /// Units are in dB. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::{subghz::LoRaPacketStatus, Ratio}; + /// + /// let example_data_from_radio: [u8; 4] = [0, 0, 40, 0]; + /// let pkt_status: LoRaPacketStatus = LoRaPacketStatus::from(example_data_from_radio); + /// assert_eq!(pkt_status.snr_pkt().to_integer(), 10); + /// ``` + pub fn snr_pkt(&self) -> Ratio { + Ratio::new_raw(i16::from(self.buf[2]), 4) + } + + /// Estimation of RSSI level of the LoRa signal after despreading. + /// + /// Units are in dBm. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::{subghz::LoRaPacketStatus, Ratio}; + /// + /// let example_data_from_radio: [u8; 4] = [0, 0, 0, 80]; + /// let pkt_status: LoRaPacketStatus = LoRaPacketStatus::from(example_data_from_radio); + /// assert_eq!(pkt_status.signal_rssi_pkt().to_integer(), -40); + /// ``` + pub fn signal_rssi_pkt(&self) -> Ratio { + Ratio::new_raw(i16::from(self.buf[3]), -2) + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for LoRaPacketStatus { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + r#"LoRaPacketStatus {{ + status: {}, + rssi_pkt: {}, + snr_pkt: {}, + signal_rssi_pkt: {}, +}}"#, + self.status(), + self.rssi_pkt().to_integer(), + self.snr_pkt().to_integer(), + self.signal_rssi_pkt().to_integer(), + ) + } +} + +impl core::fmt::Display for LoRaPacketStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LoRaPacketStatus") + .field("status", &self.status()) + .field("rssi_pkt", &self.rssi_pkt().to_integer()) + .field("snr_pkt", &self.snr_pkt().to_integer()) + .field("signal_rssi_pkt", &self.signal_rssi_pkt().to_integer()) + .finish() + } +} diff --git a/embassy-stm32/src/subghz/packet_type.rs b/embassy-stm32/src/subghz/packet_type.rs new file mode 100644 index 00000000..d953a6b9 --- /dev/null +++ b/embassy-stm32/src/subghz/packet_type.rs @@ -0,0 +1,44 @@ +/// Packet type definition. +/// +/// Argument of [`set_packet_type`] +/// +/// [`set_packet_type`]: crate::subghz::SubGhz::set_packet_type +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PacketType { + /// FSK (frequency shift keying) generic packet type. + Fsk = 0, + /// LoRa (long range) packet type. + LoRa = 1, + /// BPSK (binary phase shift keying) packet type. + Bpsk = 2, + /// MSK (minimum shift keying) generic packet type. + Msk = 3, +} + +impl PacketType { + /// Create a new `PacketType` from bits. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PacketType; + /// + /// assert_eq!(PacketType::from_raw(0), Ok(PacketType::Fsk)); + /// assert_eq!(PacketType::from_raw(1), Ok(PacketType::LoRa)); + /// assert_eq!(PacketType::from_raw(2), Ok(PacketType::Bpsk)); + /// assert_eq!(PacketType::from_raw(3), Ok(PacketType::Msk)); + /// // Other values are reserved + /// assert_eq!(PacketType::from_raw(4), Err(4)); + /// ``` + pub const fn from_raw(bits: u8) -> Result { + match bits { + 0 => Ok(PacketType::Fsk), + 1 => Ok(PacketType::LoRa), + 2 => Ok(PacketType::Bpsk), + 3 => Ok(PacketType::Msk), + _ => Err(bits), + } + } +} diff --git a/embassy-stm32/src/subghz/pkt_ctrl.rs b/embassy-stm32/src/subghz/pkt_ctrl.rs new file mode 100644 index 00000000..b4775d57 --- /dev/null +++ b/embassy-stm32/src/subghz/pkt_ctrl.rs @@ -0,0 +1,247 @@ +/// Generic packet infinite sequence selection. +/// +/// Argument of [`PktCtrl::set_inf_seq_sel`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum InfSeqSel { + /// Preamble `0x5555`. + Five = 0b00, + /// Preamble `0x0000`. + Zero = 0b01, + /// Preamble `0xFFFF`. + One = 0b10, + /// PRBS9. + Prbs9 = 0b11, +} + +impl Default for InfSeqSel { + fn default() -> Self { + InfSeqSel::Five + } +} + +/// Generic packet control. +/// +/// Argument of [`set_pkt_ctrl`](crate::subghz::SubGhz::set_pkt_ctrl). +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PktCtrl { + val: u8, +} + +impl PktCtrl { + /// Reset value of the packet control register. + pub const RESET: PktCtrl = PktCtrl { val: 0x21 }; + + /// Create a new [`PktCtrl`] structure from a raw value. + /// + /// Reserved bits will be masked. + pub const fn from_raw(raw: u8) -> Self { + Self { val: raw & 0x3F } + } + + /// Get the raw value of the [`PktCtrl`] register. + pub const fn as_bits(&self) -> u8 { + self.val + } + + /// Generic packet synchronization word detection enable. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// const PKT_CTRL: PktCtrl = PktCtrl::RESET.set_sync_det_en(true); + /// ``` + #[must_use = "set_sync_det_en returns a modified PktCtrl"] + pub const fn set_sync_det_en(mut self, en: bool) -> PktCtrl { + if en { + self.val |= 1 << 5; + } else { + self.val &= !(1 << 5); + } + self + } + + /// Returns `true` if generic packet synchronization word detection is + /// enabled. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// let pc: PktCtrl = PktCtrl::RESET; + /// assert_eq!(pc.sync_det_en(), true); + /// let pc: PktCtrl = pc.set_sync_det_en(false); + /// assert_eq!(pc.sync_det_en(), false); + /// let pc: PktCtrl = pc.set_sync_det_en(true); + /// assert_eq!(pc.sync_det_en(), true); + /// ``` + pub const fn sync_det_en(&self) -> bool { + self.val & (1 << 5) != 0 + } + + /// Generic packet continuous transmit enable. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// const PKT_CTRL: PktCtrl = PktCtrl::RESET.set_cont_tx_en(true); + /// ``` + #[must_use = "set_cont_tx_en returns a modified PktCtrl"] + pub const fn set_cont_tx_en(mut self, en: bool) -> PktCtrl { + if en { + self.val |= 1 << 4; + } else { + self.val &= !(1 << 4); + } + self + } + + /// Returns `true` if generic packet continuous transmit is enabled. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// let pc: PktCtrl = PktCtrl::RESET; + /// assert_eq!(pc.cont_tx_en(), false); + /// let pc: PktCtrl = pc.set_cont_tx_en(true); + /// assert_eq!(pc.cont_tx_en(), true); + /// let pc: PktCtrl = pc.set_cont_tx_en(false); + /// assert_eq!(pc.cont_tx_en(), false); + /// ``` + pub const fn cont_tx_en(&self) -> bool { + self.val & (1 << 4) != 0 + } + + /// Set the continuous sequence type. + #[must_use = "set_inf_seq_sel returns a modified PktCtrl"] + pub const fn set_inf_seq_sel(mut self, sel: InfSeqSel) -> PktCtrl { + self.val &= !(0b11 << 2); + self.val |= (sel as u8) << 2; + self + } + + /// Get the continuous sequence type. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{InfSeqSel, PktCtrl}; + /// + /// let pc: PktCtrl = PktCtrl::RESET; + /// assert_eq!(pc.inf_seq_sel(), InfSeqSel::Five); + /// + /// let pc: PktCtrl = pc.set_inf_seq_sel(InfSeqSel::Zero); + /// assert_eq!(pc.inf_seq_sel(), InfSeqSel::Zero); + /// + /// let pc: PktCtrl = pc.set_inf_seq_sel(InfSeqSel::One); + /// assert_eq!(pc.inf_seq_sel(), InfSeqSel::One); + /// + /// let pc: PktCtrl = pc.set_inf_seq_sel(InfSeqSel::Prbs9); + /// assert_eq!(pc.inf_seq_sel(), InfSeqSel::Prbs9); + /// + /// let pc: PktCtrl = pc.set_inf_seq_sel(InfSeqSel::Five); + /// assert_eq!(pc.inf_seq_sel(), InfSeqSel::Five); + /// ``` + pub const fn inf_seq_sel(&self) -> InfSeqSel { + match (self.val >> 2) & 0b11 { + 0b00 => InfSeqSel::Five, + 0b01 => InfSeqSel::Zero, + 0b10 => InfSeqSel::One, + _ => InfSeqSel::Prbs9, + } + } + + /// Enable infinute sequence generation. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// const PKT_CTRL: PktCtrl = PktCtrl::RESET.set_inf_seq_en(true); + /// ``` + #[must_use = "set_inf_seq_en returns a modified PktCtrl"] + pub const fn set_inf_seq_en(mut self, en: bool) -> PktCtrl { + if en { + self.val |= 1 << 1; + } else { + self.val &= !(1 << 1); + } + self + } + + /// Returns `true` if infinute sequence generation is enabled. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// let pc: PktCtrl = PktCtrl::RESET; + /// assert_eq!(pc.inf_seq_en(), false); + /// let pc: PktCtrl = pc.set_inf_seq_en(true); + /// assert_eq!(pc.inf_seq_en(), true); + /// let pc: PktCtrl = pc.set_inf_seq_en(false); + /// assert_eq!(pc.inf_seq_en(), false); + /// ``` + pub const fn inf_seq_en(&self) -> bool { + self.val & (1 << 1) != 0 + } + + /// Set the value of bit-8 (9th bit) for generic packet whitening. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// const PKT_CTRL: PktCtrl = PktCtrl::RESET.set_whitening_init(true); + /// ``` + #[must_use = "set_whitening_init returns a modified PktCtrl"] + pub const fn set_whitening_init(mut self, val: bool) -> PktCtrl { + if val { + self.val |= 1; + } else { + self.val &= !1; + } + self + } + + /// Returns `true` if bit-8 of the generic packet whitening is set. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PktCtrl; + /// + /// let pc: PktCtrl = PktCtrl::RESET; + /// assert_eq!(pc.whitening_init(), true); + /// let pc: PktCtrl = pc.set_whitening_init(false); + /// assert_eq!(pc.whitening_init(), false); + /// let pc: PktCtrl = pc.set_whitening_init(true); + /// assert_eq!(pc.whitening_init(), true); + /// ``` + pub const fn whitening_init(&self) -> bool { + self.val & 0b1 != 0 + } +} + +impl From for u8 { + fn from(pc: PktCtrl) -> Self { + pc.val + } +} + +impl Default for PktCtrl { + fn default() -> Self { + Self::RESET + } +} diff --git a/embassy-stm32/src/subghz/pmode.rs b/embassy-stm32/src/subghz/pmode.rs new file mode 100644 index 00000000..990be2fc --- /dev/null +++ b/embassy-stm32/src/subghz/pmode.rs @@ -0,0 +1,27 @@ +/// RX gain power modes. +/// +/// Argument of [`set_rx_gain`]. +/// +/// [`set_rx_gain`]: crate::subghz::SubGhz::set_rx_gain +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PMode { + /// Power saving mode. + /// + /// Reduces sensitivity. + #[allow(clippy::identity_op)] + PowerSaving = (0x25 << 2) | 0b00, + /// Boost mode level 1. + /// + /// Improves sensitivity at detriment of power consumption. + Boost1 = (0x25 << 2) | 0b01, + /// Boost mode level 2. + /// + /// Improves a set further sensitivity at detriment of power consumption. + Boost2 = (0x25 << 2) | 0b10, + /// Boost mode. + /// + /// Best receiver sensitivity. + Boost = (0x25 << 2) | 0b11, +} diff --git a/embassy-stm32/src/subghz/pwr_ctrl.rs b/embassy-stm32/src/subghz/pwr_ctrl.rs new file mode 100644 index 00000000..d0de06f1 --- /dev/null +++ b/embassy-stm32/src/subghz/pwr_ctrl.rs @@ -0,0 +1,160 @@ +/// Power-supply current limit. +/// +/// Argument of [`PwrCtrl::set_current_lim`]. +#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum CurrentLim { + /// 25 mA + Milli25 = 0x0, + /// 50 mA (default) + Milli50 = 0x1, + /// 100 mA + Milli100 = 0x2, + /// 200 mA + Milli200 = 0x3, +} + +impl CurrentLim { + /// Get the SMPS drive value as milliamps. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::CurrentLim; + /// + /// assert_eq!(CurrentLim::Milli25.as_milliamps(), 25); + /// assert_eq!(CurrentLim::Milli50.as_milliamps(), 50); + /// assert_eq!(CurrentLim::Milli100.as_milliamps(), 100); + /// assert_eq!(CurrentLim::Milli200.as_milliamps(), 200); + /// ``` + pub const fn as_milliamps(&self) -> u8 { + match self { + CurrentLim::Milli25 => 25, + CurrentLim::Milli50 => 50, + CurrentLim::Milli100 => 100, + CurrentLim::Milli200 => 200, + } + } +} + +impl Default for CurrentLim { + fn default() -> Self { + CurrentLim::Milli50 + } +} + +/// Power control. +/// +/// Argument of [`set_bit_sync`](crate::subghz::SubGhz::set_bit_sync). +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PwrCtrl { + val: u8, +} + +impl PwrCtrl { + /// Power control register reset value. + pub const RESET: PwrCtrl = PwrCtrl { val: 0x50 }; + + /// Create a new [`PwrCtrl`] structure from a raw value. + /// + /// Reserved bits will be masked. + pub const fn from_raw(raw: u8) -> Self { + Self { val: raw & 0x70 } + } + + /// Get the raw value of the [`PwrCtrl`] register. + pub const fn as_bits(&self) -> u8 { + self.val + } + + /// Set the current limiter enable. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PwrCtrl; + /// + /// const PWR_CTRL: PwrCtrl = PwrCtrl::RESET.set_current_lim_en(true); + /// # assert_eq!(u8::from(PWR_CTRL), 0x50u8); + /// ``` + #[must_use = "set_current_lim_en returns a modified PwrCtrl"] + pub const fn set_current_lim_en(mut self, en: bool) -> PwrCtrl { + if en { + self.val |= 1 << 6; + } else { + self.val &= !(1 << 6); + } + self + } + + /// Returns `true` if current limiting is enabled + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::PwrCtrl; + /// + /// let pc: PwrCtrl = PwrCtrl::RESET; + /// assert_eq!(pc.current_limit_en(), true); + /// let pc: PwrCtrl = pc.set_current_lim_en(false); + /// assert_eq!(pc.current_limit_en(), false); + /// let pc: PwrCtrl = pc.set_current_lim_en(true); + /// assert_eq!(pc.current_limit_en(), true); + /// ``` + pub const fn current_limit_en(&self) -> bool { + self.val & (1 << 6) != 0 + } + + /// Set the current limit. + #[must_use = "set_current_lim returns a modified PwrCtrl"] + pub const fn set_current_lim(mut self, lim: CurrentLim) -> PwrCtrl { + self.val &= !(0x30); + self.val |= (lim as u8) << 4; + self + } + + /// Get the current limit. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CurrentLim, PwrCtrl}; + /// + /// let pc: PwrCtrl = PwrCtrl::RESET; + /// assert_eq!(pc.current_lim(), CurrentLim::Milli50); + /// + /// let pc: PwrCtrl = pc.set_current_lim(CurrentLim::Milli25); + /// assert_eq!(pc.current_lim(), CurrentLim::Milli25); + /// + /// let pc: PwrCtrl = pc.set_current_lim(CurrentLim::Milli50); + /// assert_eq!(pc.current_lim(), CurrentLim::Milli50); + /// + /// let pc: PwrCtrl = pc.set_current_lim(CurrentLim::Milli100); + /// assert_eq!(pc.current_lim(), CurrentLim::Milli100); + /// + /// let pc: PwrCtrl = pc.set_current_lim(CurrentLim::Milli200); + /// assert_eq!(pc.current_lim(), CurrentLim::Milli200); + /// ``` + pub const fn current_lim(&self) -> CurrentLim { + match (self.val >> 4) & 0b11 { + 0x0 => CurrentLim::Milli25, + 0x1 => CurrentLim::Milli50, + 0x2 => CurrentLim::Milli100, + _ => CurrentLim::Milli200, + } + } +} + +impl From for u8 { + fn from(bs: PwrCtrl) -> Self { + bs.val + } +} + +impl Default for PwrCtrl { + fn default() -> Self { + Self::RESET + } +} diff --git a/embassy-stm32/src/subghz/reg_mode.rs b/embassy-stm32/src/subghz/reg_mode.rs new file mode 100644 index 00000000..b8322695 --- /dev/null +++ b/embassy-stm32/src/subghz/reg_mode.rs @@ -0,0 +1,18 @@ +/// Radio power supply selection. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum RegMode { + /// Linear dropout regulator + Ldo = 0b0, + /// Switch mode power supply. + /// + /// Used in standby with HSE32, FS, RX, and TX modes. + Smps = 0b1, +} + +impl Default for RegMode { + fn default() -> Self { + RegMode::Ldo + } +} diff --git a/embassy-stm32/src/subghz/rf_frequency.rs b/embassy-stm32/src/subghz/rf_frequency.rs new file mode 100644 index 00000000..7face3d0 --- /dev/null +++ b/embassy-stm32/src/subghz/rf_frequency.rs @@ -0,0 +1,138 @@ +/// RF frequency structure. +/// +/// Argument of [`set_rf_frequency`]. +/// +/// [`set_rf_frequency`]: crate::subghz::SubGhz::set_rf_frequency +#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct RfFreq { + buf: [u8; 5], +} + +impl RfFreq { + /// 915MHz, often used in Australia and North America. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// assert_eq!(RfFreq::F915.freq(), 915_000_000); + /// ``` + pub const F915: RfFreq = RfFreq::from_raw(0x39_30_00_00); + + /// 868MHz, often used in Europe. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// assert_eq!(RfFreq::F868.freq(), 868_000_000); + /// ``` + pub const F868: RfFreq = RfFreq::from_raw(0x36_40_00_00); + + /// 433MHz, often used in Europe. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// assert_eq!(RfFreq::F433.freq(), 433_000_000); + /// ``` + pub const F433: RfFreq = RfFreq::from_raw(0x1B_10_00_00); + + /// Create a new `RfFreq` from a raw bit value. + /// + /// The equation used to get the PLL frequency from the raw bits is: + /// + /// RFPLL = 32e6 × bits / 225 + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// const FREQ: RfFreq = RfFreq::from_raw(0x39300000); + /// assert_eq!(FREQ, RfFreq::F915); + /// ``` + pub const fn from_raw(bits: u32) -> RfFreq { + RfFreq { + buf: [ + super::OpCode::SetRfFrequency as u8, + ((bits >> 24) & 0xFF) as u8, + ((bits >> 16) & 0xFF) as u8, + ((bits >> 8) & 0xFF) as u8, + (bits & 0xFF) as u8, + ], + } + } + + /// Create a new `RfFreq` from a PLL frequency. + /// + /// The equation used to get the raw bits from the PLL frequency is: + /// + /// bits = RFPLL * 225 / 32e6 + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// const FREQ: RfFreq = RfFreq::from_frequency(915_000_000); + /// assert_eq!(FREQ, RfFreq::F915); + /// ``` + pub const fn from_frequency(freq: u32) -> RfFreq { + Self::from_raw((((freq as u64) * (1 << 25)) / 32_000_000) as u32) + } + + // Get the frequency bit value. + const fn as_bits(&self) -> u32 { + ((self.buf[1] as u32) << 24) + | ((self.buf[2] as u32) << 16) + | ((self.buf[3] as u32) << 8) + | (self.buf[4] as u32) + } + + /// Get the actual frequency. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// assert_eq!(RfFreq::from_raw(0x39300000).freq(), 915_000_000); + /// ``` + pub fn freq(&self) -> u32 { + (32_000_000 * (self.as_bits() as u64) / (1 << 25)) as u32 + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::RfFreq; + /// + /// assert_eq!(RfFreq::F915.as_slice(), &[0x86, 0x39, 0x30, 0x00, 0x00]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +#[cfg(test)] +mod test { + use super::RfFreq; + + #[test] + fn max() { + assert_eq!(RfFreq::from_raw(u32::MAX).freq(), 4_095_999_999); + } + + #[test] + fn min() { + assert_eq!(RfFreq::from_raw(u32::MIN).freq(), 0); + } +} diff --git a/embassy-stm32/src/subghz/rx_timeout_stop.rs b/embassy-stm32/src/subghz/rx_timeout_stop.rs new file mode 100644 index 00000000..f057d357 --- /dev/null +++ b/embassy-stm32/src/subghz/rx_timeout_stop.rs @@ -0,0 +1,21 @@ +/// Receiver event which stops the RX timeout timer. +/// +/// Used by [`set_rx_timeout_stop`]. +/// +/// [`set_rx_timeout_stop`]: crate::subghz::SubGhz::set_rx_timeout_stop +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum RxTimeoutStop { + /// Receive timeout stopped on synchronization word detection in generic + /// packet mode or header detection in LoRa packet mode. + Sync = 0b0, + /// Receive timeout stopped on preamble detection. + Preamble = 0b1, +} + +impl From for u8 { + fn from(rx_ts: RxTimeoutStop) -> Self { + rx_ts as u8 + } +} diff --git a/embassy-stm32/src/subghz/sleep_cfg.rs b/embassy-stm32/src/subghz/sleep_cfg.rs new file mode 100644 index 00000000..1aaa2c94 --- /dev/null +++ b/embassy-stm32/src/subghz/sleep_cfg.rs @@ -0,0 +1,109 @@ +/// Startup configurations when exiting sleep mode. +/// +/// Argument of [`SleepCfg::set_startup`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum Startup { + /// Cold startup when exiting Sleep mode, configuration registers reset. + Cold = 0, + /// Warm startup when exiting Sleep mode, + /// configuration registers kept in retention. + /// + /// **Note:** Only the configuration of the activated modem, + /// before going to sleep mode, is retained. + /// The configuration of the other modes is lost and must be re-configured + /// when exiting sleep mode. + Warm = 1, +} + +impl Default for Startup { + fn default() -> Self { + Startup::Warm + } +} + +/// Sleep configuration. +/// +/// Argument of [`set_sleep`]. +/// +/// [`set_sleep`]: crate::subghz::SubGhz::set_sleep +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SleepCfg(u8); + +impl SleepCfg { + /// Create a new `SleepCfg` structure. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// The defaults are a warm startup, with RTC wakeup enabled. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::SleepCfg; + /// + /// const SLEEP_CFG: SleepCfg = SleepCfg::new(); + /// assert_eq!(SLEEP_CFG, SleepCfg::default()); + /// # assert_eq!(u8::from(SLEEP_CFG), 0b101); + /// ``` + pub const fn new() -> SleepCfg { + SleepCfg(0) + .set_startup(Startup::Warm) + .set_rtc_wakeup_en(true) + } + + /// Set the startup mode. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{SleepCfg, Startup}; + /// + /// const SLEEP_CFG: SleepCfg = SleepCfg::new().set_startup(Startup::Cold); + /// # assert_eq!(u8::from(SLEEP_CFG), 0b001); + /// # assert_eq!(u8::from(SLEEP_CFG.set_startup(Startup::Warm)), 0b101); + /// ``` + pub const fn set_startup(mut self, startup: Startup) -> SleepCfg { + if startup as u8 == 1 { + self.0 |= 1 << 2 + } else { + self.0 &= !(1 << 2) + } + self + } + + /// Set the RTC wakeup enable. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::SleepCfg; + /// + /// const SLEEP_CFG: SleepCfg = SleepCfg::new().set_rtc_wakeup_en(false); + /// # assert_eq!(u8::from(SLEEP_CFG), 0b100); + /// # assert_eq!(u8::from(SLEEP_CFG.set_rtc_wakeup_en(true)), 0b101); + /// ``` + #[must_use = "set_rtc_wakeup_en returns a modified SleepCfg"] + pub const fn set_rtc_wakeup_en(mut self, en: bool) -> SleepCfg { + if en { + self.0 |= 0b1 + } else { + self.0 &= !0b1 + } + self + } +} + +impl From for u8 { + fn from(sc: SleepCfg) -> Self { + sc.0 + } +} + +impl Default for SleepCfg { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32/src/subghz/smps.rs b/embassy-stm32/src/subghz/smps.rs new file mode 100644 index 00000000..59947f2a --- /dev/null +++ b/embassy-stm32/src/subghz/smps.rs @@ -0,0 +1,45 @@ +/// SMPS maximum drive capability. +/// +/// Argument of [`set_smps_drv`](crate::subghz::SubGhz::set_smps_drv). +#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum SmpsDrv { + /// 20 mA + Milli20 = 0x0, + /// 40 mA + Milli40 = 0x1, + /// 60 mA + Milli60 = 0x2, + /// 100 mA (default) + Milli100 = 0x3, +} + +impl SmpsDrv { + /// Get the SMPS drive value as milliamps. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::SmpsDrv; + /// + /// assert_eq!(SmpsDrv::Milli20.as_milliamps(), 20); + /// assert_eq!(SmpsDrv::Milli40.as_milliamps(), 40); + /// assert_eq!(SmpsDrv::Milli60.as_milliamps(), 60); + /// assert_eq!(SmpsDrv::Milli100.as_milliamps(), 100); + /// ``` + pub const fn as_milliamps(&self) -> u8 { + match self { + SmpsDrv::Milli20 => 20, + SmpsDrv::Milli40 => 40, + SmpsDrv::Milli60 => 60, + SmpsDrv::Milli100 => 100, + } + } +} + +impl Default for SmpsDrv { + fn default() -> Self { + SmpsDrv::Milli100 + } +} diff --git a/embassy-stm32/src/subghz/standby_clk.rs b/embassy-stm32/src/subghz/standby_clk.rs new file mode 100644 index 00000000..2e6a0330 --- /dev/null +++ b/embassy-stm32/src/subghz/standby_clk.rs @@ -0,0 +1,20 @@ +/// Clock in standby mode. +/// +/// Used by [`set_standby`]. +/// +/// [`set_standby`]: crate::subghz::SubGhz::set_standby +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum StandbyClk { + /// RC 13 MHz used in standby mode. + Rc = 0b0, + /// HSE32 used in standby mode. + Hse = 0b1, +} + +impl From for u8 { + fn from(sc: StandbyClk) -> Self { + sc as u8 + } +} diff --git a/embassy-stm32/src/subghz/stats.rs b/embassy-stm32/src/subghz/stats.rs new file mode 100644 index 00000000..52a2252f --- /dev/null +++ b/embassy-stm32/src/subghz/stats.rs @@ -0,0 +1,184 @@ +use crate::subghz::status::Status; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct LoRaStats; + +impl LoRaStats { + pub const fn new() -> Self { + Self {} + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct FskStats; + +impl FskStats { + pub const fn new() -> Self { + Self {} + } +} + +/// Packet statistics. +/// +/// Returned by [`fsk_stats`] and [`lora_stats`]. +/// +/// [`fsk_stats`]: crate::subghz::SubGhz::fsk_stats +/// [`lora_stats`]: crate::subghz::SubGhz::lora_stats +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Stats { + status: Status, + pkt_rx: u16, + pkt_crc: u16, + pkt_len_or_hdr_err: u16, + ty: ModType, +} + +impl Stats { + const fn from_buf(buf: [u8; 7], ty: ModType) -> Stats { + Stats { + status: Status::from_raw(buf[0]), + pkt_rx: u16::from_be_bytes([buf[1], buf[2]]), + pkt_crc: u16::from_be_bytes([buf[3], buf[4]]), + pkt_len_or_hdr_err: u16::from_be_bytes([buf[5], buf[6]]), + ty, + } + } + + /// Get the radio status returned with the packet statistics. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CmdStatus, FskStats, Stats, StatusMode}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 0, 0, 0, 0, 0]; + /// let stats: Stats = Stats::from_raw_fsk(example_data_from_radio); + /// assert_eq!(stats.status().mode(), Ok(StatusMode::Rx)); + /// assert_eq!(stats.status().cmd(), Ok(CmdStatus::Avaliable)); + /// ``` + pub const fn status(&self) -> Status { + self.status + } + + /// Number of packets received. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskStats, Stats}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 3, 0, 0, 0, 0]; + /// let stats: Stats = Stats::from_raw_fsk(example_data_from_radio); + /// assert_eq!(stats.pkt_rx(), 3); + /// ``` + pub const fn pkt_rx(&self) -> u16 { + self.pkt_rx + } + + /// Number of packets received with a payload CRC error + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{LoRaStats, Stats}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 0, 0, 1, 0, 0]; + /// let stats: Stats = Stats::from_raw_lora(example_data_from_radio); + /// assert_eq!(stats.pkt_crc(), 1); + /// ``` + pub const fn pkt_crc(&self) -> u16 { + self.pkt_crc + } +} + +impl Stats { + /// Create a new FSK packet statistics structure from a raw buffer. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskStats, Stats}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 0, 0, 0, 0, 0]; + /// let stats: Stats = Stats::from_raw_fsk(example_data_from_radio); + /// ``` + pub const fn from_raw_fsk(buf: [u8; 7]) -> Stats { + Self::from_buf(buf, FskStats::new()) + } + + /// Number of packets received with a payload length error. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{FskStats, Stats}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 0, 0, 0, 0, 1]; + /// let stats: Stats = Stats::from_raw_fsk(example_data_from_radio); + /// assert_eq!(stats.pkt_len_err(), 1); + /// ``` + pub const fn pkt_len_err(&self) -> u16 { + self.pkt_len_or_hdr_err + } +} + +impl Stats { + /// Create a new LoRa packet statistics structure from a raw buffer. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{LoRaStats, Stats}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 0, 0, 0, 0, 0]; + /// let stats: Stats = Stats::from_raw_lora(example_data_from_radio); + /// ``` + pub const fn from_raw_lora(buf: [u8; 7]) -> Stats { + Self::from_buf(buf, LoRaStats::new()) + } + + /// Number of packets received with a header CRC error. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{LoRaStats, Stats}; + /// + /// let example_data_from_radio: [u8; 7] = [0x54, 0, 0, 0, 0, 0, 1]; + /// let stats: Stats = Stats::from_raw_lora(example_data_from_radio); + /// assert_eq!(stats.pkt_hdr_err(), 1); + /// ``` + pub const fn pkt_hdr_err(&self) -> u16 { + self.pkt_len_or_hdr_err + } +} + +impl core::fmt::Display for Stats { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Stats") + .field("status", &self.status()) + .field("pkt_rx", &self.pkt_rx()) + .field("pkt_crc", &self.pkt_crc()) + .field("pkt_len_err", &self.pkt_len_err()) + .finish() + } +} + +#[cfg(test)] +mod test { + use crate::subghz::{CmdStatus, LoRaStats, Stats, StatusMode}; + + #[test] + fn mixed() { + let example_data_from_radio: [u8; 7] = [0x54, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]; + let stats: Stats = Stats::from_raw_lora(example_data_from_radio); + assert_eq!(stats.status().mode(), Ok(StatusMode::Rx)); + assert_eq!(stats.status().cmd(), Ok(CmdStatus::Avaliable)); + assert_eq!(stats.pkt_rx(), 0x0102); + assert_eq!(stats.pkt_crc(), 0x0304); + assert_eq!(stats.pkt_hdr_err(), 0x0506); + } +} diff --git a/embassy-stm32/src/subghz/status.rs b/embassy-stm32/src/subghz/status.rs new file mode 100644 index 00000000..0b8e6da7 --- /dev/null +++ b/embassy-stm32/src/subghz/status.rs @@ -0,0 +1,202 @@ +/// sub-GHz radio operating mode. +/// +/// See `Get_Status` under section 5.8.5 "Communcation status information commands" +/// in the reference manual. +/// +/// This is returned by [`Status::mode`]. +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum StatusMode { + /// Standby mode with RC 13MHz. + StandbyRc = 0x2, + /// Standby mode with HSE32. + StandbyHse = 0x3, + /// Frequency Synthesis mode. + Fs = 0x4, + /// Receive mode. + Rx = 0x5, + /// Transmit mode. + Tx = 0x6, +} + +impl StatusMode { + /// Create a new `StatusMode` from bits. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::StatusMode; + /// + /// assert_eq!(StatusMode::from_raw(0x2), Ok(StatusMode::StandbyRc)); + /// assert_eq!(StatusMode::from_raw(0x3), Ok(StatusMode::StandbyHse)); + /// assert_eq!(StatusMode::from_raw(0x4), Ok(StatusMode::Fs)); + /// assert_eq!(StatusMode::from_raw(0x5), Ok(StatusMode::Rx)); + /// assert_eq!(StatusMode::from_raw(0x6), Ok(StatusMode::Tx)); + /// // Other values are reserved + /// assert_eq!(StatusMode::from_raw(0), Err(0)); + /// ``` + pub const fn from_raw(bits: u8) -> Result { + match bits { + 0x2 => Ok(StatusMode::StandbyRc), + 0x3 => Ok(StatusMode::StandbyHse), + 0x4 => Ok(StatusMode::Fs), + 0x5 => Ok(StatusMode::Rx), + 0x6 => Ok(StatusMode::Tx), + _ => Err(bits), + } + } +} + +/// Command status. +/// +/// See `Get_Status` under section 5.8.5 "Communcation status information commands" +/// in the reference manual. +/// +/// This is returned by [`Status::cmd`]. +#[repr(u8)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CmdStatus { + /// Data available to host. + /// + /// Packet received successfully and data can be retrieved. + Avaliable = 0x2, + /// Command time out. + /// + /// Command took too long to complete triggering a sub-GHz radio watchdog + /// timeout. + Timeout = 0x3, + /// Command processing error. + /// + /// Invalid opcode or incorrect number of parameters. + ProcessingError = 0x4, + /// Command execution failure. + /// + /// Command successfully received but cannot be executed at this time, + /// requested operating mode cannot be entered or requested data cannot be + /// sent. + ExecutionFailure = 0x5, + /// Transmit command completed. + /// + /// Current packet transmission completed. + Complete = 0x6, +} + +impl CmdStatus { + /// Create a new `CmdStatus` from bits. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::CmdStatus; + /// + /// assert_eq!(CmdStatus::from_raw(0x2), Ok(CmdStatus::Avaliable)); + /// assert_eq!(CmdStatus::from_raw(0x3), Ok(CmdStatus::Timeout)); + /// assert_eq!(CmdStatus::from_raw(0x4), Ok(CmdStatus::ProcessingError)); + /// assert_eq!(CmdStatus::from_raw(0x5), Ok(CmdStatus::ExecutionFailure)); + /// assert_eq!(CmdStatus::from_raw(0x6), Ok(CmdStatus::Complete)); + /// // Other values are reserved + /// assert_eq!(CmdStatus::from_raw(0), Err(0)); + /// ``` + pub const fn from_raw(bits: u8) -> Result { + match bits { + 0x2 => Ok(CmdStatus::Avaliable), + 0x3 => Ok(CmdStatus::Timeout), + 0x4 => Ok(CmdStatus::ProcessingError), + 0x5 => Ok(CmdStatus::ExecutionFailure), + 0x6 => Ok(CmdStatus::Complete), + _ => Err(bits), + } + } +} + +/// Radio status. +/// +/// This is returned by [`status`]. +/// +/// [`status`]: crate::subghz::SubGhz::status +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Status(u8); + +impl From for Status { + fn from(x: u8) -> Self { + Status(x) + } +} +impl From for u8 { + fn from(x: Status) -> Self { + x.0 + } +} + +impl Status { + /// Create a new `Status` from a raw `u8` value. + /// + /// This is the same as `Status::from(u8)`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CmdStatus, Status, StatusMode}; + /// + /// const STATUS: Status = Status::from_raw(0x54_u8); + /// assert_eq!(STATUS.mode(), Ok(StatusMode::Rx)); + /// assert_eq!(STATUS.cmd(), Ok(CmdStatus::Avaliable)); + /// ``` + pub const fn from_raw(value: u8) -> Status { + Status(value) + } + + /// sub-GHz radio operating mode. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{Status, StatusMode}; + /// + /// let status: Status = 0xACu8.into(); + /// assert_eq!(status.mode(), Ok(StatusMode::StandbyRc)); + /// ``` + pub const fn mode(&self) -> Result { + StatusMode::from_raw((self.0 >> 4) & 0b111) + } + + /// Command status. + /// + /// For some reason `Err(1)` is a pretty common return value for this, + /// despite being a reserved value. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{CmdStatus, Status}; + /// + /// let status: Status = 0xACu8.into(); + /// assert_eq!(status.cmd(), Ok(CmdStatus::Complete)); + /// ``` + pub const fn cmd(&self) -> Result { + CmdStatus::from_raw((self.0 >> 1) & 0b111) + } +} + +impl core::fmt::Display for Status { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Status") + .field("mode", &self.mode()) + .field("cmd", &self.cmd()) + .finish() + } +} + +#[cfg(feature = "defmt")] +impl defmt::Format for Status { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!( + fmt, + "Status {{ mode: {}, cmd: {} }}", + self.mode(), + self.cmd() + ) + } +} diff --git a/embassy-stm32/src/subghz/tcxo_mode.rs b/embassy-stm32/src/subghz/tcxo_mode.rs new file mode 100644 index 00000000..a80c493f --- /dev/null +++ b/embassy-stm32/src/subghz/tcxo_mode.rs @@ -0,0 +1,170 @@ +use crate::subghz::timeout::Timeout; + +/// TCXO trim. +/// +/// **Note:** To use VDDTCXO, the VDDRF supply must be at +/// least + 200 mV higher than the selected `TcxoTrim` voltage level. +/// +/// Used by [`TcxoMode`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum TcxoTrim { + /// 1.6V + Volts1pt6 = 0x0, + /// 1.7V + Volts1pt7 = 0x1, + /// 1.8V + Volts1pt8 = 0x2, + /// 2.2V + Volts2pt2 = 0x3, + /// 2.4V + Volts2pt4 = 0x4, + /// 2.7V + Volts2pt7 = 0x5, + /// 3.0V + Volts3pt0 = 0x6, + /// 3.3V + Volts3pt3 = 0x7, +} + +impl core::fmt::Display for TcxoTrim { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + TcxoTrim::Volts1pt6 => write!(f, "1.6V"), + TcxoTrim::Volts1pt7 => write!(f, "1.7V"), + TcxoTrim::Volts1pt8 => write!(f, "1.8V"), + TcxoTrim::Volts2pt2 => write!(f, "2.2V"), + TcxoTrim::Volts2pt4 => write!(f, "2.4V"), + TcxoTrim::Volts2pt7 => write!(f, "2.7V"), + TcxoTrim::Volts3pt0 => write!(f, "3.0V"), + TcxoTrim::Volts3pt3 => write!(f, "3.3V"), + } + } +} + +impl TcxoTrim { + /// Get the value of the TXCO trim in millivolts. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::TcxoTrim; + /// + /// assert_eq!(TcxoTrim::Volts1pt6.as_millivolts(), 1600); + /// assert_eq!(TcxoTrim::Volts1pt7.as_millivolts(), 1700); + /// assert_eq!(TcxoTrim::Volts1pt8.as_millivolts(), 1800); + /// assert_eq!(TcxoTrim::Volts2pt2.as_millivolts(), 2200); + /// assert_eq!(TcxoTrim::Volts2pt4.as_millivolts(), 2400); + /// assert_eq!(TcxoTrim::Volts2pt7.as_millivolts(), 2700); + /// assert_eq!(TcxoTrim::Volts3pt0.as_millivolts(), 3000); + /// assert_eq!(TcxoTrim::Volts3pt3.as_millivolts(), 3300); + /// ``` + pub const fn as_millivolts(&self) -> u16 { + match self { + TcxoTrim::Volts1pt6 => 1600, + TcxoTrim::Volts1pt7 => 1700, + TcxoTrim::Volts1pt8 => 1800, + TcxoTrim::Volts2pt2 => 2200, + TcxoTrim::Volts2pt4 => 2400, + TcxoTrim::Volts2pt7 => 2700, + TcxoTrim::Volts3pt0 => 3000, + TcxoTrim::Volts3pt3 => 3300, + } + } +} + +/// TCXO trim and HSE32 ready timeout. +/// +/// Argument of [`set_tcxo_mode`]. +/// +/// [`set_tcxo_mode`]: crate::subghz::SubGhz::set_tcxo_mode +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TcxoMode { + buf: [u8; 5], +} + +impl TcxoMode { + /// Create a new `TcxoMode` struct. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::TcxoMode; + /// + /// const TCXO_MODE: TcxoMode = TcxoMode::new(); + /// ``` + pub const fn new() -> TcxoMode { + TcxoMode { + buf: [super::OpCode::SetTcxoMode as u8, 0x00, 0x00, 0x00, 0x00], + } + } + + /// Set the TCXO trim. + /// + /// **Note:** To use VDDTCXO, the VDDRF supply must be + /// at least + 200 mV higher than the selected `TcxoTrim` voltage level. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{TcxoMode, TcxoTrim}; + /// + /// const TCXO_MODE: TcxoMode = TcxoMode::new().set_txco_trim(TcxoTrim::Volts1pt6); + /// # assert_eq!(TCXO_MODE.as_slice()[1], 0x00); + /// ``` + #[must_use = "set_txco_trim returns a modified TcxoMode"] + pub const fn set_txco_trim(mut self, tcxo_trim: TcxoTrim) -> TcxoMode { + self.buf[1] = tcxo_trim as u8; + self + } + + /// Set the ready timeout duration. + /// + /// # Example + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::{TcxoMode, Timeout}; + /// + /// // 15.625 ms timeout + /// const TIMEOUT: Timeout = Timeout::from_duration_sat(Duration::from_millis(15_625)); + /// const TCXO_MODE: TcxoMode = TcxoMode::new().set_timeout(TIMEOUT); + /// # assert_eq!(TCXO_MODE.as_slice()[2], 0x0F); + /// # assert_eq!(TCXO_MODE.as_slice()[3], 0x42); + /// # assert_eq!(TCXO_MODE.as_slice()[4], 0x40); + /// ``` + #[must_use = "set_timeout returns a modified TcxoMode"] + pub const fn set_timeout(mut self, timeout: Timeout) -> TcxoMode { + let timeout_bits: u32 = timeout.into_bits(); + self.buf[2] = ((timeout_bits >> 16) & 0xFF) as u8; + self.buf[3] = ((timeout_bits >> 8) & 0xFF) as u8; + self.buf[4] = (timeout_bits & 0xFF) as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{TcxoMode, TcxoTrim, Timeout}; + /// + /// const TCXO_MODE: TcxoMode = TcxoMode::new() + /// .set_txco_trim(TcxoTrim::Volts1pt7) + /// .set_timeout(Timeout::from_raw(0x123456)); + /// assert_eq!(TCXO_MODE.as_slice(), &[0x97, 0x1, 0x12, 0x34, 0x56]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for TcxoMode { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32/src/subghz/timeout.rs b/embassy-stm32/src/subghz/timeout.rs new file mode 100644 index 00000000..580d0a64 --- /dev/null +++ b/embassy-stm32/src/subghz/timeout.rs @@ -0,0 +1,469 @@ +use core::time::Duration; + +use crate::subghz::value_error::ValueError; + +const fn abs_diff(a: u64, b: u64) -> u64 { + if a > b { + a - b + } else { + b - a + } +} + +/// Timeout argument. +/// +/// This is used by: +/// * [`set_rx`] +/// * [`set_tx`] +/// * [`TcxoMode`] +/// +/// Each timeout has 3 bytes, with a resolution of 15.625µs per bit, giving a +/// range of 0s to 262.143984375s. +/// +/// [`set_rx`]: crate::subghz::SubGhz::set_rx +/// [`set_tx`]: crate::subghz::SubGhz::set_tx +/// [`TcxoMode`]: crate::subghz::TcxoMode +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Timeout { + bits: u32, +} + +impl Timeout { + const BITS_PER_MILLI: u32 = 64; // 1e-3 / 15.625e-6 + const BITS_PER_SEC: u32 = 64_000; // 1 / 15.625e-6 + + /// Disable the timeout (0s timeout). + /// + /// # Example + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::Timeout; + /// + /// const TIMEOUT: Timeout = Timeout::DISABLED; + /// assert_eq!(TIMEOUT.as_duration(), Duration::from_secs(0)); + /// ``` + pub const DISABLED: Timeout = Timeout { bits: 0x0 }; + + /// Minimum timeout, 15.625µs. + /// + /// # Example + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::Timeout; + /// + /// const TIMEOUT: Timeout = Timeout::MIN; + /// assert_eq!(TIMEOUT.into_bits(), 1); + /// ``` + pub const MIN: Timeout = Timeout { bits: 1 }; + + /// Maximum timeout, 262.143984375s. + /// + /// # Example + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::Timeout; + /// + /// const TIMEOUT: Timeout = Timeout::MAX; + /// assert_eq!(TIMEOUT.as_duration(), Duration::from_nanos(262_143_984_375)); + /// ``` + pub const MAX: Timeout = Timeout { bits: 0x00FF_FFFF }; + + /// Timeout resolution in nanoseconds, 15.625µs. + pub const RESOLUTION_NANOS: u16 = 15_625; + + /// Timeout resolution, 15.625µs. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!( + /// Timeout::RESOLUTION.as_nanos(), + /// Timeout::RESOLUTION_NANOS as u128 + /// ); + /// ``` + pub const RESOLUTION: Duration = Duration::from_nanos(Self::RESOLUTION_NANOS as u64); + + /// Create a new timeout from a [`Duration`]. + /// + /// This will return the nearest timeout value possible, or a + /// [`ValueError`] if the value is out of bounds. + /// + /// Use [`from_millis_sat`](Self::from_millis_sat) for runtime timeout + /// construction. + /// This is not _that_ useful right now, it is simply future proofing for a + /// time when `Result::unwrap` is avaliable for `const fn`. + /// + /// # Example + /// + /// Value within bounds: + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::{Timeout, ValueError}; + /// + /// const MIN: Duration = Timeout::RESOLUTION; + /// assert_eq!(Timeout::from_duration(MIN).unwrap(), Timeout::MIN); + /// ``` + /// + /// Value too low: + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::{Timeout, ValueError}; + /// + /// const LOWER_LIMIT_NANOS: u128 = 7813; + /// const TOO_LOW_NANOS: u128 = LOWER_LIMIT_NANOS - 1; + /// const TOO_LOW_DURATION: Duration = Duration::from_nanos(TOO_LOW_NANOS as u64); + /// assert_eq!( + /// Timeout::from_duration(TOO_LOW_DURATION), + /// Err(ValueError::too_low(TOO_LOW_NANOS, LOWER_LIMIT_NANOS)) + /// ); + /// ``` + /// + /// Value too high: + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::{Timeout, ValueError}; + /// + /// const UPPER_LIMIT_NANOS: u128 = Timeout::MAX.as_nanos() as u128 + 7812; + /// const TOO_HIGH_NANOS: u128 = UPPER_LIMIT_NANOS + 1; + /// const TOO_HIGH_DURATION: Duration = Duration::from_nanos(TOO_HIGH_NANOS as u64); + /// assert_eq!( + /// Timeout::from_duration(TOO_HIGH_DURATION), + /// Err(ValueError::too_high(TOO_HIGH_NANOS, UPPER_LIMIT_NANOS)) + /// ); + /// ``` + pub const fn from_duration(duration: Duration) -> Result> { + // at the time of development many methods in + // `core::Duration` were not `const fn`, which leads to the hacks + // you see here. + let nanos: u128 = duration.as_nanos(); + const UPPER_LIMIT: u128 = + Timeout::MAX.as_nanos() as u128 + (Timeout::RESOLUTION_NANOS as u128) / 2; + const LOWER_LIMIT: u128 = (((Timeout::RESOLUTION_NANOS as u128) + 1) / 2) as u128; + + if nanos > UPPER_LIMIT { + Err(ValueError::too_high(nanos, UPPER_LIMIT)) + } else if nanos < LOWER_LIMIT { + Err(ValueError::too_low(nanos, LOWER_LIMIT)) + } else { + // safe to truncate here because of previous bounds check. + let duration_nanos: u64 = nanos as u64; + + let div_floor: u64 = duration_nanos / (Self::RESOLUTION_NANOS as u64); + let div_ceil: u64 = 1 + (duration_nanos - 1) / (Self::RESOLUTION_NANOS as u64); + + let timeout_ceil: Timeout = Timeout::from_raw(div_ceil as u32); + let timeout_floor: Timeout = Timeout::from_raw(div_floor as u32); + + let error_ceil: u64 = abs_diff(timeout_ceil.as_nanos(), duration_nanos); + let error_floor: u64 = abs_diff(timeout_floor.as_nanos(), duration_nanos); + + if error_ceil < error_floor { + Ok(timeout_ceil) + } else { + Ok(timeout_floor) + } + } + } + + /// Create a new timeout from a [`Duration`]. + /// + /// This will return the nearest timeout value possible, saturating at the + /// limits. + /// + /// This is an expensive function to call outside of `const` contexts. + /// Use [`from_millis_sat`](Self::from_millis_sat) for runtime timeout + /// construction. + /// + /// # Example + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::Timeout; + /// + /// const DURATION_MAX_NS: u64 = 262_143_984_376; + /// + /// assert_eq!( + /// Timeout::from_duration_sat(Duration::from_millis(0)), + /// Timeout::MIN + /// ); + /// assert_eq!( + /// Timeout::from_duration_sat(Duration::from_nanos(DURATION_MAX_NS)), + /// Timeout::MAX + /// ); + /// assert_eq!( + /// Timeout::from_duration_sat(Timeout::RESOLUTION).into_bits(), + /// 1 + /// ); + /// ``` + pub const fn from_duration_sat(duration: Duration) -> Timeout { + // at the time of development many methods in + // `core::Duration` were not `const fn`, which leads to the hacks + // you see here. + let nanos: u128 = duration.as_nanos(); + const UPPER_LIMIT: u128 = Timeout::MAX.as_nanos() as u128; + + if nanos > UPPER_LIMIT { + Timeout::MAX + } else if nanos < (Timeout::RESOLUTION_NANOS as u128) { + Timeout::from_raw(1) + } else { + // safe to truncate here because of previous bounds check. + let duration_nanos: u64 = duration.as_nanos() as u64; + + let div_floor: u64 = duration_nanos / (Self::RESOLUTION_NANOS as u64); + let div_ceil: u64 = 1 + (duration_nanos - 1) / (Self::RESOLUTION_NANOS as u64); + + let timeout_ceil: Timeout = Timeout::from_raw(div_ceil as u32); + let timeout_floor: Timeout = Timeout::from_raw(div_floor as u32); + + let error_ceil: u64 = abs_diff(timeout_ceil.as_nanos(), duration_nanos); + let error_floor: u64 = abs_diff(timeout_floor.as_nanos(), duration_nanos); + + if error_ceil < error_floor { + timeout_ceil + } else { + timeout_floor + } + } + } + + /// Create a new timeout from a milliseconds value. + /// + /// This will round towards zero and saturate at the limits. + /// + /// This is the preferred method to call when you need to generate a + /// timeout value at runtime. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::from_millis_sat(0), Timeout::MIN); + /// assert_eq!(Timeout::from_millis_sat(262_144), Timeout::MAX); + /// assert_eq!(Timeout::from_millis_sat(1).into_bits(), 64); + /// ``` + pub const fn from_millis_sat(millis: u32) -> Timeout { + if millis == 0 { + Timeout::MIN + } else if millis >= 262_144 { + Timeout::MAX + } else { + Timeout::from_raw(millis * Self::BITS_PER_MILLI) + } + } + + /// Create a timeout from raw bits, where each bit has the resolution of + /// [`Timeout::RESOLUTION`]. + /// + /// **Note:** Only the first 24 bits of the `u32` are used, the `bits` + /// argument will be masked. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::from_raw(u32::MAX), Timeout::MAX); + /// assert_eq!(Timeout::from_raw(0x00_FF_FF_FF), Timeout::MAX); + /// assert_eq!(Timeout::from_raw(1).as_duration(), Timeout::RESOLUTION); + /// assert_eq!(Timeout::from_raw(0), Timeout::DISABLED); + /// ``` + pub const fn from_raw(bits: u32) -> Timeout { + Timeout { + bits: bits & 0x00FF_FFFF, + } + } + + /// Get the timeout as nanoseconds. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::MAX.as_nanos(), 262_143_984_375); + /// assert_eq!(Timeout::DISABLED.as_nanos(), 0); + /// assert_eq!(Timeout::from_raw(1).as_nanos(), 15_625); + /// assert_eq!(Timeout::from_raw(64_000).as_nanos(), 1_000_000_000); + /// ``` + pub const fn as_nanos(&self) -> u64 { + (self.bits as u64) * (Timeout::RESOLUTION_NANOS as u64) + } + + /// Get the timeout as microseconds, rounding towards zero. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::MAX.as_micros(), 262_143_984); + /// assert_eq!(Timeout::DISABLED.as_micros(), 0); + /// assert_eq!(Timeout::from_raw(1).as_micros(), 15); + /// assert_eq!(Timeout::from_raw(64_000).as_micros(), 1_000_000); + /// ``` + pub const fn as_micros(&self) -> u32 { + (self.as_nanos() / 1_000) as u32 + } + + /// Get the timeout as milliseconds, rounding towards zero. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::MAX.as_millis(), 262_143); + /// assert_eq!(Timeout::DISABLED.as_millis(), 0); + /// assert_eq!(Timeout::from_raw(1).as_millis(), 0); + /// assert_eq!(Timeout::from_raw(64_000).as_millis(), 1_000); + /// ``` + pub const fn as_millis(&self) -> u32 { + self.into_bits() / Self::BITS_PER_MILLI + } + + /// Get the timeout as seconds, rounding towards zero. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::MAX.as_secs(), 262); + /// assert_eq!(Timeout::DISABLED.as_secs(), 0); + /// assert_eq!(Timeout::from_raw(1).as_secs(), 0); + /// assert_eq!(Timeout::from_raw(64_000).as_secs(), 1); + /// ``` + pub const fn as_secs(&self) -> u16 { + (self.into_bits() / Self::BITS_PER_SEC) as u16 + } + + /// Get the timeout as a [`Duration`]. + /// + /// # Example + /// + /// ``` + /// use core::time::Duration; + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!( + /// Timeout::MAX.as_duration(), + /// Duration::from_nanos(262_143_984_375) + /// ); + /// assert_eq!(Timeout::DISABLED.as_duration(), Duration::from_nanos(0)); + /// assert_eq!(Timeout::from_raw(1).as_duration(), Timeout::RESOLUTION); + /// ``` + pub const fn as_duration(&self) -> Duration { + Duration::from_nanos((self.bits as u64) * (Timeout::RESOLUTION_NANOS as u64)) + } + + /// Get the bit value for the timeout. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::from_raw(u32::MAX).into_bits(), 0x00FF_FFFF); + /// assert_eq!(Timeout::from_raw(1).into_bits(), 1); + /// ``` + pub const fn into_bits(self) -> u32 { + self.bits + } + + /// Get the byte value for the timeout. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::Timeout; + /// + /// assert_eq!(Timeout::from_raw(u32::MAX).as_bytes(), [0xFF, 0xFF, 0xFF]); + /// assert_eq!(Timeout::from_raw(1).as_bytes(), [0, 0, 1]); + /// ``` + pub const fn as_bytes(self) -> [u8; 3] { + [ + ((self.bits >> 16) & 0xFF) as u8, + ((self.bits >> 8) & 0xFF) as u8, + (self.bits & 0xFF) as u8, + ] + } +} + +impl From for Duration { + fn from(to: Timeout) -> Self { + to.as_duration() + } +} + +impl From for [u8; 3] { + fn from(to: Timeout) -> Self { + to.as_bytes() + } +} + +impl From for embassy::time::Duration { + fn from(to: Timeout) -> Self { + embassy::time::Duration::from_micros(to.as_micros().into()) + } +} + +#[cfg(test)] +mod tests { + use super::{Timeout, ValueError}; + use core::time::Duration; + + #[test] + fn saturate() { + assert_eq!( + Timeout::from_duration_sat(Duration::from_secs(u64::MAX)), + Timeout::MAX + ); + } + + #[test] + fn rounding() { + const NANO1: Duration = Duration::from_nanos(1); + let res_sub_1_ns: Duration = Timeout::RESOLUTION - NANO1; + let res_add_1_ns: Duration = Timeout::RESOLUTION + NANO1; + assert_eq!(Timeout::from_duration_sat(res_sub_1_ns).into_bits(), 1); + assert_eq!(Timeout::from_duration_sat(res_add_1_ns).into_bits(), 1); + } + + #[test] + fn lower_limit() { + let low: Duration = (Timeout::RESOLUTION + Duration::from_nanos(1)) / 2; + assert_eq!(Timeout::from_duration(low), Ok(Timeout::from_raw(1))); + + let too_low: Duration = low - Duration::from_nanos(1); + assert_eq!( + Timeout::from_duration(too_low), + Err(ValueError::too_low(too_low.as_nanos(), low.as_nanos())) + ); + } + + #[test] + fn upper_limit() { + let high: Duration = Timeout::MAX.as_duration() + Timeout::RESOLUTION / 2; + assert_eq!( + Timeout::from_duration(high), + Ok(Timeout::from_raw(0xFFFFFF)) + ); + + let too_high: Duration = high + Duration::from_nanos(1); + assert_eq!( + Timeout::from_duration(too_high), + Err(ValueError::too_high(too_high.as_nanos(), high.as_nanos())) + ); + } +} diff --git a/embassy-stm32/src/subghz/tx_params.rs b/embassy-stm32/src/subghz/tx_params.rs new file mode 100644 index 00000000..17f052bc --- /dev/null +++ b/embassy-stm32/src/subghz/tx_params.rs @@ -0,0 +1,166 @@ +/// Power amplifier ramp time for FSK, MSK, and LoRa modulation. +/// +/// Argument of [`set_ramp_time`][`crate::subghz::TxParams::set_ramp_time`]. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum RampTime { + /// 10µs + Micros10 = 0x00, + /// 20µs + Micros20 = 0x01, + /// 40µs + Micros40 = 0x02, + /// 80µs + Micros80 = 0x03, + /// 200µs + Micros200 = 0x04, + /// 800µs + Micros800 = 0x05, + /// 1.7ms + Micros1700 = 0x06, + /// 3.4ms + Micros3400 = 0x07, +} + +impl From for u8 { + fn from(rt: RampTime) -> Self { + rt as u8 + } +} + +impl From for core::time::Duration { + fn from(rt: RampTime) -> Self { + match rt { + RampTime::Micros10 => core::time::Duration::from_micros(10), + RampTime::Micros20 => core::time::Duration::from_micros(20), + RampTime::Micros40 => core::time::Duration::from_micros(40), + RampTime::Micros80 => core::time::Duration::from_micros(80), + RampTime::Micros200 => core::time::Duration::from_micros(200), + RampTime::Micros800 => core::time::Duration::from_micros(800), + RampTime::Micros1700 => core::time::Duration::from_micros(1700), + RampTime::Micros3400 => core::time::Duration::from_micros(3400), + } + } +} + +impl From for embassy::time::Duration { + fn from(rt: RampTime) -> Self { + match rt { + RampTime::Micros10 => embassy::time::Duration::from_micros(10), + RampTime::Micros20 => embassy::time::Duration::from_micros(20), + RampTime::Micros40 => embassy::time::Duration::from_micros(40), + RampTime::Micros80 => embassy::time::Duration::from_micros(80), + RampTime::Micros200 => embassy::time::Duration::from_micros(200), + RampTime::Micros800 => embassy::time::Duration::from_micros(800), + RampTime::Micros1700 => embassy::time::Duration::from_micros(1700), + RampTime::Micros3400 => embassy::time::Duration::from_micros(3400), + } + } +} +/// Transmit parameters, output power and power amplifier ramp up time. +/// +/// Argument of [`set_tx_params`][`crate::subghz::SubGhz::set_tx_params`]. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct TxParams { + buf: [u8; 3], +} + +impl TxParams { + /// Create a new `TxParams` struct. + /// + /// This is the same as `default`, but in a `const` function. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::TxParams; + /// + /// const TX_PARAMS: TxParams = TxParams::new(); + /// assert_eq!(TX_PARAMS, TxParams::default()); + /// ``` + pub const fn new() -> TxParams { + TxParams { + buf: [super::OpCode::SetTxParams as u8, 0x00, 0x00], + } + } + + /// Set the output power. + /// + /// For low power selected in [`set_pa_config`]: + /// + /// * 0x0E: +14 dB + /// * ... + /// * 0x00: 0 dB + /// * ... + /// * 0xEF: -17 dB + /// * Others: reserved + /// + /// For high power selected in [`set_pa_config`]: + /// + /// * 0x16: +22 dB + /// * ... + /// * 0x00: 0 dB + /// * ... + /// * 0xF7: -9 dB + /// * Others: reserved + /// + /// # Example + /// + /// Set the output power to 0 dB. + /// + /// ``` + /// use stm32wl_hal::subghz::{RampTime, TxParams}; + /// + /// const TX_PARAMS: TxParams = TxParams::new().set_power(0x00); + /// # assert_eq!(TX_PARAMS.as_slice()[1], 0x00); + /// ``` + /// + /// [`set_pa_config`]: crate::subghz::SubGhz::set_pa_config + #[must_use = "set_power returns a modified TxParams"] + pub const fn set_power(mut self, power: u8) -> TxParams { + self.buf[1] = power; + self + } + + /// Set the Power amplifier ramp time for FSK, MSK, and LoRa modulation. + /// + /// # Example + /// + /// Set the ramp time to 200 microseconds. + /// + /// ``` + /// use stm32wl_hal::subghz::{RampTime, TxParams}; + /// + /// const TX_PARAMS: TxParams = TxParams::new().set_ramp_time(RampTime::Micros200); + /// # assert_eq!(TX_PARAMS.as_slice()[2], 0x04); + /// ``` + #[must_use = "set_ramp_time returns a modified TxParams"] + pub const fn set_ramp_time(mut self, rt: RampTime) -> TxParams { + self.buf[2] = rt as u8; + self + } + + /// Extracts a slice containing the packet. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::{RampTime, TxParams}; + /// + /// const TX_PARAMS: TxParams = TxParams::new() + /// .set_ramp_time(RampTime::Micros80) + /// .set_power(0x0E); + /// assert_eq!(TX_PARAMS.as_slice(), &[0x8E, 0x0E, 0x03]); + /// ``` + pub const fn as_slice(&self) -> &[u8] { + &self.buf + } +} + +impl Default for TxParams { + fn default() -> Self { + Self::new() + } +} diff --git a/embassy-stm32/src/subghz/value_error.rs b/embassy-stm32/src/subghz/value_error.rs new file mode 100644 index 00000000..0c470cfa --- /dev/null +++ b/embassy-stm32/src/subghz/value_error.rs @@ -0,0 +1,129 @@ +/// Error for a value that is out-of-bounds. +/// +/// Used by [`Timeout::from_duration`]. +/// +/// [`Timeout::from_duration`]: crate::subghz::Timeout::from_duration +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ValueError { + value: T, + limit: T, + over: bool, +} + +impl ValueError { + /// Create a new `ValueError` for a value that exceeded an upper bound. + /// + /// Unfortunately panic is not avaliable in `const fn`, so there are no + /// guarantees on the value being greater than the limit. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::ValueError; + /// + /// const ERROR: ValueError = ValueError::too_high(101u8, 100u8); + /// assert!(ERROR.over()); + /// assert!(!ERROR.under()); + /// ``` + pub const fn too_high(value: T, limit: T) -> ValueError { + ValueError { + value, + limit, + over: true, + } + } + + /// Create a new `ValueError` for a value that exceeded a lower bound. + /// + /// Unfortunately panic is not avaliable in `const fn`, so there are no + /// guarantees on the value being less than the limit. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::ValueError; + /// + /// const ERROR: ValueError = ValueError::too_low(200u8, 201u8); + /// assert!(ERROR.under()); + /// assert!(!ERROR.over()); + /// ``` + pub const fn too_low(value: T, limit: T) -> ValueError { + ValueError { + value, + limit, + over: false, + } + } + + /// Get the value that caused the error. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::ValueError; + /// + /// const ERROR: ValueError = ValueError::too_high(101u8, 100u8); + /// assert_eq!(ERROR.value(), &101u8); + /// ``` + pub const fn value(&self) -> &T { + &self.value + } + + /// Get the limit for the value. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::ValueError; + /// + /// const ERROR: ValueError = ValueError::too_high(101u8, 100u8); + /// assert_eq!(ERROR.limit(), &100u8); + /// ``` + pub const fn limit(&self) -> &T { + &self.limit + } + + /// Returns `true` if the value was over the limit. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::ValueError; + /// + /// const ERROR: ValueError = ValueError::too_high(101u8, 100u8); + /// assert!(ERROR.over()); + /// assert!(!ERROR.under()); + /// ``` + pub const fn over(&self) -> bool { + self.over + } + + /// Returns `true` if the value was under the limit. + /// + /// # Example + /// + /// ``` + /// use stm32wl_hal::subghz::ValueError; + /// + /// const ERROR: ValueError = ValueError::too_low(200u8, 201u8); + /// assert!(ERROR.under()); + /// assert!(!ERROR.over()); + /// ``` + pub const fn under(&self) -> bool { + !self.over + } +} + +impl core::fmt::Display for ValueError +where + T: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if self.over { + write!(f, "Value is too high {} > {}", self.value, self.limit) + } else { + write!(f, "Value is too low {} < {}", self.value, self.limit) + } + } +} diff --git a/examples/stm32wl55/Cargo.toml b/examples/stm32wl55/Cargo.toml index a7313e33..1bdfe9bc 100644 --- a/examples/stm32wl55/Cargo.toml +++ b/examples/stm32wl55/Cargo.toml @@ -19,7 +19,7 @@ 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"] } +embassy-stm32 = { version = "0.1.0", path = "../../embassy-stm32", features = ["defmt", "defmt-trace", "stm32wl55jc_cm4", "time-driver-tim2", "memory-x", "subghz"] } embassy-hal-common = {version = "0.1.0", path = "../../embassy-hal-common" } defmt = "0.2.0" diff --git a/examples/stm32wl55/src/bin/subghz.rs b/examples/stm32wl55/src/bin/subghz.rs new file mode 100644 index 00000000..1e406886 --- /dev/null +++ b/examples/stm32wl55/src/bin/subghz.rs @@ -0,0 +1,129 @@ +#![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::{traits::gpio::WaitForRisingEdge, util::InterruptFuture}; +use embassy_stm32::{ + dbgmcu::Dbgmcu, + dma::NoDma, + exti::ExtiInput, + gpio::{Input, Level, Output, Pull, Speed}, + interrupt, + subghz::*, + Peripherals, +}; +use embedded_hal::digital::v2::OutputPin; +use example_common::unwrap; + +const PING_DATA: &str = "PING"; +const DATA_LEN: u8 = PING_DATA.len() as u8; +const PING_DATA_BYTES: &[u8] = PING_DATA.as_bytes(); +const PREAMBLE_LEN: u16 = 5 * 8; + +const RF_FREQ: RfFreq = RfFreq::from_frequency(867_500_000); + +const SYNC_WORD: [u8; 8] = [0x79, 0x80, 0x0C, 0xC0, 0x29, 0x95, 0xF8, 0x4A]; +const SYNC_WORD_LEN: u8 = SYNC_WORD.len() as u8; +const SYNC_WORD_LEN_BITS: u8 = SYNC_WORD_LEN * 8; + +const TX_BUF_OFFSET: u8 = 128; +const RX_BUF_OFFSET: u8 = 0; +const LORA_PACKET_PARAMS: LoRaPacketParams = LoRaPacketParams::new() + .set_crc_en(true) + .set_preamble_len(PREAMBLE_LEN) + .set_payload_len(DATA_LEN) + .set_invert_iq(false) + .set_header_type(HeaderType::Fixed); + +const LORA_MOD_PARAMS: LoRaModParams = LoRaModParams::new() + .set_bw(LoRaBandwidth::Bw125) + .set_cr(CodingRate::Cr45) + .set_ldro_en(true) + .set_sf(SpreadingFactor::Sf7); + +// configuration for +10 dBm output power +// see table 35 "PA optimal setting and operating modes" +const PA_CONFIG: PaConfig = PaConfig::new() + .set_pa_duty_cycle(0x1) + .set_hp_max(0x0) + .set_pa(PaSel::Lp); + +const TCXO_MODE: TcxoMode = TcxoMode::new() + .set_txco_trim(TcxoTrim::Volts1pt7) + .set_timeout(Timeout::from_duration_sat( + core::time::Duration::from_millis(10), + )); + +const TX_PARAMS: TxParams = TxParams::new() + .set_power(0x0D) + .set_ramp_time(RampTime::Micros40); + +fn config() -> embassy_stm32::Config { + let mut config = embassy_stm32::Config::default(); + config.rcc = config.rcc.clock_src(embassy_stm32::rcc::ClockSrc::HSE32); + config +} + +#[embassy::main(config = "config()")] +async fn main(_spawner: embassy::executor::Spawner, p: Peripherals) { + unsafe { + Dbgmcu::enable_all(); + } + + let mut led1 = Output::new(p.PB15, Level::High, Speed::Low); + let mut led2 = Output::new(p.PB9, Level::Low, Speed::Low); + let mut led3 = Output::new(p.PB11, Level::Low, Speed::Low); + + let button = Input::new(p.PA0, Pull::Up); + let mut pin = ExtiInput::new(button, p.EXTI0); + + let mut radio_irq = interrupt::take!(SUBGHZ_RADIO); + let mut radio = SubGhz::new(p.SUBGHZSPI, p.PA5, p.PA7, p.PA6, NoDma, NoDma); + + defmt::info!("Radio ready for use"); + + unwrap!(led1.set_low()); + + unwrap!(led2.set_high()); + + unwrap!(radio.set_standby(StandbyClk::Rc)); + unwrap!(radio.set_tcxo_mode(&TCXO_MODE)); + unwrap!(radio.set_standby(StandbyClk::Hse)); + unwrap!(radio.set_regulator_mode(RegMode::Ldo)); + unwrap!(radio.set_buffer_base_address(TX_BUF_OFFSET, RX_BUF_OFFSET)); + unwrap!(radio.set_pa_config(&PA_CONFIG)); + unwrap!(radio.set_pa_ocp(Ocp::Max60m)); + unwrap!(radio.set_tx_params(&TX_PARAMS)); + unwrap!(radio.set_packet_type(PacketType::LoRa)); + unwrap!(radio.set_lora_sync_word(LoRaSyncWord::Public)); + unwrap!(radio.set_lora_mod_params(&LORA_MOD_PARAMS)); + unwrap!(radio.set_lora_packet_params(&LORA_PACKET_PARAMS)); + unwrap!(radio.calibrate_image(CalibrateImage::ISM_863_870)); + unwrap!(radio.set_rf_frequency(&RF_FREQ)); + + defmt::info!("Status: {:?}", unwrap!(radio.status())); + + unwrap!(led2.set_low()); + + loop { + pin.wait_for_rising_edge().await; + unwrap!(led3.set_high()); + unwrap!(radio.set_irq_cfg(&CfgIrq::new().irq_enable_all(Irq::TxDone))); + unwrap!(radio.write_buffer(TX_BUF_OFFSET, PING_DATA_BYTES)); + unwrap!(radio.set_tx(Timeout::DISABLED)); + + InterruptFuture::new(&mut radio_irq).await; + let (_, irq_status) = unwrap!(radio.irq_status()); + if irq_status & Irq::TxDone.mask() != 0 { + defmt::info!("TX done"); + } + unwrap!(radio.clear_irq_status(irq_status)); + unwrap!(led3.set_low()); + } +} diff --git a/stm32-data b/stm32-data index bf509120..3fb217ad 160000 --- a/stm32-data +++ b/stm32-data @@ -1 +1 @@ -Subproject commit bf50912000cd6c24ef5cb8cc7a0372a116457124 +Subproject commit 3fb217ad3eebe2d8808b8af4d04ce051c69ecb72