diff --git a/embassy-stm32/Cargo.toml b/embassy-stm32/Cargo.toml
index 135f9a918..264005850 100644
--- a/embassy-stm32/Cargo.toml
+++ b/embassy-stm32/Cargo.toml
@@ -27,6 +27,7 @@ atomic-polyfill = "0.1.5"
 stm32-metapac = { version = "0.1.0", path = "../stm32-metapac", features = ["rt"] }
 vcell = { version = "0.1.3", optional = true }
 bxcan = "0.6.2"
+nb = "1.0.0"
 
 seq-macro = "0.2.2"
 
diff --git a/embassy-stm32/src/usart/mod.rs b/embassy-stm32/src/usart/mod.rs
index b51a728c0..7e4df619c 100644
--- a/embassy-stm32/src/usart/mod.rs
+++ b/embassy-stm32/src/usart/mod.rs
@@ -192,8 +192,35 @@ impl<'d, T: Instance, TxDma, RxDma> Uart<'d, T, TxDma, RxDma> {
     }
 }
 
-impl<'d, T: Instance, RxDma> embedded_hal::blocking::serial::Write<u8>
-    for Uart<'d, T, NoDma, RxDma>
+impl<'d, T: Instance, TxDma, RxDma> embedded_hal::serial::Read<u8> for Uart<'d, T, TxDma, RxDma> {
+    type Error = Error;
+    fn read(&mut self) -> Result<u8, nb::Error<Self::Error>> {
+        let r = self.inner.regs();
+        unsafe {
+            let sr = sr(r).read();
+            if sr.pe() {
+                rdr(r).read_volatile();
+                Err(nb::Error::Other(Error::Parity))
+            } else if sr.fe() {
+                rdr(r).read_volatile();
+                Err(nb::Error::Other(Error::Framing))
+            } else if sr.ne() {
+                rdr(r).read_volatile();
+                Err(nb::Error::Other(Error::Noise))
+            } else if sr.ore() {
+                rdr(r).read_volatile();
+                Err(nb::Error::Other(Error::Overrun))
+            } else if sr.rxne() {
+                Ok(rdr(r).read_volatile())
+            } else {
+                Err(nb::Error::WouldBlock)
+            }
+        }
+    }
+}
+
+impl<'d, T: Instance, TxDma, RxDma> embedded_hal::blocking::serial::Write<u8>
+    for Uart<'d, T, TxDma, RxDma>
 {
     type Error = Error;
     fn bwrite_all(&mut self, buffer: &[u8]) -> Result<(), Self::Error> {
diff --git a/embassy-traits/Cargo.toml b/embassy-traits/Cargo.toml
index c98b583e5..a93cb3c54 100644
--- a/embassy-traits/Cargo.toml
+++ b/embassy-traits/Cargo.toml
@@ -10,3 +10,4 @@ std = []
 [dependencies]
 defmt = { version = "0.3", optional = true }
 embedded-hal = { version = "0.2.6", features = ["unproven"] }
+nb = "1.0.0"
diff --git a/embassy-traits/src/adapter.rs b/embassy-traits/src/adapter.rs
new file mode 100644
index 000000000..ce7dd411f
--- /dev/null
+++ b/embassy-traits/src/adapter.rs
@@ -0,0 +1,166 @@
+use core::future::Future;
+use embedded_hal::blocking;
+use embedded_hal::serial;
+
+/// BlockingAsync is a wrapper that implements async traits using blocking peripherals. This allows
+/// driver writers to depend on the async traits while still supporting embedded-hal peripheral implementations.
+///
+/// BlockingAsync will implement any async trait that maps to embedded-hal traits implemented for the wrapped driver.
+///
+/// Driver users are then free to choose which implementation that is available to them.
+pub struct BlockingAsync<T> {
+    wrapped: T,
+}
+
+impl<T> BlockingAsync<T> {
+    /// Create a new instance of a wrapper for a given peripheral.
+    pub fn new(wrapped: T) -> Self {
+        Self { wrapped }
+    }
+}
+
+//
+// I2C implementatinos
+//
+
+impl<T, E> crate::i2c::I2c for BlockingAsync<T>
+where
+    E: 'static,
+    T: blocking::i2c::WriteRead<Error = E>
+        + blocking::i2c::Read<Error = E>
+        + blocking::i2c::Write<Error = E>,
+{
+    type Error = E;
+
+    #[rustfmt::skip]
+    type WriteFuture<'a> where Self: 'a = impl Future<Output = Result<(), Self::Error>> + 'a;
+    #[rustfmt::skip]
+    type ReadFuture<'a> where Self: 'a = impl Future<Output = Result<(), Self::Error>> + 'a;
+    #[rustfmt::skip]
+    type WriteReadFuture<'a> where Self: 'a = impl Future<Output = Result<(), Self::Error>> + 'a;
+
+    fn read<'a>(&'a mut self, address: u8, buffer: &'a mut [u8]) -> Self::ReadFuture<'a> {
+        async move { self.wrapped.read(address, buffer) }
+    }
+
+    fn write<'a>(&'a mut self, address: u8, bytes: &'a [u8]) -> Self::WriteFuture<'a> {
+        async move { self.wrapped.write(address, bytes) }
+    }
+
+    fn write_read<'a>(
+        &'a mut self,
+        address: u8,
+        bytes: &'a [u8],
+        buffer: &'a mut [u8],
+    ) -> Self::WriteReadFuture<'a> {
+        async move { self.wrapped.write_read(address, bytes, buffer) }
+    }
+}
+
+//
+// SPI implementatinos
+//
+
+impl<T, E, Word> crate::spi::Spi<Word> for BlockingAsync<T>
+where
+    T: blocking::spi::Write<Word, Error = E>,
+{
+    type Error = E;
+}
+
+impl<T, E, Word> crate::spi::FullDuplex<Word> for BlockingAsync<T>
+where
+    E: 'static,
+    Word: Clone,
+    T: blocking::spi::Transfer<Word, Error = E> + blocking::spi::Write<Word, Error = E>,
+{
+    #[rustfmt::skip]
+    type WriteReadFuture<'a> where Word: 'a, Self: 'a = impl Future<Output = Result<(), Self::Error>> + 'a;
+
+    fn read_write<'a>(
+        &'a mut self,
+        read: &'a mut [Word],
+        write: &'a [Word],
+    ) -> Self::WriteReadFuture<'a> {
+        async move {
+            // Ensure we write the expected bytes
+            for i in 0..core::cmp::min(read.len(), write.len()) {
+                read[i] = write[i].clone();
+            }
+            self.wrapped.transfer(read)?;
+            Ok(())
+        }
+    }
+}
+
+impl<T, E, Word> crate::spi::Write<Word> for BlockingAsync<T>
+where
+    E: 'static,
+    Word: Clone,
+    T: blocking::spi::Write<Word, Error = E>,
+{
+    #[rustfmt::skip]
+    type WriteFuture<'a> where Word: 'a, Self: 'a = impl Future<Output = Result<(), Self::Error>> + 'a;
+
+    fn write<'a>(&'a mut self, data: &'a [Word]) -> Self::WriteFuture<'a> {
+        async move { self.wrapped.write(data) }
+    }
+}
+
+impl<T, E, Word> crate::spi::Read<Word> for BlockingAsync<T>
+where
+    E: 'static,
+    Word: Clone,
+    T: blocking::spi::Transfer<Word, Error = E> + blocking::spi::Write<Word, Error = E>,
+{
+    #[rustfmt::skip]
+    type ReadFuture<'a> where Word: 'a, Self: 'a = impl Future<Output = Result<(), Self::Error>> + 'a;
+
+    fn read<'a>(&'a mut self, data: &'a mut [Word]) -> Self::ReadFuture<'a> {
+        async move {
+            self.wrapped.transfer(data)?;
+            Ok(())
+        }
+    }
+}
+
+// Uart implementatinos
+impl<T> crate::uart::Read for BlockingAsync<T>
+where
+    T: serial::Read<u8>,
+{
+    #[rustfmt::skip]
+    type ReadFuture<'a> where T: 'a = impl Future<Output = Result<(), crate::uart::Error>> + 'a;
+    fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> Self::ReadFuture<'a> {
+        async move {
+            let mut pos = 0;
+            while pos < buf.len() {
+                match self.wrapped.read() {
+                    Err(nb::Error::WouldBlock) => {}
+                    Err(_) => return Err(crate::uart::Error::Other),
+                    Ok(b) => {
+                        buf[pos] = b;
+                        pos += 1;
+                    }
+                }
+            }
+            Ok(())
+        }
+    }
+}
+
+impl<T> crate::uart::Write for BlockingAsync<T>
+where
+    T: blocking::serial::Write<u8>,
+{
+    #[rustfmt::skip]
+    type WriteFuture<'a> where T: 'a = impl Future<Output = Result<(), crate::uart::Error>> + 'a;
+    fn write<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteFuture<'a> {
+        async move {
+            self.wrapped
+                .bwrite_all(buf)
+                .map_err(|_| crate::uart::Error::Other)?;
+            self.wrapped.bflush().map_err(|_| crate::uart::Error::Other)
+        }
+    }
+}
diff --git a/embassy-traits/src/lib.rs b/embassy-traits/src/lib.rs
index 65fb95bd4..a5342b77e 100644
--- a/embassy-traits/src/lib.rs
+++ b/embassy-traits/src/lib.rs
@@ -2,6 +2,7 @@
 #![feature(generic_associated_types)]
 #![feature(type_alias_impl_trait)]
 
+pub mod adapter;
 pub mod delay;
 pub mod flash;
 pub mod gpio;
diff --git a/examples/stm32l4/src/bin/i2c_blocking_async.rs b/examples/stm32l4/src/bin/i2c_blocking_async.rs
new file mode 100644
index 000000000..0339ed4d3
--- /dev/null
+++ b/examples/stm32l4/src/bin/i2c_blocking_async.rs
@@ -0,0 +1,29 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+#[path = "../example_common.rs"]
+mod example_common;
+
+use embassy::executor::Spawner;
+use embassy_stm32::dma::NoDma;
+use embassy_stm32::i2c::I2c;
+use embassy_stm32::interrupt;
+use embassy_stm32::time::Hertz;
+use embassy_stm32::Peripherals;
+use embassy_traits::{adapter::BlockingAsync, i2c::I2c as _};
+use example_common::{info, unwrap};
+
+const ADDRESS: u8 = 0x5F;
+const WHOAMI: u8 = 0x0F;
+
+#[embassy::main]
+async fn main(_spawner: Spawner, p: Peripherals) -> ! {
+    let irq = interrupt::take!(I2C2_EV);
+    let i2c = I2c::new(p.I2C2, p.PB10, p.PB11, irq, NoDma, NoDma, Hertz(100_000));
+    let mut i2c = BlockingAsync::new(i2c);
+
+    let mut data = [0u8; 1];
+    unwrap!(i2c.write_read(ADDRESS, &[WHOAMI], &mut data).await);
+    info!("Whoami: {}", data[0]);
+}
diff --git a/examples/stm32l4/src/bin/spi_blocking_async.rs b/examples/stm32l4/src/bin/spi_blocking_async.rs
new file mode 100644
index 000000000..f092706d4
--- /dev/null
+++ b/examples/stm32l4/src/bin/spi_blocking_async.rs
@@ -0,0 +1,57 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+#[path = "../example_common.rs"]
+mod example_common;
+
+use embassy::executor::Spawner;
+use embassy_stm32::dma::NoDma;
+use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
+use embassy_stm32::spi::{Config, Spi};
+use embassy_stm32::time::Hertz;
+use embassy_stm32::Peripherals;
+use embassy_traits::{adapter::BlockingAsync, spi::FullDuplex};
+use embedded_hal::digital::v2::{InputPin, OutputPin};
+use example_common::*;
+
+#[embassy::main]
+async fn main(_spawner: Spawner, p: Peripherals) {
+    info!("Hello World!");
+
+    let spi = Spi::new(
+        p.SPI3,
+        p.PC10,
+        p.PC12,
+        p.PC11,
+        NoDma,
+        NoDma,
+        Hertz(1_000_000),
+        Config::default(),
+    );
+
+    let mut spi = BlockingAsync::new(spi);
+
+    // These are the pins for the Inventek eS-Wifi SPI Wifi Adapter.
+
+    let _boot = Output::new(p.PB12, Level::Low, Speed::VeryHigh);
+    let _wake = Output::new(p.PB13, Level::Low, Speed::VeryHigh);
+    let mut reset = Output::new(p.PE8, Level::Low, Speed::VeryHigh);
+    let mut cs = Output::new(p.PE0, Level::High, Speed::VeryHigh);
+    let ready = Input::new(p.PE1, Pull::Up);
+
+    cortex_m::asm::delay(100_000);
+    unwrap!(reset.set_high());
+    cortex_m::asm::delay(100_000);
+
+    while unwrap!(ready.is_low()) {
+        info!("waiting for ready");
+    }
+
+    let write = [0x0A; 10];
+    let mut read = [0; 10];
+    unwrap!(cs.set_low());
+    spi.read_write(&mut read, &write).await.ok();
+    unwrap!(cs.set_high());
+    info!("xfer {=[u8]:x}", read);
+}
diff --git a/examples/stm32l4/src/bin/usart_blocking_async.rs b/examples/stm32l4/src/bin/usart_blocking_async.rs
new file mode 100644
index 000000000..679d4e0da
--- /dev/null
+++ b/examples/stm32l4/src/bin/usart_blocking_async.rs
@@ -0,0 +1,32 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+#[path = "../example_common.rs"]
+mod example_common;
+
+use embassy::executor::Spawner;
+use embassy::traits::{
+    adapter::BlockingAsync,
+    uart::{Read, Write},
+};
+use embassy_stm32::dma::NoDma;
+use embassy_stm32::usart::{Config, Uart};
+use embassy_stm32::Peripherals;
+use example_common::*;
+
+#[embassy::main]
+async fn main(_spawner: Spawner, p: Peripherals) {
+    let config = Config::default();
+    let usart = Uart::new(p.UART4, p.PA1, p.PA0, NoDma, NoDma, config);
+    let mut usart = BlockingAsync::new(usart);
+
+    unwrap!(usart.write(b"Hello Embassy World!\r\n").await);
+    info!("wrote Hello, starting echo");
+
+    let mut buf = [0u8; 1];
+    loop {
+        unwrap!(usart.read(&mut buf).await);
+        unwrap!(usart.write(&buf).await);
+    }
+}