AgentSkillsCN

debug-esp32

使用 ESP-IDF 构建、烧录并监控 ESP32 固件。适用于编译、烧录或调试 ESP32/ESP32-S3 固件,查看串口输出,或使用 idf.py 命令进行操作时使用。

SKILL.md
--- frontmatter
name: debug-esp32
description: GDB debugging for ESP32-S3 firmware. Use when user asks to debug, gdb, breakpoint, step through, backtrace, info stack, inspect variables, or investigate crashes in ESP32 firmware.

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

bash
# 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:

code
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:

code
esp32_gdb_command(sessionId, "monitor reset halt")

3. Verify Breakpoints Were Hit

Use MI format to check the times field:

code
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:

code
esp32_gdb_command(sessionId, "-exec-interrupt")

5. Source Code Must Match ELF

If breakpoints aren't hit or symbols are wrong:

  1. Rebuild: idf.py build
  2. Reflash: idf.py -p /dev/ttyACM0 flash
  3. Restart debug session

6. Check Firmware Works First

Before debugging issues, verify firmware runs correctly:

python
# 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

ToolDescription
esp32_start_debugStart OpenOCD + GDB session
esp32_stop_debugTerminate session
esp32_list_sessionsList active sessions

Breakpoints

ToolDescription
esp32_set_breakpointSet at function or file:line
esp32_delete_breakpointRemove by number
esp32_list_breakpointsList all breakpoints

Execution Control

ToolDescription
esp32_continueContinue (may timeout - prefer c & via gdb_command)
esp32_stepStep into
esp32_nextStep over
esp32_finishRun until return
esp32_haltPause execution
esp32_resetReset and halt

Inspection

ToolDescription
esp32_backtraceCall stack
esp32_printEvaluate expression
esp32_examine_memoryRead memory
esp32_info_registersCPU registers
esp32_info_localsLocal variables
esp32_list_sourceDisplay source

FreeRTOS

ToolDescription
esp32_list_tasksList tasks/threads
esp32_switch_taskSwitch to task

Raw GDB

ToolDescription
esp32_gdb_commandExecute any GDB command

Useful GDB Commands (via esp32_gdb_command)

gdb
# 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:
    1. Check firmware runs correctly with serial monitor
    2. Set breakpoint on app_main first to verify breakpoints work
    3. Wait longer (FreeRTOS task scheduling)

2. GDB Commands Timeout

  • Cause: Target is running, command blocked waiting
  • Solution: Use -exec-interrupt then retry

3. Stuck in Panic/Flash Code

  • Cause: Firmware crashed or corrupted state
  • Solution:
    1. Stop debug session
    2. Reflash: idf.py -p /dev/ttyACM0 flash
    3. 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_STATE in watchdog init code

6. Connection Failed on Start

  • Cause: Device busy or previous session not cleaned up
  • Solution:
    1. Stop any existing session
    2. Wait 2 seconds
    3. Try again

Project Defaults

SettingValue
Project Path/home/pncosta/domes/firmware/domes
ELF Filebuild/domes.elf
Serial Port/dev/ttyACM0
JTAGBuilt-in USB-OTG JTAG

Example: Debug LED Color Change

python
# 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:

  1. Debug one at a time: Stop the first debug session before starting one on the second device
  2. Use serial logging for the second device: While debugging one device via GDB, monitor the other via serial:
    bash
    python3 .claude/skills/esp32-firmware/scripts/monitor_serial.py /dev/ttyACM1 30
    
  3. Separate JTAG adapters: If using external JTAG (not built-in USB), two adapters can debug simultaneously

Multi-Device Debug Workflow

code
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