
debug-esp32
GDB debugging for ESP32-S3 firmware. Use when user asks to debug, gdb, breakpoint, step through, bac
ESP32-S3 GDB Debugging Skill
Use this skill when the user asks to debug, GDB debug, step through, set breakpoints, or inspect ESP32 firmware.
Quick Start Workflow
# 1. Start debug session
esp32_start_debug(projectPath="/home/pncosta/domes/firmware/domes")
# Save the sessionId!
# 2. Reset and halt the device
esp32_gdb_command(sessionId, "monitor reset halt")
# 3. Set breakpoint
esp32_set_breakpoint(sessionId, location="main.cpp:94")
# 4. Continue with async mode (IMPORTANT!)
esp32_gdb_command(sessionId, "c &")
# 5. Wait for breakpoint, then check if hit
sleep 3-5 seconds
esp32_gdb_command(sessionId, "-break-list") # Check times field
# 6. Interrupt if needed
esp32_gdb_command(sessionId, "-exec-interrupt")
# 7. Inspect state
esp32_gdb_command(sessionId, "info stack")
esp32_gdb_command(sessionId, "info locals")
Critical Learnings
1. Always Use Async Continue
The esp32_continue tool can timeout waiting for breakpoints. Instead use:
esp32_gdb_command(sessionId, "c &")
Then wait and check breakpoint status with -break-list.
2. Always Reset After Starting
The device might be in an unknown state. Always run:
esp32_gdb_command(sessionId, "monitor reset halt")
3. Verify Breakpoints Were Hit
Use MI format to check the times field:
esp32_gdb_command(sessionId, "-break-list")
# Look for times="1" or higher = breakpoint was hit
# times="0" = breakpoint not hit yet
4. Interrupt Running Target
When target is running with c &, interrupt with:
esp32_gdb_command(sessionId, "-exec-interrupt")
5. Source Code Must Match ELF
If breakpoints aren't hit or symbols are wrong:
- Rebuild:
idf.py build - Reflash:
idf.py -p /dev/ttyACM0 flash - Restart debug session
6. Check Firmware Works First
Before debugging issues, verify firmware runs correctly:
# Monitor serial output
. ~/esp/esp-idf/export.sh && python3 -c "
import serial, time
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)
ser.setRTS(True); time.sleep(0.1); ser.setRTS(False)
start = time.time()
while time.time() - start < 6:
if ser.in_waiting:
print(ser.read(ser.in_waiting).decode('utf-8', errors='ignore'), end='')
time.sleep(0.1)
ser.close()
"
Available MCP Tools
Session Management
| Tool | Description |
|---|---|
esp32_start_debug | Start OpenOCD + GDB session |
esp32_stop_debug | Terminate session |
esp32_list_sessions | List active sessions |
Breakpoints
| Tool | Description |
|---|---|
esp32_set_breakpoint | Set at function or file:line |
esp32_delete_breakpoint | Remove by number |
esp32_list_breakpoints | List all breakpoints |
Execution Control
| Tool | Description |
|---|---|
esp32_continue | Continue (may timeout - prefer c & via gdb_command) |
esp32_step | Step into |
esp32_next | Step over |
esp32_finish | Run until return |
esp32_halt | Pause execution |
esp32_reset | Reset and halt |
Inspection
| Tool | Description |
|---|---|
esp32_backtrace | Call stack |
esp32_print | Evaluate expression |
esp32_examine_memory | Read memory |
esp32_info_registers | CPU registers |
esp32_info_locals | Local variables |
esp32_list_source | Display source |
FreeRTOS
| Tool | Description |
|---|---|
esp32_list_tasks | List tasks/threads |
esp32_switch_task | Switch to task |
Raw GDB
| Tool | Description |
|---|---|
esp32_gdb_command | Execute any GDB command |
Useful GDB Commands (via esp32_gdb_command)
# Reset and halt
monitor reset halt
# Async continue (doesn't block)
c &
# Interrupt running target
-exec-interrupt
# Check breakpoints with hit count
-break-list
# Stack trace
info stack
bt
where
# Local variables
info locals
# Print variable
print variableName
print *pointer
print array[0]
# Source code
list 90,100
# All threads/tasks
info threads
# Delete all breakpoints
delete
# Memory examination
x/16xb 0x3FC94200
Common Issues & Solutions
1. Breakpoint Never Hit
- Cause: Target running in idle task, task not created yet
- Solution:
- Check firmware runs correctly with serial monitor
- Set breakpoint on
app_mainfirst to verify breakpoints work - Wait longer (FreeRTOS task scheduling)
2. GDB Commands Timeout
- Cause: Target is running, command blocked waiting
- Solution: Use
-exec-interruptthen retry
3. Stuck in Panic/Flash Code
- Cause: Firmware crashed or corrupted state
- Solution:
- Stop debug session
- Reflash:
idf.py -p /dev/ttyACM0 flash - Restart debug session
4. Wrong Source Lines / Symbols
- Cause: ELF doesn't match source code
- Solution: Rebuild and reflash firmware
5. "TWDT already initialized" Error
- Cause: ESP-IDF auto-initializes Task Watchdog
- Solution: Handle
ESP_ERR_INVALID_STATEin watchdog init code
6. Connection Failed on Start
- Cause: Device busy or previous session not cleaned up
- Solution:
- Stop any existing session
- Wait 2 seconds
- Try again
Project Defaults
| Setting | Value |
|---|---|
| Project Path | /home/pncosta/domes/firmware/domes |
| ELF File | build/domes.elf |
| Serial Port | /dev/ttyACM0 |
| JTAG | Built-in USB-OTG JTAG |
Example: Debug LED Color Change
# 1. Start session
session = esp32_start_debug(projectPath="/home/pncosta/domes/firmware/domes")
sessionId = session.sessionId
# 2. Reset device
esp32_gdb_command(sessionId, "monitor reset halt")
# 3. Set breakpoint where LED color changes
esp32_set_breakpoint(sessionId, location="main.cpp:94")
# 4. Continue async
esp32_gdb_command(sessionId, "c &")
# 5. Wait for LED task to start
time.sleep(4)
# 6. Check if breakpoint hit
esp32_gdb_command(sessionId, "-break-list") # times="1" means hit
# 7. Show stack
esp32_gdb_command(sessionId, "info stack")
# Output:
# #0 ledDemoTask (pvParameters=0x0) at main.cpp:94
# #1 0x4037b3e0 in vPortTaskWrapper ...
# 8. Show locals
esp32_gdb_command(sessionId, "info locals")
# Output: c = {r = 255, g = 0, b = 0, name = "RED"}
# 9. Continue to next color
esp32_gdb_command(sessionId, "c &")
Multi-Device Debugging
When debugging with two devices connected, each device needs its own OpenOCD + GDB instance. Only ONE debug session can use the USB-JTAG port at a time.
Debugging Device on /dev/ttyACM0 (default)
Follow the standard workflow above. The MCP debug tools default to /dev/ttyACM0.
Debugging Second Device
The second device cannot share the same JTAG interface. Options:
- Debug one at a time: Stop the first debug session before starting one on the second device
- Use serial logging for the second device: While debugging one device via GDB, monitor the other via serial:
python3 .claude/skills/esp32-firmware/scripts/monitor_serial.py /dev/ttyACM1 30 - Separate JTAG adapters: If using external JTAG (not built-in USB), two adapters can debug simultaneously
Multi-Device Debug Workflow
Device 1 (/dev/ttyACM0): Full GDB debugging
Device 2 (/dev/ttyACM1): Serial log monitoring
# Terminal 1: GDB debug device 1
esp32_start_debug(projectPath="/home/pncosta/domes/firmware/domes")
esp32_gdb_command(sessionId, "monitor reset halt")
# Terminal 2: Monitor device 2
python3 -c "
import serial, time
ser = serial.Serial('/dev/ttyACM1', 115200, timeout=1)
start = time.time()
while time.time() - start < 60:
if ser.in_waiting:
print(ser.read(ser.in_waiting).decode('utf-8', errors='ignore'), end='')
time.sleep(0.1)
ser.close()
"
ESP-NOW Debugging Tips
- Set breakpoints in ESP-NOW callback functions on the device being debugged
- Monitor the other device's serial output for corresponding messages
- Use trace points (
TRACE_SCOPE) to measure ESP-NOW latency across pods