diff --git a/.vscode/settings.json b/.vscode/settings.json
index ed01f7557..a5a656637 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,11 +6,12 @@
   "rust-analyzer.checkOnSave.allTargets": false,
   "rust-analyzer.cargo.noDefaultFeatures": true,
   "rust-analyzer.checkOnSave.noDefaultFeatures": true,
-  //"rust-analyzer.cargo.target": "thumbv7em-none-eabi",
+  "rust-analyzer.cargo.target": "thumbv7em-none-eabi",
   "rust-analyzer.cargo.features": [
     // These are needed to prevent embassy-net from failing to build
     "embassy-net/medium-ethernet",
     "embassy-net/tcp",
+    "embassy-net/pool-16",
   ],
   "rust-analyzer.procMacro.enable": true,
   "rust-analyzer.cargo.runBuildScripts": true,
diff --git a/embassy/src/util/drop_bomb.rs b/embassy-hal-common/src/drop.rs
similarity index 61%
rename from embassy/src/util/drop_bomb.rs
rename to embassy-hal-common/src/drop.rs
index efb36a97d..74a484de7 100644
--- a/embassy/src/util/drop_bomb.rs
+++ b/embassy-hal-common/src/drop.rs
@@ -1,4 +1,27 @@
 use core::mem;
+use core::mem::MaybeUninit;
+
+pub struct OnDrop<F: FnOnce()> {
+    f: MaybeUninit<F>,
+}
+
+impl<F: FnOnce()> OnDrop<F> {
+    pub fn new(f: F) -> Self {
+        Self {
+            f: MaybeUninit::new(f),
+        }
+    }
+
+    pub fn defuse(self) {
+        mem::forget(self)
+    }
+}
+
+impl<F: FnOnce()> Drop for OnDrop<F> {
+    fn drop(&mut self) {
+        unsafe { self.f.as_ptr().read()() }
+    }
+}
 
 /// An explosive ordinance that panics if it is improperly disposed of.
 ///
diff --git a/embassy-hal-common/src/lib.rs b/embassy-hal-common/src/lib.rs
index ea20747eb..01e2d3aee 100644
--- a/embassy-hal-common/src/lib.rs
+++ b/embassy-hal-common/src/lib.rs
@@ -3,6 +3,7 @@
 // This mod MUST go first, so that the others see its macros.
 pub(crate) mod fmt;
 
+pub mod drop;
 pub mod interrupt;
 mod macros;
 pub mod peripheral;
diff --git a/embassy-hal-common/src/usb/usb_serial.rs b/embassy-hal-common/src/usb/usb_serial.rs
index 8b27152b5..ca43a4d73 100644
--- a/embassy-hal-common/src/usb/usb_serial.rs
+++ b/embassy-hal-common/src/usb/usb_serial.rs
@@ -4,7 +4,7 @@ use core::pin::Pin;
 use core::task::{Context, Poll};
 
 use embassy::io::{self, AsyncBufRead, AsyncWrite};
-use embassy::util::WakerRegistration;
+use embassy::waitqueue::WakerRegistration;
 use usb_device::bus::UsbBus;
 use usb_device::class_prelude::*;
 use usb_device::UsbError;
diff --git a/embassy-net/src/stack.rs b/embassy-net/src/stack.rs
index 5f871bd1f..f26808cd0 100644
--- a/embassy-net/src/stack.rs
+++ b/embassy-net/src/stack.rs
@@ -2,9 +2,9 @@ use core::cell::RefCell;
 use core::future::Future;
 use core::task::Context;
 use core::task::Poll;
+use embassy::blocking_mutex::ThreadModeMutex;
 use embassy::time::{Instant, Timer};
-use embassy::util::ThreadModeMutex;
-use embassy::util::WakerRegistration;
+use embassy::waitqueue::WakerRegistration;
 use futures::pin_mut;
 use smoltcp::iface::InterfaceBuilder;
 #[cfg(feature = "medium-ethernet")]
diff --git a/embassy-nrf/src/buffered_uarte.rs b/embassy-nrf/src/buffered_uarte.rs
index 90ce49582..cd08875cd 100644
--- a/embassy-nrf/src/buffered_uarte.rs
+++ b/embassy-nrf/src/buffered_uarte.rs
@@ -6,7 +6,8 @@ use core::sync::atomic::{compiler_fence, Ordering};
 use core::task::{Context, Poll};
 use embassy::interrupt::InterruptExt;
 use embassy::io::{AsyncBufRead, AsyncWrite, Result};
-use embassy::util::{Unborrow, WakerRegistration};
+use embassy::util::Unborrow;
+use embassy::waitqueue::WakerRegistration;
 use embassy_hal_common::peripheral::{PeripheralMutex, PeripheralState, StateStorage};
 use embassy_hal_common::ring_buffer::RingBuffer;
 use embassy_hal_common::{low_power_wait_until, unborrow};
diff --git a/embassy-nrf/src/gpiote.rs b/embassy-nrf/src/gpiote.rs
index 847b2fbf3..001ee7fb8 100644
--- a/embassy-nrf/src/gpiote.rs
+++ b/embassy-nrf/src/gpiote.rs
@@ -4,7 +4,7 @@ use core::marker::PhantomData;
 use core::task::{Context, Poll};
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::traits::gpio::{WaitForAnyEdge, WaitForHigh, WaitForLow};
-use embassy::util::AtomicWaker;
+use embassy::waitqueue::AtomicWaker;
 use embassy_hal_common::unsafe_impl_unborrow;
 use embedded_hal::digital::v2::{InputPin, StatefulOutputPin};
 use futures::future::poll_fn;
diff --git a/embassy-nrf/src/qspi.rs b/embassy-nrf/src/qspi.rs
index 28becfd56..e87094250 100644
--- a/embassy-nrf/src/qspi.rs
+++ b/embassy-nrf/src/qspi.rs
@@ -6,7 +6,8 @@ use core::ptr;
 use core::task::Poll;
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::traits::flash::{Error, Flash};
-use embassy::util::{AtomicWaker, DropBomb, Unborrow};
+use embassy::util::Unborrow;
+use embassy_hal_common::drop::DropBomb;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 
@@ -397,6 +398,8 @@ impl<'d, T: Instance> Flash for Qspi<'d, T> {
 }
 
 pub(crate) mod sealed {
+    use embassy::waitqueue::AtomicWaker;
+
     use super::*;
 
     pub struct State {
diff --git a/embassy-nrf/src/rng.rs b/embassy-nrf/src/rng.rs
index 6cdcccf3b..20d033a12 100644
--- a/embassy-nrf/src/rng.rs
+++ b/embassy-nrf/src/rng.rs
@@ -8,9 +8,9 @@ use core::task::Poll;
 
 use embassy::interrupt::InterruptExt;
 use embassy::traits;
-use embassy::util::AtomicWaker;
-use embassy::util::OnDrop;
 use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::drop::OnDrop;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 use rand_core::RngCore;
diff --git a/embassy-nrf/src/saadc.rs b/embassy-nrf/src/saadc.rs
index bc7f34716..e909e7d5a 100644
--- a/embassy-nrf/src/saadc.rs
+++ b/embassy-nrf/src/saadc.rs
@@ -3,7 +3,8 @@ use core::marker::PhantomData;
 use core::sync::atomic::{compiler_fence, Ordering};
 use core::task::Poll;
 use embassy::interrupt::InterruptExt;
-use embassy::util::{AtomicWaker, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 
diff --git a/embassy-nrf/src/spim.rs b/embassy-nrf/src/spim.rs
index 9a7fb4f67..e88fb460c 100644
--- a/embassy-nrf/src/spim.rs
+++ b/embassy-nrf/src/spim.rs
@@ -6,7 +6,7 @@ use core::sync::atomic::{compiler_fence, Ordering};
 use core::task::Poll;
 use embassy::interrupt::InterruptExt;
 use embassy::traits;
-use embassy::util::{AtomicWaker, Unborrow};
+use embassy::util::Unborrow;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 use traits::spi::{FullDuplex, Read, Spi, Write};
@@ -359,6 +359,8 @@ impl<'d, T: Instance> embedded_hal::blocking::spi::Write<u8> for Spim<'d, T> {
 }
 
 pub(crate) mod sealed {
+    use embassy::waitqueue::AtomicWaker;
+
     use super::*;
 
     pub struct State {
diff --git a/embassy-nrf/src/time_driver.rs b/embassy-nrf/src/time_driver.rs
index f93ebb54a..19356c2d2 100644
--- a/embassy-nrf/src/time_driver.rs
+++ b/embassy-nrf/src/time_driver.rs
@@ -2,9 +2,9 @@ use core::cell::Cell;
 use core::sync::atomic::{compiler_fence, AtomicU32, AtomicU8, Ordering};
 use core::{mem, ptr};
 use critical_section::CriticalSection;
+use embassy::blocking_mutex::CriticalSectionMutex as Mutex;
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::time::driver::{AlarmHandle, Driver};
-use embassy::util::CriticalSectionMutex as Mutex;
 
 use crate::interrupt;
 use crate::pac;
diff --git a/embassy-nrf/src/timer.rs b/embassy-nrf/src/timer.rs
index 638fd8229..5690ff0d8 100644
--- a/embassy-nrf/src/timer.rs
+++ b/embassy-nrf/src/timer.rs
@@ -5,8 +5,9 @@ use core::task::Poll;
 
 use embassy::interrupt::Interrupt;
 use embassy::interrupt::InterruptExt;
-use embassy::util::OnDrop;
 use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::drop::OnDrop;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 
@@ -15,7 +16,6 @@ use crate::ppi::Event;
 use crate::ppi::Task;
 
 pub(crate) mod sealed {
-    use embassy::util::AtomicWaker;
 
     use super::*;
 
@@ -43,8 +43,8 @@ macro_rules! impl_timer {
             fn regs() -> &'static pac::timer0::RegisterBlock {
                 unsafe { &*(pac::$pac_type::ptr() as *const pac::timer0::RegisterBlock) }
             }
-            fn waker(n: usize) -> &'static ::embassy::util::AtomicWaker {
-                use ::embassy::util::AtomicWaker;
+            fn waker(n: usize) -> &'static ::embassy::waitqueue::AtomicWaker {
+                use ::embassy::waitqueue::AtomicWaker;
                 const NEW_AW: AtomicWaker = AtomicWaker::new();
                 static WAKERS: [AtomicWaker; $ccs] = [NEW_AW; $ccs];
                 &WAKERS[n]
diff --git a/embassy-nrf/src/twim.rs b/embassy-nrf/src/twim.rs
index ac263bad7..8173f66b0 100644
--- a/embassy-nrf/src/twim.rs
+++ b/embassy-nrf/src/twim.rs
@@ -12,7 +12,8 @@ use core::sync::atomic::{compiler_fence, Ordering::SeqCst};
 use core::task::Poll;
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::traits;
-use embassy::util::{AtomicWaker, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 use traits::i2c::I2c;
diff --git a/embassy-nrf/src/uarte.rs b/embassy-nrf/src/uarte.rs
index a6909be68..320426060 100644
--- a/embassy-nrf/src/uarte.rs
+++ b/embassy-nrf/src/uarte.rs
@@ -8,7 +8,8 @@ use core::sync::atomic::{compiler_fence, Ordering};
 use core::task::Poll;
 use embassy::interrupt::InterruptExt;
 use embassy::traits::uart::{Error, Read, ReadUntilIdle, Write};
-use embassy::util::{AtomicWaker, OnDrop, Unborrow};
+use embassy::util::Unborrow;
+use embassy_hal_common::drop::OnDrop;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 
@@ -439,6 +440,8 @@ impl<'d, U: Instance, T: TimerInstance> Write for UarteWithIdle<'d, U, T> {
 }
 
 pub(crate) mod sealed {
+    use embassy::waitqueue::AtomicWaker;
+
     use super::*;
 
     pub struct State {
diff --git a/embassy-rp/src/timer.rs b/embassy-rp/src/timer.rs
index ed265c47f..b3c047ca4 100644
--- a/embassy-rp/src/timer.rs
+++ b/embassy-rp/src/timer.rs
@@ -1,9 +1,9 @@
 use atomic_polyfill::{AtomicU8, Ordering};
 use core::cell::Cell;
 use critical_section::CriticalSection;
+use embassy::blocking_mutex::CriticalSectionMutex as Mutex;
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::time::driver::{AlarmHandle, Driver};
-use embassy::util::CriticalSectionMutex as Mutex;
 
 use crate::{interrupt, pac};
 
diff --git a/embassy-stm32/src/dma/bdma.rs b/embassy-stm32/src/dma/bdma.rs
index fbd753a71..35c0b3ee7 100644
--- a/embassy-stm32/src/dma/bdma.rs
+++ b/embassy-stm32/src/dma/bdma.rs
@@ -5,7 +5,8 @@ use core::sync::atomic::{fence, Ordering};
 use core::task::Poll;
 
 use embassy::interrupt::{Interrupt, InterruptExt};
-use embassy::util::{AtomicWaker, OnDrop};
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::drop::OnDrop;
 use futures::future::poll_fn;
 
 use crate::dma::{Channel, Request};
diff --git a/embassy-stm32/src/dma/dma.rs b/embassy-stm32/src/dma/dma.rs
index bce9656d1..ec5ac98a0 100644
--- a/embassy-stm32/src/dma/dma.rs
+++ b/embassy-stm32/src/dma/dma.rs
@@ -3,7 +3,8 @@ use core::sync::atomic::{fence, Ordering};
 use core::task::Poll;
 
 use embassy::interrupt::{Interrupt, InterruptExt};
-use embassy::util::{AtomicWaker, OnDrop};
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::drop::OnDrop;
 use futures::future::poll_fn;
 
 use crate::interrupt;
diff --git a/embassy-stm32/src/eth/v2/mod.rs b/embassy-stm32/src/eth/v2/mod.rs
index 42eb0680c..ff734f78c 100644
--- a/embassy-stm32/src/eth/v2/mod.rs
+++ b/embassy-stm32/src/eth/v2/mod.rs
@@ -2,7 +2,8 @@ use core::marker::PhantomData;
 use core::sync::atomic::{fence, Ordering};
 use core::task::Waker;
 
-use embassy::util::{AtomicWaker, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
 use embassy_hal_common::peripheral::{PeripheralMutex, PeripheralState, StateStorage};
 use embassy_hal_common::unborrow;
 use embassy_net::{Device, DeviceCapabilities, LinkState, PacketBuf, MTU};
diff --git a/embassy-stm32/src/exti.rs b/embassy-stm32/src/exti.rs
index 8e4989a3e..6d3de3a15 100644
--- a/embassy-stm32/src/exti.rs
+++ b/embassy-stm32/src/exti.rs
@@ -4,7 +4,8 @@ use core::marker::PhantomData;
 use core::pin::Pin;
 use core::task::{Context, Poll};
 use embassy::traits::gpio::{WaitForAnyEdge, WaitForFallingEdge, WaitForRisingEdge};
-use embassy::util::{AtomicWaker, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
 use embassy_hal_common::unsafe_impl_unborrow;
 use embedded_hal::digital::v2::InputPin;
 
diff --git a/embassy-stm32/src/i2c/v2.rs b/embassy-stm32/src/i2c/v2.rs
index fc4f52cf3..5e9e24392 100644
--- a/embassy-stm32/src/i2c/v2.rs
+++ b/embassy-stm32/src/i2c/v2.rs
@@ -4,7 +4,9 @@ use core::task::Poll;
 
 use atomic_polyfill::{AtomicUsize, Ordering};
 use embassy::interrupt::InterruptExt;
-use embassy::util::{AtomicWaker, OnDrop, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::drop::OnDrop;
 use embassy_hal_common::unborrow;
 use embedded_hal::blocking::i2c::Read;
 use embedded_hal::blocking::i2c::Write;
diff --git a/embassy-stm32/src/rng.rs b/embassy-stm32/src/rng.rs
index 0afba3ba7..5655ed967 100644
--- a/embassy-stm32/src/rng.rs
+++ b/embassy-stm32/src/rng.rs
@@ -3,7 +3,8 @@
 use core::future::Future;
 use core::task::Poll;
 use embassy::traits;
-use embassy::util::{AtomicWaker, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 use rand_core::{CryptoRng, RngCore};
diff --git a/embassy-stm32/src/sdmmc/v2.rs b/embassy-stm32/src/sdmmc/v2.rs
index aa1d68ae7..6032c2bb1 100644
--- a/embassy-stm32/src/sdmmc/v2.rs
+++ b/embassy-stm32/src/sdmmc/v2.rs
@@ -5,7 +5,9 @@ use core::marker::PhantomData;
 use core::task::Poll;
 
 use embassy::interrupt::InterruptExt;
-use embassy::util::{AtomicWaker, OnDrop, Unborrow};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::drop::OnDrop;
 use embassy_hal_common::unborrow;
 use futures::future::poll_fn;
 use sdio_host::{BusWidth, CardCapacity, CardStatus, CurrentState, SDStatus, CID, CSD, OCR, SCR};
@@ -1479,8 +1481,8 @@ crate::pac::peripherals!(
                 INNER
             }
 
-            fn state() -> &'static ::embassy::util::AtomicWaker {
-                static WAKER: ::embassy::util::AtomicWaker = ::embassy::util::AtomicWaker::new();
+            fn state() -> &'static ::embassy::waitqueue::AtomicWaker {
+                static WAKER: ::embassy::waitqueue::AtomicWaker = ::embassy::waitqueue::AtomicWaker::new();
                 &WAKER
             }
         }
diff --git a/embassy-stm32/src/usart/v2.rs b/embassy-stm32/src/usart/v2.rs
index 92c0cbc2e..fc3036404 100644
--- a/embassy-stm32/src/usart/v2.rs
+++ b/embassy-stm32/src/usart/v2.rs
@@ -4,7 +4,8 @@ use core::marker::PhantomData;
 use core::pin::Pin;
 use core::task::Context;
 use core::task::Poll;
-use embassy::util::{Unborrow, WakerRegistration};
+use embassy::util::Unborrow;
+use embassy::waitqueue::WakerRegistration;
 use embassy_hal_common::peripheral::{PeripheralMutex, PeripheralState, StateStorage};
 use embassy_hal_common::ring_buffer::RingBuffer;
 use embassy_hal_common::unborrow;
diff --git a/embassy/src/util/mutex.rs b/embassy/src/blocking_mutex/mod.rs
similarity index 99%
rename from embassy/src/util/mutex.rs
rename to embassy/src/blocking_mutex/mod.rs
index 9a00a409e..d112d2ede 100644
--- a/embassy/src/util/mutex.rs
+++ b/embassy/src/blocking_mutex/mod.rs
@@ -1,3 +1,5 @@
+//! Blocking mutex (not async)
+
 use core::cell::UnsafeCell;
 use critical_section::CriticalSection;
 
diff --git a/embassy/src/channel/mod.rs b/embassy/src/channel/mod.rs
new file mode 100644
index 000000000..9e8c67ee9
--- /dev/null
+++ b/embassy/src/channel/mod.rs
@@ -0,0 +1,4 @@
+//! Async channels
+
+pub mod mpsc;
+pub mod signal;
diff --git a/embassy/src/util/mpsc.rs b/embassy/src/channel/mpsc.rs
similarity index 98%
rename from embassy/src/util/mpsc.rs
rename to embassy/src/channel/mpsc.rs
index a0f884ec6..b20d48a95 100644
--- a/embassy/src/util/mpsc.rs
+++ b/embassy/src/channel/mpsc.rs
@@ -49,11 +49,8 @@ use core::task::Waker;
 
 use futures::Future;
 
-use super::CriticalSectionMutex;
-use super::Mutex;
-use super::NoopMutex;
-use super::ThreadModeMutex;
-use super::WakerRegistration;
+use crate::blocking_mutex::{CriticalSectionMutex, Mutex, NoopMutex, ThreadModeMutex};
+use crate::waitqueue::WakerRegistration;
 
 /// Send values to the associated `Receiver`.
 ///
@@ -108,8 +105,8 @@ unsafe impl<'ch, M, T, const N: usize> Sync for Receiver<'ch, M, T, N> where
 /// their channel. The following will therefore fail compilation:
 ////
 /// ```compile_fail
-/// use embassy::util::mpsc;
-/// use embassy::util::mpsc::{Channel, WithThreadModeOnly};
+/// use embassy::channel::mpsc;
+/// use embassy::channel::mpsc::{Channel, WithThreadModeOnly};
 ///
 /// let (sender, receiver) = {
 ///    let mut channel = Channel::<WithThreadModeOnly, u32, 3>::with_thread_mode_only();
@@ -635,8 +632,8 @@ where
     /// Establish a new bounded channel. For example, to create one with a NoopMutex:
     ///
     /// ```
-    /// use embassy::util::mpsc;
-    /// use embassy::util::mpsc::{Channel, WithNoThreads};
+    /// use embassy::channel::mpsc;
+    /// use embassy::channel::mpsc::{Channel, WithNoThreads};
     ///
     /// // Declare a bounded channel of 3 u32s.
     /// let mut channel = Channel::<WithNoThreads, u32, 3>::new();
diff --git a/embassy/src/channel/signal.rs b/embassy/src/channel/signal.rs
new file mode 100644
index 000000000..d5698732c
--- /dev/null
+++ b/embassy/src/channel/signal.rs
@@ -0,0 +1,73 @@
+use core::cell::UnsafeCell;
+use core::future::Future;
+use core::mem;
+use core::task::{Context, Poll, Waker};
+
+/// Synchronization primitive. Allows creating awaitable signals that may be passed between tasks.
+///
+/// For more advanced use cases, please consider [futures-intrusive](https://crates.io/crates/futures-intrusive) channels or mutexes.
+pub struct Signal<T> {
+    state: UnsafeCell<State<T>>,
+}
+
+enum State<T> {
+    None,
+    Waiting(Waker),
+    Signaled(T),
+}
+
+unsafe impl<T: Send> Send for Signal<T> {}
+unsafe impl<T: Send> Sync for Signal<T> {}
+
+impl<T: Send> Signal<T> {
+    pub const fn new() -> Self {
+        Self {
+            state: UnsafeCell::new(State::None),
+        }
+    }
+
+    /// Mark this Signal as completed.
+    pub fn signal(&self, val: T) {
+        critical_section::with(|_| unsafe {
+            let state = &mut *self.state.get();
+            if let State::Waiting(waker) = mem::replace(state, State::Signaled(val)) {
+                waker.wake();
+            }
+        })
+    }
+
+    pub fn reset(&self) {
+        critical_section::with(|_| unsafe {
+            let state = &mut *self.state.get();
+            *state = State::None
+        })
+    }
+
+    pub fn poll_wait(&self, cx: &mut Context<'_>) -> Poll<T> {
+        critical_section::with(|_| unsafe {
+            let state = &mut *self.state.get();
+            match state {
+                State::None => {
+                    *state = State::Waiting(cx.waker().clone());
+                    Poll::Pending
+                }
+                State::Waiting(w) if w.will_wake(cx.waker()) => Poll::Pending,
+                State::Waiting(_) => panic!("waker overflow"),
+                State::Signaled(_) => match mem::replace(state, State::None) {
+                    State::Signaled(res) => Poll::Ready(res),
+                    _ => unreachable!(),
+                },
+            }
+        })
+    }
+
+    /// Future that completes when this Signal has been signaled.
+    pub fn wait(&self) -> impl Future<Output = T> + '_ {
+        futures::future::poll_fn(move |cx| self.poll_wait(cx))
+    }
+
+    /// non-blocking method to check whether this signal has been signaled.
+    pub fn signaled(&self) -> bool {
+        critical_section::with(|_| matches!(unsafe { &*self.state.get() }, State::Signaled(_)))
+    }
+}
diff --git a/embassy/src/lib.rs b/embassy/src/lib.rs
index 9e050a57a..847142285 100644
--- a/embassy/src/lib.rs
+++ b/embassy/src/lib.rs
@@ -7,6 +7,10 @@
 // This mod MUST go first, so that the others see its macros.
 pub(crate) mod fmt;
 
+pub mod blocking_mutex;
+pub mod channel;
+pub mod waitqueue;
+
 pub mod executor;
 pub mod interrupt;
 pub mod io;
diff --git a/embassy/src/util/mod.rs b/embassy/src/util/mod.rs
index e66576b33..f832fa2f6 100644
--- a/embassy/src/util/mod.rs
+++ b/embassy/src/util/mod.rs
@@ -1,21 +1,7 @@
-//! Async utilities
-mod drop_bomb;
+//! Misc utilities
 mod forever;
-mod mutex;
-mod on_drop;
-mod signal;
 
-#[cfg_attr(feature = "executor-agnostic", path = "waker_agnostic.rs")]
-mod waker;
-
-pub use drop_bomb::*;
 pub use forever::*;
-pub mod mpsc;
-pub use mutex::*;
-pub use on_drop::*;
-pub use signal::*;
-pub use waker::*;
-
 /// Unsafely unborrow an owned singleton out of a `&mut`.
 ///
 /// It is intended to be implemented for owned peripheral singletons, such as `USART3` or `AnyPin`.
diff --git a/embassy/src/util/on_drop.rs b/embassy/src/util/on_drop.rs
deleted file mode 100644
index 10f3407f4..000000000
--- a/embassy/src/util/on_drop.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use core::mem;
-use core::mem::MaybeUninit;
-
-pub struct OnDrop<F: FnOnce()> {
-    f: MaybeUninit<F>,
-}
-
-impl<F: FnOnce()> OnDrop<F> {
-    pub fn new(f: F) -> Self {
-        Self {
-            f: MaybeUninit::new(f),
-        }
-    }
-
-    pub fn defuse(self) {
-        mem::forget(self)
-    }
-}
-
-impl<F: FnOnce()> Drop for OnDrop<F> {
-    fn drop(&mut self) {
-        unsafe { self.f.as_ptr().read()() }
-    }
-}
diff --git a/embassy/src/util/signal.rs b/embassy/src/util/signal.rs
deleted file mode 100644
index bb832533c..000000000
--- a/embassy/src/util/signal.rs
+++ /dev/null
@@ -1,171 +0,0 @@
-use core::cell::UnsafeCell;
-use core::future::Future;
-use core::mem;
-use core::ptr;
-use core::task::{Context, Poll, Waker};
-use cortex_m::peripheral::NVIC;
-use cortex_m::peripheral::{scb, SCB};
-use executor::raw::TaskHeader;
-use ptr::NonNull;
-
-use crate::executor;
-use crate::interrupt::{Interrupt, InterruptExt};
-
-/// Synchronization primitive. Allows creating awaitable signals that may be passed between tasks.
-///
-/// For more advanced use cases, please consider [futures-intrusive](https://crates.io/crates/futures-intrusive) channels or mutexes.
-pub struct Signal<T> {
-    state: UnsafeCell<State<T>>,
-}
-
-enum State<T> {
-    None,
-    Waiting(Waker),
-    Signaled(T),
-}
-
-unsafe impl<T: Send> Send for Signal<T> {}
-unsafe impl<T: Send> Sync for Signal<T> {}
-
-impl<T: Send> Signal<T> {
-    pub const fn new() -> Self {
-        Self {
-            state: UnsafeCell::new(State::None),
-        }
-    }
-
-    /// Mark this Signal as completed.
-    pub fn signal(&self, val: T) {
-        critical_section::with(|_| unsafe {
-            let state = &mut *self.state.get();
-            if let State::Waiting(waker) = mem::replace(state, State::Signaled(val)) {
-                waker.wake();
-            }
-        })
-    }
-
-    pub fn reset(&self) {
-        critical_section::with(|_| unsafe {
-            let state = &mut *self.state.get();
-            *state = State::None
-        })
-    }
-
-    pub fn poll_wait(&self, cx: &mut Context<'_>) -> Poll<T> {
-        critical_section::with(|_| unsafe {
-            let state = &mut *self.state.get();
-            match state {
-                State::None => {
-                    *state = State::Waiting(cx.waker().clone());
-                    Poll::Pending
-                }
-                State::Waiting(w) if w.will_wake(cx.waker()) => Poll::Pending,
-                State::Waiting(_) => panic!("waker overflow"),
-                State::Signaled(_) => match mem::replace(state, State::None) {
-                    State::Signaled(res) => Poll::Ready(res),
-                    _ => unreachable!(),
-                },
-            }
-        })
-    }
-
-    /// Future that completes when this Signal has been signaled.
-    pub fn wait(&self) -> impl Future<Output = T> + '_ {
-        futures::future::poll_fn(move |cx| self.poll_wait(cx))
-    }
-
-    /// non-blocking method to check whether this signal has been signaled.
-    pub fn signaled(&self) -> bool {
-        critical_section::with(|_| matches!(unsafe { &*self.state.get() }, State::Signaled(_)))
-    }
-}
-
-// ==========
-
-pub fn wake_on_interrupt(interrupt: &mut impl Interrupt, waker: &Waker) {
-    interrupt.disable();
-    interrupt.set_handler(irq_wake_handler);
-    interrupt.set_handler_context(unsafe { executor::raw::task_from_waker(waker) }.as_ptr() as _);
-    interrupt.enable();
-}
-
-unsafe fn irq_wake_handler(ctx: *mut ()) {
-    if let Some(task) = NonNull::new(ctx as *mut TaskHeader) {
-        executor::raw::wake_task(task);
-    }
-
-    let irq = match SCB::vect_active() {
-        scb::VectActive::Interrupt { irqn } => irqn,
-        _ => unreachable!(),
-    };
-
-    NVIC::mask(crate::interrupt::NrWrap(irq as u16));
-}
-
-// ==========
-
-struct NrWrap(u8);
-unsafe impl cortex_m::interrupt::Nr for NrWrap {
-    fn nr(&self) -> u8 {
-        self.0
-    }
-}
-
-/// Creates a future that completes when the specified Interrupt is triggered.
-///
-/// The input handler is unregistered when this Future is dropped.
-///
-/// Example:
-/// ``` no_compile
-/// use embassy::traits::*;
-/// use embassy::util::InterruptFuture;
-/// use embassy_stm32::interrupt; // Adjust this to your MCU's embassy HAL.
-/// #[embassy::task]
-/// async fn demo_interrupt_future() {
-///     // Using STM32f446 interrupt names, adjust this to your application as necessary.
-///     // Wait for TIM2 to tick.
-///     let mut tim2_interrupt = interrupt::take!(TIM2);
-///     InterruptFuture::new(&mut tim2_interrupt).await;
-///     // TIM2 interrupt went off, do something...
-/// }
-/// ```
-pub struct InterruptFuture<'a, I: Interrupt> {
-    interrupt: &'a mut I,
-}
-
-impl<'a, I: Interrupt> Drop for InterruptFuture<'a, I> {
-    fn drop(&mut self) {
-        self.interrupt.disable();
-        self.interrupt.remove_handler();
-    }
-}
-
-impl<'a, I: Interrupt> InterruptFuture<'a, I> {
-    pub fn new(interrupt: &'a mut I) -> Self {
-        interrupt.disable();
-        interrupt.set_handler(irq_wake_handler);
-        interrupt.set_handler_context(ptr::null_mut());
-        interrupt.unpend();
-        interrupt.enable();
-
-        Self { interrupt }
-    }
-}
-
-impl<'a, I: Interrupt> Unpin for InterruptFuture<'a, I> {}
-
-impl<'a, I: Interrupt> Future for InterruptFuture<'a, I> {
-    type Output = ();
-
-    fn poll(self: core::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
-        let s = unsafe { self.get_unchecked_mut() };
-        s.interrupt.set_handler_context(unsafe {
-            executor::raw::task_from_waker(&cx.waker()).cast().as_ptr()
-        });
-        if s.interrupt.is_enabled() {
-            Poll::Pending
-        } else {
-            Poll::Ready(())
-        }
-    }
-}
diff --git a/embassy/src/waitqueue/mod.rs b/embassy/src/waitqueue/mod.rs
new file mode 100644
index 000000000..a2bafad99
--- /dev/null
+++ b/embassy/src/waitqueue/mod.rs
@@ -0,0 +1,5 @@
+//! Async low-level wait queues
+
+#[cfg_attr(feature = "executor-agnostic", path = "waker_agnostic.rs")]
+mod waker;
+pub use waker::*;
diff --git a/embassy/src/util/waker.rs b/embassy/src/waitqueue/waker.rs
similarity index 99%
rename from embassy/src/util/waker.rs
rename to embassy/src/waitqueue/waker.rs
index 1ac6054f9..9eddbdaa1 100644
--- a/embassy/src/util/waker.rs
+++ b/embassy/src/waitqueue/waker.rs
@@ -1,8 +1,7 @@
+use atomic_polyfill::{compiler_fence, AtomicPtr, Ordering};
 use core::ptr::{self, NonNull};
 use core::task::Waker;
 
-use atomic_polyfill::{compiler_fence, AtomicPtr, Ordering};
-
 use crate::executor::raw::{task_from_waker, wake_task, TaskHeader};
 
 /// Utility struct to register and wake a waker.
diff --git a/embassy/src/util/waker_agnostic.rs b/embassy/src/waitqueue/waker_agnostic.rs
similarity index 97%
rename from embassy/src/util/waker_agnostic.rs
rename to embassy/src/waitqueue/waker_agnostic.rs
index 1675c53a0..f583fa6f4 100644
--- a/embassy/src/util/waker_agnostic.rs
+++ b/embassy/src/waitqueue/waker_agnostic.rs
@@ -2,7 +2,7 @@ use core::cell::Cell;
 use core::mem;
 use core::task::Waker;
 
-use crate::util::CriticalSectionMutex as Mutex;
+use crate::blocking_mutex::CriticalSectionMutex as Mutex;
 
 /// Utility struct to register and wake a waker.
 #[derive(Debug)]
diff --git a/examples/nrf/src/bin/mpsc.rs b/examples/nrf/src/bin/mpsc.rs
index c8cc67d77..79fa3dfb9 100644
--- a/examples/nrf/src/bin/mpsc.rs
+++ b/examples/nrf/src/bin/mpsc.rs
@@ -6,14 +6,13 @@
 mod example_common;
 
 use defmt::unwrap;
+use embassy::channel::mpsc::{self, Channel, Sender, TryRecvError, WithNoThreads};
 use embassy::executor::Spawner;
 use embassy::time::{Duration, Timer};
-use embassy::util::mpsc::TryRecvError;
-use embassy::util::{mpsc, Forever};
+use embassy::util::Forever;
 use embassy_nrf::gpio::{Level, Output, OutputDrive};
 use embassy_nrf::Peripherals;
 use embedded_hal::digital::v2::OutputPin;
-use mpsc::{Channel, Sender, WithNoThreads};
 
 enum LedState {
     On,
diff --git a/examples/stm32wl55/src/bin/subghz.rs b/examples/stm32wl55/src/bin/subghz.rs
index 1e406886a..89549c766 100644
--- a/examples/stm32wl55/src/bin/subghz.rs
+++ b/examples/stm32wl55/src/bin/subghz.rs
@@ -8,16 +8,16 @@
 #[path = "../example_common.rs"]
 mod example_common;
 
-use embassy::{traits::gpio::WaitForRisingEdge, util::InterruptFuture};
-use embassy_stm32::{
-    dbgmcu::Dbgmcu,
-    dma::NoDma,
-    exti::ExtiInput,
-    gpio::{Input, Level, Output, Pull, Speed},
-    interrupt,
-    subghz::*,
-    Peripherals,
-};
+use embassy::channel::signal::Signal;
+use embassy::interrupt::{Interrupt, InterruptExt};
+use embassy::traits::gpio::WaitForRisingEdge;
+use embassy_stm32::dbgmcu::Dbgmcu;
+use embassy_stm32::dma::NoDma;
+use embassy_stm32::exti::ExtiInput;
+use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
+use embassy_stm32::interrupt;
+use embassy_stm32::subghz::*;
+use embassy_stm32::Peripherals;
 use embedded_hal::digital::v2::OutputPin;
 use example_common::unwrap;
 
@@ -83,7 +83,13 @@ async fn main(_spawner: embassy::executor::Spawner, p: Peripherals) {
     let button = Input::new(p.PA0, Pull::Up);
     let mut pin = ExtiInput::new(button, p.EXTI0);
 
-    let mut radio_irq = interrupt::take!(SUBGHZ_RADIO);
+    static IRQ_SIGNAL: Signal<()> = Signal::new();
+    let radio_irq = interrupt::take!(SUBGHZ_RADIO);
+    radio_irq.set_handler(|_| {
+        IRQ_SIGNAL.signal(());
+        unsafe { interrupt::SUBGHZ_RADIO::steal() }.disable();
+    });
+
     let mut radio = SubGhz::new(p.SUBGHZSPI, p.PA5, p.PA7, p.PA6, NoDma, NoDma);
 
     defmt::info!("Radio ready for use");
@@ -118,7 +124,9 @@ async fn main(_spawner: embassy::executor::Spawner, p: Peripherals) {
         unwrap!(radio.write_buffer(TX_BUF_OFFSET, PING_DATA_BYTES));
         unwrap!(radio.set_tx(Timeout::DISABLED));
 
-        InterruptFuture::new(&mut radio_irq).await;
+        radio_irq.enable();
+        IRQ_SIGNAL.wait().await;
+
         let (_, irq_status) = unwrap!(radio.irq_status());
         if irq_status & Irq::TxDone.mask() != 0 {
             defmt::info!("TX done");