Bare Metal STM32 · Part 6 of 7

Debugging with SWO Trace & ITM Printf

STM32 Debug SWO Intermediate Source Code
Bare Metal STM32 Part 6
Bare Metal STM32 — Part 6
Debugging with SWO Trace & ITM Printf
18:30
Part 6 / 7
18:30
Intermediate
SWO · SWD · CubeIDE
01

Overview

Adding a UART just for debug output wastes a peripheral and burns CPU cycles waiting for the TX FIFO. The SWO (Single Wire Output) pin on Cortex-M is already wired to your ST-Link — all you need is 10 lines of code to redirect printf() through the ITM stimulus port and read it live in CubeIDE's SWV Console. Zero extra hardware, zero CPU blocking, zero peripheral cost.

  • Configure the ITM and TPIU to output on SWO at the correct trace clock frequency
  • Override __io_putchar() to redirect printf() through ITM stimulus port 0
  • Set up SWV in STM32CubeIDE to capture and display the trace output
  • Use multiple ITM ports to separate log categories without extra parsing
02

Core Concepts

🔌
SWO Pin
Single Wire Output — a dedicated pin on the SWD connector (pin 6 on the standard 10-pin ARM header). Already connected on Nucleo and Discovery boards via ST-Link. No extra wiring needed.
PB3 = TRACESWO on STM32F4
📮
ITM Stimulus Ports
32 independent FIFO ports. Port 0 is the standard printf channel. Use ports 1–31 for structured data (ADC values, task names, event IDs) without mixing with console text.
ITM->PORT[n].u8 = byte
Non-Blocking Writes
The ITM FIFO can fill up if the debugger can't drain it fast enough. Always check FIFOREADY before writing, or use the CMSIS ITM_SendChar() helper which does this automatically.
ITM->PORT[0].u32 != 0
03

Code

01
ITM init and printf redirect — drop this in main.c
itm_trace.c
C
/* ── 1. Enable ITM trace ────────────────────────────────── */
void ITM_Trace_Init(void) {
    /* Enable TPIU: set SWO baud = CoreClock / (prescaler+1)
       With CoreClock=168MHz and prescaler=0 → 168 MHz SWO clock
       Match this value exactly in CubeIDE → Run → Debug Configurations
       → Debugger tab → Serial Wire Viewer → Core Clock field          */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    TPI->ACPR  = 0;                  // SWO prescaler (0 = divide by 1)
    TPI->SPPR  = 2;                  // 2 = NRZ (UART) encoding
    TPI->FFCR  = 0x100;             // Continuous formatter
    ITM->LAR   = 0xC5ACCE55;        // Unlock ITM registers
    ITM->TCR   = ITM_TCR_TraceBusID_Msk
               | ITM_TCR_SWOENA_Msk
               | ITM_TCR_ITMENA_Msk; // Enable ITM
    ITM->TER   = 0xFFFFFFFF;        // Enable all 32 stimulus ports
}

/* ── 2. Redirect printf → ITM port 0 ───────────────────── */
int __io_putchar(int ch) {
    // Wait for FIFO ready (bit 0 of TER/PORT register)
    if (ITM->PORT[0].u32) {
        ITM->PORT[0].u8 = (uint8_t)ch;
    }
    return ch;
}

/* ── 3. Usage ───────────────────────────────────────────── */
void main_loop(void) {
    ITM_Trace_Init();
    uint32_t count = 0;
    while(1) {
        printf("count=%lu  adc=%u\r\n", count++, adc_buf[0]);
        // Structured data on port 1 — no parsing in viewer
        ITM->PORT[1].u16 = adc_buf[0];
        HAL_Delay(100);
    }
}
💡
Set the SWO clock in CubeIDE to match your CoreClock
In CubeIDE: Run → Debug Configurations → Debugger tab → Enable Serial Wire Viewer → set Core Clock to 168000000. A mismatch produces garbled output. The SWO baud is CoreClock ÷ (ACPR+1), so ACPR=0 means SWO runs at full core speed.

Continue the Series

Work through all 7 parts of Bare Metal STM32 to master low-level embedded development from the ground up.