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.
+