The Debugger
Motivation
When a bug pops up in code, what is your first instinct?
Some choose to use print statements
Some choose to remove code until it works again
Some choose to use asserts and see where a program aborts
While these methods may be good for simple debugging, they rely on having an interface that you can communicate the data out with. In an embedded system, a simple print statement cannot be taken for granted as you may not have a UART interface. Sometimes, you just want a way to pause your code on a specific line and look at what’s happening.
Note: For the purposes of this guide, I [Sahil] will be using the STM32CubeIDE debugger to demonstrate the concepts. Additionally, some points are specific to using a debugger in an embedded context (such as limited breakpoints & watchpoints).
Theory of operation
The STM32 supports 2 protocols for debugging: Serial Wire Debug (SWD), and JTAG. For this guide, we will be using SWD as it is an ARM-specific protocol.
Most debuggers use GDB (the GNU debugger) under the hood. On an embedded project, you have a limited number of watchpoints and breakpoints. Be aware of this when setting them.
How to use
Open up STM32CubeIDE
Click on the green “bug” in the top toolbar
Toolbar with the green bug highlighted
Your screen should now look like this:
There are a few things to explain. Let’s break it down.
Breakpoints
‘Breaking’ is the iconic aspect of a debugger - the ability to stop at points in your code on command.
To set a breakpoint, simply double click in the tiny empty space immediately to the left of the line number. It should make a tiny dot.
Toolbar
From left to right, the options are:
Terminate and relaunch
Continue (or as I call it, “Play”)
Pause
This will stop and break wherever the processor is right now
Stop
Disconnect
Step Into
This will perform the action on the line. Note that if the line involves a function call, it will go into the function call and spiral deeper, hence why it is referred to as “into”. Use this to go deeper into functions.
Step Over
This will execute the actions on the line, but it will not spiral deeper. Use this to essentially ‘run’ a line.
Step Return
This option is only available in functions. Pressing this option will mean that the debugger will execute the program normally and break where the function returns.
Watchpoints
Watchpoints allow you to ‘watch’ certain points in your code. Unlike breakpoints, they do not refer to physical points in your code. To add a new ‘thing’ to watch, simply click on the green plus sign at the bottom and type what you want to watch.
Notice how I said ‘thing’. The above example only shows basic integers, but you can watch whole objects, such as structs and other ‘things' that you need to keep track of.
Application tip: It is possible that you may want to watch a variable that has been optimized out by the compiler (i.e. say you want to keep track of a variable that is not used), or you want to set a breakpoint in an empty function. To ensure that the breakpoint or variable does not get optimized out, it is helpful to create a temporary volatile variable like so:
Doing so will prevent the variable and surrounding context from being optimized out
Further reading on this topic can be found in my article about volatile: Volatile
Homework
This is really just showing you how to use it.
Make a new project using the STM32 CubeIDE. Initializes some peripherals.
Initialize the debugger, and try to use the step in, out, and return functions to understand their use
Additionally, try setting breakpoints and seeing how the debugger travels to them.