Welcome to your next lesson on the path to becoming a professional Embedded Systems Engineer.
In our last session, you successfully compiled your bare-metal C code and flashed it onto your Blue Pill board, bringing your firmware to life. You now know how to get your program from your computer onto the microcontroller. But what happens when the program doesn't behave as expected? An LED that doesn't blink, a sensor that gives no data, or a system that crashes are all common scenarios. Simply re-flashing code and hoping for the best is not an efficient strategy.
This lesson tackles one of the most critical skills for any firmware developer: on-chip debugging. We'll address the final learning outcome for this module: Debug embedded code using breakpoints, register monitoring, and variable inspection.
Instead of guessing what your code is doing, you will learn to use your ST-Link and STM32CubeIDE to pause the live execution on the microcontroller, peer inside its brain, and see exactly what is happening, line by line.
1. The Debugging Ecosystem: How It Works
On-chip debugging might seem like magic, but it's a well-defined system of hardware and software working together.
-
The Hardware Interface: Your ST-Link probe connects to the STM32 microcontroller using a Serial Wire Debug (SWD) interface. This is a two-pin protocol (SWDIO for data and SWCLK for clock) designed by ARM specifically for debugging and programming Cortex-M cores. It allows a debugger to halt the processor, read/write memory and registers, and control execution without significantly affecting the program's behavior.
-
The Software Stack:
- Debug Probe: Your ST-Link hardware.
- GDB Server: A program on your PC (like OpenOCD, which we configured in the last lesson, or the default ST-LINK GDB server) that communicates with the ST-Link over USB and translates high-level commands (e.g., "set breakpoint") into low-level SWD signals.
- GDB Client: The user interface within STM32CubeIDE that you interact with. When you click "Step Over" or inspect a variable, the IDE (acting as the client) sends a command to the GDB server, which then instructs the MCU.
Understanding this client-server model is key to demystifying the process and troubleshooting connection issues.
2. Preparing Your Firmware for Debugging
Before you can debug effectively, you must compile your code with the right settings. An optimized "release" build will often result in code that is difficult to debug, as the compiler may reorder instructions or optimize away variables. For debugging, we need a build that closely matches our source code.
This requires two key settings in the compiler:
- Include Debug Symbols (
-g): This tells the compiler to embed information in the output.elffile that maps the machine code back to your original C source file and line numbers. Without this, the debugger wouldn't know wheremain()is. - Disable Optimization (
-O0): This instructs the compiler not to alter the code's structure for performance. This ensures that when you step through your code, the execution flow follows your written logic exactly.
Fortunately, STM32CubeIDE manages this automatically when you use the "Debug" build configuration.
To understand these crucial settings, please read the following short section from a helpful blog post on the Ampheo website.
Read section 2, titled 'Create a debug-ready firmware build'. Pay close attention to the purpose of the 'Debug' build configuration, the -g flag for symbols, and the -O0 optimization level.
3. Launching and Navigating a Debug Session
Let's put this into practice with the BareMetal_Blinky project from our last lesson.
- Ensure 'Debug' Configuration: In the Project Explorer, right-click your project ->
Build Configurations->Set Active->Debug. - Build the Project: Click the hammer icon to build the project, ensuring it's compiled with the correct debug settings.
- Launch the Debugger: Instead of the "Run" button (green play icon), click the "Debug" button (bug icon).
The IDE will now perform several steps: flash the new firmware to the board, start the GDB server, and halt the microcontroller's execution. It will ask you to switch to the "Debug Perspective," which is a layout optimized for debugging.
The video below gives a clear visual walkthrough of this process and introduces the main user interface.
STM32 + SWD + ST-Link + CubeIDE | Debugging on Custom Hardware Tutorial - Phil's Lab #4
This video from Phil's Lab demonstrates how to initiate a debug session and navigate the basic interface. We'll use this as our guide to get started.
Watch the segment from 9:41 to 12:18. Focus on: How to launch the debug session using the 'bug' icon. The core actions: setting a breakpoint, resuming execution (Resume), and pausing (Suspend). The different windows that appear in the Debug Perspective. We'll explore these in detail next.
4. Core Debugging Techniques in Action
Once in the Debug Perspective, you have a powerful set of tools at your disposal. Your code will likely be paused at the very beginning of main().

Let's explore the essential techniques using your blinky code.
A. Breakpoints and Code Stepping
A breakpoint is a marker on a line of code that tells the debugger: "Run at full speed, but stop here."
- Set a Breakpoint: Double-click in the blue margin to the left of the line number for
GPIOC->ODR ^= (1 << 13);. A blue dot will appear. - Resume Execution: Click the "Resume" button (or press F8). The program will run and immediately halt at your breakpoint. The LED state may have changed once.
- Step Over: Click the "Step Over" button (or press F6). The debugger will execute the highlighted line and move to the next one. In our case, it will step over the toggle line to the delay line.
- Resume Again: Press F8. The code will execute the delay and loop back around, stopping at your breakpoint again. You can repeat this to see the program execute one loop at a time.
B. Variable Inspection
Let's add a variable to watch. Modify your main function's while(1) loop:
// Inside main() before the loop
uint32_t blink_count = 0;
while (1)
{
GPIOC->ODR ^= (1 << 13); // Toggle PC13
// Simple delay loop
for(int i = 0; i < 500000; i++);
blink_count++; // Increment our counter
}
Rebuild and start a new debug session. Set a breakpoint on the blink_count++; line.
In the "Expressions" window (usually top-right), click "Add new expression" and type blink_count.
Now, each time you press "Resume" (F8), the program will stop at the breakpoint, and you will see the value of blink_count update in the Expressions window. This is how you can track the state of your program's logic.
C. Register Monitoring
This is the most powerful technique for bare-metal development. You can directly inspect the state of the microcontroller's hardware registers.
- While paused at your breakpoint, go to the "SFRs" (Special Function Registers) window. If you don't see it, go to
Window->Show View->SFRs. - In the SFRs window, find and expand the
GPIOCperipheral. - Locate the
ODR(Output Data Register).
Now, with your breakpoint set on the toggle line, press "Step Over" (F6). Watch the ODR value in the SFRs window. You will see the 13th bit (from the right, starting at 0) flip between 0 and 1.

You are no longer guessing if your code worked; you are verifying it by observing the hardware's state directly.
Test your understanding!
Imagine your LED is not blinking. You launch a debug session and place a breakpoint inside the while(1) loop, but the program never halts there. What is your next logical step?
Show answer
The most logical next step is to move the breakpoint earlier in the program's execution. A good place would be the very first line of main(). If the debugger stops there, it means the startup code ran correctly, but something is preventing the program from reaching your while(1) loop (e.g., a misconfigured clock that causes the system to hang). If it doesn't even stop at main(), there is a fundamental problem with the startup or connection.
Conclusion
In this lesson, you have acquired a foundational and indispensable skill for embedded systems engineering. You have moved beyond simply flashing code to actively diagnosing it on the target hardware.
Key Takeaways:
- On-Chip Debugging is an interactive process that lets you pause execution and inspect the MCU's internal state.
- It relies on a hardware interface (SWD) and a software stack (GDB Client/Server).
- Always use a Debug build (
-O0,-g) to ensure your compiled code matches your source code for predictable stepping. - The three core debugging actions are:
- Setting Breakpoints to halt execution at specific points of interest.
- Stepping Through Code to control execution flow line by line.
- Inspecting State by watching variables in the "Expressions" window and hardware registers in the "SFRs" window.
This concludes our first module on STM32 foundations. You have successfully set up your entire toolchain, written bare-metal C code to control hardware, and learned how to compile, flash, and debug it.
Preview of the Next Lesson:
With these fundamental skills, you are now ready to tackle more complex systems. In our next lesson, we will begin Module 2, "System Core: Clocks, Timers, and Interrupts." We will start with the most critical system of all: the Reset and Clock Control (RCC) peripheral. You will learn how to configure the microcontroller's system clock, moving from the default internal oscillator to a more precise external crystal, and use the debugger to verify that the core is running at the exact frequency you command.