# 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](#introduction) 2. [Why GDB Debugging?](#why-gdb-debugging) 3. [ESP32-C5 Debug Capabilities](#esp32-c5-debug-capabilities) 4. [Prerequisites](#prerequisites) 5. [Building with Debug Symbols](#building-with-debug-symbols) 6. [Starting a Debug Session](#starting-a-debug-session) 7. [Essential GDB Commands](#essential-gdb-commands) 8. [Debugging Strategies](#debugging-strategies) 9. [Real-World Examples](#real-world-examples) 10. [Troubleshooting](#troubleshooting) 11. [Advanced Techniques](#advanced-techniques) 12. [Resources](#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 ```bash # 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) ```bash 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 ```bash 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 ```bash 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 ```bash # 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 ```bash cd ~/your-project idf.py -p /dev/ttyUSB0 flash ``` ### Step 2: Start OpenOCD (Terminal 1) ```bash 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) ```bash 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 ```gdb # 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 ` | `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 ` | `u` | Run until line number | `u 100` | | `run` | `r` | Start program | `r` | ### Inspection | Command | Shortcut | Description | Example | |---------|----------|-------------|---------| | `print ` | `p` | Print variable value | `p my_variable` | | `print *` | `p *` | Dereference pointer | `p *config` | | `x/ ` | `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 ` | Break on function entry | `b esp_wifi_init` | | `break :` | Break at specific line | `b main.c:42` | | `break *` | Break at memory address | `b *0x42008a4e` | | `break if ` | Conditional breakpoint | `b send_data if len > 1000` | | `tbreak ` | Temporary breakpoint (one-time) | `tb app_main` | | `info breakpoints` | List all breakpoints | `i b` | | `delete ` | Delete breakpoint | `d 1` | | `disable ` | Disable breakpoint | `dis 1` | | `enable ` | Enable breakpoint | `en 1` | ### Watchpoints | Command | Description | Example | |---------|-------------|---------| | `watch ` | Break when variable changes | `watch my_counter` | | `watch *` | Break when memory changes | `watch *(int*)0x3ff00000` | | `rwatch ` | Break on read | `rwatch secret_key` | | `awatch ` | Break on read or write | `awatch buffer[0]` | ### Memory Examination | Format | Description | Example | |--------|-------------|---------| | `x/32xb ` | 32 bytes in hex | `x/32xb &config` | | `x/8xw ` | 8 words (32-bit) in hex | `x/8xw 0x40000000` | | `x/s ` | String (null-terminated) | `x/s ssid_buffer` | | `x/i ` | 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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. ```gdb # 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 (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**: ```bash # 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 (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**: ```gdb # 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**: ```gdb # 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**: ```gdb # 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 (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**: ```gdb # 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: ```gdb # ~/.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: ```bash 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: ```gdb # 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 ```gdb # 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: ```gdb # 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: ```gdb # 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**: 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