On [STM32](https://www.ampheo.com/search/STM32) “creating debug code” usually means 3 things:
1. Build a “Debug” version of your firmware (with symbols, no heavy optimizations)
2. Sprinkle helpful hooks in code (LEDs, asserts, logging, error handler, etc.)
3. Use the debugger (ST-LINK + IDE) to watch what’s going on
I’ll walk through a practical setup you can copy.

**1. Make a proper Debug build**
**In STM32CubeIDE**
1. Open your project.
2. In the toolbar, pick Configuration: Debug (not Release).
3. Right-click your project → Properties → C/C++ Build → Settings
* Under MCU GCC Compiler → Debugging:
* Make sure -g3 is enabled (debug symbols)
* Under MCU GCC Compiler → Optimization:
* Use -Og or -O0 (much easier to debug than -O2/-Os).
Now when you build in Debug, the ELF will contain full symbols and line info, so breakpoints, watches, and stepping behave nicely.
In other IDEs (Keil, IAR, VisualGDB) it’s the same idea: Debug config = symbols + low optimization.
**2. Add “debug hooks” into your code**
**2.1 A simple ERROR handler**
In CubeMX projects you already have something like this in main.c:
```
void Error_Handler(void)
{
__disable_irq();
while (1)
{
// Debug: blink a LED, or stay here so debugger can catch it
}
}
```
You can improve it:
```
void Error_Handler(void)
{
__disable_irq();
// Example: toggle LED fast in an endless loop
while (1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(100);
}
}
```
Anywhere in your code when something is “impossible”, call:
```
if (some_fatal_condition) {
Error_Handler();
}
```
Now, when the board gets stuck blinking, you attach the debugger and see it’s sitting in Error_Handler().
**2.2 Compile-time debug macros**
Create a debug.h with something like:
```
#pragma once
#include <stdio.h>
// Enable this in Debug build only
#ifdef DEBUG
#define DBG_PRINTF(...) printf(__VA_ARGS__)
#define DBG_BREAK() __BKPT(0) // cause a breakpoint (Cortex-M)
#else
#define DBG_PRINTF(...) ((void)0)
#define DBG_BREAK() ((void)0)
#endif
```
In Debug builds, DBG_PRINTF and DBG_BREAK are active;
In Release, they compile away.
Usage:
```
if (adc_value > MAX_ALLOWED) {
DBG_PRINTF("ADC too high: %d\r\n", adc_value);
DBG_BREAK(); // debugger will break here if attached
}
```
**2.3 UART printf for runtime logs**
A very common STM32 trick: route printf() to a UART (or ITM SWO), so you can see logs in a serial terminal.
Minimal version using HAL:
1. Initialize, for example, USART2 using CubeMX.
2. Add in usart.c or main.c:
```
int _write(int file, char *ptr, int len)
{
HAL_UART_Transmit(&huart2, (uint8_t*)ptr, len, HAL_MAX_DELAY);
return len;
}
```
Now printf() will send out over USART2:
`printf("System clock: %lu Hz\r\n", HAL_RCC_GetHCLKFreq());`
Open a serial terminal on your PC at the right baud and you’ll see all your debug messages.
**2.4 LED “heartbeat” and state codes**
LEDs are an underrated debug tool:
* Slow blink (1 Hz) → main loop alive
* Fast blink → error
* Pattern (e.g. short-short-long) → specific error code
Example:
```
void debug_heartbeat(void)
{
static uint32_t last = 0;
if (HAL_GetTick() - last > 500) {
last = HAL_GetTick();
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
```
Call debug_heartbeat() once each main loop iteration. If it stops blinking, you know your code is stuck somewhere.
**3. Use the debugger features**
After you’ve built the Debug target:
1. Connect ST-LINK (on-board [Nucleo](https://www.onzuu.com/search/Nucleo)/[Discovery](https://www.onzuu.com/search/Discovery) or external).
2. Click the bug icon in STM32CubeIDE (Debug).
3. It’ll program the chip and stop at main().
Now you can:
* Set breakpoints on any line:
* Click in the left margin → blue dot
* Step through code:
* Step into (F5), step over (F6), step out (F7)
* Watch variables:
* Hover over them or add them to the Expressions view
* Use breakpoints in your debug macros:
* When DBG_BREAK() executes, the CPU hits a breakpoint and the IDE stops there.
Also handy:
* Watchpoints (data breakpoints): pause when a variable changes
* Live view of peripherals: in the Peripherals / SFR view you can open GPIO, timers, ADC, etc.
**4. Putting it all together (mini template)**
In main.c:
```
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
printf("Booting STM32...\r\n");
while (1)
{
debug_heartbeat();
// Example debug check
if (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_SET) {
DBG_PRINTF("Button pressed!\r\n");
}
// Simulated bug
// if (error_condition) {
// DBG_PRINTF("Fatal error, value=%d\r\n", some_val);
// Error_Handler();
// }
}
}
```
Compile Debug, start a debug session:
* Watch the heartbeat LED
* See printf messages on UART
* Put breakpoints on suspicious lines
* Use DBG_BREAK() inside weird conditions to stop exactly when they happen
**5. Quick checklist for “good debug code” on [STM32](https://www.ampheoelec.de/search/STM32)**
* Separate Debug vs Release build configs
* Use DEBUG macro to enable/disable heavy logging
* Redirect printf() to UART or SWO
* Have a strong Error_Handler() (LED pattern, trap)
* Use asserts / DBG_BREAK() in impossible paths
* Learn to use IDE: breakpoints, watch variables, peripheral registers