Debugging & Testing
Debugging embedded systems is uniquely challenging—you're dealing with
real-time constraints, hardware interactions, and limited visibility.
Mastering debugging tools and testing methodologies is essential for
shipping reliable products.
[Diagram: Debug flow - Problem → Hypothesis → Instrument → Analyze →
Fix → Verify]
Debug Interfaces
Modern microcontrollers provide dedicated hardware interfaces for
debugging:
JTAG (Joint Test Action Group)
- Industry-standard debug interface
- 4-5 wire interface: TCK, TMS, TDI, TDO, (TRST optional)
- Supports boundary scan for manufacturing tests
- Full access to processor state and memory
- Allows daisy-chaining multiple devices
SWD (Serial Wire Debug)
- ARM-specific, uses only 2 wires: SWDIO and SWCLK
- Functionally equivalent to JTAG for most debugging
- Saves PCB space and pins
- Common on Cortex-M devices
💡 Pro Tip: SWD is preferred for Cortex-M projects
due to fewer pins. Reserve JTAG for multi-core debugging or when
boundary scan is needed.
Debug Probes & Tools
Hardware tools that connect your PC to the target MCU:
Popular Debug Probes
-
ST-Link: Integrated on STM32 Nucleo/Discovery
boards, low cost
-
J-Link: Industry standard, fast, extensive device
support (Segger)
-
CMSIS-DAP: Open standard, many implementations
available
-
Black Magic Probe: Open source, works as GDB server
itself
-
PicoProbe: Use a Raspberry Pi Pico as a debug probe
Software Debugging Techniques
1. Breakpoints
Pause execution at specific lines of code to examine program state.
-
Hardware Breakpoints: Limited number (usually 4-8),
but work on any code including flash
-
Software Breakpoints: Unlimited in RAM, modifies
code to insert break instruction
-
Conditional Breakpoints: Break only when a
condition is true
2. Watch Expressions
Monitor variable values in real-time without breaking execution.
// Variables to watch volatile uint32_t adc_reading; volatile int32_t
motor_position; volatile uint8_t error_flags;
⚠️ Important: Mark watched variables as
volatile to prevent compiler optimization from hiding
their values during debugging!
3. Printf Debugging (UART/SWO)
Classic technique using serial output for runtime logging:
// Retarget printf to UART int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart2, (uint8_t*)ptr, len, HAL_MAX_DELAY); return
len; } // Debug output printf("[DEBUG] Sensor value: %d, State: %s\n",
value, state_names[state]);
4. SWO/ITM Trace
ARM's Instrumentation Trace Macrocell allows printf over the debug
probe—no extra UART needed!
// Send character via ITM (ARM Cortex-M) int _write(int file, char
*ptr, int len) { for (int i = 0; i < len; i++) { ITM_SendChar(*ptr++);
} return len; }
Logic Analyzers & Oscilloscopes
Essential tools for debugging hardware-related issues:
Logic Analyzer
- Capture and decode digital signals
- Analyze protocol transactions (SPI, I2C, UART, CAN)
- Timing analysis between events
- Popular: Saleae, DSLogic, Sigrok-compatible devices
[Diagram: Logic analyzer capture of I2C communication]
Oscilloscope
- View analog waveforms and signal quality
- Measure rise times, overshoot, ringing
- Debug power supply issues
- Mixed-Signal Oscilloscopes (MSO) combine analog + digital
Testing Methodologies
Unit Testing
Test individual functions in isolation, often on host PC:
// Using Unity test framework void test_calculate_checksum(void) {
uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; uint8_t expected = 0x0A;
TEST_ASSERT_EQUAL(expected, calculate_checksum(data, 4)); }
Popular embedded testing frameworks:
Unity
CppUTest
Google Test
Ceedling
Integration Testing
- Test multiple modules working together
- Verify communication between components
- Test on actual target hardware
Hardware-in-the-Loop (HIL) Testing
Simulate the physical environment while testing real firmware:
- Replace physical sensors/actuators with simulators
- Inject edge cases and fault conditions
- Automate regression testing
-
Essential for automotive, aerospace, and safety-critical systems
[Diagram: HIL setup with simulator, DUT, and test automation]
Common Debugging Scenarios
Hard Fault Debugging (ARM Cortex-M)
// Hard Fault Handler - capture fault info void
HardFault_Handler(void) { __asm volatile ( "TST LR, #4 \n" "ITE EQ \n"
"MRSEQ R0, MSP \n" "MRSNE R0, PSP \n" "B hard_fault_handler_c \n" ); }
void hard_fault_handler_c(uint32_t *stack) { volatile uint32_t r0 =
stack[0]; volatile uint32_t r1 = stack[1]; volatile uint32_t pc =
stack[6]; // Program counter at fault volatile uint32_t lr = stack[5];
// Link register // Inspect these in debugger or log them while(1); //
Halt for debugging }
Timing Issues
- Use GPIO toggling + oscilloscope to measure timing
- Check for interrupt latency with trace tools
- Profile code sections with DWT cycle counter
// Measure execution time using DWT (ARM Cortex-M) CoreDebug->DEMCR |=
CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |=
DWT_CTRL_CYCCNTENA_Msk; uint32_t start = DWT->CYCCNT; // ... code to
measure ... uint32_t cycles = DWT->CYCCNT - start; float time_us =
cycles / (SystemCoreClock / 1000000.0f);
Static Analysis & Code Quality
-
Compiler Warnings: Enable -Wall -Wextra -Werror
-
Static Analyzers: PC-lint, Polyspace, Coverity,
cppcheck
-
MISRA C: Coding standard for safety-critical
systems
-
Code Review: Peer review catches many bugs early
🎯 Key Takeaway: Effective debugging requires the
right mindset (be systematic!), the right tools, and experience.
Build a debugging toolkit and document your troubleshooting steps!