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
--smpexposes both LX7 cores as GDB threads, soinfo threadsshows what each core is doing. Essential for a cross-core hang.--attachhalts the board in place without resetting it, so you can examine a live hang or crash exactly where it stopped.
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.
- VS Code — install the AdaCore Ada &
SPARK extension for ALS, and Native Debug
(
webfreak.debug) for debugging (cppdbg cannot walk the Xtensa stack). The committed.vscode/tasks.jsongives youAda: build(with a GNAT problem matcher),flash,monitor,runandclean;launch.jsongivesAda: debug (OpenOCD + GDB). Ctrl-Shift-B builds, F5 debugs. - Vim / Neovim — point your LSP client at
ada_language_server; use:make ./x build <ex>for actions. GNAT'sfile:line:col: error:format matches a standarderrorformat.
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.