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
- Introduction
- Why GDB Debugging?
- ESP32-C5 Debug Capabilities
- Prerequisites
- Building with Debug Symbols
- Starting a Debug Session
- Essential GDB Commands
- Debugging Strategies
- Real-World Examples
- Troubleshooting
- Advanced Techniques
- 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.
Method 1: Using menuconfig (Recommended)
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
- Flash the firmware to the device
- Start OpenOCD to connect to the device
- 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:
- Debug symbols not built
- Wrong ELF file loaded
- 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:
- Variable optimized out
- Variable not in scope
- 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:
- Code never executed
- Breakpoint at wrong location
- 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:
- Watchdog timeout
- CPU held too long at breakpoint
- 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:
- Code in flash, not RAM (need flash breakpoints)
- Out of hardware breakpoints
- 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
- ESP-IDF GDB Guide: https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/api-guides/jtag-debugging/
- ESP32-C5 Datasheet: https://www.espressif.com/sites/default/files/documentation/esp32-c5_datasheet_en.pdf
- OpenOCD Manual: http://openocd.org/doc/html/index.html
- GDB Manual: https://sourceware.org/gdb/current/onlinedocs/gdb/
GDB Cheat Sheets
- GDB Quick Reference: https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
- RISC-V GDB Guide: https://github.com/riscv/riscv-gnu-toolchain
ESP32 Community
- ESP32 Forum: https://esp32.com/
- r/esp32 Subreddit: https://reddit.com/r/esp32
- Espressif GitHub: https://github.com/espressif/esp-idf
GDB Tutorials
- Debugging with GDB: https://sourceware.org/gdb/onlinedocs/gdb/
- RMS's GDB Tutorial: https://www.gnu.org/software/gdb/documentation/
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:
- Always build with Debug (-Og) optimization for best debug experience
- Use conditional breakpoints to break only when needed
- Combine watchpoints with breakpoints to find memory corruption
- Script common tasks in
.gdbinitfor faster debugging - 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