diff --git a/src/backtrace/espidf.rs b/src/backtrace/espidf.rs
new file mode 100644
index 000000000..d9a26e58b
--- /dev/null
+++ b/src/backtrace/espidf.rs
@@ -0,0 +1,98 @@
+//! Implementation of backtracing for ESP-IDF.
+
+use core::ffi::c_void;
+
+#[derive(Clone, Debug)]
+pub struct Frame {
+    ip: *mut c_void,
+    sp: *mut c_void,
+}
+
+// SAFETY: The pointers returned in this struct can be used from any thread.
+unsafe impl Send for Frame {}
+unsafe impl Sync for Frame {}
+
+impl Frame {
+    pub fn ip(&self) -> *mut c_void {
+        self.ip
+    }
+
+    pub fn sp(&self) -> *mut c_void {
+        self.sp
+    }
+
+    pub fn symbol_address(&self) -> *mut c_void {
+        0 as *mut _
+    }
+
+    pub fn module_base_address(&self) -> Option<*mut c_void> {
+        None
+    }
+}
+
+// Reference: https://github.com/espressif/esp-idf/blob/master/components/esp_system/include/esp_debug_helpers.h
+#[cfg(target_arch = "xtensa")]
+#[inline(always)]
+pub fn trace(cb: &mut dyn FnMut(&super::Frame) -> bool) {
+    #[repr(C)]
+    struct esp_backtrace_frame_t {
+        pc: *mut c_void,          // PC of the current frame
+        sp: *mut c_void,          // SP of the current frame
+        next_pc: *mut c_void,     // PC of the current frame's caller
+        exc_frame: *const c_void, // Pointer to the full frame data structure, if applicable
+    }
+
+    impl From<&esp_backtrace_frame_t> for Frame {
+        #[inline(always)]
+        fn from(esp_frame: &esp_backtrace_frame_t) -> Self {
+            let mut pc = esp_frame.pc as u32;
+
+            // Reference: https://github.com/espressif/esp-idf/blob/master/components/esp_hw_support/include/soc/cpu.h#L38
+            if (pc & 0x80000000) != 0 {
+                // Top two bits of a0 (return address) specify window increment. Overwrite to map to address space.
+                pc = (pc & 0x3fffffff) | 0x40000000;
+            }
+            pc = pc - 3; // Minus 3 to get PC of previous instruction (i.e. instruction executed before return address)
+
+            Self {
+                ip: pc as *mut _,
+                sp: esp_frame.sp,
+            }
+        }
+    }
+
+    extern "C" {
+        fn esp_backtrace_get_start(
+            pc: *mut *mut c_void,
+            sp: *mut *mut c_void,
+            next_pc: *mut *mut c_void,
+        );
+        fn esp_backtrace_get_next_frame(frame: *mut esp_backtrace_frame_t) -> bool;
+    }
+
+    let mut frame: esp_backtrace_frame_t = unsafe { ::core::mem::zeroed() };
+
+    unsafe {
+        esp_backtrace_get_start(
+            &mut frame.pc as *mut _,
+            &mut frame.sp as *mut _,
+            &mut frame.next_pc as *mut _,
+        )
+    };
+
+    cb(&super::Frame {
+        inner: (&frame).into(),
+    });
+
+    while unsafe { esp_backtrace_get_next_frame(&mut frame as *mut _) } {
+        cb(&super::Frame {
+            inner: (&frame).into(),
+        });
+    }
+}
+
+#[cfg(not(target_arch = "xtensa"))]
+#[inline(always)]
+pub fn trace(_cb: &mut dyn FnMut(&super::Frame) -> bool) {
+    // RiscV is not supported yet
+}
diff --git a/src/backtrace/mod.rs b/src/backtrace/mod.rs
index ca1e9148e..17a0f8691 100644
--- a/src/backtrace/mod.rs
+++ b/src/backtrace/mod.rs
@@ -137,6 +137,7 @@ cfg_if::cfg_if! {
             all(
                 unix,
                 not(target_os = "emscripten"),
+                not(target_os = "espidf"),
                 not(all(target_os = "ios", target_arch = "arm")),
             ),
             all(
@@ -154,6 +155,10 @@ cfg_if::cfg_if! {
         pub(crate) use self::dbghelp::Frame as FrameImp;
         #[cfg(target_env = "msvc")] // only used in dbghelp symbolize
         pub(crate) use self::dbghelp::StackFrame;
+    } else if #[cfg(target_os = "espidf")] {
+        pub(crate) mod espidf;
+        use self::espidf::trace as trace_imp;
+        pub(crate) use self::espidf::Frame as FrameImp;
     } else {
         mod noop;
         use self::noop::trace as trace_imp;
diff --git a/src/print.rs b/src/print.rs
index cc677122a..0b3c25f7c 100644
--- a/src/print.rs
+++ b/src/print.rs
@@ -191,6 +191,15 @@ impl BacktraceFrameFmt<'_, '_, '_> {
         // printing addresses in our own format here.
         if cfg!(target_os = "fuchsia") {
             self.print_raw_fuchsia(frame_ip)?;
+        }
+        // ESP-IDF is unable to symbolize because the firmware running on the chip
+        // does not have any symbolic information.
+        // The TTY monitor software however is symbolizing all outputted hexadecimal
+        // addresses which happen to fall within the memory region where the executed
+        // firmware is mapped.
+        // Therefore, it is enough to just print the frame instruction pointer value.
+        else if cfg!(target_os = "espidf") {
+            write!(self.fmt.fmt, "{:4}: {:?}", self.fmt.frame_index, frame_ip)
         } else {
             self.print_raw_generic(frame_ip, symbol_name, filename, lineno, colno)?;
         }
diff --git a/src/symbolize/mod.rs b/src/symbolize/mod.rs
index cf8d90f65..76c885bd1 100644
--- a/src/symbolize/mod.rs
+++ b/src/symbolize/mod.rs
@@ -474,6 +474,7 @@ cfg_if::cfg_if! {
         any(unix, windows),
         not(target_vendor = "uwp"),
         not(target_os = "emscripten"),
+        not(target_os = "espidf"),
         any(not(backtrace_in_libstd), feature = "backtrace"),
     ))] {
         mod gimli;