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..ea20747e 100644 --- a/embassy-hal-common/src/lib.rs +++ b/embassy-hal-common/src/lib.rs @@ -6,6 +6,7 @@ pub(crate) mod fmt; pub mod interrupt; mod macros; pub mod peripheral; +pub mod ratio; pub mod ring_buffer; pub mod usb; 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/adc/g0.rs b/embassy-stm32/src/adc/g0.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/embassy-stm32/src/adc/g0.rs @@ -0,0 +1 @@ + diff --git a/embassy-stm32/src/adc/mod.rs b/embassy-stm32/src/adc/mod.rs index 1a32f0b9..9955502a 100644 --- a/embassy-stm32/src/adc/mod.rs +++ b/embassy-stm32/src/adc/mod.rs @@ -1,6 +1,7 @@ #![macro_use] #[cfg_attr(adc_v3, path = "v3.rs")] +#[cfg_attr(adc_g0, path = "g0.rs")] mod _version; #[allow(unused)] diff --git a/embassy-stm32/src/lib.rs b/embassy-stm32/src/lib.rs index a423e155..073e79f2 100644 --- a/embassy-stm32/src/lib.rs +++ b/embassy-stm32/src/lib.rs @@ -50,6 +50,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/g0.rs b/embassy-stm32/src/pwr/g0.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/embassy-stm32/src/pwr/g0.rs @@ -0,0 +1 @@ + diff --git a/embassy-stm32/src/pwr/mod.rs b/embassy-stm32/src/pwr/mod.rs index 1bb104bd..b19ab326 100644 --- a/embassy-stm32/src/pwr/mod.rs +++ b/embassy-stm32/src/pwr/mod.rs @@ -1,5 +1,7 @@ #[cfg_attr(any(pwr_h7, pwr_h7smps), path = "h7.rs")] #[cfg_attr(pwr_f4, path = "f4.rs")] +#[cfg_attr(pwr_wl5, path = "wl5.rs")] +#[cfg_attr(pwr_g0, path = "g0.rs")] mod _version; pub use _version::*; diff --git a/embassy-stm32/src/pwr/wl5.rs b/embassy-stm32/src/pwr/wl5.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/embassy-stm32/src/pwr/wl5.rs @@ -0,0 +1 @@ + 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..c37c0e8e --- /dev/null +++ b/embassy-stm32/src/subghz/mod.rs @@ -0,0 +1,1681 @@ +//! 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