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