diff --git a/embassy-executor/Cargo.toml b/embassy-executor/Cargo.toml
index d8ac4ac00..d10752b3e 100644
--- a/embassy-executor/Cargo.toml
+++ b/embassy-executor/Cargo.toml
@@ -53,6 +53,7 @@ time-tick-16mhz = ["time"]
 [dependencies]
 defmt = { version = "0.3", optional = true }
 log = { version = "0.4.14", optional = true }
+rtos-trace = { version = "0.1.2", optional = true }
 
 embedded-hal-02 = { package = "embedded-hal", version = "0.2.6" }
 embedded-hal-1 = { package = "embedded-hal", version = "1.0.0-alpha.8", optional = true}
diff --git a/embassy-executor/src/executor/raw/mod.rs b/embassy-executor/src/executor/raw/mod.rs
index fb4cc6288..c943ecd84 100644
--- a/embassy-executor/src/executor/raw/mod.rs
+++ b/embassy-executor/src/executor/raw/mod.rs
@@ -22,6 +22,8 @@ use core::{mem, ptr};
 
 use atomic_polyfill::{AtomicU32, Ordering};
 use critical_section::CriticalSection;
+#[cfg(feature = "rtos-trace")]
+use rtos_trace::trace;
 
 use self::run_queue::{RunQueue, RunQueueItem};
 use self::util::UninitCell;
@@ -306,6 +308,9 @@ impl Executor {
     /// - `task` must NOT be already enqueued (in this executor or another one).
     #[inline(always)]
     unsafe fn enqueue(&self, cs: CriticalSection, task: NonNull<TaskHeader>) {
+        #[cfg(feature = "rtos-trace")]
+        trace::task_ready_begin(task.as_ptr() as u32);
+
         if self.run_queue.enqueue(cs, task) {
             (self.signal_fn)(self.signal_ctx)
         }
@@ -323,6 +328,9 @@ impl Executor {
     pub(super) unsafe fn spawn(&'static self, task: NonNull<TaskHeader>) {
         task.as_ref().executor.set(self);
 
+        #[cfg(feature = "rtos-trace")]
+        trace::task_new(task.as_ptr() as u32);
+
         critical_section::with(|cs| {
             self.enqueue(cs, task);
         })
@@ -365,9 +373,15 @@ impl Executor {
                 return;
             }
 
+            #[cfg(feature = "rtos-trace")]
+            trace::task_exec_begin(p.as_ptr() as u32);
+
             // Run the task
             task.poll_fn.read()(p as _);
 
+            #[cfg(feature = "rtos-trace")]
+            trace::task_exec_end();
+
             // Enqueue or update into timer_queue
             #[cfg(feature = "time")]
             self.timer_queue.update(p);
@@ -381,6 +395,9 @@ impl Executor {
             let next_expiration = self.timer_queue.next_expiration();
             driver::set_alarm(self.alarm, next_expiration.as_ticks());
         }
+
+        #[cfg(feature = "rtos-trace")]
+        trace::system_idle();
     }
 
     /// Get a spawner that spawns tasks in this executor.
@@ -425,3 +442,21 @@ pub(crate) unsafe fn register_timer(at: Instant, waker: &core::task::Waker) {
     let expires_at = task.expires_at.get();
     task.expires_at.set(expires_at.min(at));
 }
+
+#[cfg(feature = "rtos-trace")]
+impl rtos_trace::RtosTraceOSCallbacks for Executor {
+    fn task_list() {
+        // We don't know what tasks exist, so we can't send them.
+    }
+    #[cfg(feature = "time")]
+    fn time() -> u64 {
+        Instant::now().as_micros()
+    }
+    #[cfg(not(feature = "time"))]
+    fn time() -> u64 {
+        0
+    }
+}
+
+#[cfg(feature = "rtos-trace")]
+rtos_trace::global_os_callbacks!{Executor}
diff --git a/embassy-executor/src/lib.rs b/embassy-executor/src/lib.rs
index 69e4aeb4b..dd99f9e52 100644
--- a/embassy-executor/src/lib.rs
+++ b/embassy-executor/src/lib.rs
@@ -19,4 +19,25 @@ pub use embassy_macros::{main, task};
 /// Implementation details for embassy macros. DO NOT USE.
 pub mod export {
     pub use atomic_polyfill as atomic;
+
+    #[cfg(feature = "rtos-trace")]
+    pub use rtos_trace::trace;
+
+    /// Expands the given block of code when `embassy-executor` is compiled with
+    /// the `rtos-trace` feature.
+    #[doc(hidden)]
+    #[macro_export]
+    #[cfg(feature = "rtos-trace")]
+    macro_rules! rtos_trace {
+        ($($tt:tt)*) => { $($tt)* };
+    }
+
+    /// Does not expand the given block of code when `embassy-executor` is
+    /// compiled without the `rtos-trace` feature.
+    #[doc(hidden)]
+    #[macro_export]
+    #[cfg(not(feature = "rtos-trace"))]
+    macro_rules! rtos_trace {
+        ($($tt:tt)*) => {};
+    }
 }
diff --git a/embassy-macros/src/macros/cortex_m_interrupt_take.rs b/embassy-macros/src/macros/cortex_m_interrupt_take.rs
index 133eb5c26..5431704da 100644
--- a/embassy-macros/src/macros/cortex_m_interrupt_take.rs
+++ b/embassy-macros/src/macros/cortex_m_interrupt_take.rs
@@ -19,7 +19,13 @@ pub fn run(name: syn::Ident) -> Result<TokenStream, TokenStream> {
                 let func = HANDLER.func.load(::embassy_executor::export::atomic::Ordering::Relaxed);
                 let ctx = HANDLER.ctx.load(::embassy_executor::export::atomic::Ordering::Relaxed);
                 let func: fn(*mut ()) = ::core::mem::transmute(func);
-                func(ctx)
+                ::embassy_executor::rtos_trace! {
+                    ::embassy_executor::export::trace::isr_enter();
+                }
+                func(ctx);
+                ::embassy_executor::rtos_trace! {
+                    ::embassy_executor::export::trace::isr_exit();
+                }
             }
 
             static TAKEN: ::embassy_executor::export::atomic::AtomicBool = ::embassy_executor::export::atomic::AtomicBool::new(false);