Day 4 of 5
⏱ ~60 minutes
Firmware Development in 5 Days — Day 4

Debugging Firmware

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

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.

GDB Commands for Embedded

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.

Printf Debugging with SWO

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.

bash
#!/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
💡
Always compile firmware with debug symbols: add -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.
📝 Day 4 Exercise
Debug a Real Firmware Bug
  1. Compile the interrupt example from Day 3 with -g3 -Og debug flags.
  2. Connect ST-Link, start OpenOCD, connect GDB. Set a breakpoint at EXTI0_IRQHandler.
  3. Press the button. When GDB halts in the ISR, print all registers with info registers. Note the PC and LR values.
  4. Use x/8xw 0x40020010 to read GPIOA IDR directly. Does it match what you expect?
  5. Introduce a bug: comment out the pending flag clear line. What happens when you press the button? Use GDB to observe the CPU stuck in the ISR loop.

Day 4 Summary

  • SWD is ARM's 2-wire debug interface — simpler than JTAG, works with all Cortex-M MCUs
  • GDB + OpenOCD: connect with 'target remote :3333', set breakpoints, inspect registers and memory
  • Always compile with -g3 for debugging; switch to -O2/-O3 for release
  • SWO/ITM gives printf-style debugging over a single trace pin — faster than UART, no blocking
Challenge

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?

Finished this lesson?