diff --git a/.gitignore b/.gitignore
index f8d7c8b49..7f487cce0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
 **/*.rs.bk
 .#*
-/target/
+target
 Cargo.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad4fb44ed..8ac881cb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Added
 - `Error` traits for Can, SPI, I2C and Serial are implemented for Infallible
 
+### Changed
+- `spi`: traits now enforce all impls on the same struct (eg `Transfer` and `Write`) have the same `Error` type. 
+- `spi/blocking`: unified traits into `Read`, `Write`, `ReadWrite`.
+- `spi/blocking`: renamed Transactional `exec` to `batch`.
+- `spi/blocking`: Added `read_batch`, `write_batch` methods.
+
 ## [v1.0.0-alpha.6] - 2021-11-19
 
 *** This is (also) an alpha release with breaking changes (sorry) ***
diff --git a/embedded-hal-async/Cargo.toml b/embedded-hal-async/Cargo.toml
new file mode 100644
index 000000000..6226f7e50
--- /dev/null
+++ b/embedded-hal-async/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "embedded-hal-async"
+version = "0.1.0"
+edition = "2021"
+categories = ["asynchronous", "embedded", "hardware-support", "no-std"]
+description = " A Hardware Abstraction Layer (HAL) for embedded systems, async version"
+documentation = "https://docs.rs/embedded-hal-async"
+keywords = ["hal", "IO", "async"]
+license = "MIT OR Apache-2.0"
+readme = "README.md"
+repository = "https://github.com/rust-embedded/embedded-hal"
+
+[dependencies]
+nb = "1"
+embedded-hal = { version = "1.0.0-alpha.6", path = ".." }
\ No newline at end of file
diff --git a/embedded-hal-async/src/delay.rs b/embedded-hal-async/src/delay.rs
new file mode 100644
index 000000000..a7b8f6313
--- /dev/null
+++ b/embedded-hal-async/src/delay.rs
@@ -0,0 +1,52 @@
+//! Delays
+
+use core::future::Future;
+
+/// Microsecond delay
+pub trait DelayUs {
+    /// Enumeration of errors
+    type Error: core::fmt::Debug;
+
+    /// The future returned by the `delay_us` function.
+    type DelayUsFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Pauses execution for at minimum `us` microseconds. Pause can be longer
+    /// if the implementation requires it due to precision/timing issues.
+    fn delay_us(&mut self, us: u32) -> Self::DelayUsFuture<'_>;
+
+    /// The future returned by the `delay_ms` function.
+    type DelayMsFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Pauses execution for at minimum `ms` milliseconds. Pause can be longer
+    /// if the implementation requires it due to precision/timing issues.
+    fn delay_ms(&mut self, ms: u32) -> Self::DelayMsFuture<'_>;
+}
+
+impl<T> DelayUs for &mut T
+where
+    T: DelayUs,
+{
+    type Error = T::Error;
+
+    type DelayUsFuture<'a>
+    where
+        Self: 'a,
+    = T::DelayUsFuture<'a>;
+
+    fn delay_us(&mut self, us: u32) -> Self::DelayUsFuture<'_> {
+        T::delay_us(self, us)
+    }
+
+    type DelayMsFuture<'a>
+    where
+        Self: 'a,
+    = T::DelayMsFuture<'a>;
+
+    fn delay_ms(&mut self, ms: u32) -> Self::DelayMsFuture<'_> {
+        T::delay_ms(self, ms)
+    }
+}
diff --git a/embedded-hal-async/src/digital.rs b/embedded-hal-async/src/digital.rs
new file mode 100644
index 000000000..ae1d6c543
--- /dev/null
+++ b/embedded-hal-async/src/digital.rs
@@ -0,0 +1,100 @@
+//! Asynchronous digital I/O
+//!
+//! # Example
+//!
+//! ```rust
+//! # use embedded_hal_async::digital::WaitForHigh;
+//! /// Asynchronously wait until the `ready_pin` becomes high.
+//! async fn wait_until_ready<P>(ready_pin: &mut P)
+//! where
+//!     P: WaitForHigh,
+//! {
+//!     ready_pin
+//!         .wait_for_high()
+//!         .await
+//!         .expect("failed to await input pin")
+//! }
+//! ```
+
+use core::future::Future;
+
+/// Asynchronously wait for a pin to be high.
+pub trait WaitForHigh {
+    /// Enumeration of errors.
+    type Error: core::fmt::Debug;
+
+    /// The future returned by the `wait_for_high` function.
+    type WaitForHighFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Returns a future that resolves when this pin _is_ high. If the pin
+    /// is already high, the future resolves immediately.
+    ///
+    /// # Note for implementers
+    /// The pin may have switched back to low before the task was run after
+    /// being woken. The future should still resolve in that case.
+    fn wait_for_high<'a>(&'a mut self) -> Self::WaitForHighFuture<'a>;
+}
+
+/// Asynchronously wait for a pin to be low.
+pub trait WaitForLow {
+    /// Enumeration of errors.
+    type Error: core::fmt::Debug;
+
+    /// The future returned by `wait_for_low`.
+    type WaitForLowFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Returns a future that resolves when this pin _is_ low. If the pin
+    /// is already low, the future resolves immediately.
+    ///
+    /// # Note for implementers
+    /// The pin may have switched back to high before the task was run after
+    /// being woken. The future should still resolve in that case.
+    fn wait_for_low<'a>(&'a mut self) -> Self::WaitForLowFuture<'a>;
+}
+
+/// Wait for a rising edge (transition from low to high).
+pub trait WaitForRisingEdge {
+    /// Enumeration of errors.
+    type Error: core::fmt::Debug;
+
+    /// The future returned from `wait_for_rising_edge`.
+    type WaitForRisingEdgeFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Returns a future that resolves when this pin transitions from low to high.
+    fn wait_for_rising_edge<'a>(&'a mut self) -> Self::WaitForRisingEdgeFuture<'a>;
+}
+
+/// Wait for a falling edge (transition from high to low).
+pub trait WaitForFallingEdge {
+    /// Enumeration of errors.
+    type Error: core::fmt::Debug;
+
+    /// The future returned from `wait_for_falling_edge`.
+    type WaitForFallingEdgeFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Returns a future that resolves when this pin transitions from high to low.
+    fn wait_for_falling_edge<'a>(&'a mut self) -> Self::WaitForFallingEdgeFuture<'a>;
+}
+
+/// Wait for any edge (transition from low to high OR high to low).
+pub trait WaitForAnyEdge {
+    /// Enumeration of errors.
+    type Error: core::fmt::Debug;
+
+    /// The future returned from `wait_for_any_edge`.
+    type WaitForAnyEdgeFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Returns a future that resolves when this pin undergoes any transition, e.g.
+    /// low to high OR high to low.
+    fn wait_for_any_edge<'a>(&'a mut self) -> Self::WaitForAnyEdgeFuture<'a>;
+}
diff --git a/embedded-hal-async/src/i2c.rs b/embedded-hal-async/src/i2c.rs
new file mode 100644
index 000000000..ef0718757
--- /dev/null
+++ b/embedded-hal-async/src/i2c.rs
@@ -0,0 +1,163 @@
+//! Async I2C API
+//!
+//! This API supports 7-bit and 10-bit addresses. Traits feature an `AddressMode`
+//! marker type parameter. Two implementation of the `AddressMode` exist:
+//! `SevenBitAddress` and `TenBitAddress`.
+//!
+//! Through this marker types it is possible to implement each address mode for
+//! the traits independently in `embedded-hal` implementations and device drivers
+//! can depend only on the mode that they support.
+//!
+//! Additionally, the I2C 10-bit address mode has been developed to be fully
+//! backwards compatible with the 7-bit address mode. This allows for a
+//! software-emulated 10-bit addressing implementation if the address mode
+//! is not supported by the hardware.
+//!
+//! Since 7-bit addressing is the mode of the majority of I2C devices,
+//! `SevenBitAddress` has been set as default mode and thus can be omitted if desired.
+
+use core::future::Future;
+pub use embedded_hal::i2c::{
+    AddressMode, Error, ErrorKind, NoAcknowledgeSource, SevenBitAddress, TenBitAddress,
+};
+
+/// Async read
+pub trait Read<A: AddressMode = SevenBitAddress> {
+    /// Error type
+    type Error: Error;
+    /// The future associated with the `read` method.
+    type ReadFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Reads enough bytes from slave with `address` to fill `buffer`
+    ///
+    /// # I2C Events (contract)
+    ///
+    /// ``` text
+    /// Master: ST SAD+R        MAK    MAK ...    NMAK SP
+    /// Slave:           SAK B0     B1     ... BN
+    /// ```
+    ///
+    /// Where
+    ///
+    /// - `ST` = start condition
+    /// - `SAD+R` = slave address followed by bit 1 to indicate reading
+    /// - `SAK` = slave acknowledge
+    /// - `Bi` = ith byte of data
+    /// - `MAK` = master acknowledge
+    /// - `NMAK` = master no acknowledge
+    /// - `SP` = stop condition
+    fn read<'a>(&'a mut self, address: A, read: &'a mut [u8]) -> Self::ReadFuture<'a>;
+}
+
+impl<A: AddressMode, T: Read<A>> Read<A> for &mut T {
+    type Error = T::Error;
+
+    type ReadFuture<'a>
+    where
+        Self: 'a,
+    = T::ReadFuture<'a>;
+
+    fn read<'a>(&'a mut self, address: A, buffer: &'a mut [u8]) -> Self::ReadFuture<'a> {
+        T::read(self, address, buffer)
+    }
+}
+
+/// Async write
+pub trait Write<A: AddressMode = SevenBitAddress> {
+    /// Error type
+    type Error: Error;
+    /// The future associated with the `write` method.
+    type WriteFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes bytes to slave with address `address`
+    ///
+    /// # I2C Events (contract)
+    ///
+    /// ``` text
+    /// Master: ST SAD+W     B0     B1     ... BN     SP
+    /// Slave:           SAK    SAK    SAK ...    SAK
+    /// ```
+    ///
+    /// Where
+    ///
+    /// - `ST` = start condition
+    /// - `SAD+W` = slave address followed by bit 0 to indicate writing
+    /// - `SAK` = slave acknowledge
+    /// - `Bi` = ith byte of data
+    /// - `SP` = stop condition
+    fn write<'a>(&'a mut self, address: A, write: &'a [u8]) -> Self::WriteFuture<'a>;
+}
+
+impl<A: AddressMode, T: Write<A>> Write<A> for &mut T {
+    type Error = T::Error;
+
+    type WriteFuture<'a>
+    where
+        Self: 'a,
+    = T::WriteFuture<'a>;
+
+    fn write<'a>(&'a mut self, address: A, bytes: &'a [u8]) -> Self::WriteFuture<'a> {
+        T::write(self, address, bytes)
+    }
+}
+
+/// Async write + read
+pub trait WriteRead<A: AddressMode = SevenBitAddress> {
+    /// Error type
+    type Error: Error;
+    /// The future associated with the `write_read` method.
+    type WriteReadFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes bytes to slave with address `address` and then reads enough bytes to fill `read` *in a
+    /// single transaction*.
+    ///
+    /// # I2C Events (contract)
+    ///
+    /// ``` text
+    /// Master: ST SAD+W     O0     O1     ... OM     SR SAD+R        MAK    MAK ...    NMAK SP
+    /// Slave:           SAK    SAK    SAK ...    SAK          SAK I0     I1     ... IN
+    /// ```
+    ///
+    /// Where
+    ///
+    /// - `ST` = start condition
+    /// - `SAD+W` = slave address followed by bit 0 to indicate writing
+    /// - `SAK` = slave acknowledge
+    /// - `Oi` = ith outgoing byte of data
+    /// - `SR` = repeated start condition
+    /// - `SAD+R` = slave address followed by bit 1 to indicate reading
+    /// - `Ii` = ith incoming byte of data
+    /// - `MAK` = master acknowledge
+    /// - `NMAK` = master no acknowledge
+    /// - `SP` = stop condition
+    fn write_read<'a>(
+        &'a mut self,
+        address: A,
+        write: &'a [u8],
+        read: &'a mut [u8],
+    ) -> Self::WriteReadFuture<'a>;
+}
+
+impl<A: AddressMode, T: WriteRead<A>> WriteRead<A> for &mut T {
+    type Error = T::Error;
+
+    type WriteReadFuture<'a>
+    where
+        Self: 'a,
+    = T::WriteReadFuture<'a>;
+
+    fn write_read<'a>(
+        &'a mut self,
+        address: A,
+        bytes: &'a [u8],
+        buffer: &'a mut [u8],
+    ) -> Self::WriteReadFuture<'a> {
+        T::write_read(self, address, bytes, buffer)
+    }
+}
diff --git a/embedded-hal-async/src/lib.rs b/embedded-hal-async/src/lib.rs
new file mode 100644
index 000000000..1026b5fc5
--- /dev/null
+++ b/embedded-hal-async/src/lib.rs
@@ -0,0 +1,13 @@
+#![feature(generic_associated_types)]
+#![no_std]
+#![deny(missing_docs)]
+
+//! Asynchronous APIs
+//!
+//! This traits use `core::future::Future` and generic associated types.
+
+pub mod delay;
+pub mod digital;
+pub mod i2c;
+pub mod serial;
+pub mod spi;
diff --git a/embedded-hal-async/src/serial.rs b/embedded-hal-async/src/serial.rs
new file mode 100644
index 000000000..95bc2cea3
--- /dev/null
+++ b/embedded-hal-async/src/serial.rs
@@ -0,0 +1,74 @@
+//! Serial interface
+
+use core::future::Future;
+pub use embedded_hal::serial::{Error, ErrorKind};
+
+/// Read half of a serial interface
+///
+/// Some serial interfaces support different data sizes (8 bits, 9 bits, etc.);
+/// This can be encoded in this trait via the `Word` type parameter.
+pub trait Read<Word: 'static = u8> {
+    /// Read error
+    type Error: Error;
+
+    /// The future associated with the `read` method.
+    type ReadFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Reads words from the serial interface into the supplied slice.
+    fn read<'a>(&'a mut self, read: &'a mut [Word]) -> Self::ReadFuture<'a>;
+}
+
+impl<T: Read<Word>, Word: 'static> Read<Word> for &mut T {
+    type Error = T::Error;
+    type ReadFuture<'a>
+    where
+        Self: 'a,
+    = T::ReadFuture<'a>;
+
+    fn read<'a>(&'a mut self, read: &'a mut [Word]) -> Self::ReadFuture<'a> {
+        T::read(self, read)
+    }
+}
+/// Write half of a serial interface
+pub trait Write<Word: 'static = u8> {
+    /// Write error
+    type Error: Error;
+
+    /// The future associated with the `write` method.
+    type WriteFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// The future associated with the `flush` method.
+    type FlushFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes a single word to the serial interface
+    fn write<'a>(&'a mut self, words: &'a [Word]) -> Self::WriteFuture<'a>;
+
+    /// Ensures that none of the previously written words are still buffered
+    fn flush<'a>(&'a mut self) -> Self::FlushFuture<'a>;
+}
+
+impl<T: Write<Word>, Word: 'static> Write<Word> for &mut T {
+    type Error = T::Error;
+    type WriteFuture<'a>
+    where
+        Self: 'a,
+    = T::WriteFuture<'a>;
+    type FlushFuture<'a>
+    where
+        Self: 'a,
+    = T::FlushFuture<'a>;
+
+    fn write<'a>(&'a mut self, words: &'a [Word]) -> Self::WriteFuture<'a> {
+        T::write(self, words)
+    }
+
+    fn flush<'a>(&'a mut self) -> Self::FlushFuture<'a> {
+        T::flush(self)
+    }
+}
diff --git a/embedded-hal-async/src/spi.rs b/embedded-hal-async/src/spi.rs
new file mode 100644
index 000000000..2a6d79697
--- /dev/null
+++ b/embedded-hal-async/src/spi.rs
@@ -0,0 +1,157 @@
+//! Serial Peripheral Interface
+
+use core::future::Future;
+
+pub use embedded_hal::spi::blocking::Operation;
+pub use embedded_hal::spi::{
+    Error, ErrorKind, ErrorType, Mode, Phase, Polarity, MODE_0, MODE_1, MODE_2, MODE_3,
+};
+
+/// Read-only SPI
+pub trait Read<W: 'static = u8>: ErrorType {
+    /// Associated future for the `read` method.
+    type ReadFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Reads `words` from the slave.
+    ///
+    /// The word value sent on MOSI during reading is implementation-defined,
+    /// typically `0x00`, `0xFF`, or configurable.
+    fn read<'a>(&'a mut self, words: &'a mut [W]) -> Self::ReadFuture<'a>;
+
+    /// Associated future for the `read_batch` method.
+    type ReadBatchFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Reads all slices in `words` from the slave as part of a single SPI transaction.
+    ///
+    /// The word value sent on MOSI during reading is implementation-defined,
+    /// typically `0x00`, `0xFF`, or configurable.
+    fn read_batch<'a>(&'a mut self, words: &'a mut [&'a mut [W]]) -> Self::ReadBatchFuture<'a>;
+}
+
+impl<T: Read<W>, W: 'static> Read<W> for &mut T {
+    type ReadFuture<'a>
+    where
+        Self: 'a,
+    = T::ReadFuture<'a>;
+
+    fn read<'a>(&'a mut self, words: &'a mut [W]) -> Self::ReadFuture<'a> {
+        T::read(self, words)
+    }
+
+    type ReadBatchFuture<'a>
+    where
+        Self: 'a,
+    = T::ReadBatchFuture<'a>;
+
+    fn read_batch<'a>(&'a mut self, words: &'a mut [&'a mut [W]]) -> Self::ReadBatchFuture<'a> {
+        T::read_batch(self, words)
+    }
+}
+
+/// Write-only SPI
+pub trait Write<W: 'static = u8>: ErrorType {
+    /// Associated future for the `write` method.
+    type WriteFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes `words` to the slave, ignoring all the incoming words
+    fn write<'a>(&'a mut self, words: &'a [W]) -> Self::WriteFuture<'a>;
+
+    /// Associated future for the `write_batch` method.
+    type WriteBatchFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes all slices in `words` to the slave as part of a single SPI transaction, ignoring all the incoming words
+    fn write_batch<'a>(&'a mut self, words: &'a [&'a [W]]) -> Self::WriteBatchFuture<'a>;
+}
+
+impl<T: Write<W>, W: 'static> Write<W> for &mut T {
+    type WriteFuture<'a>
+    where
+        Self: 'a,
+    = T::WriteFuture<'a>;
+
+    fn write<'a>(&'a mut self, words: &'a [W]) -> Self::WriteFuture<'a> {
+        T::write(self, words)
+    }
+
+    type WriteBatchFuture<'a>
+    where
+        Self: 'a,
+    = T::WriteBatchFuture<'a>;
+
+    fn write_batch<'a>(&'a mut self, words: &'a [&'a [W]]) -> Self::WriteBatchFuture<'a> {
+        T::write_batch(self, words)
+    }
+}
+
+/// Read-write SPI
+pub trait ReadWrite<W: 'static = u8>: Read<W> + Write<W> {
+    /// Associated future for the `transfer` method.
+    type TransferFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes and reads simultaneously. `write` is written to the slave on MOSI and
+    /// words received on MISO are stored in `read`.
+    ///
+    /// It is allowed for `read` and `write` to have different lengths, even zero length.
+    /// The transfer runs for `max(read.len(), write.len())` words. If `read` is shorter,
+    /// incoming words after `read` has been filled will be discarded. If `write` is shorter,
+    /// the value of words sent in MOSI after all `write` has been sent is implementation-defined,
+    /// typically `0x00`, `0xFF`, or configurable.
+    fn transfer<'a>(&'a mut self, read: &'a mut [W], write: &'a [W]) -> Self::TransferFuture<'a>;
+
+    /// Associated future for the `transfer_in_place` method.
+    type TransferInPlaceFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes and reads simultaneously. The contents of `words` are
+    /// written to the slave, and the received words are stored into the same
+    /// `words` buffer, overwriting it.
+    fn transfer_in_place<'a>(&'a mut self, words: &'a mut [W]) -> Self::TransferInPlaceFuture<'a>;
+
+    /// Associated future for the `batch` method.
+    type BatchFuture<'a>: Future<Output = Result<(), Self::Error>> + 'a
+    where
+        Self: 'a;
+
+    /// Execute multiple actions as part of a single SPI transaction
+    fn batch<'a>(&'a mut self, operations: &'a mut [Operation<'a, W>]) -> Self::BatchFuture<'a>;
+}
+
+impl<T: ReadWrite<W>, W: 'static> ReadWrite<W> for &mut T {
+    type TransferFuture<'a>
+    where
+        Self: 'a,
+    = T::TransferFuture<'a>;
+
+    fn transfer<'a>(&'a mut self, read: &'a mut [W], write: &'a [W]) -> Self::TransferFuture<'a> {
+        T::transfer(self, read, write)
+    }
+
+    type TransferInPlaceFuture<'a>
+    where
+        Self: 'a,
+    = T::TransferInPlaceFuture<'a>;
+
+    fn transfer_in_place<'a>(&'a mut self, words: &'a mut [W]) -> Self::TransferInPlaceFuture<'a> {
+        T::transfer_in_place(self, words)
+    }
+
+    type BatchFuture<'a>
+    where
+        Self: 'a,
+    = T::BatchFuture<'a>;
+
+    fn batch<'a>(&'a mut self, operations: &'a mut [Operation<'a, W>]) -> Self::BatchFuture<'a> {
+        T::batch(self, operations)
+    }
+}
diff --git a/src/spi/blocking.rs b/src/spi/blocking.rs
index cde2baae9..0fb169e41 100644
--- a/src/spi/blocking.rs
+++ b/src/spi/blocking.rs
@@ -1,89 +1,39 @@
 //! Blocking SPI API
 
-/// Blocking transfer with separate buffers
-pub trait Transfer<W = u8> {
-    /// Error type
-    type Error: crate::spi::Error;
+use super::ErrorType;
 
-    /// Writes and reads simultaneously. `write` is written to the slave on MOSI and
-    /// words received on MISO are stored in `read`.
+/// Blocking read-only SPI
+pub trait Read<W = u8>: ErrorType {
+    /// Reads `words` from the slave.
     ///
-    /// It is allowed for `read` and `write` to have different lengths, even zero length.
-    /// The transfer runs for `max(read.len(), write.len())` words. If `read` is shorter,
-    /// incoming words after `read` has been filled will be discarded. If `write` is shorter,
-    /// the value of words sent in MOSI after all `write` has been sent is implementation-defined,
+    /// The word value sent on MOSI during reading is implementation-defined,
     /// typically `0x00`, `0xFF`, or configurable.
-    fn transfer(&mut self, read: &mut [W], write: &[W]) -> Result<(), Self::Error>;
-}
-
-impl<T: Transfer<W>, W> Transfer<W> for &mut T {
-    type Error = T::Error;
-
-    fn transfer(&mut self, read: &mut [W], write: &[W]) -> Result<(), Self::Error> {
-        T::transfer(self, read, write)
-    }
-}
-
-/// Blocking transfer with single buffer (in-place)
-pub trait TransferInplace<W = u8> {
-    /// Error type
-    type Error: crate::spi::Error;
-
-    /// Writes and reads simultaneously. The contents of `words` are
-    /// written to the slave, and the received words are stored into the same
-    /// `words` buffer, overwriting it.
-    fn transfer_inplace(&mut self, words: &mut [W]) -> Result<(), Self::Error>;
-}
-
-impl<T: TransferInplace<W>, W> TransferInplace<W> for &mut T {
-    type Error = T::Error;
-
-    fn transfer_inplace(&mut self, words: &mut [W]) -> Result<(), Self::Error> {
-        T::transfer_inplace(self, words)
-    }
-}
-
-/// Blocking read
-pub trait Read<W = u8> {
-    /// Error type
-    type Error: crate::spi::Error;
+    fn read(&mut self, words: &mut [W]) -> Result<(), Self::Error>;
 
-    /// Reads `words` from the slave.
+    /// Reads all slices in `words` from the slave as part of a single SPI transaction.
     ///
     /// The word value sent on MOSI during reading is implementation-defined,
     /// typically `0x00`, `0xFF`, or configurable.
-    fn read(&mut self, words: &mut [W]) -> Result<(), Self::Error>;
+    fn read_batch(&mut self, words: &mut [&mut [W]]) -> Result<(), Self::Error>;
 }
 
 impl<T: Read<W>, W> Read<W> for &mut T {
-    type Error = T::Error;
-
     fn read(&mut self, words: &mut [W]) -> Result<(), Self::Error> {
         T::read(self, words)
     }
-}
 
-/// Blocking write
-pub trait Write<W = u8> {
-    /// Error type
-    type Error: crate::spi::Error;
+    fn read_batch(&mut self, words: &mut [&mut [W]]) -> Result<(), Self::Error> {
+        T::read_batch(self, words)
+    }
+}
 
+/// Blocking write-only SPI
+pub trait Write<W = u8>: ErrorType {
     /// Writes `words` to the slave, ignoring all the incoming words
     fn write(&mut self, words: &[W]) -> Result<(), Self::Error>;
-}
-
-impl<T: Write<W>, W> Write<W> for &mut T {
-    type Error = T::Error;
-
-    fn write(&mut self, words: &[W]) -> Result<(), Self::Error> {
-        T::write(self, words)
-    }
-}
 
-/// Blocking write (iterator version)
-pub trait WriteIter<W = u8> {
-    /// Error type
-    type Error: crate::spi::Error;
+    /// Writes all slices in `words` to the slave as part of a single SPI transaction, ignoring all the incoming words
+    fn write_batch(&mut self, words: &[&[W]]) -> Result<(), Self::Error>;
 
     /// Writes `words` to the slave, ignoring all the incoming words
     fn write_iter<WI>(&mut self, words: WI) -> Result<(), Self::Error>
@@ -91,8 +41,14 @@ pub trait WriteIter<W = u8> {
         WI: IntoIterator<Item = W>;
 }
 
-impl<T: WriteIter<W>, W> WriteIter<W> for &mut T {
-    type Error = T::Error;
+impl<T: Write<W>, W> Write<W> for &mut T {
+    fn write(&mut self, words: &[W]) -> Result<(), Self::Error> {
+        T::write(self, words)
+    }
+
+    fn write_batch(&mut self, words: &[&[W]]) -> Result<(), Self::Error> {
+        T::write_batch(self, words)
+    }
 
     fn write_iter<WI>(&mut self, words: WI) -> Result<(), Self::Error>
     where
@@ -102,7 +58,7 @@ impl<T: WriteIter<W>, W> WriteIter<W> for &mut T {
     }
 }
 
-/// Operation for transactional SPI trait
+/// Operation for ReadWrite::batch
 ///
 /// This allows composition of SPI operations into a single bus transaction
 #[derive(Debug, PartialEq)]
@@ -114,23 +70,40 @@ pub enum Operation<'a, W: 'static = u8> {
     /// Write data out while reading data into the provided buffer
     Transfer(&'a mut [W], &'a [W]),
     /// Write data out while reading data into the provided buffer
-    TransferInplace(&'a mut [W]),
+    TransferInPlace(&'a mut [W]),
 }
 
-/// Transactional trait allows multiple actions to be executed
-/// as part of a single SPI transaction
-pub trait Transactional<W: 'static = u8> {
-    /// Associated error type
-    type Error: crate::spi::Error;
+/// Blocking read-write SPI
+pub trait ReadWrite<W = u8>: Read<W> + Write<W> {
+    /// Writes and reads simultaneously. `write` is written to the slave on MOSI and
+    /// words received on MISO are stored in `read`.
+    ///
+    /// It is allowed for `read` and `write` to have different lengths, even zero length.
+    /// The transfer runs for `max(read.len(), write.len())` words. If `read` is shorter,
+    /// incoming words after `read` has been filled will be discarded. If `write` is shorter,
+    /// the value of words sent in MOSI after all `write` has been sent is implementation-defined,
+    /// typically `0x00`, `0xFF`, or configurable.
+    fn transfer(&mut self, read: &mut [W], write: &[W]) -> Result<(), Self::Error>;
+
+    /// Writes and reads simultaneously. The contents of `words` are
+    /// written to the slave, and the received words are stored into the same
+    /// `words` buffer, overwriting it.
+    fn transfer_in_place(&mut self, words: &mut [W]) -> Result<(), Self::Error>;
 
-    /// Execute the provided transactions
-    fn exec<'a>(&mut self, operations: &mut [Operation<'a, W>]) -> Result<(), Self::Error>;
+    /// Execute multiple actions as part of a single SPI transaction
+    fn batch<'a>(&mut self, operations: &mut [Operation<'a, W>]) -> Result<(), Self::Error>;
 }
 
-impl<T: Transactional<W>, W: 'static> Transactional<W> for &mut T {
-    type Error = T::Error;
+impl<T: ReadWrite<W>, W> ReadWrite<W> for &mut T {
+    fn transfer(&mut self, read: &mut [W], write: &[W]) -> Result<(), Self::Error> {
+        T::transfer(self, read, write)
+    }
+
+    fn transfer_in_place(&mut self, words: &mut [W]) -> Result<(), Self::Error> {
+        T::transfer_in_place(self, words)
+    }
 
-    fn exec<'a>(&mut self, operations: &mut [Operation<'a, W>]) -> Result<(), Self::Error> {
-        T::exec(self, operations)
+    fn batch<'a>(&mut self, operations: &mut [Operation<'a, W>]) -> Result<(), Self::Error> {
+        T::batch(self, operations)
     }
 }
diff --git a/src/spi/mod.rs b/src/spi/mod.rs
index 048de49a4..a1924251c 100644
--- a/src/spi/mod.rs
+++ b/src/spi/mod.rs
@@ -113,3 +113,15 @@ impl core::fmt::Display for ErrorKind {
         }
     }
 }
+
+/// SPI error type trait
+///
+/// This just defines the error type, to be used by the other SPI traits.
+pub trait ErrorType {
+    /// Error type
+    type Error: Error;
+}
+
+impl<T: ErrorType> ErrorType for &mut T {
+    type Error = T::Error;
+}
diff --git a/src/spi/nb.rs b/src/spi/nb.rs
index 6d540a762..2b9bbe5db 100644
--- a/src/spi/nb.rs
+++ b/src/spi/nb.rs
@@ -1,5 +1,7 @@
 //! Serial Peripheral Interface
 
+use super::ErrorType;
+
 /// Full duplex (master mode)
 ///
 /// # Notes
@@ -16,10 +18,7 @@
 ///
 /// - Some SPIs can work with 8-bit *and* 16-bit words. You can overload this trait with different
 /// `Word` types to allow operation in both modes.
-pub trait FullDuplex<Word = u8> {
-    /// An enumeration of SPI errors
-    type Error: crate::spi::Error;
-
+pub trait FullDuplex<Word = u8>: ErrorType {
     /// Reads the word stored in the shift register
     ///
     /// **NOTE** A word must be sent to the slave before attempting to call this
@@ -31,8 +30,6 @@ pub trait FullDuplex<Word = u8> {
 }
 
 impl<T: FullDuplex<Word>, Word> FullDuplex<Word> for &mut T {
-    type Error = T::Error;
-
     fn read(&mut self) -> nb::Result<Word, Self::Error> {
         T::read(self)
     }