Skip to content
Home
Debugging Embedded Systems: Tools, Techniques, and Best Practices

Debugging Embedded Systems: Tools, Techniques, and Best Practices

Embedded Systems Embedded Systems 8 min read 1517 words Beginner ExcellentWiki Editorial Team

The Embedded Debugging Challenge

Debugging embedded systems is fundamentally harder than debugging desktop software for several reasons. You cannot easily print to a console because the device may not have a display or network connection. The mere act of adding debug code changes the system’s timing behavior, potentially masking or altering the bug — a phenomenon known as the Heisenberg effect in debugging. Hardware interactions introduce non-deterministic failures that depend on signal integrity, temperature, and supply voltage. Intermittent bugs may only appear hours into a test run and disappear when you connect a debugger. A systematic approach that combines the right tools with disciplined methodology is essential for efficient problem-solving in this constrained environment.

Debugging Tools

JTAG (IEEE 1149.1) and SWD (Serial Wire Debug) debug probes provide hardware breakpoints, single-stepping through code, real-time memory and register inspection, and Flash programming. The SEGGER J-Link is the industry standard with the fastest download speeds, broad target support covering ARM Cortex, RISC-V, and other architectures, and comprehensive software support including GDB server, Ozone debugger, and command-line tools. Prices range from $70 for the EDU edition to $1,000+ for the PRO model with trace support. The ST-Link is included free with STM32 Nucleo and Discovery boards and provides adequate debugging functionality for STM32 development through STM32CubeIDE. The Black Magic Probe (BMP) runs open-source firmware and exposes a native GDB server over USB for ARM Cortex targets, eliminating the need for GDB server software on the host.

Logic analyzers capture digital signals over time and decode communication protocols into human-readable form. Entry-level 8-channel analyzers based on the Cypress CY7C68013A (Saleae clone) work up to 24 MHz and cost under $20 on common retail platforms. Professional analyzers from Saleae offer 8–16 channels at up to 500 MHz sampling, hardware protocol decoding for I2C, SPI, UART, CAN, 1-Wire, and custom protocols, and analog signal capture for cross-domain analysis.

Oscilloscopes are essential for viewing analog signals. Use them to measure power supply noise and ripple that causes intermittent resets, verify signal integrity on communication buses (overshoot, undershoot, ringing), check clock accuracy and jitter against the specification, analyze switching waveforms in power converters for efficiency and EMI issues, and validate ADC input quality for sensor applications. The oscilloscope bandwidth should be at least 5 times the highest frequency of interest, following the rule of thumb for accurate amplitude measurement. The Rigol DS1054Z at $350 is the most popular entry-level scope for embedded work, offering 50 MHz bandwidth (upgradable to 100 MHz) and four channels.

Serial Wire Output (SWO) is a single-pin trace output available on Cortex-M3, M4, and M7 processors. SWO provides non-intrusive printf-style debugging that does not halt the processor — the ITM (Instrumentation Trace Macrocell) sends data over SWO at up to 50 Mbps while the application runs at full speed. SWO also provides real-time event counters (interrupt counts, cycle counts) and statistical profiling data via the DWT (Data Watchpoint and Trace) module.

Debugging Techniques

Blinking LED as an Oscilloscope Substitute

The humble LED connected to a GPIO pin is the most basic yet effective debug tool. A fast blink pattern indicates an error condition, a slow blink shows normal operation, and an LED that is off suggests the firmware has crashed or the MCU is not running. Different blink codes can indicate different error states. This technique requires no additional tools beyond the LED and resistor, making it invaluable during initial hardware bring-up when the debugger may not yet be connected.

Serial Print Debugging

Output diagnostic information over a UART at 115200 baud. Use a non-blocking ring buffer implementation that stores characters in RAM while the UART transmits them via DMA in the background. This approach adds zero execution-time overhead to the main application after the initial write. On Cortex-M processors, SWO provides even lower overhead by using the ITM’s stimulus ports, which adds only a single store instruction per character.

Using GDB for Embedded Debugging

The GNU Debugger (GDB) is the most powerful and flexible debugger for embedded systems, supported by virtually all debug probes (J-Link, ST-Link, OpenOCD, Black Magic Probe). GDB provides source-level debugging with breakpoints, watchpoints, and single-stepping. GDB’s command set includes info registers to display CPU register contents, x/10x $sp to examine stack memory as hexadecimal, thread and info threads for RTOS-aware debugging, and monitor commands specific to the debug probe for flash programming and target power control. The .gdbinit file can automate common setup commands. GDB scripting with Python allows creating custom debug commands for project-specific data structures.

Post-Mortem Analysis

When a deployed device crashes without a debugger attached, post-mortem analysis reconstructs the execution state at the moment of failure. The HardFault_Handler (or the specific fault handler for MemManage, BusFault, or UsageFault on Cortex-M) should capture the CPU register contents including R0–R15, Program Status Register (xPSR), Link Register (LR), and Program Counter (PC). The handler also saves the contents of the current stack (MSP or PSP depending on whether the fault occurred in handler or thread mode) along with a magic number and CRC for validity checking. Store this crash context information in a reserved section of battery-backed SRAM or a dedicated region of Flash that survives reset.

Profiling and Optimization

The DWT cycle counter on Cortex-M processors provides a 32-bit cycle counter accessible via DWT->CYCCNT that increments at the CPU clock frequency. Measure function execution time by reading the counter before and after the function: cycles = DWT->CYCCNT; function(); cycles = DWT->CYCCNT - cycles. FreeRTOS provides vTaskGetRunTimeStats() for task-level CPU usage measurement. Profile interrupt latency by reading a free-running timer at ISR entry and comparing to the expected interrupt rate. Focus optimization on hot code paths identified by profiling data rather than guessing where bottlenecks are.

Systematic Debugging Approach

When you encounter a problem, follow a structured methodology. Isolate the issue by removing components or disabling features one at a time until the problem disappears — this identifies the responsible subsystem. Reproduce the failure reliably by documenting exact conditions, inputs, and environmental factors. Gather data through instrumented builds, event logging, and state capture. Formulate hypotheses about possible root causes. Test each hypothesis experimentally by changing one variable at a time and observing the effect. Implement the verified fix. Document the root cause, solution, and any new test cases that should be added to prevent regression. This systematic approach is dramatically more efficient than random trial and error, as emphasized by Jack Ganssle in his embedded debugging literature.

Prevention Strategies

Prevent bugs before they reach deployment. Conduct thorough code reviews with checklists covering common embedded pitfalls. Maintain automated test suites that run on every build. Practice defensive programming — validate all inputs, check return values, assert invariants, and use hardware watchdog timers as a last resort against firmware hangs. Implement monitoring in deployed systems through health-check endpoints, error counters, and watchdog alerts. Conduct post-mortem reviews after every significant incident and capture lessons learned.

Debugging with Trace

Arm’s CoreSight trace technology provides the most powerful debugging capability for Cortex-M processors. The Embedded Trace Macrocell (ETM) captures every instruction executed, enabling full program flow reconstruction. The trace data is output through a 4-bit Trace Port at up to 200 Mbps or through the single-pin SWO at lower bandwidth. Combined with a trace-capable debug probe like the SEGGER J-Link PRO or Lauterbach, the developer can see exactly which code path was executed leading up to a crash or fault. This is invaluable for debugging race conditions, intermittent faults, and optimization opportunities where adding instrumentation code would alter the system’s timing.

RTOS-Aware Debugging

When debugging RTOS-based firmware, the debugger must be aware of the RTOS’s task scheduling to provide meaningful information. The debug probe connects to the RTOS kernel’s data structures through a debugger plug-in or an RTOS-aware debugging interface. FreeRTOS-aware debugging in SEGGER Ozone and STM32CubeIDE shows each task’s state, stack usage, current priority, pending events, and the ready queue order. The developer can switch between tasks in the debugger, inspect each task’s stack, and set task-specific breakpoints. This capability is essential for debugging priority inversion, stack overflow, and deadlock conditions that are otherwise extremely difficult to diagnose.

Frequently Asked Questions

What is the difference between JTAG and SWD? JTAG is a 5-pin interface (TDI, TDO, TCK, TMS, optional TRST) supporting boundary scan testing and debugging. SWD is ARM’s 2-pin alternative (SWDIO, SWCLK) providing equivalent debugging with fewer pins, preferred for space-constrained designs.

Why use an oscilloscope when I have a logic analyzer? Oscilloscopes measure actual analog voltage levels including noise amplitude, signal ringing, overshoot, and rise/fall times. Logic analyzers only detect digital logic thresholds and miss analog problems entirely.

How do I debug intermittent bugs? Create a stress test that runs the offending operation in a tight loop while logging state to a circular buffer that can be dumped when the crash occurs. Use a triggered capture on a logic analyzer or oscilloscope to catch the event.

What should a crash dump contain? The program counter, stack pointer, link register, general-purpose CPU registers, the last 50–100 entries from a circular event log buffer, and a status byte indicating which fault handler was invoked.

Related: Firmware Development | Embedded C Programming

Section: Embedded Systems 1517 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top