The Anatomy of an ESP32 Crash (and Why You Need File Logging)

When an ESP32 encounters a fatal error, it halts execution and prints a Guru Meditation Error to the serial monitor. In a bench environment, this is fine. In a remote deployment or enclosed IoT product, you cannot rely on a live serial connection. To effectively debug field failures, you must capture the ESP32 Arduino core dump backtrace to a file stored on local media (like an SD card or LittleFS) or a dedicated flash partition before the hardware watchdog resets the chip.

The direct answer: To save a backtrace to a file in the Arduino IDE, you must use the <esp_debug_helpers.h> library to iterate through the esp_backtrace_frame_t array and write the Program Counter (PC) addresses to an SD card during a software fault, or configure a dedicated coredump partition in your sdkconfig for bare-metal hardware panics.

Hardware Panic Limitation: During a Level 3 hardware panic (e.g., null pointer dereference), the ESP32 masks all interrupts and suspends FreeRTOS. You cannot write to an SD card via SPI during a hard fault because the SPI driver relies on interrupts. For hard faults, you must use the ESP-IDF Core Dump to Flash feature. The code provided below targets software-level faults, stack overflows, and caught assertions where the OS is still alive to process file I/O.

ESP32 Exception Causes & Hex Codes

Understanding the exception code printed in the panic handler tells you exactly what went wrong. Here are the most common hardware exceptions that trigger a dump:

Exception Name Hex Code Typical Root Cause Recovery Possible?
LoadProhibited 0x01 Reading from an invalid/unmapped memory address (e.g., dangling pointer). No (Hard Fault)
StoreProhibited 0x02 Writing to an invalid memory address (e.g., null pointer dereference). No (Hard Fault)
Privileged 0x04 Attempting to execute a privileged CPU instruction in user mode. No (Hard Fault)
IntegerDivideByZero 0x06 Dividing an integer by zero without a software check. No (Hard Fault)
Alloca / Stack Overflow N/A (SW) Exceeding the FreeRTOS task stack limit, corrupting the stack canary. Yes (via Hooks)

Hardware Setup & Pin Mapping

For this guide, we are targeting the ESP32-S3 DevKitC-1 (N8R8) variant, which features 8MB of Flash and 8MB of Octal PSRAM. We will use a standard MicroSD card breakout module connected via hardware SPI to log the backtrace data.

Parts List

  • MCU: ESP32-S3 DevKitC-1 (N8R8) - Ensure you select the "ESP32S3 Dev Module" in the Arduino IDE Boards Manager.
  • Storage: MicroSD Card Breakout Module (3.3V logic compatible, e.g., Adafruit 254).
  • Wiring: 24 AWG silicone jumper wires for SPI bus.
  • Software: Arduino IDE 2.x with ESP32 Core v2.0.14 or v3.x installed.

SPI Pin Mapping Table

The ESP32-S3 allows flexible GPIO routing for SPI, but these are the default hardware SPI pins for the S3 DevKitC-1 that avoid conflicts with internal PSRAM routing:

SD Card Breakout Pin ESP32-S3 DevKitC-1 GPIO Function Notes
VCC 3V3 Power Do NOT use 5V; S3 logic is strictly 3.3V.
GND GND Ground Connect to any common ground rail.
MOSI GPIO 11 Master Out Slave In Default SPI MOSI for ESP32-S3.
MISO GPIO 13 Master In Slave Out Default SPI MISO for ESP32-S3.
SCK GPIO 12 Serial Clock Default SPI CLK for ESP32-S3.
CS GPIO 10 Chip Select Active LOW. Must be defined in code.

The Code: Capturing and Writing the Backtrace

The following complete, compilable Arduino sketch initializes the SD card, sets up a deliberate software fault (a failed assertion), intercepts the backtrace using esp_backtrace_get(), and writes the raw Program Counter (PC) addresses to a text file on the SD card.

#include <Arduino.h>
#include <SD.h>
#include <SPI.h>
#include <esp_debug_helpers.h>
#include <esp_system.h>

// --- PIN DEFINITIONS (ESP32-S3 DevKitC-1) ---
#define SD_CS_PIN   10
#define SD_MOSI_PIN 11
#define SD_MISO_PIN 13
#define SD_SCK_PIN  12
#define TRIGGER_PIN 0 // BOOT button on DevKitC-1

// --- FUNCTION PROTOTYPES ---
void initSDCard();
void dumpBacktraceToSD();
void triggerSoftwareFault();

void setup() {
  Serial.begin(115200);
  delay(1000); // Allow serial monitor to connect
  Serial.println("[BOOT] ESP32-S3 Backtrace Logger Initialized.");

  pinMode(TRIGGER_PIN, INPUT_PULLUP);
  
  initSDCard();
}

void loop() {
  // Press the BOOT button (GPIO 0) to trigger a controlled software fault
  if (digitalRead(TRIGGER_PIN) == LOW) {
    delay(50); // Debounce
    if (digitalRead(TRIGGER_PIN) == LOW) {
      Serial.println("[FAULT] Triggering software assertion failure...");
      triggerSoftwareFault();
    }
  }
  delay(100);
}

void initSDCard() {
  SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("[ERROR] SD Card Mount Failed. Check wiring and FAT32 format.");
    return;
  }
  uint8_t cardType = SD.cardType();
  if (cardType == CARD_NONE) {
    Serial.println("[ERROR] No SD card attached.");
    return;
  }
  Serial.printf("[OK] SD Card Initialized. Type: %d, Size: %lluMB\n", cardType, SD.cardSize() / (1024 * 1024));
}

void dumpBacktraceToSD() {
  esp_backtrace_frame_t frame;
  // Initialize the backtrace iterator
  esp_err_t ret = esp_backtrace_get_start(&(frame.pc), &(frame.sp), &(frame.next_pc));
  
  if (ret != ESP_OK) {
    Serial.println("[ERROR] Failed to start backtrace capture.");
    return;
  }

  File logFile = SD.open("/crash_dump.txt", FILE_APPEND);
  if (!logFile) {
    Serial.println("[ERROR] Failed to open crash_dump.txt for writing.");
    return;
  }

  logFile.println("--- ESP32 BACKTRACE DUMP ---");
  logFile.printf("Timestamp: %lu\n", millis());
  logFile.println("Backtrace (PC Addresses):");
  
  // Write the first frame
  logFile.printf("0x%08x\n", frame.pc);
  Serial.printf("Backtrace: 0x%08x ", frame.pc);

  // Iterate through the stack frames
  while (esp_backtrace_get_next_frame(&frame) == ESP_OK) {
    logFile.printf("0x%08x\n", frame.pc);
    Serial.printf("0x%08x ", frame.pc);
  }
  
  logFile.println("\n--- END DUMP ---\n");
  logFile.close();
  
  Serial.println("\n[OK] Backtrace saved to /crash_dump.txt");
}

void triggerSoftwareFault() {
  // This simulates a software-level failure (like a failed config check or API error)
  // We capture the backtrace BEFORE calling abort() or resetting.
  dumpBacktraceToSD();
  
  // Force a restart after logging
  Serial.println("[SYSTEM] Restarting in 2 seconds...");
  delay(2000);
  ESP.restart();
}
Pro-Tip for FreeRTOS Tasks: If your crash happens inside a specific FreeRTOS task, the backtrace will only show the call stack for that specific task. Ensure your task stack size (defined in xTaskCreate) is large enough (minimum 4096 bytes recommended for logging operations) so the SD write operation doesn't trigger a secondary stack overflow.

Decoding the Backtrace File

The crash_dump.txt file on your SD card will contain a list of raw hex addresses (e.g., 0x42005A1C). To translate these into human-readable function names and line numbers, you need the xtensa-esp32s3-elf-addr2line tool included in the ESP32 Arduino toolchain.

  1. Locate the Tool: Navigate to your Arduino ESP32 tools directory. On Windows, this is typically C:\Users\[User]\AppData\Local\Arduino15\packages\esp32\tools\xtensa-esp-elf-gcc\[version]\bin\.
  2. Locate the ELF File: In the Arduino IDE, go to Sketch > Export Compiled Binary. This generates a .elf file in your sketch's build folder.
  3. Run the Decoder: Open your terminal and run the following command for each address in your dump file:
    xtensa-esp32s3-elf-addr2line -e YourSketch.ino.elf -f -p -a -C 0x42005A1C

This will output the exact function name, source file, and line number where the fault originated. For automated decoding, refer to the official Espressif Core Dump Documentation, which details using the espcoredump.py script for flash-based dumps.

Troubleshooting: When the Dump Fails

If your ESP32 crashes but the SD card file remains empty, or you encounter the following exact error string in your serial monitor:

Guru Meditation Error: Core 1 panic'ed (StoreProhibited). Exception was unhandled.
Core 1 register dump:
PC : 0x42005a1c PS : 0x00060030 A0 : 0x82005a1c A1 : 0x3fcb1d50

Here are the first three things to check when your backtrace logging fails to execute:

  1. Interrupt Masking (The Hard Fault Trap): As noted earlier, a StoreProhibited (0x02) error is a Level 3 hardware panic. The CPU instantly halts and masks interrupts. The SPI bus cannot clock data to the SD card without interrupts. Fix: You must use the ESP-IDF Core Dump to Flash partition method for hard faults, or use a secondary "watchdog" microcontroller (like an ATtiny85) on a secondary UART to log the serial output before the ESP32 resets.
  2. Task Stack Overflow During Logging: If the crash was caused by a stack overflow, attempting to allocate memory for the SD file buffer (File logFile = SD.open...) will trigger a secondary panic, aborting the dump. Fix: Pre-allocate a static buffer for the file name, or ensure the task running the logger has at least 8192 bytes of stack space.
  3. SD Card SPI Bus Contention: If the crash occurred while another task was actively writing to the SD card or using the SPI bus, the bus mutex will be locked forever. The panic handler will deadlock waiting for the SPI lock. Fix: Use a dedicated SPI bus (SPI2_HOST) for the crash logger, separate from your main application peripherals.

How to Extend or Simplify the Build

To Simplify: If you don't want to wire an SD card, simplify the build by using a host-side Python script (like pyserial) to monitor the UART port and regex-match the Backtrace: 0x... string, saving it to a local .txt file on your PC. This is ideal for bench debugging.

To Extend: For production IoT deployments, extend the code by adding AES-256 encryption to the backtrace file before writing it to LittleFS, and implement an MQTT or HTTPS POST routine on the next boot cycle to upload the crash_dump.txt file to an AWS S3 bucket or Grafana Loki instance for centralized fleet monitoring. Check the Arduino ESP32 Core GitHub repository for the latest esp_http_client examples.