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 |
- 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
Optimization Levels Explained
| Level | GCC Flag | Code Speed | Debug Quality | Use Case |
|---|---|---|---|---|
| Debug | -Og |
Medium | Excellent | GDB debugging ✅ |
| None | -O0 |
Slow | Excellent | Extreme debugging |
| Size | -Os |
Medium | Poor | Production |
| Performance | -O2 |
Fast | Poor | Production |
-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
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
Step 3: Start GDB (Terminal 2)
cd ~/your-project
idf.py gdb
You're now in the GDB prompt and ready to debug!
Quick Start Commands
(gdb) target remote :3333
(gdb) file build/your-project.elf
(gdb) monitor reset halt
(gdb) thbreak app_main
(gdb) continue
Essential GDB Commands
Navigation and Execution
| Command | Shortcut | Description |
|---|---|---|
break <location> |
b |
Set breakpoint |
continue |
c |
Resume execution |
next |
n |
Step over (skip function calls) |
step |
s |
Step into (enter functions) |
finish |
fin |
Run until function returns |
Inspection
| Command | Description | Example |
|---|---|---|
print <var> |
Print variable value | p my_variable |
print *<ptr> |
Dereference pointer | p *config |
x/<fmt> <addr> |
Examine memory | x/32xb 0x40000000 |
info locals |
Show local variables | i lo |
backtrace |
Show call stack | bt |
list |
Show source code | l |
Breakpoints & Watchpoints
| Command | Description | Example |
|---|---|---|
break <func> |
Break on function entry | b esp_wifi_init |
break <func> if <cond> |
Conditional breakpoint | b send if len > 1000 |
watch <var> |
Break when variable changes | watch my_counter |
info breakpoints |
List all breakpoints | i b |
delete <num> |
Delete breakpoint | d 1 |
Debugging Strategies
Strategy 1: Breakpoint at Function Entry
Use case: Understand when and why a function is called.
(gdb) break esp_wifi_set_csi_config
(gdb) continue
# When it breaks...
(gdb) info args
(gdb) print *config
(gdb) backtrace
(gdb) continue
Strategy 2: Conditional Breakpoints
Use case: Break only when specific conditions occur.
# Break only when error occurs
(gdb) break esp_wifi_set_csi_config if $a0 != 0
# Break only for specific SSID
(gdb) break wifi_connect if strcmp(ssid, "MyNetwork") == 0
# Break when buffer is full
(gdb) break send_packet if queue_size >= 100
Strategy 3: Step Through Algorithm
Use case: Understand complex logic step by step.
(gdb) break process_csi_data
(gdb) continue
(gdb) next # Execute current line
(gdb) next # Next line
(gdb) step # Step into function call if any
(gdb) finish # Complete current function
Strategy 4: Watch for Variable Changes
Use case: Find where a variable gets corrupted.
(gdb) watch connection_state
(gdb) continue
# GDB will break when variable changes
(gdb) backtrace
(gdb) print connection_state
Real-World Examples
Example 1: Debug CSI Configuration Failure
Problem: esp_wifi_set_csi_config() returns ESP_FAIL but we don't know why.
(gdb) break esp_wifi_set_csi_config
Breakpoint 1 at 0x42012a4e
(gdb) continue
Breakpoint 1, esp_wifi_set_csi_config (config=0x3ffb0000)
# Examine the config structure
(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
}
# Step through to see where it fails
(gdb) step
(gdb) step
...
(gdb) print error_code
$2 = 259 ← ESP_FAIL (0x103)
# Found it! channel_filter_en must be 0 on ESP32-C5
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
# 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!
141 return;
142 }
Example 3: Understand WiFi Connection Failure
Problem: WiFi connects but immediately disconnects.
(gdb) break event_handler
(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
Troubleshooting
Problem: "No symbol table is loaded"
(gdb) break app_main
Function "app_main" not defined.
Solutions:
# 1. Rebuild with debug symbols
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..."
Causes: Variable optimized out, out of scope, or invalid pointer
Solutions:
# Check if variable exists
(gdb) info locals
(gdb) info args
# Examine raw memory
(gdb) print &my_variable
(gdb) x/4xw 0x3ffb0000
Problem: Breakpoint Not Hitting
Solutions:
# Check breakpoint status
(gdb) info breakpoints
# Try software breakpoint
(gdb) delete 1
(gdb) break my_func
Advanced Techniques
Technique 1: Scripting GDB
Create a .gdbinit file to automate common tasks:
# ~/.gdbinit or project/.gdbinit
# Connect automatically
target remote :3333
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
Technique 2: Debugging FreeRTOS Tasks
# Show all tasks
(gdb) info threads
Id Target Id Frame
* 1 Remote target vTaskDelay ()
2 Remote target prvIdleTask ()
3 Remote target wifi_task ()
# Switch to different task
(gdb) thread 3
# See that task's stack
(gdb) backtrace
Technique 3: Live Variable Modification
Change variables on-the-fly without recompiling:
(gdb) break send_packet
(gdb) continue
# Change packet size before sending
(gdb) print packet_size
$1 = 1024
(gdb) set packet_size = 64
# Continue with modified value
(gdb) continue
Resources
Official Documentation
ESP32 Community
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.