ESP32/ESP32-C5_GDB_Debugging_Guid...

23 KiB

GDB Debugging on ESP32-C5: Complete Guide

A comprehensive guide to debugging ESP32-C5 firmware using GDB and the built-in USB-JTAG interface.

Author: Bob McMahon
Hardware: ESP32-C5 DevKit (RISC-V)
ESP-IDF: v6.0 or later
Last Updated: December 2025


Table of Contents

  1. Introduction
  2. Why GDB Debugging?
  3. ESP32-C5 Debug Capabilities
  4. Prerequisites
  5. Building with Debug Symbols
  6. Starting a Debug Session
  7. Essential GDB Commands
  8. Debugging Strategies
  9. Real-World Examples
  10. Troubleshooting
  11. Advanced Techniques
  12. Resources

Introduction

The ESP32-C5 is Espressif's first RISC-V microcontroller with dual-band WiFi 6 (802.11ax) support. Unlike its Xtensa predecessors (ESP32, ESP32-S3), the ESP32-C5's RISC-V architecture and built-in USB-JTAG interface make debugging significantly easier.

This guide demonstrates how to use GDB (GNU Debugger) to debug ESP32-C5 firmware, focusing on real-world scenarios like troubleshooting WiFi driver issues, CSI configuration problems, and memory corruption.


Why GDB Debugging?

Traditional debugging with ESP_LOGI() statements has limitations:

Method Limitations
Printf Debugging - Alters timing and behavior
- Cannot inspect internal driver state
- Requires recompilation for each change
- Output floods serial console
LED Blink Debugging - Very limited information
- Time-consuming iteration
- Cannot show complex state

GDB debugging solves these problems:

  • Set breakpoints without modifying code
  • Inspect variables at any point in execution
  • Step through code line by line
  • Examine memory and registers
  • Watch variables for changes
  • View call stacks to understand program flow
  • Debug ESP-IDF internals (WiFi driver, FreeRTOS, etc.)

ESP32-C5 Debug Capabilities

The ESP32-C5 has built-in USB-JTAG support, eliminating the need for external debug adapters:

Hardware Features

  • Built-in USB-JTAG: Debug over the same USB cable used for flashing
  • 4 Hardware Breakpoints: No speed penalty
  • Unlimited Software Breakpoints: Via flash patching
  • 2 Watchpoints: Trigger on memory read/write
  • Real-time Debugging: Debug live, running firmware

Comparison with Other ESP32 Chips

Feature ESP32 (Xtensa) ESP32-S3 (Xtensa) ESP32-C5 (RISC-V)
Debug Interface External JTAG required Built-in USB-JTAG Built-in USB-JTAG
Debugger xt-gdb (Xtensa) xt-gdb (Xtensa) riscv32-esp-elf-gdb
Setup Complexity High (extra hardware) Medium Low (just USB)
OpenOCD Support Mature Mature Good (ESP-IDF v6.0+)

Prerequisites

Hardware

  • ESP32-C5 DevKit with USB-C cable
  • Host Computer running Linux, macOS, or Windows (WSL2)

Software

  • ESP-IDF v6.0 or later (ESP32-C5 support)
  • OpenOCD (included with ESP-IDF)
  • GDB for RISC-V (riscv32-esp-elf-gdb, included with ESP-IDF)

Verify Installation

# Check ESP-IDF version
idf.py --version
# Should show: ESP-IDF v6.0 or later

# Check GDB
riscv32-esp-elf-gdb --version
# Should show: GNU gdb (esp-gdb) 12.1 or later

# Check OpenOCD
openocd --version
# Should show: Open On-Chip Debugger 0.12.0-esp32 or later

Building with Debug Symbols

Debug symbols allow GDB to map machine code back to source code, showing variable names, function names, and line numbers.

cd ~/your-project
idf.py menuconfig

Navigate to and configure:

Component config
  → Compiler options
    → Optimization Level → Debug (-Og)       ← Select this
    → [*] Generate debug symbols (-g)        ← Enable
    → Debug information format → DWARF-4     ← Select

Additional recommended settings:

Component config
  → Compiler options
    → [*] Enable assertions (assert)         ← Enable
    → [ ] Strip function/variable names      ← DISABLE

Component config
  → FreeRTOS
    → [*] Enable stack overflow checks       ← Enable
    → Check method → Canary bytes           ← Select

Save and exit (S then Q).

Method 2: Direct sdkconfig Edit

cd ~/your-project

# Backup current config
cp sdkconfig sdkconfig.backup

# Add debug settings
cat >> sdkconfig << 'EOF'
# Debug optimization
CONFIG_COMPILER_OPTIMIZATION_DEBUG=y
CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y

# Enable assertions
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y

# Stack checking
CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y
CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y

# Debug info
CONFIG_COMPILER_CXX_EXCEPTIONS=y
EOF

Optimization Levels Explained

Level GCC Flag Code Speed Code Size Debug Quality Use Case
Debug -Og Medium Medium Excellent GDB debugging
None -O0 Slow Large Excellent Extreme debugging
Size -Os Medium Small Poor Production
Performance -O2 Fast Medium Poor Production
Max Performance -O3 Fastest Large Very Poor Benchmarks

For debugging, always use -Og (Debug level). It provides good performance while preserving all variable information for GDB.

Build Process

cd ~/your-project

# Clean previous build
idf.py fullclean

# Build with debug symbols
idf.py build

# Flash to device
idf.py -p /dev/ttyUSB0 flash

Verify Debug Symbols

# Check if ELF file contains debug sections
riscv32-esp-elf-readelf -S build/your-project.elf | grep debug

# Expected output (debug sections present):
#   [27] .debug_aranges    PROGBITS        00000000 0f8a2c 004638 00      0   0  8
#   [28] .debug_info       PROGBITS        00000000 0fd064 19d4f4 00      0   0  1
#   [29] .debug_abbrev     PROGBITS        00000000 29a558 02b8f9 00      0   0  1
#   [30] .debug_line       PROGBITS        00000000 2c5e51 0e7a3c 00      0   0  1
#   [31] .debug_str        PROGBITS        00000000 3ad88d 036184 01  MS  0   0  1

If you don't see .debug_* sections, debug symbols are missing. Check your optimization settings.


Starting a Debug Session

Three-Step Debug Process

  1. Flash the firmware to the device
  2. Start OpenOCD to connect to the device
  3. Start GDB to control debugging

Step 1: Flash Firmware

cd ~/your-project
idf.py -p /dev/ttyUSB0 flash

Step 2: Start OpenOCD (Terminal 1)

cd ~/your-project
idf.py openocd

Expected output:

Open On-Chip Debugger v0.12.0-esp32-20230419 (2023-04-19-13:01)
Licensed under GNU GPL v2
...
Info : [esp32c5] Target halted, PC=0x42008a4e, debug_reason=00000001
Info : [esp32c5] Reset cause (3) - (Software core reset)

Leave this terminal running. OpenOCD acts as a bridge between GDB and the ESP32-C5.

Step 3: Start GDB (Terminal 2)

cd ~/your-project
idf.py gdb

Expected output:

GNU gdb (esp-gdb) 12.1_20221002
...
(gdb) 

You're now in the GDB prompt and ready to debug!

Quick Start Commands

# Connect to OpenOCD (usually done automatically)
target remote :3333

# Load symbols
file build/your-project.elf

# Reset and halt at app_main
monitor reset halt
thbreak app_main
continue

Essential GDB Commands

Navigation and Execution

Command Shortcut Description Example
break <location> b Set breakpoint b app_main
continue c Resume execution c
next n Step over (skip function calls) n
step s Step into (enter functions) s
finish fin Run until function returns fin
until <line> u Run until line number u 100
run r Start program r

Inspection

Command Shortcut Description Example
print <var> p Print variable value p my_variable
print *<ptr> p * Dereference pointer p *config
x/<fmt> <addr> x Examine memory x/32xb 0x40000000
info locals i lo Show local variables i lo
info args i ar Show function arguments i ar
info registers i r Show CPU registers i r
backtrace bt Show call stack bt
list l Show source code l

Breakpoints

Command Description Example
break <func> Break on function entry b esp_wifi_init
break <file>:<line> Break at specific line b main.c:42
break *<addr> Break at memory address b *0x42008a4e
break <func> if <cond> Conditional breakpoint b send_data if len > 1000
tbreak <location> Temporary breakpoint (one-time) tb app_main
info breakpoints List all breakpoints i b
delete <num> Delete breakpoint d 1
disable <num> Disable breakpoint dis 1
enable <num> Enable breakpoint en 1

Watchpoints

Command Description Example
watch <var> Break when variable changes watch my_counter
watch *<addr> Break when memory changes watch *(int*)0x3ff00000
rwatch <var> Break on read rwatch secret_key
awatch <var> Break on read or write awatch buffer[0]

Memory Examination

Format Description Example
x/32xb <addr> 32 bytes in hex x/32xb &config
x/8xw <addr> 8 words (32-bit) in hex x/8xw 0x40000000
x/s <addr> String (null-terminated) x/s ssid_buffer
x/i <addr> Instruction (disassembly) x/10i $pc

Control Flow

Command Description
monitor reset halt Reset chip and stop at bootloader
monitor reset Reset chip and run
interrupt Pause execution (Ctrl+C)
quit Exit GDB

Debugging Strategies

Strategy 1: Breakpoint at Function Entry

Use case: Understand when and why a function is called.

# Break when WiFi CSI configuration is attempted
break esp_wifi_set_csi_config

# Run until breakpoint
continue

# When it breaks, examine arguments
info args
print *config

# Check who called this function
backtrace

# Continue execution
continue

Strategy 2: Conditional Breakpoints

Use case: Break only when specific conditions occur.

# Break only when error occurs
break esp_wifi_set_csi_config if $a0 != 0

# Break only for specific SSID
break wifi_connect if strcmp(ssid, "MyNetwork") == 0

# Break when buffer is full
break send_packet if queue_size >= 100

Strategy 3: Step Through Algorithm

Use case: Understand complex logic step by step.

# Break at start of function
break process_csi_data

# Run until breakpoint
continue

# Step through line by line
next    # Execute current line
next    # Next line
step    # Step into function call if any
finish  # Complete current function

Strategy 4: Watch for Variable Changes

Use case: Find where a variable gets corrupted.

# Watch a variable
watch connection_state

# Run - GDB will break when variable changes
continue

# When it breaks, see the call stack
backtrace

# See old and new values
print connection_state

Strategy 5: Post-Mortem Debugging

Use case: Analyze crash dumps.

# After a crash, examine the panic
backtrace

# See register state at crash
info registers

# Examine memory around crash
x/32xw $sp    # Stack pointer
x/10i $pc     # Instructions at crash

# Check for stack overflow
info frame

Real-World Examples

Example 1: Debug CSI Configuration Failure

Problem: esp_wifi_set_csi_config() returns ESP_FAIL but we don't know why.

# Start GDB session
(gdb) target remote :3333
(gdb) file build/CSI.elf
(gdb) monitor reset halt

# Break on CSI configuration
(gdb) break esp_wifi_set_csi_config
Breakpoint 1 at 0x42012a4e

# Run until breakpoint
(gdb) continue
Breakpoint 1, esp_wifi_set_csi_config (config=0x3ffb0000)

# Examine the config structure being passed
(gdb) print *config
$1 = {
  enable = 1,
  lltf_en = 1,
  htltf_en = 1,
  stbc_htltf2_en = 1,
  ltf_merge_en = 1,
  channel_filter_en = 1,    ← Suspicious!
  manu_scale = 0
}

# channel_filter_en = 1 is known to cause ESP_FAIL on some chips
# Let's step through to confirm

(gdb) step
(gdb) step
...
# Reaches error check for channel_filter_en

(gdb) print error_code
$2 = 259  ← ESP_FAIL (0x103)

# Found it! channel_filter_en must be 0 on ESP32-C5

Solution: Set channel_filter_en = 0 in the code.

Example 2: Find Memory Corruption

Problem: A pointer is getting corrupted, causing crashes.

# Set watchpoint on the pointer
(gdb) watch *(void**)&my_buffer_ptr
Hardware watchpoint 2: *(void**)&my_buffer_ptr

# Run until it changes
(gdb) continue
Hardware watchpoint 2: *(void**)&my_buffer_ptr
Old value = (void *) 0x3ffb1000
New value = (void *) 0x00000000

# See what code changed it
(gdb) backtrace
#0  process_packet (data=0x3ffb0800) at network.c:142
#1  0x42008654 in network_task () at network.c:201
#2  0x4200a123 in vTaskDelay () at FreeRTOS.c:1543

# Look at the source
(gdb) list
137     void process_packet(uint8_t *data) {
138         if (data == NULL) {
139             ESP_LOGE(TAG, "Null data!");
140             my_buffer_ptr = NULL;    ← Found it! Setting to NULL here
141             return;
142         }

Solution: Fix the null-pointer handling logic.

Example 3: Understand WiFi Connection Failure

Problem: WiFi connects but immediately disconnects.

# Break on disconnect event
(gdb) break event_handler
Breakpoint 1 at 0x42009876

# Add condition for disconnect events only
(gdb) condition 1 event_id == WIFI_EVENT_STA_DISCONNECTED

(gdb) continue
Breakpoint 1, event_handler (event_id=3, event_data=0x3ffb2000)

# Examine disconnect reason
(gdb) print *(wifi_event_sta_disconnected_t*)event_data
$1 = {
  ssid = "ClubHouse",
  ssid_len = 9,
  bssid = {0xe0, 0x46, 0xee, 0x07, 0xdf, 0x01},
  reason = 2,    ← WIFI_REASON_AUTH_EXPIRE
  rssi = -75
}

# Reason 2 = Authentication expired = weak signal or interference

Solution: Improve antenna placement or reduce distance to AP.

Example 4: Profile Function Performance

Use case: Measure time spent in a critical function.

# Break at function entry
(gdb) break process_csi_data
Breakpoint 1 at 0x42010a00

# Continue to breakpoint
(gdb) continue
Breakpoint 1, process_csi_data ()

# Get current cycle count (RISC-V has cycle counter)
(gdb) print $cycle
$1 = 12456789

# Step out of function
(gdb) finish

# Check cycles again
(gdb) print $cycle
$2 = 12501234

# Calculate time (assuming 240 MHz clock)
# (12501234 - 12456789) / 240,000,000 = 0.185 ms

Example 5: Debug Stack Overflow

Problem: Task crashes with stack overflow.

# Break after crash
(gdb) backtrace
#0  0x420089a4 in panic_abort ()
#1  0x4200a123 in vTaskStackOverflow ()
#2  0x42012456 in my_task ()

# Check stack usage
(gdb) info frame
Stack level 2, frame at 0x3ffb0ff8:
 pc = 0x42012456 in my_task
 saved pc = 0x4200a123
 Arglist at 0x3ffb0ff8, args: 
 Locals at 0x3ffb0ff8, Previous frame's sp is 0x3ffb1000

# Stack grew to 0x3ffb0ff8 but task stack base is 0x3ffb1000
# Only 8 bytes left! Stack is too small.

# Check task stack size in code
(gdb) print task_stack_size
$1 = 2048    ← Too small!

Solution: Increase task stack size to 4096 or 6144 bytes.


Troubleshooting

Problem: "No symbol table is loaded"

Symptom:

(gdb) break app_main
Function "app_main" not defined.

Causes:

  1. Debug symbols not built
  2. Wrong ELF file loaded
  3. Optimization stripped symbols

Solutions:

# 1. Rebuild with debug symbols
cd ~/your-project
idf.py menuconfig  # Set optimization to Debug (-Og)
idf.py fullclean build

# 2. Load correct ELF file in GDB
(gdb) file build/your-project.elf

# 3. Verify symbols exist
riscv32-esp-elf-nm build/your-project.elf | grep app_main

Problem: "Cannot access memory at address 0x..."

Symptom:

(gdb) print my_variable
Cannot access memory at address 0x3ffb0000

Causes:

  1. Variable optimized out
  2. Variable not in scope
  3. Pointer is invalid

Solutions:

# Check if variable exists
(gdb) info locals    # Show all local variables
(gdb) info args      # Show function arguments

# If optimized out, rebuild with -Og
# If out of scope, break where variable is accessible
# If pointer invalid, examine pointer value
(gdb) print &my_variable    # Get address
(gdb) x/4xw 0x3ffb0000      # Examine raw memory

Problem: Breakpoint Not Hitting

Symptom: Breakpoint set but never triggers.

Causes:

  1. Code never executed
  2. Breakpoint at wrong location
  3. Out of hardware breakpoints

Solutions:

# Check breakpoint status
(gdb) info breakpoints
Num Type           Disp Enb Address    What
1   breakpoint     keep y   0x42012000 in my_func at main.c:42

# If address is 0x00000000, function doesn't exist
# If "Enb" is "n", breakpoint is disabled
(gdb) enable 1

# Try software breakpoint instead
(gdb) delete 1
(gdb) break my_func

Problem: GDB Disconnects Randomly

Symptom: "Remote connection closed" during debugging.

Causes:

  1. Watchdog timeout
  2. CPU held too long at breakpoint
  3. OpenOCD crash

Solutions:

# Disable watchdog in menuconfig
# Component config → ESP System Settings → 
#   → [*] Interrupt watchdog timeout (ms) → 0 (disabled)

# In GDB, don't hold breakpoints too long
# Continue quickly or disable watchdog:
(gdb) monitor esp wdt off

Problem: "Cannot insert breakpoint"

Symptom:

(gdb) break my_func
Cannot insert breakpoint 1.
Error accessing memory address 0x42012000

Causes:

  1. Code in flash, not RAM (need flash breakpoints)
  2. Out of hardware breakpoints
  3. Region not writable

Solutions:

# Use hardware breakpoint
(gdb) hbreak my_func

# Check breakpoint count
(gdb) info breakpoints
# ESP32-C5 has 4 hardware breakpoints max

# Delete unused breakpoints
(gdb) delete 2 3 4

Advanced Techniques

Technique 1: Scripting GDB

Create a .gdbinit file to automate common tasks:

# ~/.gdbinit or project/.gdbinit

# Connect automatically
target remote :3333

# Load symbols
file build/CSI.elf

# Define custom commands
define reset-and-break
    monitor reset halt
    thbreak app_main
    continue
end

# Set common breakpoints
break esp_wifi_set_csi_config
break esp_wifi_connect

# Custom print for WiFi config
define print-wifi-config
    printf "SSID: %s\n", wifi_config.sta.ssid
    printf "Password: %s\n", wifi_config.sta.password
    printf "Channel: %d\n", wifi_config.sta.channel
end

# Display instructions
echo \n=== ESP32-C5 Debug Session ===\n
echo Commands:\n
echo   reset-and-break  : Reset chip and break at app_main\n
echo   print-wifi-config: Show WiFi configuration\n
echo \n

Usage:

idf.py gdb
# Automatically connects and loads symbols
(gdb) reset-and-break    # Your custom command

Technique 2: Debugging FreeRTOS Tasks

List all tasks and their states:

# Show all tasks
(gdb) info threads
  Id   Target Id                    Frame 
* 1    Remote target                vTaskDelay () at FreeRTOS.c:1543
  2    Remote target                prvIdleTask () at FreeRTOS.c:2301
  3    Remote target                wifi_task () at esp_wifi_driver.c:456

# Switch to different task
(gdb) thread 3
[Switching to thread 3 (Remote target)]

# See that task's stack
(gdb) backtrace
#0  wifi_task () at esp_wifi_driver.c:456
#1  0x4200a456 in vPortTaskWrapper ()

Technique 3: Examine WiFi Driver Internals

# Break in WiFi driver
(gdb) break esp_wifi_internal.c:esp_wifi_set_bandwidth

# When it breaks, examine internal state
(gdb) print g_wifi_state
(gdb) print g_wifi_config
(gdb) print g_sta_netif

# Step through WiFi driver code
(gdb) step
(gdb) step

Technique 4: Live Variable Modification

Change variables on-the-fly without recompiling:

# Break at function
(gdb) break send_packet
(gdb) continue

# Change packet size before sending
(gdb) print packet_size
$1 = 1024
(gdb) set packet_size = 64
(gdb) print packet_size
$2 = 64

# Continue with modified value
(gdb) continue

Technique 5: Reverse Debugging (Limited)

Record execution to step backwards:

# Enable recording (only works for short sequences)
(gdb) target record-full

# Run forward
(gdb) continue
(gdb) next

# Step backwards!
(gdb) reverse-step
(gdb) reverse-next

# Disable recording (uses lots of memory)
(gdb) record stop

Resources

Official Documentation

GDB Cheat Sheets

ESP32 Community

GDB Tutorials


Summary

GDB debugging on the ESP32-C5 provides powerful insights into firmware behavior:

  • Built-in USB-JTAG eliminates external hardware requirements
  • Hardware and software breakpoints for flexible debugging
  • Real-time variable inspection without printf statements
  • Watchpoints to catch memory corruption
  • Call stack analysis to understand program flow
  • ESP-IDF driver debugging to troubleshoot library issues

Key takeaways:

  1. Always build with Debug (-Og) optimization for best debug experience
  2. Use conditional breakpoints to break only when needed
  3. Combine watchpoints with breakpoints to find memory corruption
  4. Script common tasks in .gdbinit for faster debugging
  5. The WiFi driver log is still the ground truth for connection status

GDB debugging significantly reduces debug time compared to printf-based approaches, especially for complex issues like WiFi driver bugs, FreeRTOS task interactions, and memory corruption.


About

This guide was created based on real-world ESP32-C5 development experience, specifically debugging WiFi 6 CSI (Channel State Information) capture issues for the iperf WiFi Analyzer project.

Hardware: ESP32-C5 DevKit
Project: WiFi Collapse Detection using CSI
Repository: https://github.com/iperf2/iperf2

For questions or corrections, please open an issue on GitHub.


Last Updated: December 4, 2025