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