diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 767e42243..f1256320d 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -12,11 +12,11 @@ env:
 jobs:
   all:
     runs-on: ubuntu-20.04
-    needs: [build, test]
+    needs: [build-nightly, build-stable, test]
     steps:
       - name: Done
         run: exit 0
-  build:
+  build-nightly:
     runs-on: ubuntu-latest
     permissions:
       id-token: write
@@ -41,6 +41,28 @@ jobs:
           chmod +x /usr/local/bin/cargo-batch
           ./ci.sh
           rm -rf target_ci/*{,/release}/{build,deps,.fingerprint}/{lib,}{embassy,stm32}*
+  build-stable:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          submodules: true
+      - name: Cache multiple paths
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/bin/
+            ~/.cargo/registry/index/
+            ~/.cargo/registry/cache/
+            ~/.cargo/git/db/
+            target_ci_stable
+          key: rust-stable-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml') }}
+      - name: build
+        run: |
+          curl -L -o /usr/local/bin/cargo-batch https://github.com/embassy-rs/cargo-batch/releases/download/batch-0.1.0/cargo-batch
+          chmod +x /usr/local/bin/cargo-batch
+          ./ci_stable.sh
+          rm -rf target_ci_stable/*{,/release}/{build,deps,.fingerprint}/{lib,}{embassy,stm32}*
 
   test:
     runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 92f9a32b2..144dd703f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 target
 target_ci
+target_ci_stable
 Cargo.lock
 third_party
 /Cargo.toml
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3defc9077..79433a7c9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,16 +4,19 @@
   "rust-analyzer.assist.importGranularity": "module",
   "rust-analyzer.checkOnSave.allFeatures": false,
   "rust-analyzer.checkOnSave.allTargets": false,
-  "rust-analyzer.checkOnSave.command": "clippy",
+  "rust-analyzer.checkOnSave.noDefaultFeatures": true,
+  "rust-analyzer.cargo.allFeatures": false,
   "rust-analyzer.cargo.noDefaultFeatures": true,
   "rust-analyzer.experimental.procAttrMacros": false,
-  "rust-analyzer.checkOnSave.noDefaultFeatures": true,
+  "rust-analyzer.procMacro.enable": true,
+  "rust-analyzer.cargo.runBuildScripts": true,
   "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",
+    "nightly",
   ],
   "rust-analyzer.linkedProjects": [
     // Declare for the target you wish to develop
@@ -35,12 +38,4 @@
     // "examples/stm32wl55/Cargo.toml",
     // "examples/wasm/Cargo.toml",
   ],
-  "rust-analyzer.procMacro.enable": true,
-  "rust-analyzer.cargo.runBuildScripts": true,
-  "files.watcherExclude": {
-    "**/.git/objects/**": true,
-    "**/.git/subtree-cache/**": true,
-    "**/target/**": true
-  },
-  "git.ignoreLimitWarning": true
 }
\ No newline at end of file
diff --git a/ci.sh b/ci.sh
index 452130be2..cd0db286e 100755
--- a/ci.sh
+++ b/ci.sh
@@ -24,24 +24,24 @@ rm -rf stm32-metapac
 mv stm32-metapac-gen/out stm32-metapac
 
 cargo batch  \
-    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi \
-    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features log,executor-agnostic \
-    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features defmt \
-    --- build --release --manifest-path embassy/Cargo.toml --target thumbv6m-none-eabi --features defmt \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52805,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52810,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52811,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52820,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52832,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52833,gpiote,time-driver-rtc1,unstable-traits \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf9160-s,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf9160-ns,gpiote,time-driver-rtc1,unstable-traits \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf5340-app-s,gpiote,time-driver-rtc1,unstable-traits \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf5340-app-ns,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf5340-net,gpiote,time-driver-rtc1,unstable-traits \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52840,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52840,log,gpiote,time-driver-rtc1 \
-    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52840,defmt,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features nightly \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features nightly,log,executor-agnostic \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features nightly,defmt \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv6m-none-eabi --features nightly,defmt \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52805,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52810,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52811,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52820,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52832,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52833,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nightly,nrf9160-s,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nightly,nrf9160-ns,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nightly,nrf5340-app-s,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nightly,nrf5340-app-ns,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nightly,nrf5340-net,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52840,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52840,log,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nightly,nrf52840,defmt,gpiote,time-driver-rtc1,unstable-traits \
     --- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7em-none-eabi --features stm32f411ce,defmt,time-driver-any \
     --- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7em-none-eabi --features stm32f411ce,defmt,time-driver-any,unstable-traits \
     --- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7em-none-eabi --features stm32f429zi,log,time-driver-any \
diff --git a/ci_stable.sh b/ci_stable.sh
new file mode 100755
index 000000000..c4ec30824
--- /dev/null
+++ b/ci_stable.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+set -euo pipefail
+
+export CARGO_TARGET_DIR=$PWD/target_ci_stable
+export RUSTFLAGS=-Dwarnings
+export DEFMT_LOG=trace
+
+sed -i 's/channel.*/channel = "stable"/g' rust-toolchain.toml
+
+cargo batch  \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features log,executor-agnostic \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv7em-none-eabi --features defmt \
+    --- build --release --manifest-path embassy/Cargo.toml --target thumbv6m-none-eabi --features defmt \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52805,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52810,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52811,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52820,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52832,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52833,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf9160-s,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf9160-ns,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf5340-app-s,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf5340-app-ns,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv8m.main-none-eabihf --features nrf5340-net,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52840,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52840,log,gpiote,time-driver-rtc1 \
+    --- build --release --manifest-path embassy-nrf/Cargo.toml --target thumbv7em-none-eabi --features nrf52840,defmt,gpiote,time-driver-rtc1,unstable-traits \
+    --- build --release --manifest-path examples/nrf/Cargo.toml --target thumbv7em-none-eabi --no-default-features --out-dir out/examples/nrf --bin raw_spawn \
diff --git a/docs/modules/ROOT/examples/basic/Cargo.toml b/docs/modules/ROOT/examples/basic/Cargo.toml
index 0f1c30da3..ed1c3cb1c 100644
--- a/docs/modules/ROOT/examples/basic/Cargo.toml
+++ b/docs/modules/ROOT/examples/basic/Cargo.toml
@@ -5,8 +5,8 @@ name = "embassy-basic-example"
 version = "0.1.0"
 
 [dependencies]
-embassy = { version = "0.1.0", path = "../../../../../embassy", features = ["defmt"] }
-embassy-nrf = { version = "0.1.0", path = "../../../../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote"] }
+embassy = { version = "0.1.0", path = "../../../../../embassy", features = ["defmt", "nightly"] }
+embassy-nrf = { version = "0.1.0", path = "../../../../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "nightly"] }
 
 defmt = "0.3"
 defmt-rtt = "0.3"
diff --git a/embassy-macros/src/chip/nrf.rs b/embassy-macros/src/chip/nrf.rs
deleted file mode 100644
index 3ff6a74cf..000000000
--- a/embassy-macros/src/chip/nrf.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use crate::path::ModulePrefix;
-use proc_macro2::TokenStream;
-use quote::quote;
-
-pub fn generate(embassy_prefix: &ModulePrefix, config: syn::Expr) -> TokenStream {
-    let embassy_nrf_path = embassy_prefix.append("embassy_nrf").path();
-
-    quote!(
-        let p = #embassy_nrf_path::init(#config);
-    )
-}
diff --git a/embassy-macros/src/chip/rp.rs b/embassy-macros/src/chip/rp.rs
deleted file mode 100644
index ba0a97ada..000000000
--- a/embassy-macros/src/chip/rp.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-use crate::path::ModulePrefix;
-use proc_macro2::TokenStream;
-use quote::quote;
-
-pub fn generate(embassy_prefix: &ModulePrefix, config: syn::Expr) -> TokenStream {
-    let embassy_rp_path = embassy_prefix.append("embassy_rp").path();
-    quote!(
-        let p = #embassy_rp_path::init(#config);
-    )
-}
diff --git a/embassy-macros/src/chip/stm32.rs b/embassy-macros/src/chip/stm32.rs
deleted file mode 100644
index c6938836c..000000000
--- a/embassy-macros/src/chip/stm32.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use crate::path::ModulePrefix;
-use proc_macro2::TokenStream;
-use quote::quote;
-
-pub fn generate(embassy_prefix: &ModulePrefix, config: syn::Expr) -> TokenStream {
-    let embassy_stm32_path = embassy_prefix.append("embassy_stm32").path();
-
-    quote!(
-        let p = #embassy_stm32_path::init(#config);
-    )
-}
diff --git a/embassy-macros/src/lib.rs b/embassy-macros/src/lib.rs
index 44a8d3b93..085f7889d 100644
--- a/embassy-macros/src/lib.rs
+++ b/embassy-macros/src/lib.rs
@@ -1,228 +1,39 @@
-#![feature(proc_macro_diagnostic)]
-
 extern crate proc_macro;
 
-use darling::FromMeta;
 use proc_macro::TokenStream;
-use proc_macro2::Span;
-use quote::{format_ident, quote};
-use std::iter;
-use syn::spanned::Spanned;
-use syn::{parse, Type, Visibility};
-use syn::{ItemFn, ReturnType};
 
-mod path;
-
-use path::ModulePrefix;
-
-#[derive(Debug, FromMeta)]
-struct TaskArgs {
-    #[darling(default)]
-    pool_size: Option<usize>,
-    #[darling(default)]
-    send: bool,
-    #[darling(default)]
-    embassy_prefix: ModulePrefix,
-}
+mod macros;
+mod util;
+use macros::*;
 
 #[proc_macro_attribute]
 pub fn task(args: TokenStream, item: TokenStream) -> TokenStream {
-    let macro_args = syn::parse_macro_input!(args as syn::AttributeArgs);
-    let mut task_fn = syn::parse_macro_input!(item as syn::ItemFn);
+    let args = syn::parse_macro_input!(args as syn::AttributeArgs);
+    let f = syn::parse_macro_input!(item as syn::ItemFn);
 
-    let macro_args = match TaskArgs::from_list(&macro_args) {
-        Ok(v) => v,
-        Err(e) => {
-            return TokenStream::from(e.write_errors());
-        }
-    };
-
-    let embassy_prefix = macro_args.embassy_prefix.append("embassy");
-    let embassy_path = embassy_prefix.path();
-
-    let pool_size: usize = macro_args.pool_size.unwrap_or(1);
-
-    let mut fail = false;
-    if task_fn.sig.asyncness.is_none() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("task functions must be async")
-            .emit();
-        fail = true;
-    }
-    if !task_fn.sig.generics.params.is_empty() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("task functions must not be generic")
-            .emit();
-        fail = true;
-    }
-    if pool_size < 1 {
-        return parse::Error::new(Span::call_site(), "pool_size must be 1 or greater")
-            .to_compile_error()
-            .into();
-    }
-
-    let mut arg_names: syn::punctuated::Punctuated<syn::Ident, syn::Token![,]> =
-        syn::punctuated::Punctuated::new();
-    let mut args = task_fn.sig.inputs.clone();
-
-    for arg in args.iter_mut() {
-        match arg {
-            syn::FnArg::Receiver(_) => {
-                arg.span()
-                    .unwrap()
-                    .error("task functions must not have receiver arguments")
-                    .emit();
-                fail = true;
-            }
-            syn::FnArg::Typed(t) => match t.pat.as_mut() {
-                syn::Pat::Ident(i) => {
-                    arg_names.push(i.ident.clone());
-                    i.mutability = None;
-                }
-                _ => {
-                    arg.span()
-                        .unwrap()
-                        .error("pattern matching in task arguments is not yet supporteds")
-                        .emit();
-                    fail = true;
-                }
-            },
-        }
-    }
-
-    if fail {
-        return TokenStream::new();
-    }
-
-    let name = task_fn.sig.ident.clone();
-
-    let visibility = &task_fn.vis;
-    task_fn.sig.ident = format_ident!("task");
-    let impl_ty = if macro_args.send {
-        quote!(impl ::core::future::Future + Send + 'static)
-    } else {
-        quote!(impl ::core::future::Future + 'static)
-    };
-
-    let attrs = &task_fn.attrs;
-
-    let result = quote! {
-        #(#attrs)*
-        #visibility fn #name(#args) -> #embassy_path::executor::SpawnToken<#impl_ty> {
-            use #embassy_path::executor::raw::TaskStorage;
-            #task_fn
-            type F = #impl_ty;
-            #[allow(clippy::declare_interior_mutable_const)]
-            const NEW_TASK: TaskStorage<F> = TaskStorage::new();
-            static POOL: [TaskStorage<F>; #pool_size] = [NEW_TASK; #pool_size];
-            unsafe { TaskStorage::spawn_pool(&POOL, move || task(#arg_names)) }
-        }
-    };
-    result.into()
+    task::run(args, f).unwrap_or_else(|x| x).into()
 }
 
 #[proc_macro_attribute]
-pub fn interrupt(args: TokenStream, input: TokenStream) -> TokenStream {
-    let mut f: ItemFn = syn::parse(input).expect("`#[interrupt]` must be applied to a function");
+pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
+    let args = syn::parse_macro_input!(args as syn::AttributeArgs);
+    let f = syn::parse_macro_input!(item as syn::ItemFn);
+    main::run(args, f).unwrap_or_else(|x| x).into()
+}
 
-    if !args.is_empty() {
-        return parse::Error::new(Span::call_site(), "This attribute accepts no arguments")
-            .to_compile_error()
-            .into();
-    }
-
-    let fspan = f.span();
-    let ident = f.sig.ident.clone();
-    let ident_s = ident.to_string();
-
-    // XXX should we blacklist other attributes?
-
-    let valid_signature = f.sig.constness.is_none()
-        && f.vis == Visibility::Inherited
-        && f.sig.abi.is_none()
-        && f.sig.inputs.is_empty()
-        && f.sig.generics.params.is_empty()
-        && f.sig.generics.where_clause.is_none()
-        && f.sig.variadic.is_none()
-        && match f.sig.output {
-            ReturnType::Default => true,
-            ReturnType::Type(_, ref ty) => match **ty {
-                Type::Tuple(ref tuple) => tuple.elems.is_empty(),
-                Type::Never(..) => true,
-                _ => false,
-            },
-        };
-
-    if !valid_signature {
-        return parse::Error::new(
-            fspan,
-            "`#[interrupt]` handlers must have signature `[unsafe] fn() [-> !]`",
-        )
-        .to_compile_error()
-        .into();
-    }
-
-    f.block.stmts = iter::once(
-        syn::parse2(quote! {{
-            // Check that this interrupt actually exists
-            let __irq_exists_check: interrupt::#ident;
-        }})
-        .unwrap(),
-    )
-    .chain(f.block.stmts)
-    .collect();
-
-    quote!(
-        #[doc(hidden)]
-        #[export_name = #ident_s]
-        #[allow(non_snake_case)]
-        #f
-    )
-    .into()
+#[proc_macro_attribute]
+pub fn interrupt(args: TokenStream, item: TokenStream) -> TokenStream {
+    let args = syn::parse_macro_input!(args as syn::AttributeArgs);
+    let f = syn::parse_macro_input!(item as syn::ItemFn);
+    interrupt::run(args, f).unwrap_or_else(|x| x).into()
 }
 
 #[proc_macro]
 pub fn interrupt_declare(item: TokenStream) -> TokenStream {
     let name = syn::parse_macro_input!(item as syn::Ident);
-    let name = format_ident!("{}", name);
-    let name_interrupt = format_ident!("{}", name);
-    let name_handler = format!("__EMBASSY_{}_HANDLER", name);
-
-    let result = quote! {
-        #[allow(non_camel_case_types)]
-        pub struct #name_interrupt(());
-        unsafe impl ::embassy::interrupt::Interrupt for #name_interrupt {
-            type Priority = crate::interrupt::Priority;
-            fn number(&self) -> u16 {
-                use cortex_m::interrupt::InterruptNumber;
-                let irq = InterruptEnum::#name;
-                irq.number() as u16
-            }
-            unsafe fn steal() -> Self {
-                Self(())
-            }
-            unsafe fn __handler(&self) -> &'static ::embassy::interrupt::Handler {
-                #[export_name = #name_handler]
-                static HANDLER: ::embassy::interrupt::Handler = ::embassy::interrupt::Handler::new();
-                &HANDLER
-            }
-        }
-
-        unsafe impl ::embassy::util::Unborrow for #name_interrupt {
-            type Target = #name_interrupt;
-            unsafe fn unborrow(self) -> #name_interrupt {
-                self
-            }
-        }
-    };
-    result.into()
+    interrupt_declare::run(name).unwrap_or_else(|x| x).into()
 }
+
 /// # interrupt_take procedural macro
 ///
 /// core::panic! is used as a default way to panic in this macro as there is no sensible way of enabling/disabling defmt for macro generation.
@@ -231,312 +42,5 @@ pub fn interrupt_declare(item: TokenStream) -> TokenStream {
 #[proc_macro]
 pub fn interrupt_take(item: TokenStream) -> TokenStream {
     let name = syn::parse_macro_input!(item as syn::Ident);
-    let name = format!("{}", name);
-    let name_interrupt = format_ident!("{}", name);
-    let name_handler = format!("__EMBASSY_{}_HANDLER", name);
-
-    let result = quote! {
-        {
-            #[allow(non_snake_case)]
-            #[export_name = #name]
-            pub unsafe extern "C" fn trampoline() {
-                extern "C" {
-                    #[link_name = #name_handler]
-                    static HANDLER: ::embassy::interrupt::Handler;
-                }
-
-                let func = HANDLER.func.load(::embassy::export::atomic::Ordering::Relaxed);
-                let ctx = HANDLER.ctx.load(::embassy::export::atomic::Ordering::Relaxed);
-                let func: fn(*mut ()) = ::core::mem::transmute(func);
-                func(ctx)
-            }
-
-            static TAKEN: ::embassy::export::atomic::AtomicBool = ::embassy::export::atomic::AtomicBool::new(false);
-
-            if TAKEN.compare_exchange(false, true, ::embassy::export::atomic::Ordering::AcqRel, ::embassy::export::atomic::Ordering::Acquire).is_err() {
-                core::panic!("IRQ Already taken");
-            }
-
-            let irq: interrupt::#name_interrupt = unsafe { ::core::mem::transmute(()) };
-            irq
-        }
-    };
-    result.into()
-}
-
-#[cfg(feature = "stm32")]
-#[path = "chip/stm32.rs"]
-mod chip;
-
-#[cfg(feature = "nrf")]
-#[path = "chip/nrf.rs"]
-mod chip;
-
-#[cfg(feature = "rp")]
-#[path = "chip/rp.rs"]
-mod chip;
-
-#[cfg(any(
-    feature = "nrf",
-    feature = "rp",
-    feature = "stm32",
-    feature = "wasm",
-    feature = "std"
-))]
-#[derive(Debug, FromMeta)]
-struct MainArgs {
-    #[darling(default)]
-    embassy_prefix: ModulePrefix,
-
-    #[allow(unused)]
-    #[darling(default)]
-    config: Option<syn::LitStr>,
-}
-
-#[cfg(any(feature = "nrf", feature = "rp", feature = "stm32"))]
-#[proc_macro_attribute]
-pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
-    let macro_args = syn::parse_macro_input!(args as syn::AttributeArgs);
-    let task_fn = syn::parse_macro_input!(item as syn::ItemFn);
-
-    let macro_args = match MainArgs::from_list(&macro_args) {
-        Ok(v) => v,
-        Err(e) => {
-            return TokenStream::from(e.write_errors());
-        }
-    };
-
-    let mut fail = false;
-    if task_fn.sig.asyncness.is_none() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("task functions must be async")
-            .emit();
-        fail = true;
-    }
-    if !task_fn.sig.generics.params.is_empty() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("main function must not be generic")
-            .emit();
-        fail = true;
-    }
-
-    let args = task_fn.sig.inputs.clone();
-
-    if args.len() != 2 {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("main function must have 2 arguments")
-            .emit();
-        fail = true;
-    }
-
-    if fail {
-        return TokenStream::new();
-    }
-
-    let embassy_prefix = macro_args.embassy_prefix;
-    let embassy_prefix_lit = embassy_prefix.literal();
-    let embassy_path = embassy_prefix.append("embassy").path();
-    let task_fn_body = task_fn.block;
-
-    let config = macro_args
-        .config
-        .map(|s| s.parse::<syn::Expr>().unwrap())
-        .unwrap_or_else(|| {
-            syn::Expr::Verbatim(quote! {
-                Default::default()
-            })
-        });
-
-    let chip_setup = chip::generate(&embassy_prefix, config);
-
-    let result = quote! {
-        #[#embassy_path::task(embassy_prefix = #embassy_prefix_lit)]
-        async fn __embassy_main(#args) {
-            #task_fn_body
-        }
-
-        #[cortex_m_rt::entry]
-        fn main() -> ! {
-            unsafe fn make_static<T>(t: &mut T) -> &'static mut T {
-                ::core::mem::transmute(t)
-            }
-
-            #chip_setup
-
-            let mut executor = #embassy_path::executor::Executor::new();
-            let executor = unsafe { make_static(&mut executor) };
-
-            executor.run(|spawner| {
-                spawner.must_spawn(__embassy_main(spawner, p));
-            })
-        }
-    };
-    result.into()
-}
-
-#[cfg(feature = "std")]
-#[proc_macro_attribute]
-pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
-    let macro_args = syn::parse_macro_input!(args as syn::AttributeArgs);
-    let task_fn = syn::parse_macro_input!(item as syn::ItemFn);
-
-    let macro_args = match MainArgs::from_list(&macro_args) {
-        Ok(v) => v,
-        Err(e) => {
-            return TokenStream::from(e.write_errors());
-        }
-    };
-
-    let embassy_path = macro_args.embassy_prefix.append("embassy");
-
-    let mut fail = false;
-    if task_fn.sig.asyncness.is_none() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("task functions must be async")
-            .emit();
-        fail = true;
-    }
-    if !task_fn.sig.generics.params.is_empty() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("main function must not be generic")
-            .emit();
-        fail = true;
-    }
-
-    let args = task_fn.sig.inputs.clone();
-
-    if args.len() != 1 {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("main function must have one argument")
-            .emit();
-        fail = true;
-    }
-
-    if fail {
-        return TokenStream::new();
-    }
-
-    let task_fn_body = task_fn.block.clone();
-
-    let embassy_path = embassy_path.path();
-    let embassy_prefix_lit = macro_args.embassy_prefix.literal();
-
-    let result = quote! {
-        #[#embassy_path::task(embassy_prefix = #embassy_prefix_lit)]
-        async fn __embassy_main(#args) {
-            #task_fn_body
-        }
-
-        fn main() -> ! {
-            unsafe fn make_static<T>(t: &mut T) -> &'static mut T {
-                ::core::mem::transmute(t)
-            }
-
-            let mut executor = #embassy_path::executor::Executor::new();
-            let executor = unsafe { make_static(&mut executor) };
-
-            executor.run(|spawner| {
-                spawner.spawn(__embassy_main(spawner)).unwrap();
-            })
-
-        }
-    };
-    result.into()
-}
-
-#[cfg(feature = "wasm")]
-#[proc_macro_attribute]
-pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
-    let macro_args = syn::parse_macro_input!(args as syn::AttributeArgs);
-    let task_fn = syn::parse_macro_input!(item as syn::ItemFn);
-
-    let macro_args = match MainArgs::from_list(&macro_args) {
-        Ok(v) => v,
-        Err(e) => {
-            return TokenStream::from(e.write_errors());
-        }
-    };
-
-    let embassy_path = macro_args.embassy_prefix.append("embassy");
-
-    let mut fail = false;
-    if task_fn.sig.asyncness.is_none() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("task functions must be async")
-            .emit();
-        fail = true;
-    }
-    if !task_fn.sig.generics.params.is_empty() {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("main function must not be generic")
-            .emit();
-        fail = true;
-    }
-
-    let args = task_fn.sig.inputs.clone();
-
-    if args.len() != 1 {
-        task_fn
-            .sig
-            .span()
-            .unwrap()
-            .error("main function must have one argument")
-            .emit();
-        fail = true;
-    }
-
-    if fail {
-        return TokenStream::new();
-    }
-
-    let task_fn_body = task_fn.block.clone();
-
-    let embassy_path = embassy_path.path();
-    let embassy_prefix_lit = macro_args.embassy_prefix.literal();
-
-    let result = quote! {
-        #[#embassy_path::task(embassy_prefix = #embassy_prefix_lit)]
-        async fn __embassy_main(#args) {
-            #task_fn_body
-        }
-
-        use wasm_bindgen::prelude::*;
-
-        #[wasm_bindgen(start)]
-        pub fn main() -> Result<(), JsValue> {
-            static EXECUTOR: #embassy_path::util::Forever<#embassy_path::executor::Executor> = #embassy_path::util::Forever::new();
-            let executor = EXECUTOR.put(#embassy_path::executor::Executor::new());
-
-            executor.start(|spawner| {
-                spawner.spawn(__embassy_main(spawner)).unwrap();
-            });
-
-            Ok(())
-        }
-    };
-    result.into()
+    interrupt_take::run(name).unwrap_or_else(|x| x).into()
 }
diff --git a/embassy-macros/src/macros/interrupt.rs b/embassy-macros/src/macros/interrupt.rs
new file mode 100644
index 000000000..32cc0e010
--- /dev/null
+++ b/embassy-macros/src/macros/interrupt.rs
@@ -0,0 +1,66 @@
+use darling::FromMeta;
+use proc_macro2::TokenStream;
+use quote::quote;
+use std::iter;
+use syn::ReturnType;
+use syn::{Type, Visibility};
+
+use crate::util::ctxt::Ctxt;
+
+#[derive(Debug, FromMeta)]
+struct Args {}
+
+pub fn run(args: syn::AttributeArgs, mut f: syn::ItemFn) -> Result<TokenStream, TokenStream> {
+    let _args = Args::from_list(&args).map_err(|e| e.write_errors())?;
+
+    let ident = f.sig.ident.clone();
+    let ident_s = ident.to_string();
+
+    // XXX should we blacklist other attributes?
+
+    let valid_signature = f.sig.constness.is_none()
+        && f.vis == Visibility::Inherited
+        && f.sig.abi.is_none()
+        && f.sig.inputs.is_empty()
+        && f.sig.generics.params.is_empty()
+        && f.sig.generics.where_clause.is_none()
+        && f.sig.variadic.is_none()
+        && match f.sig.output {
+            ReturnType::Default => true,
+            ReturnType::Type(_, ref ty) => match **ty {
+                Type::Tuple(ref tuple) => tuple.elems.is_empty(),
+                Type::Never(..) => true,
+                _ => false,
+            },
+        };
+
+    let ctxt = Ctxt::new();
+
+    if !valid_signature {
+        ctxt.error_spanned_by(
+            &f.sig,
+            "`#[interrupt]` handlers must have signature `[unsafe] fn() [-> !]`",
+        );
+    }
+
+    ctxt.check()?;
+
+    f.block.stmts = iter::once(
+        syn::parse2(quote! {{
+            // Check that this interrupt actually exists
+            let __irq_exists_check: interrupt::#ident;
+        }})
+        .unwrap(),
+    )
+    .chain(f.block.stmts)
+    .collect();
+
+    let result = quote!(
+        #[doc(hidden)]
+        #[export_name = #ident_s]
+        #[allow(non_snake_case)]
+        #f
+    );
+
+    Ok(result)
+}
diff --git a/embassy-macros/src/macros/interrupt_declare.rs b/embassy-macros/src/macros/interrupt_declare.rs
new file mode 100644
index 000000000..0059936d9
--- /dev/null
+++ b/embassy-macros/src/macros/interrupt_declare.rs
@@ -0,0 +1,37 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+
+pub fn run(name: syn::Ident) -> Result<TokenStream, TokenStream> {
+    let name = format_ident!("{}", name);
+    let name_interrupt = format_ident!("{}", name);
+    let name_handler = format!("__EMBASSY_{}_HANDLER", name);
+
+    let result = quote! {
+        #[allow(non_camel_case_types)]
+        pub struct #name_interrupt(());
+        unsafe impl ::embassy::interrupt::Interrupt for #name_interrupt {
+            type Priority = crate::interrupt::Priority;
+            fn number(&self) -> u16 {
+                use cortex_m::interrupt::InterruptNumber;
+                let irq = InterruptEnum::#name;
+                irq.number() as u16
+            }
+            unsafe fn steal() -> Self {
+                Self(())
+            }
+            unsafe fn __handler(&self) -> &'static ::embassy::interrupt::Handler {
+                #[export_name = #name_handler]
+                static HANDLER: ::embassy::interrupt::Handler = ::embassy::interrupt::Handler::new();
+                &HANDLER
+            }
+        }
+
+        unsafe impl ::embassy::util::Unborrow for #name_interrupt {
+            type Target = #name_interrupt;
+            unsafe fn unborrow(self) -> #name_interrupt {
+                self
+            }
+        }
+    };
+    Ok(result)
+}
diff --git a/embassy-macros/src/macros/interrupt_take.rs b/embassy-macros/src/macros/interrupt_take.rs
new file mode 100644
index 000000000..230b9c741
--- /dev/null
+++ b/embassy-macros/src/macros/interrupt_take.rs
@@ -0,0 +1,36 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+
+pub fn run(name: syn::Ident) -> Result<TokenStream, TokenStream> {
+    let name = format!("{}", name);
+    let name_interrupt = format_ident!("{}", name);
+    let name_handler = format!("__EMBASSY_{}_HANDLER", name);
+
+    let result = quote! {
+        {
+            #[allow(non_snake_case)]
+            #[export_name = #name]
+            pub unsafe extern "C" fn trampoline() {
+                extern "C" {
+                    #[link_name = #name_handler]
+                    static HANDLER: ::embassy::interrupt::Handler;
+                }
+
+                let func = HANDLER.func.load(::embassy::export::atomic::Ordering::Relaxed);
+                let ctx = HANDLER.ctx.load(::embassy::export::atomic::Ordering::Relaxed);
+                let func: fn(*mut ()) = ::core::mem::transmute(func);
+                func(ctx)
+            }
+
+            static TAKEN: ::embassy::export::atomic::AtomicBool = ::embassy::export::atomic::AtomicBool::new(false);
+
+            if TAKEN.compare_exchange(false, true, ::embassy::export::atomic::Ordering::AcqRel, ::embassy::export::atomic::Ordering::Acquire).is_err() {
+                core::panic!("IRQ Already taken");
+            }
+
+            let irq: interrupt::#name_interrupt = unsafe { ::core::mem::transmute(()) };
+            irq
+        }
+    };
+    Ok(result)
+}
diff --git a/embassy-macros/src/macros/main.rs b/embassy-macros/src/macros/main.rs
new file mode 100644
index 000000000..01e302921
--- /dev/null
+++ b/embassy-macros/src/macros/main.rs
@@ -0,0 +1,135 @@
+use darling::FromMeta;
+use proc_macro2::TokenStream;
+use quote::quote;
+
+use crate::util::ctxt::Ctxt;
+use crate::util::path::ModulePrefix;
+
+#[cfg(feature = "stm32")]
+const HAL: Option<&str> = Some("embassy_stm32");
+#[cfg(feature = "nrf")]
+const HAL: Option<&str> = Some("embassy_nrf");
+#[cfg(feature = "rp")]
+const HAL: Option<&str> = Some("embassy_rp");
+#[cfg(not(any(feature = "stm32", feature = "nrf", feature = "rp")))]
+const HAL: Option<&str> = None;
+
+#[derive(Debug, FromMeta)]
+struct Args {
+    #[darling(default)]
+    embassy_prefix: ModulePrefix,
+
+    #[allow(unused)]
+    #[darling(default)]
+    config: Option<syn::LitStr>,
+}
+
+pub fn run(args: syn::AttributeArgs, f: syn::ItemFn) -> Result<TokenStream, TokenStream> {
+    let args = Args::from_list(&args).map_err(|e| e.write_errors())?;
+
+    let fargs = f.sig.inputs.clone();
+
+    let ctxt = Ctxt::new();
+
+    if f.sig.asyncness.is_none() {
+        ctxt.error_spanned_by(&f.sig, "task functions must be async");
+    }
+    if !f.sig.generics.params.is_empty() {
+        ctxt.error_spanned_by(&f.sig, "task functions must not be generic");
+    }
+
+    if HAL.is_some() && fargs.len() != 2 {
+        ctxt.error_spanned_by(&f.sig, "main function must have 2 arguments");
+    }
+    if HAL.is_none() && fargs.len() != 1 {
+        ctxt.error_spanned_by(&f.sig, "main function must have 2 arguments");
+    }
+
+    ctxt.check()?;
+
+    let embassy_prefix = args.embassy_prefix;
+    let embassy_prefix_lit = embassy_prefix.literal();
+    let embassy_path = embassy_prefix.append("embassy").path();
+    let f_body = f.block;
+
+    #[cfg(feature = "wasm")]
+    let main = quote! {
+        #[wasm_bindgen::prelude::wasm_bindgen(start)]
+        pub fn main() -> Result<(), wasm_bindgen::JsValue> {
+            static EXECUTOR: #embassy_path::util::Forever<#embassy_path::executor::Executor> = #embassy_path::util::Forever::new();
+            let executor = EXECUTOR.put(#embassy_path::executor::Executor::new());
+
+            executor.start(|spawner| {
+                spawner.spawn(__embassy_main(spawner)).unwrap();
+            });
+
+            Ok(())
+        }
+    };
+
+    #[cfg(all(feature = "std", not(feature = "wasm")))]
+    let main = quote! {
+        fn main() -> ! {
+            let mut executor = #embassy_path::executor::Executor::new();
+            let executor = unsafe { __make_static(&mut executor) };
+
+            executor.run(|spawner| {
+                spawner.must_spawn(__embassy_main(spawner));
+            })
+        }
+    };
+
+    #[cfg(all(not(feature = "std"), not(feature = "wasm")))]
+    let main = {
+        let config = args
+            .config
+            .map(|s| s.parse::<syn::Expr>().unwrap())
+            .unwrap_or_else(|| {
+                syn::Expr::Verbatim(quote! {
+                    Default::default()
+                })
+            });
+
+        let (hal_setup, peris_arg) = match HAL {
+            Some(hal) => {
+                let embassy_hal_path = embassy_prefix.append(hal).path();
+                (
+                    quote!(
+                        let p = #embassy_hal_path::init(#config);
+                    ),
+                    quote!(p),
+                )
+            }
+            None => (quote!(), quote!()),
+        };
+
+        quote! {
+            #[cortex_m_rt::entry]
+            fn main() -> ! {
+                #hal_setup
+
+                let mut executor = #embassy_path::executor::Executor::new();
+                let executor = unsafe { __make_static(&mut executor) };
+
+                executor.run(|spawner| {
+                    spawner.must_spawn(__embassy_main(spawner, #peris_arg));
+                })
+            }
+        }
+    };
+
+    let result = quote! {
+        #[#embassy_path::task(embassy_prefix = #embassy_prefix_lit)]
+        async fn __embassy_main(#fargs) {
+            #f_body
+        }
+
+        unsafe fn __make_static<T>(t: &mut T) -> &'static mut T {
+            ::core::mem::transmute(t)
+        }
+
+        #main
+    };
+
+    Ok(result)
+}
diff --git a/embassy-macros/src/macros/mod.rs b/embassy-macros/src/macros/mod.rs
new file mode 100644
index 000000000..4350f229f
--- /dev/null
+++ b/embassy-macros/src/macros/mod.rs
@@ -0,0 +1,5 @@
+pub mod interrupt;
+pub mod interrupt_declare;
+pub mod interrupt_take;
+pub mod main;
+pub mod task;
diff --git a/embassy-macros/src/macros/task.rs b/embassy-macros/src/macros/task.rs
new file mode 100644
index 000000000..f0c78c596
--- /dev/null
+++ b/embassy-macros/src/macros/task.rs
@@ -0,0 +1,90 @@
+use darling::FromMeta;
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+
+use crate::util::ctxt::Ctxt;
+use crate::util::path::ModulePrefix;
+
+#[derive(Debug, FromMeta)]
+struct Args {
+    #[darling(default)]
+    pool_size: Option<usize>,
+    #[darling(default)]
+    send: bool,
+    #[darling(default)]
+    embassy_prefix: ModulePrefix,
+}
+
+pub fn run(args: syn::AttributeArgs, mut f: syn::ItemFn) -> Result<TokenStream, TokenStream> {
+    let args = Args::from_list(&args).map_err(|e| e.write_errors())?;
+
+    let embassy_prefix = args.embassy_prefix.append("embassy");
+    let embassy_path = embassy_prefix.path();
+
+    let pool_size: usize = args.pool_size.unwrap_or(1);
+
+    let ctxt = Ctxt::new();
+
+    if f.sig.asyncness.is_none() {
+        ctxt.error_spanned_by(&f.sig, "task functions must be async");
+    }
+    if !f.sig.generics.params.is_empty() {
+        ctxt.error_spanned_by(&f.sig, "task functions must not be generic");
+    }
+    if pool_size < 1 {
+        ctxt.error_spanned_by(&f.sig, "pool_size must be 1 or greater");
+    }
+
+    let mut arg_names: syn::punctuated::Punctuated<syn::Ident, syn::Token![,]> =
+        syn::punctuated::Punctuated::new();
+    let mut fargs = f.sig.inputs.clone();
+
+    for arg in fargs.iter_mut() {
+        match arg {
+            syn::FnArg::Receiver(_) => {
+                ctxt.error_spanned_by(arg, "task functions must not have receiver arguments");
+            }
+            syn::FnArg::Typed(t) => match t.pat.as_mut() {
+                syn::Pat::Ident(i) => {
+                    arg_names.push(i.ident.clone());
+                    i.mutability = None;
+                }
+                _ => {
+                    ctxt.error_spanned_by(
+                        arg,
+                        "pattern matching in task arguments is not yet supporteds",
+                    );
+                }
+            },
+        }
+    }
+
+    ctxt.check()?;
+
+    let name = f.sig.ident.clone();
+
+    let visibility = &f.vis;
+    f.sig.ident = format_ident!("task");
+    let impl_ty = if args.send {
+        quote!(impl ::core::future::Future + Send + 'static)
+    } else {
+        quote!(impl ::core::future::Future + 'static)
+    };
+
+    let attrs = &f.attrs;
+
+    let result = quote! {
+        #(#attrs)*
+        #visibility fn #name(#fargs) -> #embassy_path::executor::SpawnToken<#impl_ty> {
+            use #embassy_path::executor::raw::TaskStorage;
+            #f
+            type F = #impl_ty;
+            #[allow(clippy::declare_interior_mutable_const)]
+            const NEW_TASK: TaskStorage<F> = TaskStorage::new();
+            static POOL: [TaskStorage<F>; #pool_size] = [NEW_TASK; #pool_size];
+            unsafe { TaskStorage::spawn_pool(&POOL, move || task(#arg_names)) }
+        }
+    };
+
+    Ok(result)
+}
diff --git a/embassy-macros/src/util/ctxt.rs b/embassy-macros/src/util/ctxt.rs
new file mode 100644
index 000000000..d668ae780
--- /dev/null
+++ b/embassy-macros/src/util/ctxt.rs
@@ -0,0 +1,72 @@
+// nifty utility borrowed from serde :)
+// https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/ctxt.rs
+
+use proc_macro2::TokenStream;
+use quote::{quote, ToTokens};
+use std::cell::RefCell;
+use std::fmt::Display;
+use std::thread;
+use syn;
+
+/// A type to collect errors together and format them.
+///
+/// Dropping this object will cause a panic. It must be consumed using `check`.
+///
+/// References can be shared since this type uses run-time exclusive mut checking.
+#[derive(Default)]
+pub struct Ctxt {
+    // The contents will be set to `None` during checking. This is so that checking can be
+    // enforced.
+    errors: RefCell<Option<Vec<syn::Error>>>,
+}
+
+impl Ctxt {
+    /// Create a new context object.
+    ///
+    /// This object contains no errors, but will still trigger a panic if it is not `check`ed.
+    pub fn new() -> Self {
+        Ctxt {
+            errors: RefCell::new(Some(Vec::new())),
+        }
+    }
+
+    /// Add an error to the context object with a tokenenizable object.
+    ///
+    /// The object is used for spanning in error messages.
+    pub fn error_spanned_by<A: ToTokens, T: Display>(&self, obj: A, msg: T) {
+        self.errors
+            .borrow_mut()
+            .as_mut()
+            .unwrap()
+            // Curb monomorphization from generating too many identical methods.
+            .push(syn::Error::new_spanned(obj.into_token_stream(), msg));
+    }
+
+    /// Add one of Syn's parse errors.
+    #[allow(unused)]
+    pub fn syn_error(&self, err: syn::Error) {
+        self.errors.borrow_mut().as_mut().unwrap().push(err);
+    }
+
+    /// Consume this object, producing a formatted error string if there are errors.
+    pub fn check(self) -> Result<(), TokenStream> {
+        let errors = self.errors.borrow_mut().take().unwrap();
+        match errors.len() {
+            0 => Ok(()),
+            _ => Err(to_compile_errors(errors)),
+        }
+    }
+}
+
+fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
+    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
+    quote!(#(#compile_errors)*)
+}
+
+impl Drop for Ctxt {
+    fn drop(&mut self) {
+        if !thread::panicking() && self.errors.borrow().is_some() {
+            panic!("forgot to check for errors");
+        }
+    }
+}
diff --git a/embassy-macros/src/util/mod.rs b/embassy-macros/src/util/mod.rs
new file mode 100644
index 000000000..c2f2dfd65
--- /dev/null
+++ b/embassy-macros/src/util/mod.rs
@@ -0,0 +1,2 @@
+pub mod ctxt;
+pub mod path;
diff --git a/embassy-macros/src/path.rs b/embassy-macros/src/util/path.rs
similarity index 100%
rename from embassy-macros/src/path.rs
rename to embassy-macros/src/util/path.rs
diff --git a/embassy-nrf/Cargo.toml b/embassy-nrf/Cargo.toml
index 4ed922ba9..5ced9b1b2 100644
--- a/embassy-nrf/Cargo.toml
+++ b/embassy-nrf/Cargo.toml
@@ -6,14 +6,18 @@ edition = "2018"
 
 [features]
 
+# Enable nightly-only features
+nightly = ["embassy/nightly", "embedded-hal-1", "embedded-hal-async"]
+
 # Reexport the PAC for the currently enabled chip at `embassy_nrf::pac`.
 # This is unstable because semver-minor (non-breaking) releases of embassy-nrf may major-bump (breaking) the PAC version.
 # If this is an issue for you, you're encouraged to directly depend on a fixed version of the PAC.
 # There are no plans to make this stable.
 unstable-pac = []
 
-# Implement embedded-hal 1.0 alpha and embedded-hal-async traits.
-unstable-traits = ["embedded-hal-1", "embedded-hal-async"]
+# Implement embedded-hal 1.0 alpha traits.
+# Implement embedded-hal-async traits if `nightly` is set as well.
+unstable-traits = ["embedded-hal-1"]
 
 nrf52805 = ["nrf52805-pac", "_ppi"]
 nrf52810 = ["nrf52810-pac", "_ppi"]
diff --git a/embassy-nrf/src/gpiote.rs b/embassy-nrf/src/gpiote.rs
index a4c24058c..b856c2dfe 100644
--- a/embassy-nrf/src/gpiote.rs
+++ b/embassy-nrf/src/gpiote.rs
@@ -463,7 +463,6 @@ mod eh02 {
 #[cfg(feature = "unstable-traits")]
 mod eh1 {
     use super::*;
-    use futures::FutureExt;
 
     impl<'d, C: Channel, T: GpioPin> embedded_hal_1::digital::ErrorType for InputChannel<'d, C, T> {
         type Error = Infallible;
@@ -480,6 +479,12 @@ mod eh1 {
             self.pin.is_low()
         }
     }
+}
+
+#[cfg(all(feature = "unstable-traits", feature = "nightly"))]
+mod eh1a {
+    use super::*;
+    use futures::FutureExt;
 
     impl<'d, T: GpioPin> embedded_hal_async::digital::Wait for Input<'d, T> {
         type WaitForHighFuture<'a>
diff --git a/embassy-nrf/src/lib.rs b/embassy-nrf/src/lib.rs
index 8e05d9b6a..b448f6ab6 100644
--- a/embassy-nrf/src/lib.rs
+++ b/embassy-nrf/src/lib.rs
@@ -1,6 +1,8 @@
 #![no_std]
-#![feature(generic_associated_types)]
-#![feature(type_alias_impl_trait)]
+#![cfg_attr(
+    feature = "nightly",
+    feature(generic_associated_types, type_alias_impl_trait)
+)]
 
 #[cfg(not(any(
     feature = "nrf51",
diff --git a/embassy-nrf/src/spim.rs b/embassy-nrf/src/spim.rs
index 976a546ce..a45b9390d 100644
--- a/embassy-nrf/src/spim.rs
+++ b/embassy-nrf/src/spim.rs
@@ -374,7 +374,6 @@ mod eh02 {
 #[cfg(feature = "unstable-traits")]
 mod eh1 {
     use super::*;
-    use core::future::Future;
 
     impl embedded_hal_1::spi::Error for Error {
         fn kind(&self) -> embedded_hal_1::spi::ErrorKind {
@@ -437,7 +436,7 @@ mod eh1 {
 
         fn transaction<'a>(
             &mut self,
-            operations: &mut [embedded_hal_async::spi::Operation<'a, u8>],
+            operations: &mut [embedded_hal_1::spi::blocking::Operation<'a, u8>],
         ) -> Result<(), Self::Error> {
             use embedded_hal_1::spi::blocking::Operation;
             for o in operations {
@@ -451,6 +450,12 @@ mod eh1 {
             Ok(())
         }
     }
+}
+
+#[cfg(all(feature = "unstable-traits", feature = "nightly"))]
+mod eh1a {
+    use super::*;
+    use core::future::Future;
 
     impl<'d, T: Instance> embedded_hal_async::spi::Read<u8> for Spim<'d, T> {
         type ReadFuture<'a>
diff --git a/embassy-nrf/src/time_driver.rs b/embassy-nrf/src/time_driver.rs
index 19356c2d2..4240b9ac4 100644
--- a/embassy-nrf/src/time_driver.rs
+++ b/embassy-nrf/src/time_driver.rs
@@ -2,6 +2,7 @@ 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::raw::CriticalSectionRawMutex;
 use embassy::blocking_mutex::CriticalSectionMutex as Mutex;
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::time::driver::{AlarmHandle, Driver};
@@ -94,7 +95,7 @@ const ALARM_STATE_NEW: AlarmState = AlarmState::new();
 embassy::time_driver_impl!(static DRIVER: RtcDriver = RtcDriver {
     period: AtomicU32::new(0),
     alarm_count: AtomicU8::new(0),
-    alarms: Mutex::new([ALARM_STATE_NEW; ALARM_COUNT]),
+    alarms: Mutex::const_new(CriticalSectionRawMutex::new(), [ALARM_STATE_NEW; ALARM_COUNT]),
 });
 
 impl RtcDriver {
diff --git a/embassy-nrf/src/twim.rs b/embassy-nrf/src/twim.rs
index 4cd47c897..ed2844f79 100644
--- a/embassy-nrf/src/twim.rs
+++ b/embassy-nrf/src/twim.rs
@@ -679,7 +679,7 @@ mod eh1 {
         fn transaction<'a>(
             &mut self,
             _address: u8,
-            _operations: &mut [embedded_hal_async::i2c::Operation<'a>],
+            _operations: &mut [embedded_hal_1::i2c::blocking::Operation<'a>],
         ) -> Result<(), Self::Error> {
             todo!();
         }
@@ -690,58 +690,59 @@ mod eh1 {
             _operations: O,
         ) -> Result<(), Self::Error>
         where
-            O: IntoIterator<Item = embedded_hal_async::i2c::Operation<'a>>,
+            O: IntoIterator<Item = embedded_hal_1::i2c::blocking::Operation<'a>>,
         {
             todo!();
         }
     }
+}
 
-    impl<'d, T: Instance> embedded_hal_async::i2c::I2c for Twim<'d, T> {
-        type ReadFuture<'a>
-        where
-            Self: 'a,
-        = impl Future<Output = Result<(), Self::Error>> + 'a;
+#[cfg(all(feature = "unstable-traits", feature = "nightly"))]
+impl<'d, T: Instance> embedded_hal_async::i2c::I2c for Twim<'d, T> {
+    type ReadFuture<'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> {
-            self.read(address, buffer)
-        }
+    fn read<'a>(&'a mut self, address: u8, buffer: &'a mut [u8]) -> Self::ReadFuture<'a> {
+        self.read(address, buffer)
+    }
 
-        type WriteFuture<'a>
-        where
-            Self: 'a,
-        = impl Future<Output = Result<(), Self::Error>> + 'a;
+    type WriteFuture<'a>
+    where
+        Self: 'a,
+    = impl Future<Output = Result<(), Self::Error>> + 'a;
 
-        fn write<'a>(&'a mut self, address: u8, bytes: &'a [u8]) -> Self::WriteFuture<'a> {
-            self.write(address, bytes)
-        }
+    fn write<'a>(&'a mut self, address: u8, bytes: &'a [u8]) -> Self::WriteFuture<'a> {
+        self.write(address, bytes)
+    }
 
-        type WriteReadFuture<'a>
-        where
-            Self: 'a,
-        = impl Future<Output = Result<(), Self::Error>> + 'a;
+    type WriteReadFuture<'a>
+    where
+        Self: 'a,
+    = impl Future<Output = Result<(), Self::Error>> + 'a;
 
-        fn write_read<'a>(
-            &'a mut self,
-            address: u8,
-            wr_buffer: &'a [u8],
-            rd_buffer: &'a mut [u8],
-        ) -> Self::WriteReadFuture<'a> {
-            self.write_read(address, wr_buffer, rd_buffer)
-        }
+    fn write_read<'a>(
+        &'a mut self,
+        address: u8,
+        wr_buffer: &'a [u8],
+        rd_buffer: &'a mut [u8],
+    ) -> Self::WriteReadFuture<'a> {
+        self.write_read(address, wr_buffer, rd_buffer)
+    }
 
-        type TransactionFuture<'a>
-        where
-            Self: 'a,
-        = impl Future<Output = Result<(), Self::Error>> + 'a;
+    type TransactionFuture<'a>
+    where
+        Self: 'a,
+    = impl Future<Output = Result<(), Self::Error>> + 'a;
 
-        fn transaction<'a>(
-            &'a mut self,
-            address: u8,
-            operations: &mut [embedded_hal_async::i2c::Operation<'a>],
-        ) -> Self::TransactionFuture<'a> {
-            let _ = address;
-            let _ = operations;
-            async move { todo!() }
-        }
+    fn transaction<'a>(
+        &'a mut self,
+        address: u8,
+        operations: &mut [embedded_hal_async::i2c::Operation<'a>],
+    ) -> Self::TransactionFuture<'a> {
+        let _ = address;
+        let _ = operations;
+        async move { todo!() }
     }
 }
diff --git a/embassy-nrf/src/uarte.rs b/embassy-nrf/src/uarte.rs
index a1c47cff5..119a17981 100644
--- a/embassy-nrf/src/uarte.rs
+++ b/embassy-nrf/src/uarte.rs
@@ -844,7 +844,6 @@ mod eh02 {
 #[cfg(feature = "unstable-traits")]
 mod eh1 {
     use super::*;
-    use core::future::Future;
 
     impl embedded_hal_1::serial::Error for Error {
         fn kind(&self) -> embedded_hal_1::serial::ErrorKind {
@@ -872,6 +871,36 @@ mod eh1 {
         }
     }
 
+    impl<'d, T: Instance> embedded_hal_1::serial::ErrorType for UarteTx<'d, T> {
+        type Error = Error;
+    }
+
+    impl<'d, T: Instance> embedded_hal_1::serial::blocking::Write for UarteTx<'d, T> {
+        fn write(&mut self, buffer: &[u8]) -> Result<(), Self::Error> {
+            self.blocking_write(buffer)
+        }
+
+        fn flush(&mut self) -> Result<(), Self::Error> {
+            Ok(())
+        }
+    }
+
+    impl<'d, T: Instance> embedded_hal_1::serial::ErrorType for UarteRx<'d, T> {
+        type Error = Error;
+    }
+
+    impl<'d, U: Instance, T: TimerInstance> embedded_hal_1::serial::ErrorType
+        for UarteWithIdle<'d, U, T>
+    {
+        type Error = Error;
+    }
+}
+
+#[cfg(all(feature = "unstable-traits", feature = "nightly"))]
+mod eh1a {
+    use super::*;
+    use core::future::Future;
+
     impl<'d, T: Instance> embedded_hal_async::serial::Read for Uarte<'d, T> {
         type ReadFuture<'a>
         where
@@ -903,22 +932,6 @@ mod eh1 {
         }
     }
 
-    // =====================
-
-    impl<'d, T: Instance> embedded_hal_1::serial::ErrorType for UarteTx<'d, T> {
-        type Error = Error;
-    }
-
-    impl<'d, T: Instance> embedded_hal_1::serial::blocking::Write for UarteTx<'d, T> {
-        fn write(&mut self, buffer: &[u8]) -> Result<(), Self::Error> {
-            self.blocking_write(buffer)
-        }
-
-        fn flush(&mut self) -> Result<(), Self::Error> {
-            Ok(())
-        }
-    }
-
     impl<'d, T: Instance> embedded_hal_async::serial::Write for UarteTx<'d, T> {
         type WriteFuture<'a>
         where
@@ -939,12 +952,6 @@ mod eh1 {
         }
     }
 
-    // =====================
-
-    impl<'d, T: Instance> embedded_hal_1::serial::ErrorType for UarteRx<'d, T> {
-        type Error = Error;
-    }
-
     impl<'d, T: Instance> embedded_hal_async::serial::Read for UarteRx<'d, T> {
         type ReadFuture<'a>
         where
@@ -956,14 +963,6 @@ mod eh1 {
         }
     }
 
-    // =====================
-
-    impl<'d, U: Instance, T: TimerInstance> embedded_hal_1::serial::ErrorType
-        for UarteWithIdle<'d, U, T>
-    {
-        type Error = Error;
-    }
-
     impl<'d, U: Instance, T: TimerInstance> embedded_hal_async::serial::Read
         for UarteWithIdle<'d, U, T>
     {
diff --git a/embassy-rp/Cargo.toml b/embassy-rp/Cargo.toml
index be41f95eb..cb6544028 100644
--- a/embassy-rp/Cargo.toml
+++ b/embassy-rp/Cargo.toml
@@ -13,7 +13,7 @@ edition = "2018"
 unstable-pac = []
 
 [dependencies]
-embassy = { version = "0.1.0", path = "../embassy", features = [ "time-tick-1mhz" ] }
+embassy = { version = "0.1.0", path = "../embassy", features = [ "time-tick-1mhz", "nightly"] }
 embassy-hal-common = {version = "0.1.0", path = "../embassy-hal-common" }
 embassy-macros = { version = "0.1.0", path = "../embassy-macros", features = ["rp"]}
 atomic-polyfill = "0.1.5"
diff --git a/embassy-rp/src/timer.rs b/embassy-rp/src/timer.rs
index b3c047ca4..f449df000 100644
--- a/embassy-rp/src/timer.rs
+++ b/embassy-rp/src/timer.rs
@@ -1,7 +1,8 @@
 use atomic_polyfill::{AtomicU8, Ordering};
 use core::cell::Cell;
 use critical_section::CriticalSection;
-use embassy::blocking_mutex::CriticalSectionMutex as Mutex;
+use embassy::blocking_mutex::raw::CriticalSectionRawMutex;
+use embassy::blocking_mutex::Mutex;
 use embassy::interrupt::{Interrupt, InterruptExt};
 use embassy::time::driver::{AlarmHandle, Driver};
 
@@ -20,12 +21,12 @@ const DUMMY_ALARM: AlarmState = AlarmState {
 };
 
 struct TimerDriver {
-    alarms: Mutex<[AlarmState; ALARM_COUNT]>,
+    alarms: Mutex<CriticalSectionRawMutex, [AlarmState; ALARM_COUNT]>,
     next_alarm: AtomicU8,
 }
 
 embassy::time_driver_impl!(static DRIVER: TimerDriver = TimerDriver{
-    alarms:  Mutex::new([DUMMY_ALARM; ALARM_COUNT]),
+    alarms:  Mutex::const_new(CriticalSectionRawMutex::new(), [DUMMY_ALARM; ALARM_COUNT]),
     next_alarm: AtomicU8::new(0),
 });
 
diff --git a/embassy-stm32/Cargo.toml b/embassy-stm32/Cargo.toml
index 386722d47..55a646d4a 100644
--- a/embassy-stm32/Cargo.toml
+++ b/embassy-stm32/Cargo.toml
@@ -6,7 +6,7 @@ edition = "2018"
 resolver = "2"
 
 [dependencies]
-embassy = { version = "0.1.0", path = "../embassy" }
+embassy = { version = "0.1.0", path = "../embassy", features = ["nightly"]}
 embassy-macros = { version = "0.1.0", path = "../embassy-macros", features = ["stm32"] }
 embassy-hal-common = {version = "0.1.0", path = "../embassy-hal-common" }
 embassy-net = { version = "0.1.0", path = "../embassy-net", default-features = false, optional = true }
diff --git a/embassy-stm32/src/time_driver.rs b/embassy-stm32/src/time_driver.rs
index d1596c5f8..7efe0d3a5 100644
--- a/embassy-stm32/src/time_driver.rs
+++ b/embassy-stm32/src/time_driver.rs
@@ -3,13 +3,15 @@ use core::cell::Cell;
 use core::convert::TryInto;
 use core::sync::atomic::{compiler_fence, Ordering};
 use core::{mem, ptr};
+use embassy::blocking_mutex::raw::CriticalSectionRawMutex;
+use embassy::blocking_mutex::Mutex;
 use embassy::interrupt::InterruptExt;
 use embassy::time::driver::{AlarmHandle, Driver};
 use embassy::time::TICKS_PER_SECOND;
 use stm32_metapac::timer::regs;
 
 use crate::interrupt;
-use crate::interrupt::{CriticalSection, Interrupt, Mutex};
+use crate::interrupt::{CriticalSection, Interrupt};
 use crate::pac::timer::{vals, TimGp16};
 use crate::peripherals;
 use crate::rcc::sealed::RccPeripheral;
@@ -95,7 +97,7 @@ struct RtcDriver {
     period: AtomicU32,
     alarm_count: AtomicU8,
     /// Timestamp at which to fire alarm. u64::MAX if no alarm is scheduled.
-    alarms: Mutex<[AlarmState; ALARM_COUNT]>,
+    alarms: Mutex<CriticalSectionRawMutex, [AlarmState; ALARM_COUNT]>,
 }
 
 const ALARM_STATE_NEW: AlarmState = AlarmState::new();
@@ -103,7 +105,7 @@ const ALARM_STATE_NEW: AlarmState = AlarmState::new();
 embassy::time_driver_impl!(static DRIVER: RtcDriver = RtcDriver {
     period: AtomicU32::new(0),
     alarm_count: AtomicU8::new(0),
-    alarms: Mutex::new([ALARM_STATE_NEW; ALARM_COUNT]),
+    alarms: Mutex::const_new(CriticalSectionRawMutex::new(), [ALARM_STATE_NEW; ALARM_COUNT]),
 });
 
 impl RtcDriver {
diff --git a/embassy/Cargo.toml b/embassy/Cargo.toml
index ccb5574d7..d10a8874e 100644
--- a/embassy/Cargo.toml
+++ b/embassy/Cargo.toml
@@ -10,8 +10,12 @@ default = []
 std = ["futures/std", "time", "time-tick-1mhz", "embassy-macros/std"]
 wasm = ["wasm-bindgen", "js-sys", "embassy-macros/wasm", "wasm-timer", "time", "time-tick-1mhz"]
 
+# Enable nightly-only features
+nightly = ["embedded-hal-async"]
+
 # Implement embedded-hal 1.0 alpha and embedded-hal-async traits.
-unstable-traits = ["embedded-hal-1", "embedded-hal-async"]
+# Implement embedded-hal-async traits if `nightly` is set as well.
+unstable-traits = ["embedded-hal-1"]
 
 # Enable `embassy::time` module. 
 # NOTE: This feature is only intended to be enabled by crates providing the time driver implementation.
diff --git a/embassy/src/blocking_mutex/kind.rs b/embassy/src/blocking_mutex/kind.rs
deleted file mode 100644
index a4a45605f..000000000
--- a/embassy/src/blocking_mutex/kind.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-pub trait MutexKind {
-    type Mutex<T>: super::Mutex<Data = T>;
-}
-
-pub enum CriticalSection {}
-impl MutexKind for CriticalSection {
-    type Mutex<T> = super::CriticalSectionMutex<T>;
-}
-
-#[cfg(any(cortex_m, feature = "std"))]
-pub enum ThreadMode {}
-#[cfg(any(cortex_m, feature = "std"))]
-impl MutexKind for ThreadMode {
-    type Mutex<T> = super::ThreadModeMutex<T>;
-}
-
-pub enum Noop {}
-impl MutexKind for Noop {
-    type Mutex<T> = super::NoopMutex<T>;
-}
diff --git a/embassy/src/blocking_mutex/mod.rs b/embassy/src/blocking_mutex/mod.rs
index 949531392..859eca075 100644
--- a/embassy/src/blocking_mutex/mod.rs
+++ b/embassy/src/blocking_mutex/mod.rs
@@ -1,67 +1,107 @@
 //! Blocking mutex (not async)
 
-pub mod kind;
+pub mod raw;
 
+use self::raw::RawMutex;
 use core::cell::UnsafeCell;
-use critical_section::CriticalSection;
 
 /// Any object implementing this trait guarantees exclusive access to the data contained
 /// within the mutex for the duration of the lock.
 /// Adapted from <https://github.com/rust-embedded/mutex-trait>.
-pub trait Mutex {
-    /// Data protected by the mutex.
-    type Data;
-
-    fn new(data: Self::Data) -> Self;
-
-    /// Creates a critical section and grants temporary access to the protected data.
-    fn lock<R>(&self, f: impl FnOnce(&Self::Data) -> R) -> R;
+pub struct Mutex<R, T: ?Sized> {
+    // NOTE: `raw` must be FIRST, so when using ThreadModeMutex the "can't drop in non-thread-mode" gets
+    // to run BEFORE dropping `data`.
+    raw: R,
+    data: UnsafeCell<T>,
 }
 
-/// A "mutex" based on critical sections
-///
-/// # Safety
-///
-/// **This Mutex is only safe on single-core systems.**
-///
-/// On multi-core systems, a `CriticalSection` **is not sufficient** to ensure exclusive access.
-pub struct CriticalSectionMutex<T> {
-    inner: UnsafeCell<T>,
-}
+unsafe impl<R: RawMutex + Send, T: ?Sized + Send> Send for Mutex<R, T> {}
+unsafe impl<R: RawMutex + Sync, T: ?Sized + Send> Sync for Mutex<R, T> {}
 
-// NOTE: A `CriticalSectionMutex` can be used as a channel so the protected data must be `Send`
-// to prevent sending non-Sendable stuff (e.g. access tokens) across different
-// execution contexts (e.g. interrupts)
-unsafe impl<T> Sync for CriticalSectionMutex<T> where T: Send {}
-
-impl<T> CriticalSectionMutex<T> {
-    /// Creates a new mutex
-    pub const fn new(value: T) -> Self {
-        CriticalSectionMutex {
-            inner: UnsafeCell::new(value),
+impl<R: RawMutex, T> Mutex<R, T> {
+    /// Creates a new mutex in an unlocked state ready for use.
+    #[cfg(feature = "nightly")]
+    #[inline]
+    pub const fn new(val: T) -> Mutex<R, T> {
+        Mutex {
+            raw: R::INIT,
+            data: UnsafeCell::new(val),
         }
     }
+
+    /// Creates a new mutex in an unlocked state ready for use.
+    #[cfg(not(feature = "nightly"))]
+    #[inline]
+    pub fn new(val: T) -> Mutex<R, T> {
+        Mutex {
+            raw: R::INIT,
+            data: UnsafeCell::new(val),
+        }
+    }
+
+    /// Creates a critical section and grants temporary access to the protected data.
+    pub fn lock<U>(&self, f: impl FnOnce(&T) -> U) -> U {
+        self.raw.lock(|| {
+            let ptr = self.data.get() as *const T;
+            let inner = unsafe { &*ptr };
+            f(inner)
+        })
+    }
 }
 
-impl<T> CriticalSectionMutex<T> {
+impl<R, T> Mutex<R, T> {
+    /// Creates a new mutex based on a pre-existing raw mutex.
+    ///
+    /// This allows creating a mutex in a constant context on stable Rust.
+    #[inline]
+    pub const fn const_new(raw_mutex: R, val: T) -> Mutex<R, T> {
+        Mutex {
+            raw: raw_mutex,
+            data: UnsafeCell::new(val),
+        }
+    }
+
+    /// Consumes this mutex, returning the underlying data.
+    #[inline]
+    pub fn into_inner(self) -> T {
+        self.data.into_inner()
+    }
+
+    /// Returns a mutable reference to the underlying data.
+    ///
+    /// Since this call borrows the `Mutex` mutably, no actual locking needs to
+    /// take place---the mutable borrow statically guarantees no locks exist.
+    #[inline]
+    pub fn get_mut(&mut self) -> &mut T {
+        unsafe { &mut *self.data.get() }
+    }
+}
+
+pub type CriticalSectionMutex<T> = Mutex<raw::CriticalSectionRawMutex, T>;
+pub type NoopMutex<T> = Mutex<raw::NoopRawMutex, T>;
+
+impl<T> Mutex<raw::CriticalSectionRawMutex, T> {
     /// Borrows the data for the duration of the critical section
-    pub fn borrow<'cs>(&'cs self, _cs: CriticalSection<'cs>) -> &'cs T {
-        unsafe { &*self.inner.get() }
+    pub fn borrow<'cs>(&'cs self, _cs: critical_section::CriticalSection<'cs>) -> &'cs T {
+        let ptr = self.data.get() as *const T;
+        unsafe { &*ptr }
     }
 }
 
-impl<T> Mutex for CriticalSectionMutex<T> {
-    type Data = T;
-
-    fn new(data: T) -> Self {
-        Self::new(data)
-    }
-
-    fn lock<R>(&self, f: impl FnOnce(&Self::Data) -> R) -> R {
-        critical_section::with(|cs| f(self.borrow(cs)))
+impl<T> Mutex<raw::NoopRawMutex, T> {
+    /// Borrows the data
+    pub fn borrow(&self) -> &T {
+        let ptr = self.data.get() as *const T;
+        unsafe { &*ptr }
     }
 }
 
+// ThreadModeMutex does NOT use the generic mutex from above because it's special:
+// it's Send+Sync even if T: !Send. There's no way to do that without specialization (I think?).
+//
+// There's still a ThreadModeRawMutex for use with the generic Mutex (handy with Channel, for example),
+// but that will require T: Send even though it shouldn't be needed.
+
 #[cfg(any(cortex_m, feature = "std"))]
 pub use thread_mode_mutex::*;
 #[cfg(any(cortex_m, feature = "std"))]
@@ -75,15 +115,15 @@ mod thread_mode_mutex {
     /// **This Mutex is only safe on single-core systems.**
     ///
     /// On multi-core systems, a `ThreadModeMutex` **is not sufficient** to ensure exclusive access.
-    pub struct ThreadModeMutex<T> {
+    pub struct ThreadModeMutex<T: ?Sized> {
         inner: UnsafeCell<T>,
     }
 
     // NOTE: ThreadModeMutex only allows borrowing from one execution context ever: thread mode.
     // Therefore it cannot be used to send non-sendable stuff between execution contexts, so it can
     // be Send+Sync even if T is not Send (unlike CriticalSectionMutex)
-    unsafe impl<T> Sync for ThreadModeMutex<T> {}
-    unsafe impl<T> Send for ThreadModeMutex<T> {}
+    unsafe impl<T: ?Sized> Sync for ThreadModeMutex<T> {}
+    unsafe impl<T: ?Sized> Send for ThreadModeMutex<T> {}
 
     impl<T> ThreadModeMutex<T> {
         /// Creates a new mutex
@@ -92,79 +132,35 @@ mod thread_mode_mutex {
                 inner: UnsafeCell::new(value),
             }
         }
+    }
+
+    impl<T: ?Sized> ThreadModeMutex<T> {
+        pub fn lock<R>(&self, f: impl FnOnce(&T) -> R) -> R {
+            f(self.borrow())
+        }
 
         /// Borrows the data
         pub fn borrow(&self) -> &T {
             assert!(
-                in_thread_mode(),
+                raw::in_thread_mode(),
                 "ThreadModeMutex can only be borrowed from thread mode."
             );
             unsafe { &*self.inner.get() }
         }
     }
 
-    impl<T> Mutex for ThreadModeMutex<T> {
-        type Data = T;
-
-        fn new(data: T) -> Self {
-            Self::new(data)
-        }
-
-        fn lock<R>(&self, f: impl FnOnce(&Self::Data) -> R) -> R {
-            f(self.borrow())
-        }
-    }
-
-    impl<T> Drop for ThreadModeMutex<T> {
+    impl<T: ?Sized> Drop for ThreadModeMutex<T> {
         fn drop(&mut self) {
             // Only allow dropping from thread mode. Dropping calls drop on the inner `T`, so
             // `drop` needs the same guarantees as `lock`. `ThreadModeMutex<T>` is Send even if
             // T isn't, so without this check a user could create a ThreadModeMutex in thread mode,
             // send it to interrupt context and drop it there, which would "send" a T even if T is not Send.
             assert!(
-                in_thread_mode(),
+                raw::in_thread_mode(),
                 "ThreadModeMutex can only be dropped from thread mode."
             );
 
             // Drop of the inner `T` happens after this.
         }
     }
-
-    pub fn in_thread_mode() -> bool {
-        #[cfg(feature = "std")]
-        return Some("main") == std::thread::current().name();
-
-        #[cfg(not(feature = "std"))]
-        return cortex_m::peripheral::SCB::vect_active()
-            == cortex_m::peripheral::scb::VectActive::ThreadMode;
-    }
-}
-
-/// A "mutex" that does nothing and cannot be shared between threads.
-pub struct NoopMutex<T> {
-    inner: T,
-}
-
-impl<T> NoopMutex<T> {
-    pub const fn new(value: T) -> Self {
-        NoopMutex { inner: value }
-    }
-}
-
-impl<T> NoopMutex<T> {
-    pub fn borrow(&self) -> &T {
-        &self.inner
-    }
-}
-
-impl<T> Mutex for NoopMutex<T> {
-    type Data = T;
-
-    fn new(data: T) -> Self {
-        Self::new(data)
-    }
-
-    fn lock<R>(&self, f: impl FnOnce(&Self::Data) -> R) -> R {
-        f(self.borrow())
-    }
 }
diff --git a/embassy/src/blocking_mutex/raw.rs b/embassy/src/blocking_mutex/raw.rs
new file mode 100644
index 000000000..ebeb6dccf
--- /dev/null
+++ b/embassy/src/blocking_mutex/raw.rs
@@ -0,0 +1,112 @@
+use core::marker::PhantomData;
+
+pub trait RawMutex {
+    const INIT: Self;
+
+    fn lock<R>(&self, f: impl FnOnce() -> R) -> R;
+}
+
+pub struct CriticalSectionRawMutex {
+    _phantom: PhantomData<()>,
+}
+unsafe impl Send for CriticalSectionRawMutex {}
+unsafe impl Sync for CriticalSectionRawMutex {}
+
+impl CriticalSectionRawMutex {
+    pub const fn new() -> Self {
+        Self {
+            _phantom: PhantomData,
+        }
+    }
+}
+
+impl RawMutex for CriticalSectionRawMutex {
+    const INIT: Self = Self::new();
+
+    fn lock<R>(&self, f: impl FnOnce() -> R) -> R {
+        critical_section::with(|_| f())
+    }
+}
+
+// ================
+
+pub struct NoopRawMutex {
+    _phantom: PhantomData<*mut ()>,
+}
+
+unsafe impl Send for NoopRawMutex {}
+
+impl NoopRawMutex {
+    pub const fn new() -> Self {
+        Self {
+            _phantom: PhantomData,
+        }
+    }
+}
+
+impl RawMutex for NoopRawMutex {
+    const INIT: Self = Self::new();
+    fn lock<R>(&self, f: impl FnOnce() -> R) -> R {
+        f()
+    }
+}
+
+// ================
+
+#[cfg(any(cortex_m, feature = "std"))]
+mod thread_mode {
+    use super::*;
+
+    pub struct ThreadModeRawMutex {
+        _phantom: PhantomData<()>,
+    }
+
+    unsafe impl Send for ThreadModeRawMutex {}
+    unsafe impl Sync for ThreadModeRawMutex {}
+
+    impl ThreadModeRawMutex {
+        pub const fn new() -> Self {
+            Self {
+                _phantom: PhantomData,
+            }
+        }
+    }
+
+    impl RawMutex for ThreadModeRawMutex {
+        const INIT: Self = Self::new();
+        fn lock<R>(&self, f: impl FnOnce() -> R) -> R {
+            assert!(
+                in_thread_mode(),
+                "ThreadModeMutex can only be locked from thread mode."
+            );
+
+            f()
+        }
+    }
+
+    impl Drop for ThreadModeRawMutex {
+        fn drop(&mut self) {
+            // Only allow dropping from thread mode. Dropping calls drop on the inner `T`, so
+            // `drop` needs the same guarantees as `lock`. `ThreadModeMutex<T>` is Send even if
+            // T isn't, so without this check a user could create a ThreadModeMutex in thread mode,
+            // send it to interrupt context and drop it there, which would "send" a T even if T is not Send.
+            assert!(
+                in_thread_mode(),
+                "ThreadModeMutex can only be dropped from thread mode."
+            );
+
+            // Drop of the inner `T` happens after this.
+        }
+    }
+
+    pub(crate) fn in_thread_mode() -> bool {
+        #[cfg(feature = "std")]
+        return Some("main") == std::thread::current().name();
+
+        #[cfg(not(feature = "std"))]
+        return cortex_m::peripheral::SCB::vect_active()
+            == cortex_m::peripheral::scb::VectActive::ThreadMode;
+    }
+}
+#[cfg(any(cortex_m, feature = "std"))]
+pub use thread_mode::*;
diff --git a/embassy/src/channel/mpsc.rs b/embassy/src/channel/mpsc.rs
index 04709cb90..32787d810 100644
--- a/embassy/src/channel/mpsc.rs
+++ b/embassy/src/channel/mpsc.rs
@@ -47,7 +47,7 @@ use core::task::Waker;
 use futures::Future;
 use heapless::Deque;
 
-use crate::blocking_mutex::kind::MutexKind;
+use crate::blocking_mutex::raw::RawMutex;
 use crate::blocking_mutex::Mutex;
 use crate::waitqueue::WakerRegistration;
 
@@ -56,7 +56,7 @@ use crate::waitqueue::WakerRegistration;
 /// Instances are created by the [`split`](split) function.
 pub struct Sender<'ch, M, T, const N: usize>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     channel: &'ch Channel<M, T, N>,
 }
@@ -66,7 +66,7 @@ where
 /// Instances are created by the [`split`](split) function.
 pub struct Receiver<'ch, M, T, const N: usize>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     channel: &'ch Channel<M, T, N>,
 }
@@ -99,7 +99,7 @@ pub fn split<M, T, const N: usize>(
     channel: &mut Channel<M, T, N>,
 ) -> (Sender<M, T, N>, Receiver<M, T, N>)
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     let sender = Sender { channel };
     let receiver = Receiver { channel };
@@ -112,7 +112,7 @@ where
 
 impl<'ch, M, T, const N: usize> Receiver<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     /// Receives the next value for this receiver.
     ///
@@ -161,7 +161,7 @@ where
 
 impl<'ch, M, T, const N: usize> Drop for Receiver<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     fn drop(&mut self) {
         self.channel.lock(|c| c.deregister_receiver())
@@ -170,14 +170,14 @@ where
 
 pub struct RecvFuture<'ch, M, T, const N: usize>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     channel: &'ch Channel<M, T, N>,
 }
 
 impl<'ch, M, T, const N: usize> Future for RecvFuture<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     type Output = Option<T>;
 
@@ -193,7 +193,7 @@ where
 
 impl<'ch, M, T, const N: usize> Sender<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     /// Sends a value, waiting until there is capacity.
     ///
@@ -268,7 +268,7 @@ where
 
 pub struct SendFuture<'ch, M, T, const N: usize>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     channel: &'ch Channel<M, T, N>,
     message: Option<T>,
@@ -276,7 +276,7 @@ where
 
 impl<'ch, M, T, const N: usize> Future for SendFuture<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     type Output = Result<(), SendError<T>>;
 
@@ -295,18 +295,18 @@ where
     }
 }
 
-impl<'ch, M, T, const N: usize> Unpin for SendFuture<'ch, M, T, N> where M: MutexKind {}
+impl<'ch, M, T, const N: usize> Unpin for SendFuture<'ch, M, T, N> where M: RawMutex {}
 
 struct CloseFuture<'ch, M, T, const N: usize>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     channel: &'ch Channel<M, T, N>,
 }
 
 impl<'ch, M, T, const N: usize> Future for CloseFuture<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     type Output = ();
 
@@ -321,7 +321,7 @@ where
 
 impl<'ch, M, T, const N: usize> Drop for Sender<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     fn drop(&mut self) {
         self.channel.lock(|c| c.deregister_sender())
@@ -330,7 +330,7 @@ where
 
 impl<'ch, M, T, const N: usize> Clone for Sender<'ch, M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     fn clone(&self) -> Self {
         self.channel.lock(|c| c.register_sender());
@@ -546,30 +546,50 @@ impl<T, const N: usize> ChannelState<T, N> {
 /// All data sent will become available in the same order as it was sent.
 pub struct Channel<M, T, const N: usize>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
-    inner: M::Mutex<RefCell<ChannelState<T, N>>>,
+    inner: Mutex<M, RefCell<ChannelState<T, N>>>,
 }
 
 impl<M, T, const N: usize> Channel<M, T, N>
 where
-    M: MutexKind,
+    M: RawMutex,
 {
     /// Establish a new bounded channel. For example, to create one with a NoopMutex:
     ///
     /// ```
     /// use embassy::channel::mpsc;
-    /// use embassy::blocking_mutex::kind::Noop;
+    /// use embassy::blocking_mutex::raw::NoopRawMutex;
     /// use embassy::channel::mpsc::Channel;
     ///
     /// // Declare a bounded channel of 3 u32s.
-    /// let mut channel = Channel::<Noop, u32, 3>::new();
+    /// let mut channel = Channel::<NoopRawMutex, u32, 3>::new();
     /// // once we have a channel, obtain its sender and receiver
     /// let (sender, receiver) = mpsc::split(&mut channel);
     /// ```
+    #[cfg(feature = "nightly")]
+    pub const fn new() -> Self {
+        Self {
+            inner: Mutex::new(RefCell::new(ChannelState::new())),
+        }
+    }
+
+    /// Establish a new bounded channel. For example, to create one with a NoopMutex:
+    ///
+    /// ```
+    /// use embassy::channel::mpsc;
+    /// use embassy::blocking_mutex::raw::NoopRawMutex;
+    /// use embassy::channel::mpsc::Channel;
+    ///
+    /// // Declare a bounded channel of 3 u32s.
+    /// let mut channel = Channel::<NoopRawMutex, u32, 3>::new();
+    /// // once we have a channel, obtain its sender and receiver
+    /// let (sender, receiver) = mpsc::split(&mut channel);
+    /// ```
+    #[cfg(not(feature = "nightly"))]
     pub fn new() -> Self {
         Self {
-            inner: M::Mutex::new(RefCell::new(ChannelState::new())),
+            inner: Mutex::new(RefCell::new(ChannelState::new())),
         }
     }
 
@@ -586,7 +606,7 @@ mod tests {
     use futures_executor::ThreadPool;
     use futures_timer::Delay;
 
-    use crate::blocking_mutex::kind::{CriticalSection, Noop};
+    use crate::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex};
     use crate::util::Forever;
 
     use super::*;
@@ -655,7 +675,7 @@ mod tests {
 
     #[test]
     fn simple_send_and_receive() {
-        let mut c = Channel::<Noop, u32, 3>::new();
+        let mut c = Channel::<NoopRawMutex, u32, 3>::new();
         let (s, r) = split(&mut c);
         assert!(s.clone().try_send(1).is_ok());
         assert_eq!(r.try_recv().unwrap(), 1);
@@ -663,7 +683,7 @@ mod tests {
 
     #[test]
     fn should_close_without_sender() {
-        let mut c = Channel::<Noop, u32, 3>::new();
+        let mut c = Channel::<NoopRawMutex, u32, 3>::new();
         let (s, r) = split(&mut c);
         drop(s);
         match r.try_recv() {
@@ -674,7 +694,7 @@ mod tests {
 
     #[test]
     fn should_close_once_drained() {
-        let mut c = Channel::<Noop, u32, 3>::new();
+        let mut c = Channel::<NoopRawMutex, u32, 3>::new();
         let (s, r) = split(&mut c);
         assert!(s.try_send(1).is_ok());
         drop(s);
@@ -687,7 +707,7 @@ mod tests {
 
     #[test]
     fn should_reject_send_when_receiver_dropped() {
-        let mut c = Channel::<Noop, u32, 3>::new();
+        let mut c = Channel::<NoopRawMutex, u32, 3>::new();
         let (s, r) = split(&mut c);
         drop(r);
         match s.try_send(1) {
@@ -698,7 +718,7 @@ mod tests {
 
     #[test]
     fn should_reject_send_when_channel_closed() {
-        let mut c = Channel::<Noop, u32, 3>::new();
+        let mut c = Channel::<NoopRawMutex, u32, 3>::new();
         let (s, mut r) = split(&mut c);
         assert!(s.try_send(1).is_ok());
         r.close();
@@ -714,7 +734,7 @@ mod tests {
     async fn receiver_closes_when_sender_dropped_async() {
         let executor = ThreadPool::new().unwrap();
 
-        static CHANNEL: Forever<Channel<CriticalSection, u32, 3>> = Forever::new();
+        static CHANNEL: Forever<Channel<CriticalSectionRawMutex, u32, 3>> = Forever::new();
         let c = CHANNEL.put(Channel::new());
         let (s, mut r) = split(c);
         assert!(executor
@@ -729,7 +749,7 @@ mod tests {
     async fn receiver_receives_given_try_send_async() {
         let executor = ThreadPool::new().unwrap();
 
-        static CHANNEL: Forever<Channel<CriticalSection, u32, 3>> = Forever::new();
+        static CHANNEL: Forever<Channel<CriticalSectionRawMutex, u32, 3>> = Forever::new();
         let c = CHANNEL.put(Channel::new());
         let (s, mut r) = split(c);
         assert!(executor
@@ -742,7 +762,7 @@ mod tests {
 
     #[futures_test::test]
     async fn sender_send_completes_if_capacity() {
-        let mut c = Channel::<CriticalSection, u32, 1>::new();
+        let mut c = Channel::<CriticalSectionRawMutex, u32, 1>::new();
         let (s, mut r) = split(&mut c);
         assert!(s.send(1).await.is_ok());
         assert_eq!(r.recv().await, Some(1));
@@ -750,7 +770,7 @@ mod tests {
 
     #[futures_test::test]
     async fn sender_send_completes_if_closed() {
-        static CHANNEL: Forever<Channel<CriticalSection, u32, 1>> = Forever::new();
+        static CHANNEL: Forever<Channel<CriticalSectionRawMutex, u32, 1>> = Forever::new();
         let c = CHANNEL.put(Channel::new());
         let (s, r) = split(c);
         drop(r);
@@ -764,7 +784,7 @@ mod tests {
     async fn senders_sends_wait_until_capacity() {
         let executor = ThreadPool::new().unwrap();
 
-        static CHANNEL: Forever<Channel<CriticalSection, u32, 1>> = Forever::new();
+        static CHANNEL: Forever<Channel<CriticalSectionRawMutex, u32, 1>> = Forever::new();
         let c = CHANNEL.put(Channel::new());
         let (s0, mut r) = split(c);
         assert!(s0.try_send(1).is_ok());
@@ -784,7 +804,7 @@ mod tests {
 
     #[futures_test::test]
     async fn sender_close_completes_if_closing() {
-        static CHANNEL: Forever<Channel<CriticalSection, u32, 1>> = Forever::new();
+        static CHANNEL: Forever<Channel<CriticalSectionRawMutex, u32, 1>> = Forever::new();
         let c = CHANNEL.put(Channel::new());
         let (s, mut r) = split(c);
         r.close();
@@ -793,7 +813,7 @@ mod tests {
 
     #[futures_test::test]
     async fn sender_close_completes_if_closed() {
-        static CHANNEL: Forever<Channel<CriticalSection, u32, 1>> = Forever::new();
+        static CHANNEL: Forever<Channel<CriticalSectionRawMutex, u32, 1>> = Forever::new();
         let c = CHANNEL.put(Channel::new());
         let (s, r) = split(c);
         drop(r);
diff --git a/embassy/src/channel/signal.rs b/embassy/src/channel/signal.rs
index 87922b2fd..027f4f47c 100644
--- a/embassy/src/channel/signal.rs
+++ b/embassy/src/channel/signal.rs
@@ -32,13 +32,15 @@ enum State<T> {
 unsafe impl<T: Send> Send for Signal<T> {}
 unsafe impl<T: Send> Sync for Signal<T> {}
 
-impl<T: Send> Signal<T> {
+impl<T> Signal<T> {
     pub const fn new() -> Self {
         Self {
             state: UnsafeCell::new(State::None),
         }
     }
+}
 
+impl<T: Send> Signal<T> {
     /// Mark this Signal as completed.
     pub fn signal(&self, val: T) {
         critical_section::with(|_| unsafe {
diff --git a/embassy/src/executor/raw/mod.rs b/embassy/src/executor/raw/mod.rs
index 5ee38715d..3ae52ae31 100644
--- a/embassy/src/executor/raw/mod.rs
+++ b/embassy/src/executor/raw/mod.rs
@@ -57,6 +57,7 @@ pub struct TaskHeader {
 }
 
 impl TaskHeader {
+    #[cfg(feature = "nightly")]
     pub(crate) const fn new() -> Self {
         Self {
             state: AtomicU32::new(0),
@@ -71,6 +72,21 @@ impl TaskHeader {
         }
     }
 
+    #[cfg(not(feature = "nightly"))]
+    pub(crate) fn new() -> Self {
+        Self {
+            state: AtomicU32::new(0),
+            run_queue_item: RunQueueItem::new(),
+            executor: Cell::new(ptr::null()),
+            poll_fn: UninitCell::uninit(),
+
+            #[cfg(feature = "time")]
+            expires_at: Cell::new(Instant::from_ticks(0)),
+            #[cfg(feature = "time")]
+            timer_queue_item: timer_queue::TimerQueueItem::new(),
+        }
+    }
+
     pub(crate) unsafe fn enqueue(&self) {
         critical_section::with(|cs| {
             let state = self.state.load(Ordering::Relaxed);
@@ -113,7 +129,8 @@ pub struct TaskStorage<F: Future + 'static> {
 }
 
 impl<F: Future + 'static> TaskStorage<F> {
-    /// Create a new Task, in not-spawned state.
+    /// Create a new TaskStorage, in not-spawned state.
+    #[cfg(feature = "nightly")]
     pub const fn new() -> Self {
         Self {
             raw: TaskHeader::new(),
@@ -121,6 +138,15 @@ impl<F: Future + 'static> TaskStorage<F> {
         }
     }
 
+    /// Create a new TaskStorage, in not-spawned state.
+    #[cfg(not(feature = "nightly"))]
+    pub fn new() -> Self {
+        Self {
+            raw: TaskHeader::new(),
+            future: UninitCell::uninit(),
+        }
+    }
+
     /// Try to spawn a task in a pool.
     ///
     /// See [`Self::spawn()`] for details.
diff --git a/embassy/src/lib.rs b/embassy/src/lib.rs
index 2be0e0052..acc71e105 100644
--- a/embassy/src/lib.rs
+++ b/embassy/src/lib.rs
@@ -1,8 +1,13 @@
 #![cfg_attr(not(any(feature = "std", feature = "wasm")), no_std)]
-#![feature(generic_associated_types)]
-#![feature(const_fn_trait_bound)]
-#![feature(const_fn_fn_ptr_basics)]
-#![feature(type_alias_impl_trait)]
+#![cfg_attr(
+    feature = "nightly",
+    feature(
+        const_fn_trait_bound,
+        const_fn_fn_ptr_basics,
+        generic_associated_types,
+        type_alias_impl_trait
+    )
+)]
 #![allow(clippy::new_without_default)]
 
 // This mod MUST go first, so that the others see its macros.
@@ -20,7 +25,8 @@ pub mod io;
 pub mod time;
 pub mod util;
 
-pub use embassy_macros::*;
+#[cfg(feature = "nightly")]
+pub use embassy_macros::{main, task};
 
 #[doc(hidden)]
 /// Implementation details for embassy macros. DO NOT USE.
diff --git a/embassy/src/time/delay.rs b/embassy/src/time/delay.rs
index ff32941ee..27ec61fe6 100644
--- a/embassy/src/time/delay.rs
+++ b/embassy/src/time/delay.rs
@@ -16,11 +16,7 @@ pub struct Delay;
 
 #[cfg(feature = "unstable-traits")]
 mod eh1 {
-    use core::future::Future;
-    use futures::FutureExt;
-
     use super::*;
-    use crate::time::Timer;
 
     impl embedded_hal_1::delay::blocking::DelayUs for Delay {
         type Error = core::convert::Infallible;
@@ -33,6 +29,14 @@ mod eh1 {
             Ok(block_for(Duration::from_millis(ms as u64)))
         }
     }
+}
+
+#[cfg(all(feature = "unstable-traits", feature = "nightly"))]
+mod eh1a {
+    use super::*;
+    use crate::time::Timer;
+    use core::future::Future;
+    use futures::FutureExt;
 
     impl embedded_hal_async::delay::DelayUs for Delay {
         type Error = core::convert::Infallible;
diff --git a/embassy/src/waitqueue/waker_agnostic.rs b/embassy/src/waitqueue/waker_agnostic.rs
index f583fa6f4..89430aa4c 100644
--- a/embassy/src/waitqueue/waker_agnostic.rs
+++ b/embassy/src/waitqueue/waker_agnostic.rs
@@ -2,7 +2,8 @@ use core::cell::Cell;
 use core::mem;
 use core::task::Waker;
 
-use crate::blocking_mutex::CriticalSectionMutex as Mutex;
+use crate::blocking_mutex::raw::CriticalSectionRawMutex;
+use crate::blocking_mutex::Mutex;
 
 /// Utility struct to register and wake a waker.
 #[derive(Debug)]
@@ -50,13 +51,13 @@ impl WakerRegistration {
 
 /// Utility struct to register and wake a waker.
 pub struct AtomicWaker {
-    waker: Mutex<Cell<Option<Waker>>>,
+    waker: Mutex<CriticalSectionRawMutex, Cell<Option<Waker>>>,
 }
 
 impl AtomicWaker {
     pub const fn new() -> Self {
         Self {
-            waker: Mutex::new(Cell::new(None)),
+            waker: Mutex::const_new(CriticalSectionRawMutex::new(), Cell::new(None)),
         }
     }
 
diff --git a/examples/nrf/Cargo.toml b/examples/nrf/Cargo.toml
index da16bcbaf..2d9c99530 100644
--- a/examples/nrf/Cargo.toml
+++ b/examples/nrf/Cargo.toml
@@ -4,6 +4,9 @@ edition = "2018"
 name = "embassy-nrf-examples"
 version = "0.1.0"
 
+[features]
+default = ["nightly"]
+nightly = ["embassy-nrf/nightly"]
 
 [dependencies]
 embassy = { version = "0.1.0", path = "../../embassy", features = ["defmt"] }
diff --git a/examples/nrf/src/bin/mpsc.rs b/examples/nrf/src/bin/mpsc.rs
index 454fb9541..d50736d82 100644
--- a/examples/nrf/src/bin/mpsc.rs
+++ b/examples/nrf/src/bin/mpsc.rs
@@ -6,7 +6,7 @@
 mod example_common;
 
 use defmt::unwrap;
-use embassy::blocking_mutex::kind::Noop;
+use embassy::blocking_mutex::raw::NoopRawMutex;
 use embassy::channel::mpsc::{self, Channel, Sender, TryRecvError};
 use embassy::executor::Spawner;
 use embassy::time::{Duration, Timer};
@@ -19,10 +19,10 @@ enum LedState {
     Off,
 }
 
-static CHANNEL: Forever<Channel<Noop, LedState, 1>> = Forever::new();
+static CHANNEL: Forever<Channel<NoopRawMutex, LedState, 1>> = Forever::new();
 
 #[embassy::task(pool_size = 1)]
-async fn my_task(sender: Sender<'static, Noop, LedState, 1>) {
+async fn my_task(sender: Sender<'static, NoopRawMutex, LedState, 1>) {
     loop {
         let _ = sender.send(LedState::On).await;
         Timer::after(Duration::from_secs(1)).await;
diff --git a/examples/nrf/src/bin/uart_split.rs b/examples/nrf/src/bin/uart_split.rs
index 750798378..19d438c40 100644
--- a/examples/nrf/src/bin/uart_split.rs
+++ b/examples/nrf/src/bin/uart_split.rs
@@ -6,7 +6,7 @@
 mod example_common;
 use example_common::*;
 
-use embassy::blocking_mutex::kind::Noop;
+use embassy::blocking_mutex::raw::NoopRawMutex;
 use embassy::channel::mpsc::{self, Channel, Sender};
 use embassy::executor::Spawner;
 use embassy::util::Forever;
@@ -14,7 +14,7 @@ use embassy_nrf::peripherals::UARTE0;
 use embassy_nrf::uarte::UarteRx;
 use embassy_nrf::{interrupt, uarte, Peripherals};
 
-static CHANNEL: Forever<Channel<Noop, [u8; 8], 1>> = Forever::new();
+static CHANNEL: Forever<Channel<NoopRawMutex, [u8; 8], 1>> = Forever::new();
 
 #[embassy::main]
 async fn main(spawner: Spawner, p: Peripherals) {
@@ -56,7 +56,7 @@ async fn main(spawner: Spawner, p: Peripherals) {
 }
 
 #[embassy::task]
-async fn reader(mut rx: UarteRx<'static, UARTE0>, s: Sender<'static, Noop, [u8; 8], 1>) {
+async fn reader(mut rx: UarteRx<'static, UARTE0>, s: Sender<'static, NoopRawMutex, [u8; 8], 1>) {
     let mut buf = [0; 8];
     loop {
         info!("reading...");
diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml
index 450911fa1..ef60fe992 100644
--- a/examples/std/Cargo.toml
+++ b/examples/std/Cargo.toml
@@ -5,7 +5,7 @@ name = "embassy-std-examples"
 version = "0.1.0"
 
 [dependencies]
-embassy = { version = "0.1.0", path = "../../embassy", features = ["log", "std", "time"] }
+embassy = { version = "0.1.0", path = "../../embassy", features = ["log", "std", "time", "nightly"] }
 embassy-net = { version = "0.1.0", path = "../../embassy-net", features=["std", "log", "medium-ethernet", "tcp", "dhcpv4"] }
 
 async-io = "1.6.0"
diff --git a/examples/stm32f3/src/bin/button_events.rs b/examples/stm32f3/src/bin/button_events.rs
index 720ed9d11..1218edd2b 100644
--- a/examples/stm32f3/src/bin/button_events.rs
+++ b/examples/stm32f3/src/bin/button_events.rs
@@ -12,7 +12,7 @@
 
 #[path = "../example_common.rs"]
 mod example_common;
-use embassy::blocking_mutex::kind::Noop;
+use embassy::blocking_mutex::raw::NoopRawMutex;
 use embassy::channel::mpsc::{self, Channel, Receiver, Sender};
 use embassy::executor::Spawner;
 use embassy::time::{with_timeout, Duration, Timer};
@@ -77,7 +77,7 @@ enum ButtonEvent {
     Hold,
 }
 
-static BUTTON_EVENTS_QUEUE: Forever<Channel<Noop, ButtonEvent, 4>> = Forever::new();
+static BUTTON_EVENTS_QUEUE: Forever<Channel<NoopRawMutex, ButtonEvent, 4>> = Forever::new();
 
 #[embassy::main]
 async fn main(spawner: Spawner, p: Peripherals) {
@@ -103,7 +103,10 @@ async fn main(spawner: Spawner, p: Peripherals) {
 }
 
 #[embassy::task]
-async fn led_blinker(mut leds: Leds<'static>, queue: Receiver<'static, Noop, ButtonEvent, 4>) {
+async fn led_blinker(
+    mut leds: Leds<'static>,
+    queue: Receiver<'static, NoopRawMutex, ButtonEvent, 4>,
+) {
     loop {
         leds.blink().await;
         match queue.try_recv() {
@@ -121,7 +124,7 @@ async fn led_blinker(mut leds: Leds<'static>, queue: Receiver<'static, Noop, But
 #[embassy::task]
 async fn button_waiter(
     mut button: ExtiInput<'static, PA0>,
-    queue: Sender<'static, Noop, ButtonEvent, 4>,
+    queue: Sender<'static, NoopRawMutex, ButtonEvent, 4>,
 ) {
     const DOUBLE_CLICK_DELAY: u64 = 250;
     const HOLD_DELAY: u64 = 1000;
diff --git a/examples/wasm/Cargo.toml b/examples/wasm/Cargo.toml
index c6218a805..6750f6a6c 100644
--- a/examples/wasm/Cargo.toml
+++ b/examples/wasm/Cargo.toml
@@ -8,7 +8,7 @@ version = "0.1.0"
 crate-type = ["cdylib"]
 
 [dependencies]
-embassy = { version = "0.1.0", path = "../../embassy", features = ["log", "wasm"] }
+embassy = { version = "0.1.0", path = "../../embassy", features = ["log", "wasm", "nightly"] }
 
 wasm-logger = "0.2.0"
 wasm-bindgen = "0.2"