Debugging ARM Cortex-M3 with OpenOCD and GDB can seem daunting at first, but it becomes easier with some helpful tips and tricks. This comprehensive guide will provide key techniques for efficiently debugging your Cortex-M3 projects.
Connecting the Hardware
The first step is getting your hardware and connections set up properly. Here are some tips:
- Use a JTAG or SWD adapter that is compatible with OpenOCD and supports Cortex-M3. Popular options include ST-LINK, J-Link, and FTDI-based adapters.
- Connect the adapter to your board’s debug header, matching up the pinout. The adapter will need GND, VCC, SWCLK, SWDIO pins at a minimum.
- For best results, provide the JTAG/SWD adapter with a dedicated power supply instead of using the target power.
- Double check that jumper settings on the board enable debug/programming and disable bootloaders or watchdogs that could interfere.
- Test the physical connectivity by running basic OpenOCD commands like “telnet localhost 4444” and “init.” You should get feedback indicating the adapter and chip are detected.
Configuring OpenOCD
Once the hardware is ready, the OpenOCD configuration needs to be set up. Good config settings will streamline the debugging process. Here are key points for the config:
- Specify the correct JTAG/SWD adapter with its interface and port numbers.
- Set the transport command based on the adapter. SWD is faster than JTAG.
- Define the target chip using its Cortex-M3 core and the exact model number.
- Enable debug options like allowing to reprogram flash memory and use faster clock speeds.
- Initialize necessary flash algorithms for erasing and programming flash memory.
- Load the correct .elf file symbols to match your debug binary.
Test the config by starting OpenOCD, then connect with telnet and type “reset halt” to halt the chip after restart. If the config works, it will halt as expected. The config can then be tweaked to improve performance.
Launching GDB
With OpenOCD running, GDB can be launched to start full debugging. Here are helpful launch steps:
- Launch GDB and direct it to your debug build .elf file when prompted.
- Type “target remote localhost:3333” to attach to OpenOCD.
- Set any useful breakpoints you need right away.
- Type “monitor reset” then “load” to reset the chip and load the program.
- The debugger is now fully connected! Type “continue” to begin execution.
Useful GDB commands to try next include: “print” to get data values, “step” to step through code line by line, and “backtrace” to see the call stack.
Setting Breakpoints
One key to efficient debugging is strategically setting breakpoints. Here are some tips for using breakpoints effectively:
- Set breakpoints on key functions related to the issue being debugged.
- Use “watchpoints” to break when a memory address is accessed or a variable value changes.
- Place breakpoints just before areas where you suspect a crash is occurring.
- Use “finish” and “until” type commands to break out of loops or functions.
- Set temporary breakpoints with “tbreak” for points you only want to hit once.
- Comment out breakpoints instead of deleting so they can be easily reused.
Smart use of breakpoints during debugging can save significant time over manually single stepping through code.
Inspecting Variables
A primary task during debugging is inspecting variable values in your program. Here are helpful techniques for inspecting variables in GDB:
- Use “print” to output variable values at any breakpoint.
- Inspect variables in higher stack frames with commands like “up” and “frame.”
- Check structs and arrays by printing single elements or looping through values.
- Use “display” to continuously output a variable value each step.
- Type “info locals” to show all locals in the current stack frame.
- Enable GDB pretty printing for easier human-readable output.
Get in the habit of checking values after each breakpoint to gain insights during debugging.
Debugging Tips and Tricks
Here are some additional useful tips and tricks to help with debugging:
- Step through code slowly, line by line, to watch program flow.
- Keep debug output simple – use LEDs, UART, semihosting printf, etc.
- Comment out sections of code as a quick way to isolate bugs.
- Use regression testing with small repeatable test cases to reproduce bugs.
- Make sure compiler optimizations are disabled for full debugging.
- Triple check hardware connections and configuration settings at each step.
- Take detailed notes about what was done before each crash or odd behavior.
Following methodical habits will lead to debugging success. Never give up!
Hardware Debugging Tips
Software debugging can be augmented by using hardware tools. Here are some tips for hardware debugging:
- Use a logic analyzer to check pin states, bus activity, peripherals, etc.
- Verify voltages and power connections with a multimeter.
- Check for issues with an oscilloscope such as clock glitches or noise.
- Use passive probes like current clamps to measure power without modifying circuits.
- Temporary use LEDs or buttons to inject hardware state changes.
- Swap IC chips to quickly verify if hardware is faulty.
- Check board connections by wiggling wires and listening for short circuits.
An ideal debugging setup will include both software and hardware tools.
Debugging Small Projects
Debugging smaller Cortex-M3 projects brings its own challenges. Here are helpful tips for small projects:
- Use printf() over semihosting to the console for debug output.
- Debug code directly on the development board when possible.
- Burn a test program verifying LEDs, buttons, and peripherals work.
- Start with minimal code focused only on the task at hand.
- Add components incrementally after verifying each piece works.
- Use simulators and emulators for testing when boards aren’t available.
- Build prototypes on breadboards or stripped down “dev boards” for testing.
Careful incremental development and testing is key for small projects.
Debugging Large Projects
Large and complex Cortex-M3 projects require specialized debugging techniques. Helpful tips include:
- Create unit tests to validate modules and components in isolation.
- Use debug LEDs, serial output, and other indicators across subsystems.
- Print log messages in code during execution to follow program flow.
- Document issues thoroughly as they occur to aid future debugging.
- Simplify and minimize code when possible for easier debugging.
- Assign team members to be experts on specific subsystems.
- Use simulators to debug interactions between components.
- Add debugging features like assertions only active in debug builds.
Continuous testing and meticulous documentation are essential to taming large, complex projects.
Optimizing OpenOCD
Optimizing OpenOCD configuration and use can improve debugging performance. Useful techniques include:
- Reduce JTAG clock speed to the minimum needed for stable debugging.
- Disable options like RTCK that aren’t needed.
- Use adaptive clocking for fast flash programming.
- Set telnet ports and TCL startup scripts for convenience.
- Check logs for performance warnings and adapter limitations.
- Upgrade to the newest stable OpenOCD release for improvements.
- Shorten reset signals and delays to speed up restarts.
- Reduce logging verbosity by adjusting debug_level.
Measure reset times, breakpoint hit times, and other metrics to quantify optimizations.
Common Debugging Issues
Some common issues that arise during Cortex-M3 debugging include:
- Crash on startup before main() due to stack/heap problems.
- Infinite loops or hang from a missed interrupt disable.
- Buggy initialization code in platform/HAL files.
- Out of bounds memory access corrupting RAM contents.
- Peripherals behaving incorrectly or freezing.
- Code compiling but not running as expected on hardware.
- Power, reset, or clock issues disrupting normal execution.
- Flash memory not being read/written as expected.
Having a thorough checklist can help quickly identify and resolve common problems.
Selecting Debug Tools
Choosing quality tools is a key part of productive debugging. Here are some factors to consider:
- Select a debug adapter that is performant, reliable, and has good OpenOCD support.
- Use GDB integrations in IDEs like Eclipse for convenience.
- Evaluate CPU simulator options for early software debugging.
- Invest in at least a basic multimeter and maybe oscilloscope.
- Standardize on tools and adapters across your organization/team.
- Look for tools that integrate together, like Python scripting in GDB.
- Consider investing in advanced commercial tools for maximum effectiveness.
- Focus on open source tools for flexibility, customizations, and cost savings.
Leverage demos, trials, and rentals when evaluating expensive tool purchases.
Troubleshooting OpenOCD
Problems with OpenOCD can cause headaches during debugging. Some troubleshooting tips:
- Update OpenOCD to the latest stable release for bug fixes.
- Try both JTAG and SWD interfaces in case one has issues.
- Check logs and console output for error messages.
- Remove unneeded config options to simplify debugging setup.
- Retry repeatedly if commands like halt or reset are not working.
- Reset OpenOCD server process fully if it becomes unresponsive.
- Reconnect debugger and restart to clear any stale state.
- Make sure you have sufficient permissions to access adapter hardware.
A systematic, step-by-step approach helps resolve most OpenOCD issues.
Advanced Debugging Concepts
Some advanced debugging techniques to master include:
- RTOS Debugging – Debug task switching, resource contention, and inter-task data sharing bugs.
- Multi-core Debugging – Debug complex interactions between cores and maintain synchronization.
- Linux Kernel Debugging – Find kernel crashes, memory issues, driver problems, and lock contention.
- Interrupt and Exception Debugging – Catch hard to reproduce concurrency bugs.
- Automated Debugging – Use scripting and tools to automate finding regressions and long standing bugs.
- Post-Mortem Debugging – Analyze crash dumps, core files, and log data to fix catastrophic failures.
Continually expanding your debugging skills will make you a more effective embedded developer.
Integrating with IDEs
Debugging can be enhanced by integrating OpenOCD and GDB with IDEs like Eclipse, VSCode, and CLion. Benefits include:
- Graphical interface with debugging menus, breakpoints, and variable inspection.
- Code navigation features like autocomplete and call hierarchy.
- Build integration to compile, flash, and debug directly from the IDE.
- Plotting data visually in charts during debug sessions.
- Leveraging IDE debugging extensions and plugins.
- Working simultaneously on remote hardware and in simulation.
- Scripting repetitive debug workflows and configurations.
A full-featured IDE provides convenience but also more complexity to configure.
Debugging with GDB Scripts
GDB functionality can be extended via scripting for debugging automation. Script techniques include:
- Record commonly used interactions into a gdb macro file.
- Script starting debug sessions and loading symbols consistently.
- Automate inspecting memory regions or walking data structures.
- Add custom commands to print formatted output.
- Develop tests to run and validate execution results.
- Create conditional breakpoints based on complex expressions.
- Generate HTML/PDF reports summarizing debug sessions.
GDB Python scripting provides the most flexibility for customization and advanced uses.
Real-Time Debugging
Debugging projects with real-time constraints brings additional challenges. Useful real-time debugging tips include:
- Make debug instrumentation optional or configurable at runtime.
- Minimize the timing impact of any debugging additions.
- Simulate real-time input data like sensor readings.
- Confirm timing deadlines are still being met even while debugging.
- Use timestamps and logs to understand timing issues.
- Avoid halting execution which could cause missed deadlines.
- Focus on non-invasive tools like logic analyzers.
- Test real-time performance under a range of simulated loads.
Debugging should not perturb normal real-time behavior to avoid masking bugs.
Debugging Safety-Critical Systems
For safety-critical Cortex-M3 systems, debugging requires special care to avoid complications. Useful techniques include:
- Ensure the end system is proven to be still safe and secure when debugging options are enabled.
- Make debug access more restrictive and closely audited.
- Require hardware debugging to be explicitly enabled with jumpers or switches.
- Turn on MPU, MMU, or Memory Protection Unit to protect memory from accidental corruption.
- Use lock-step dual MCU debugging for redundancy.
- Add cyclical redundancy checks (CRC) on memory to detect tampering.
- Create a formal specification of expected debugging behavior.
Residual debug mechanisms left in production code can pose unacceptable risks.
Leveraging Debugging Tools
Beyond GDB and OpenOCD, a variety of complementary tools are useful for debugging. Helpful tools include:
- Simulators – Step through code, inspect state, and exercise corner cases.
- Disassemblers – Examine code execution down to the assembly level.
- Log Analy