Firmware bugs are invisible — no stack traces, no logs by default. Today you'll use GDB and OpenOCD to debug running hardware with breakpoints and memory inspection.
JTAG (Joint Test Action Group): 4 pins (TDI, TDO, TCK, TMS) + optional TRST. SWD (Serial Wire Debug): 2 pins (SWDIO, SWCLK) — ARM's optimized debug interface. Both allow: halt the CPU, set breakpoints, read/write memory and registers, step through code. For STM32: use ST-Link V2 (~$3). For ESP32: use JTAG with openocd -f interface/ftdi/esp32_devkitj_v1.cfg. GDB connects to OpenOCD over a local TCP socket on port 3333.
Key commands: target extended-remote :3333 — connect to OpenOCD. monitor reset halt — reset CPU, halt at reset vector. load — flash the ELF file. break main — set breakpoint at main. continue — run. step/next/stepi — single step. info registers — dump all registers. x/8xw 0x20000000 — examine 8 words of SRAM as hex. print gpio_state — print variable value. watch variable — break when variable changes.
SWO (Serial Wire Output) is a single-wire trace port that outputs ITM (Instrumentation Trace Macrocell) printf messages without blocking. Configure: enable TRC in CoreDebug, set baud in ITM_TCR, write to ITM->PORT[0].u8. OpenOCD captures SWO output. This gives you printf-style debugging with no UART connection needed and minimal performance impact. VS Code Cortex-Debug extension shows SWO output directly.
#!/bin/bash
# Debug firmware with GDB + OpenOCD
# Terminal 1: Start OpenOCD (adapt config to your hardware)
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "init" -c "reset halt"
# OpenOCD now listens on port 3333 (GDB) and 4444 (telnet)
# Terminal 2: Connect GDB
arm-none-eabi-gdb firmware.elf << 'EOF'
# Connect to OpenOCD
target extended-remote :3333
# Reset and halt the CPU
monitor reset halt
# Flash the firmware
load
# Set breakpoints
break main
break SysTick_Handler
break EXTI0_IRQHandler
# Run to main
continue
# When stopped at breakpoint:
# info registers — all register values
# print systick_ms — print variable
# x/16xw 0x20000000 — dump 16 words of SRAM
# watch button_pressed — break when this changes
# backtrace — call stack (if -g compiled)
# stepi — one assembly instruction
# Continue execution
continue
EOF
# Useful OpenOCD telnet commands (port 4444):
# echo 'flash list' | nc localhost 4444
# echo 'mdw 0x40020014' | nc localhost 4444 # read GPIO ODR
# echo 'mww 0x40020018 0x01' | nc localhost 4444 # write BSRR
-g3 -Og to your compiler flags. -Og is 'optimization for debugging' — it enables some optimizations while preserving variable names and line numbers. Release builds use -O2 or -O3.-g3 -Og debug flags.EXTI0_IRQHandler.info registers. Note the PC and LR values.x/8xw 0x40020010 to read GPIOA IDR directly. Does it match what you expect?Set up a hardware fault handler. When a firmware bug causes a HardFault (null pointer dereference, stack overflow, etc.), the default handler is an infinite loop — completely unhelpful. Implement a HardFault_Handler that reads the faulting address from the SCB (System Control Block), prints it via UART, and stores it in RTC memory before resetting. Research: what are CFSR, HFSR, and MMAR registers in the SCB? What information do they contain?