Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 12 of 13

Debugging: GDB over the same cable

The USB-Serial-JTAG port is both the console and a JTAG debug interface, so one cable gets you breakpoints, both cores as GDB threads, and a live halt on a hung board.

Fetch the tools, once

./x get-debug-tools      # pinned, SHA-256-verified OpenOCD + xtensa-esp32s3-elf-gdb
./x debug gpio0_blink    # build, flash, start OpenOCD, attach GDB, rest at Main

Both are debug-only downloads; build, flash and run need neither.

The S3 needs the S3-specific xtensa-esp32s3-elf-gdb. The Alire crate's esp32 (LX6) GDB silently fails on it — hardware-confirmed. ./x debug selects the right one for you, which is the main reason to go through it rather than invoking GDB by hand.

Two options that matter on this target

From there it is ordinary GDB on app.elf: breakpoints, step, print, backtrace. When you are done, ./x kill-openocd releases the USB-JTAG port — a captured port is the usual reason the next flash fails.

In the editor

The split is deliberate and worth understanding: the Ada Language Server provides language features (completion, diagnostics, go-to-definition, hover) by reading a project's .gpr file, and ./x provides the actions. Any editor with an LSP client gets the first for free; the editor integration only has to wrap the second.

Post-mortem: decoding a Guru Meditation

An unhandled fault prints a register dump and a backtrace of program-counter values over the console. You do not need a live debugger to read it — turn the addresses back into source:

xtensa-esp32s3-elf-addr2line -fe build/app.elf 0x4200abcd 0x4200ef01

In the dump, EXCVADDR is the faulting data address and EXCCAUSE the reason: a LoadStoreError is a bad data access; an InstructionFetchError is a wild jump, usually a corrupted stack or a stray pointer. Flash, reproduce, decode — that loop localises most faults without attaching anything.

What the runtime catches for you

Before you reach for a tool at all: every profile guards the running task with a hardware watchpoint a redzone above its stack limit, and the context switch re-arms it for the incoming thread on every switch. An overflowing write therefore faults precisely, at the instruction that did it, instead of silently corrupting a neighbouring task's stack and failing somewhere unrelated.