ESP32-C5 GDB Debugging Guide

Author: Bob McMahon

Hardware: ESP32-C5 DevKit (RISC-V)

ESP-IDF: v6.0 or later

Last Updated: December 2025

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

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

Software

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

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

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

# 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         }
Solution: Fix the null-pointer handling logic.

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
Solution: Improve antenna placement or reduce distance to AP.

Troubleshooting

Problem: "No symbol table is loaded"

Symptom:
(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:

  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.