A debugger is a tool that lets a developer inspect and control the execution of a program running on a target processor, typically by setting breakpoints, stepping through instructions, and reading or writing memory and registers. In embedded systems, a debugger usually consists of two parts: host-side software (such as GDB, OpenOCD, or a vendor IDE) and a hardware debug probe that connects to the target via a debug interface such as JTAG or SWD, though some setups use probe-less simulators or emulators instead.
In practice
On many ARM Cortex-M targets, debugging is done through a hardware probe (J-Link, ST-Link, CMSIS-DAP, or similar) connected to the target's SWD or JTAG pins, though some boards integrate on-board debug circuitry and some targets expose only SWD. The probe translates between USB on the host side and the debug interface on the target, allowing GDB or an IDE to set breakpoints, read registers, and program flash. Vendors such as STMicroelectronics, NXP, and Nordic ship evaluation boards with an on-board probe so no separate hardware is needed for initial bringup.
Debugger capabilities vary significantly by architecture and silicon support. Cortex-M cores include the Flash Patch and Breakpoint (FPB) unit, which allows a limited number of hardware breakpoints (the exact count depends on the specific core and implementation, but 4 to 8 is common), and communicate with the host through the Debug Access Port (DAP) defined in the ARM debug architecture. Unlimited software breakpoints are also possible in RAM-resident code. Older or simpler architectures — including many 8-bit PIC and AVR parts — may offer only a small number of hardware breakpoints and may require proprietary debug protocols (such as debugWIRE for AVR or ICSP-based protocols for PIC) rather than JTAG or SWD; tools like PICkit implement these protocols but are not protocols themselves.
A hardware debugger is not the only way to observe program behavior. Lightweight alternatives include toggling a GPIO or LED to signal execution state (as covered in "Debugging with Heartbeat LEDs"), emitting structured data over UART or RTT ("Debug, visualize and test embedded C/C++ through instrumentation"), or using a logic analyzer to correlate signal timings with code events ("Tracing code and checking timings"). These approaches are especially useful when hardware breakpoints halt the processor and disturb time-sensitive peripherals.
Common pitfalls include forgetting that halting the CPU at a breakpoint does not freeze all peripherals — many peripherals and timers continue running unless explicitly configured to stop in debug-halt mode — which can trigger watchdog resets, corrupt SPI/I2C transactions, or miss RTOS tick events. Optimized code (compiled with -O2 or higher) frequently confuses source-level debugging: variables are optimized away, lines execute out of order, and stepping behavior becomes unpredictable. Debugging at -O0 or using the volatile keyword selectively can help, but may mask bugs that only appear under optimization.
Discussed on EmbeddedRelated
Frequently asked
What is the difference between a debug probe and a debugger?
The debug probe (J-Link, ST-Link, CMSIS-DAP adapter, etc.) is the physical hardware that bridges
USB on the host to
JTAG or
SWD on the target. The debugger is the software (
GDB,
OpenOCD, a vendor IDE) that uses the probe to control and inspect the target. The two are often discussed interchangeably, but they are distinct components.
Why does halting at a breakpoint sometimes reset or crash the target?
When the CPU halts, many hardware peripherals and timers continue running. If a
watchdog timer is enabled and the firmware normally services it in the main loop or an
ISR, halting for more than the watchdog timeout period causes a reset. Where supported, the fix is to either disable the watchdog during debug sessions or configure the watchdog to freeze while the core is halted — a feature available on many STM32 and Kinetis parts via a debug register. Not all MCUs provide this option, so some watchdogs cannot be paused during debug halts.
How many breakpoints can I set?
The number of hardware breakpoints is determined by the silicon. ARM Cortex-M0/M0+ parts typically provide 2 to 4 hardware breakpoints via the FPB unit; Cortex-M3/M4/M7 parts commonly offer 6 to 8. Software breakpoints (replacing an instruction with a trap opcode) are unlimited but generally only work in
RAM-resident code on most embedded targets — though some devices with
flash patching or remapping support may allow them in flash as well.
Can I debug a target without a hardware probe?
Yes, though with reduced visibility. Common probe-free techniques include printf-style logging over
UART, toggling GPIOs or LEDs to mark execution points, and using
semihosting (which routes printf through the debug probe if one is connected but avoids true breakpoint-based debugging). For timing-sensitive issues, a
logic analyzer or oscilloscope capturing
GPIO toggles is often more informative than halting the CPU, since halting disturbs real-time behavior. The blog post 'Debugging with Heartbeat LEDs' covers a practical example of this approach.
Why do my variables show as 'optimized out' when I try to inspect them?
When compiled with optimization (-O1 or higher), the compiler may keep a variable in a CPU register rather than memory, or eliminate it entirely if it can prove the value is not needed. The debugger cannot show the value of a register-allocated variable if the register has been reused. Compiling with -O0 solves this but may hide bugs that only manifest under optimization. Declaring a variable
volatile forces it to memory and prevents the compiler from eliminating it, but use this only for debugging purposes unless volatile semantics are actually required.
Differentiators vs similar concepts
A debugger is sometimes confused with a programmer (also called a flasher). A programmer only writes firmware into the target's
flash memory; it does not provide real-time control over execution. Many hardware probes (J-Link, ST-Link, PICkit) function as both a programmer and a debugger, which contributes to the confusion. A simulator or emulator runs the target instruction set on the host machine without real hardware, which allows debugging without a physical device but may not accurately model peripheral timing or hardware interactions.