Embedded printf via ITM and SWO-Embedded C Course Online

Embedded printf via ITM and SWO

Lecture 03 — Debug Output & SWD

Embedded printf via ITM and SWO

Master the embedded “Hello World” — how to send debug messages from an ARM Cortex-M microcontroller to your PC without a display, using the ITM hardware unit, SWO pin, and the SWD debug interface.

Beginner Friendly Estimated read: 25 min Target: STM32F407 / STM32F411 (Cortex-M4)

The Core Problem: No Screen on a Microcontroller

When you learn to program a desktop computer, your very first program prints “Hello World” to a terminal. The output appears instantly on your screen. Easy.

On an embedded microcontroller, there is no terminal, no screen, and no operating system to write to. The chip just runs your firmware in a tight loop. So how do you know what is happening inside? How do you print a variable value? How do you know if your code reached a particular line?

This is one of the first real hurdles every embedded beginner faces, and it has several solutions. This lecture teaches you the most powerful and convenient one: routing printf through the ITM (Instrumentation Trace Macrocell) and out the SWO pin, straight to your IDE — no extra wires, no UART, no USB-to-serial adapter needed.

Why This Matters More Than You Think

Without a debug output channel, embedded developers rely entirely on LEDs blinking at different rates, oscilloscope probing, and debugger step-through to understand what their code is doing. All of those are valid tools — but being able to print printf(“ADC value = %d\n”, adc_result); and see it live in your IDE is a massive productivity multiplier. Master this early and you will debug 10× faster than engineers who never set it up.

Four Ways to Get Debug Output — Compared

Before diving deep into ITM/SWO, let’s survey all the options. Each has different hardware requirements, code complexity, and real-time impact on the running application.

ITM / SWO (printf via SWD)
Best for Cortex-M3/M4/M7
printf is retargeted to the ITM hardware unit. Trace data flows out the SWO pin, through the ST-LINK on your board, and appears in the IDE. No extra wiring.
+ Zero extra hardware
+ Non-blocking (FIFO buffered)
+ Works at full CPU speed
– Only Cortex-M3/M4/M7 (not M0/M0+)
– Needs SWO pin connected to ST-LINK
UART / Virtual COM Port
Works on All MCUs
Retarget printf to a USART peripheral. Connect a USB-to-UART adapter to your PC. Open a terminal (PuTTY, minicom) to see output.
+ Works on every microcontroller
+ Standard, well understood
– Needs extra adapter or board connector
– Consumes USART peripheral
– Slower than ITM
Semihosting
Debugger Dependent
ARM mechanism where printf calls are trapped by the debugger and routed to the host PC console over the SWD/JTAG connection.
+ No extra hardware
+ Full printf support (file I/O too)
– Extremely slow (halts CPU per call)
– Requires debugger always attached
– Breaks production code
LED / GPIO Toggle
Primitive
Blink an LED at different rates to signal different code paths. No text, no values — just binary state indication.
+ Zero software overhead
+ Works on any hardware
– No variable values
– Very limited information
– Hard to interpret timing

This lecture focuses on ITM/SWO — the professional’s choice for Cortex-M3/M4/M7 development. We will also show the UART approach since it is essential for Cortex-M0/M0+ boards that lack ITM hardware.

SWD — Serial Wire Debug Interface

Before understanding how printf gets to your PC, you need to understand the physical debug interface that carries all debug traffic. ARM designed SWD (Serial Wire Debug) specifically for the Cortex-M family as a minimal-pin replacement for JTAG.

SWD Physical Interface — 2 Wires for Debug + 1 Wire for Trace
STM32 Microcontroller ARM Cortex-M4 Core
+ SWD-DP debug port
+ ITM trace unit
SWDIO
Bidirectional data
SWCLK
Clock from host
SWO
Trace output (ITM)
ST-LINK V2 Circuit On Nucleo / Discovery board
Translates SWD to USB
Captures SWO trace data
→
USB to PC STM32CubeIDE
shows debug &
trace output
SWDIO + SWCLK are the two mandatory SWD wires. SWO (Serial Wire Output) is a third optional unidirectional wire for trace data. All three are physically present on Nucleo and Discovery boards, routed through the on-board ST-LINK.

What SWDIO and SWCLK Actually Do

SWDIO is a bidirectional data line. When the host (your PC, via the ST-LINK) is sending commands — read a memory address, set a breakpoint, halt the CPU — it drives SWDIO as output. When the target chip sends back data (register values, memory contents, acknowledgements), the chip drives SWDIO as output. One pin, both directions, time-multiplexed.

SWCLK is always driven by the host. Each rising edge clocks one bit of data on SWDIO. The default SWD clock speed for STM32 is typically set to 4 MHz in most debugger configurations, though higher speeds up to ~20 MHz are possible with quality probes and short wires.

Through just SWDIO and SWCLK you can: program the entire flash memory, read or write any memory address (including peripheral registers), set hardware breakpoints, halt and resume the CPU, read all 16 CPU registers, and trigger a system reset. This is the power of SWD — everything through two wires.

SWD vs JTAG — Full Comparison

JTAG (Joint Test Action Group) is an older standard (IEEE 1149.1) originally designed for board-level circuit testing, later adopted for chip programming and debugging. It uses 4–5 dedicated pins in a daisy-chain topology. ARM used JTAG for the ARM7 and ARM9 families before Cortex-M.

With Cortex-M, ARM introduced SWD as a streamlined alternative. Here is how they compare across every practical dimension:

Feature JTAG SWD
Number of signals 4 mandatory: TMS, TCK, TDI, TDO
(+ optional TRST)
2 mandatory: SWDIO, SWCLK
(+ optional SWO for trace)
GPIO pins used 4–5 GPIO pins dedicated to JTAG 2 GPIO pins (PA13=SWDIO, PA14=SWCLK)
Multi-device chain Yes — multiple chips in a JTAG chain No — SWD is point-to-point only
Trace output Requires separate trace pins (TRACED[3:0]) for ETM instruction trace SWO pin carries ITM trace data (single-wire trace)
Programming flash Yes Yes
Breakpoints Yes Yes
Memory access Yes Yes
STM32 default Available but not default Default on all STM32
Connector size 20-pin or 10-pin JTAG connector 10-pin Cortex Debug connector or 4-pin header
Best used when Testing multiple chips on one PCB; need full ETM trace Single-chip debug; want SWV trace (printf via SWO); pin-constrained design
ST-LINK V2 Connector Pinout (SWD + SWV Mode)
PinSignalDirectionPurpose
1VCC (3.3V ref)OutputTarget voltage sense / reference
23.3VOutputPower supply to target (if needed)
3GND—Ground reference
4SWCLKHost → TargetSWD clock
5SWOTarget → HostITM trace output (Serial Wire Output)
6SWDIOBidirectionalSWD data — debug commands & responses
7GND—Ground
8NC—Not connected
9NC—Not connected
10NRSTHost → TargetHardware reset line
On Nucleo and Discovery boards the ST-LINK is built in — these signals are already wired internally. For standalone chips on a custom PCB, expose at minimum GND, SWDIO, SWCLK, and SWO as test points.

ITM — Instrumentation Trace Macrocell

The ITM (Instrumentation Trace Macrocell) is a hardware block inside every ARM Cortex-M3, M4, and M7 processor. It is the engine that makes printf-over-SWO possible. Understanding how it works internally helps you use it correctly and avoid common pitfalls.

How ITM Printf Data Flows from Your Code to the IDE
Step 1: Your C code calls printf(“value = %d”, x)
The C library formats the string: “value = 42”
↓
Step 2: _write() syscall retarget — each character is written to ITM Stimulus Port 0
ITM_STIM0 is memory-mapped at 0xE0000000. Writing a byte here enqueues it in the ITM FIFO.
↓
Step 3: ITM hardware FIFO buffers characters and serialises them
The FIFO decouples your CPU from the SWO output rate. Your code continues running; the FIFO drains in background.
↓
Step 4: TPIU serialises the ITM frames and drives the SWO pin
The Trace Port Interface Unit (TPIU) formats ITM packets and clocks them out the SWO pin, typically using Manchester or NRZ encoding.
↓
Step 5: ST-LINK captures SWO and forwards over USB
The ST-LINK microcontroller on the Nucleo/Discovery board samples the SWO pin and bundles the data into USB packets sent to your PC.
↓
Step 6: STM32CubeIDE SWV console displays “value = 42”
The IDE receives the USB data, decodes the ITM packets, and displays the formatted string in the Serial Wire Viewer console — no terminal program needed.
The entire path from your printf() call to text appearing in the IDE takes microseconds. The key advantage: the ITM FIFO means your CPU never blocks waiting for the serial output to drain.

ITM Stimulus Ports — 32 Independent Channels

The ITM provides 32 stimulus ports (port 0 through port 31). Each port is a separate FIFO queue, memory-mapped at a specific address. Port 0 is conventionally used for printf text output. Other ports can carry different types of information simultaneously:

ITM PortAddressConventional Use
Port 00xE0000000printf / text debug output (standard use)
Port 10xE0000004Custom event logging (e.g. state machine transitions)
Port 20xE0000008RTOS task-switch events
Port 3–70xE000000C–0x1CCustom application-specific channels
Port 8–15…DWT-generated hardware trace packets (routed automatically)
Port 16–31…Reserved / vendor-specific

Writing to a stimulus port is as simple as a 32-bit memory write. The ITM hardware handles everything else — checking if the port is enabled, buffering in the FIFO, merging with other port traffic, and scheduling for output. You never need to manage timing or encoding yourself.

Complete Implementation: Retargeting printf to ITM

There are three distinct tasks:

  1. Enable the ITM hardware — configure TPIU clock, enable the ITM and stimulus port 0
  2. Retarget the C library _write() syscall — redirect stdout to ITM port 0
  3. Configure the IDE — tell STM32CubeIDE the SWO clock frequency and enable the SWV console
Step 1 — ITM hardware initialisation (bare-metal C, STM32F4)
#include <stdint.h>

/* ── CoreDebug register (enables DWT and ITM) ── */
#define CoreDebug_DEMCR  (*((volatile uint32_t *)0xE000EDFCU))

/* ── ITM registers ── */
#define ITM_TCR          (*((volatile uint32_t *)0xE0000E80U))  /* Trace Control Reg    */
#define ITM_TER          (*((volatile uint32_t *)0xE0000E00U))  /* Trace Enable Reg     */
#define ITM_TPR          (*((volatile uint32_t *)0xE0000E40U))  /* Trace Privilege Reg  */
#define ITM_LAR          (*((volatile uint32_t *)0xE0000FB0U))  /* Lock Access Reg      */

/* ── TPIU registers ── */
#define TPIU_SPPR        (*((volatile uint32_t *)0xE00400F0U))  /* Selected Pin Protocol */
#define TPIU_ACPR        (*((volatile uint32_t *)0xE0040010U))  /* Async Clock Prescaler */
#define TPIU_FFCR        (*((volatile uint32_t *)0xE0040304U))  /* Formatter/Flush Ctrl  */

/* ── ITM Stimulus Port 0 (byte-wide write for single characters) ── */
#define ITM_STIM0_BYTE   (*((volatile uint8_t  *)0xE0000000U))
#define ITM_STIM0_WORD   (*((volatile uint32_t *)0xE0000000U))

void itm_init(uint32_t cpu_hz, uint32_t swo_hz)
{
    /* Step A: Enable DWT and ITM via CoreDebug DEMCR */
    CoreDebug_DEMCR |= (1U << 24);  /* TRCENA bit — master trace enable */

    /* Step B: Unlock ITM registers (write magic key to Lock Access Register) */
    ITM_LAR = 0xC5ACCE55U;

    /* Step C: Configure TPIU
       SPPR = 2 → NRZ (UART) encoding on SWO pin (most compatible)
       ACPR = (cpu_hz / swo_hz) - 1 → sets the SWO baud rate prescaler */
    TPIU_SPPR = 2U;
    TPIU_ACPR = (cpu_hz / swo_hz) - 1U;
    TPIU_FFCR = 0U;  /* Disable formatter (single-wire SWO mode) */

    /* Step D: Enable ITM with:
       - ATB ID = 1 (trace source identifier)
       - Synchronisation packets enabled
       - DWT stimulus enabled
       - ITM enabled (bit 0) */
    ITM_TCR = (1U << 16) |  /* ATB ID = 1 (bits 22:16, set to 0x01) */
              (1U <<  4) |  /* SWOENA: SWO enabled                   */
              (1U <<  3) |  /* DWTENA: DWT packet forwarding enabled */
              (1U <<  2) |  /* SYNCENA: synchronisation enabled      */
              (1U <<  0);   /* ITMENA: ITM master enable             */

    /* Step E: Enable stimulus port 0 (bit 0 of TER) */
    ITM_TER = (1U << 0);

    /* Step F: Allow unprivileged code to write to ports 0–7 (TPR bits 0–3) */
    ITM_TPR = 0x0000000FU;
}

/* ── Send one character through ITM port 0 ── */
void itm_putchar(uint8_t ch)
{
    /* Check bit 0 of ITM_STIM0: 1 = port ready, 0 = FIFO full (wait) */
    while ((ITM_STIM0_WORD & 1U) == 0);
    ITM_STIM0_BYTE = ch;
}
Step 2 — Retarget _write() so printf routes to ITM (syscalls.c)
#include <stdint.h>
#include <errno.h>

/* This function is called by the C library for every write() / printf() call.
   File descriptor 1 = stdout, 2 = stderr. We route both to ITM port 0.
   Any other fd returns an error (no filesystem in bare-metal). */

extern void itm_putchar(uint8_t ch);  /* defined in itm_init.c */

int _write(int fd, char *buf, int len)
{
    if (fd == 1 || fd == 2)           /* stdout or stderr */
    {
        for (int i = 0; i < len; i++)
        {
            itm_putchar((uint8_t)buf[i]);
        }
        return len;
    }
    errno = EBADF;
    return -1;
}
Step 3 — main.c: initialise ITM then use printf normally
#include <stdio.h>
#include <stdint.h>

extern void itm_init(uint32_t cpu_hz, uint32_t swo_hz);

int main(void)
{
    /* System clock assumed 100 MHz (STM32F411 at max speed).
       SWO baud rate 2 MHz — a safe value for most ST-LINK V2 hardware. */
    itm_init(100000000U, 2000000U);

    printf("=== STM32 ITM Printf Demo ===\n");

    uint32_t counter = 0;
    while (1)
    {
        counter++;
        printf("Loop count : %lu\n", counter);

        /* Simple busy-wait delay */
        for (volatile uint32_t d = 0; d < 1000000U; d++);
    }
}
Expected Output in STM32CubeIDE SWV Console
=== STM32 ITM Printf Demo ===
Loop count : 1
Loop count : 2
Loop count : 3
Loop count : 4
...

Using the Higher-Level STM32 HAL Version

If you are using STM32CubeIDE with HAL (Hardware Abstraction Layer), the setup is simpler — the generated startup code and linker scripts are already configured. You only need to add the syscall retarget and call the HAL initialisation, then configure the SWV in the IDE:

Minimal ITM printf with HAL (in main.c, CubeIDE generated project)
#include "main.h"
#include <stdio.h>

/* Add this to syscalls.c (already in the project — just add the _write body) */
int _write(int fd, char *ptr, int len)
{
    for (int i = 0; i < len; i++)
    {
        /* ITM Stimulus Port 0 — wait until ready then write one byte */
        while (!(ITM->PORT[0].u32 & 1));
        ITM->PORT[0].u8 = (uint8_t)ptr[i];
    }
    return len;
}

/* In main() — after SystemClock_Config(): */
int main(void)
{
    HAL_Init();
    SystemClock_Config();       /* Generated by CubeMX */

    /* Enable CoreDebug trace (required for ITM) */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;

    /* Unlock and enable ITM */
    ITM->LAR  = 0xC5ACCE55;
    ITM->TCR  = ITM_TCR_ITMENA_Msk | ITM_TCR_SWOENA_Msk |
                ITM_TCR_SYNCENA_Msk | (1 << ITM_TCR_TraceBusID_Pos);
    ITM->TER  = 1;             /* Enable stimulus port 0 */

    printf("Hello from HAL + ITM!\n");

    while (1)
    {
        printf("Tick: %lu ms\n", HAL_GetTick());
        HAL_Delay(500);
    }
}

Configuring STM32CubeIDE Serial Wire Viewer

The code alone is not enough — you must tell STM32CubeIDE the SWO clock frequency so it can decode the incoming bit stream correctly. Getting this wrong results in garbage characters or no output at all.

1
Open Debug Configuration
In STM32CubeIDE: Run → Debug Configurations → select your project configuration → click the “Debugger” tab.
2
Enable SWV
Check the box “Enable Serial Wire Viewer (SWV)”. This tells the ST-LINK to capture data from the SWO pin.
3
Set Core Clock and SWO Clock
Enter the Core Clock frequency (e.g., 100 for 100 MHz on STM32F411). Enter the SWO Clock frequency — use the same value as swo_hz in your itm_init() call. Common values: 2000000 (2 MHz), 4000000 (4 MHz). The two values must match exactly, otherwise the ST-LINK cannot decode the bit stream.
4
Open SWV ITM Data Console
Window → Show View → SWV → SWV ITM Data Console. In the console toolbar, click the “Configure trace” icon, check “ITM Stimulus Port 0”, then click Start Trace (the red circle).
5
Flash and Run
Click Debug (F11). The IDE flashes the firmware and halts at main(). Press Resume (F8) to run. Text from printf should start appearing in the SWV ITM Data Console immediately.
6
Troubleshoot if Nothing Appears
Check: (a) SWO clock in IDE matches your code. (b) ITM_TER bit 0 is set. (c) CoreDebug DEMCR TRCENA bit is set. (d) The SWO pin (PB3 on STM32F4) is not configured as a GPIO — it must be in AF0/SWO mode.
SWO Pin on STM32F4: It is PB3

On STM32F407 and STM32F411 chips, the SWO function is on pin PB3 (alternate function 0). On the Discovery board, PB3 is already connected to the ST-LINK circuit internally — you do not need to wire anything. But on a custom PCB, you must route PB3 to your debug connector’s SWO pin. If PB3 is configured as a GPIO output by your code, SWV will not work.

Beyond printf: Advanced ITM Uses

Sending Binary Data (Not Just Text)

ITM stimulus ports accept 8-bit, 16-bit, or 32-bit writes. Writing 32 bits at once is more efficient than writing 4 bytes individually because the ITM hardware can pack them into a single trace packet, reducing SWO bandwidth.

Sending 32-bit sensor values over ITM port 1 (custom channel)
#define ITM_STIM1_WORD  (*((volatile uint32_t *)0xE0000004U))

/* Send a 32-bit value on ITM port 1 (e.g., raw ADC reading) */
void itm_send_u32(uint32_t value)
{
    /* Check ITM stimulus port 1 ready bit */
    while ((ITM_STIM1_WORD & 1U) == 0);
    ITM_STIM1_WORD = value;
}

/* In application code */
void adc_complete_callback(uint32_t raw_value)
{
    itm_send_u32(raw_value);       /* 32-bit value on port 1 */
    printf("ADC = %lu\n", raw_value);  /* Text on port 0 */
}

Timestamped Event Logging

By combining ITM trace output with the DWT cycle counter (covered in Lecture 2), you can create timestamped event logs with microsecond accuracy — without any hardware logic analyser:

Timestamped event log via ITM
#include <stdio.h>
#include <stdint.h>

#define DWT_CYCCNT   (*((volatile uint32_t *)0xE0001004U))
#define CPU_HZ       100000000U

/* Returns elapsed microseconds from a start timestamp */
static uint32_t elapsed_us(uint32_t start_cycles)
{
    return (DWT_CYCCNT - start_cycles) / (CPU_HZ / 1000000U);
}

void process_sensor(void)
{
    uint32_t t0 = DWT_CYCCNT;

    read_i2c_sensor();              /* some blocking I2C read */

    uint32_t read_time = elapsed_us(t0);
    printf("[%lu us] I2C read complete\n", read_time);

    process_data();                 /* compute something */

    uint32_t total_time = elapsed_us(t0);
    printf("[%lu us] Processing done\n", total_time);
}

UART Printf — For Cortex-M0 / M0+ (No ITM)

Cortex-M0 and Cortex-M0+ processors do not include the ITM unit — it is optional in the ARM architecture, and chip vendors omit it to keep cost and power low. If you are working on a Cortex-M0-based chip (like the STM32F030 or the RP2040’s Cortex-M0+), you must use UART for printf output.

The technique is the same — retarget _write() — but route characters through a USART peripheral instead of ITM:

Retarget printf to USART2 — works on any Cortex-M chip
#include <stdint.h>
#include <errno.h>

/* USART2 registers (STM32F4 / STM32F0 base address 0x40004400) */
#define USART2_SR   (*((volatile uint32_t *)0x40004400U))
#define USART2_DR   (*((volatile uint32_t *)0x40004404U))
#define USART2_BRR  (*((volatile uint32_t *)0x40004408U))
#define USART2_CR1  (*((volatile uint32_t *)0x4000440CU))

/* RCC: enable USART2 clock (APB1ENR bit 17) and GPIOA clock (AHB1ENR bit 0) */
#define RCC_APB1ENR (*((volatile uint32_t *)0x40023840U))
#define RCC_AHB1ENR (*((volatile uint32_t *)0x40023830U))

void usart2_init(uint32_t pclk_hz, uint32_t baud)
{
    /* Enable clocks */
    RCC_AHB1ENR  |= (1U << 0);   /* GPIOA clock */
    RCC_APB1ENR  |= (1U << 17);  /* USART2 clock */

    /* PA2 = USART2_TX: set to AF7, push-pull, medium speed */
    volatile uint32_t *GPIOA_MODER  = (volatile uint32_t *)0x40020000U;
    volatile uint32_t *GPIOA_AFRL   = (volatile uint32_t *)0x40020020U;
    *GPIOA_MODER &= ~(3U << (2*2));
    *GPIOA_MODER |=  (2U << (2*2));   /* Alternate function mode */
    *GPIOA_AFRL  &= ~(0xFU << (2*4));
    *GPIOA_AFRL  |=  (7U   << (2*4)); /* AF7 = USART2 */

    /* Set baud rate: BRR = pclk_hz / baud */
    USART2_BRR = pclk_hz / baud;

    /* Enable transmitter and USART */
    USART2_CR1 = (1U << 3) | (1U << 13); /* TE bit | UE bit */
}

/* Send one character via USART2 */
static void usart2_putchar(char c)
{
    while (!(USART2_SR & (1U << 7))); /* Wait for TXE (TX empty) */
    USART2_DR = (uint32_t)c;
}

/* Retarget _write for printf */
int _write(int fd, char *buf, int len)
{
    if (fd == 1 || fd == 2)
    {
        for (int i = 0; i < len; i++)
            usart2_putchar(buf[i]);
        return len;
    }
    errno = EBADF;
    return -1;
}

/* main.c */
#include <stdio.h>
int main(void)
{
    /* APB1 clock = 50 MHz on STM32F411 */
    usart2_init(50000000U, 115200U);

    printf("UART printf works!\n");

    while (1)
    {
        printf("Running...\n");
        for (volatile uint32_t d = 0; d < 500000U; d++);
    }
}
Viewing UART output on your PC

Connect PA2 (USART2 TX) to a USB-to-UART adapter (CP2102, CH340, or FT232). On Linux: minicom -D /dev/ttyUSB0 -b 115200. On Windows: use PuTTY or Tera Term. On macOS: screen /dev/tty.usbserial-* 115200. The STM32 Nucleo boards also expose a Virtual COM Port over the ST-LINK USB — this appears as a COM port and connects to USART2 internally, so you can use UART printf without any extra hardware.

SWO Encoding: NRZ vs Manchester

The TPIU (Trace Port Interface Unit) can encode SWO data in two different line-coding schemes, configured via the TPIU_SPPR register:

SPPR ValueEncodingHow It WorksWhen to Use
1 Manchester Each bit is encoded as a transition: 0 = falling edge mid-bit, 1 = rising edge mid-bit. Self-clocking. When probe firmware supports it. The transition encodes both clock and data in one wire — more robust at long cable runs.
2 NRZ (UART-like) Standard asynchronous UART framing: idle high, start bit low, 8 data bits, no parity, stop bit high. Baud rate fixed by TPIU_ACPR. Recommended for ST-LINK V2 — the most widely supported format. Set SPPR=2 for compatibility with STM32CubeIDE SWV.

STM32CubeIDE’s SWV viewer works with both, but NRZ (SPPR=2) is more universally supported across different ST-LINK versions and IDE releases. Use Manchester (SPPR=1) only if your probe documentation specifically recommends it.

Complete System Data Path: printf to IDE

Full printf → ITM → SWO → ST-LINK → USB → IDE Data Path
STM32F407VGT6 MCU
ARM Cortex-M4 Core Executes printf()
Writes to ITM_STIM0
ITM Unit 32 stimulus ports
FIFO buffers chars
TPIU Serialises ITM data
NRZ encoding
Drives SWO pin
SWO pin
(PB3)
→
SWDIO
SWCLK
ST-LINK V2 Debug Circuit
SWD Protocol Handler Sends debug commands
Reads memory / regs
SWO Capture Engine Samples SWO pin
Decodes NRZ frames
Buffers ITM packets
USB FS Controller Bundles trace + debug
into USB HID packets
USB cable
(Mini-USB)
→
Host PC
STM32CubeIDE ST-LINK GDB Server
Handles debug commands
SWV ITM Data Console Decodes ITM port 0
Displays text output
Output visible! “Hello from STM32!\n”
“Loop count: 42\n”
The entire path from writing to ITM_STIM0 to text appearing in the IDE is hardware-accelerated. Your CPU writes one byte and continues — the ITM FIFO, TPIU, ST-LINK, and USB handle the rest asynchronously.

Common Mistakes and How to Fix Them

Mistake 1: SWO Clock Mismatch

Symptom: Garbage characters in the SWV console, or completely missing output.
Cause: The SWO baud rate configured in the IDE does not match the value written to TPIU_ACPR in your code.
Fix: Calculate TPIU_ACPR = (cpu_freq / swo_freq) – 1 and enter swo_freq exactly in the IDE debug configuration. Start with a conservative 2 MHz SWO frequency and increase only if needed.

Mistake 2: Forgetting TRCENA

Symptom: No output at all, even though ITM registers look correct.
Cause: The global trace enable bit (TRCENA, bit 24 of CoreDebug->DEMCR) was not set. Without TRCENA, all DWT and ITM hardware is powered down and ignores writes.
Fix: Always set CoreDebug->DEMCR |= (1 << 24) as the very first step in your ITM initialisation.

Mistake 3: PB3 Configured as GPIO

Symptom: SWV capture enabled but SWO pin reads constant HIGH or LOW noise.
Cause: Your code (or STM32CubeMX generated code) has configured PB3 as a GPIO output for some other purpose (e.g., driving an LED), overriding the SWO alternate function.
Fix: Do not use PB3 as a GPIO when SWD trace is needed. In CubeMX, check the Pinout view — PB3 should show “SYS_JTDO-SWO” or left unassigned (SWO function is active by default).

Mistake 4: Not Starting the Trace in the IDE

Symptom: The SWV console window is open but completely blank despite correct code.
Cause: You must explicitly click “Start Trace” (the red circle button) in the SWV ITM Data Console toolbar. The console does not auto-start capture.
Fix: After opening the SWV ITM Data Console, click “Configure trace”, enable port 0, then click the red circle. Do this before pressing Resume.

Mistake 5: Using printf in an ISR

Symptom: printf in interrupt handlers causes unpredictable behaviour, output corruption, or system hang.
Cause: printf is not reentrant in newlib-nano. If an interrupt fires while the main code is inside printf and the ISR also calls printf, internal library state gets corrupted.
Fix: Never call printf directly in an ISR. Instead, write to a circular buffer inside the ISR and have the main loop drain and print the buffer. Alternatively, use direct itm_putchar() calls (which are reentrant) instead of printf in ISRs.

Frequently Asked Questions

Does SWO/ITM printf affect my application’s real-time timing?
Only if the ITM FIFO overflows. When you write to a stimulus port and the FIFO is full, the while ((ITM_STIM0 & 1) == 0) spin-wait blocks until there is space. This is the only timing impact. In practice, at 2 MHz SWO speed and 100 MHz CPU, the FIFO can drain roughly 20,000 characters per second — far more than most printf-heavy applications produce. For extremely time-critical ISRs, avoid printf entirely and use direct 32-bit ITM writes on a separate port.
Can I use SWO without a debugger attached?
No — SWO data is captured by the ST-LINK debug circuit on your board, which is only powered and active when a USB cable is connected. More importantly, the ITM hardware requires TRCENA to be set, and on some chip versions the ARM debug interface must be powered up for trace to function. In production firmware (without ST-LINK), you would use UART for any diagnostic output. ITM/SWO is a development-time tool only.
What is the maximum SWO data rate I can achieve?
The theoretical maximum is determined by the TPIU prescaler and the ST-LINK hardware. ST-LINK V2 (on Discovery boards) supports SWO speeds up to around 4–8 MHz reliably. ST-LINK V3 (on newer Nucleo boards) supports up to ~24 MHz. Higher speeds require shorter cables and good signal integrity. For most debug printf usage, 2 MHz is more than sufficient. For high-bandwidth trace (e.g., logging thousands of ADC samples per second), use 4–8 MHz and 32-bit writes on the stimulus port.
Why does ITM not work on Cortex-M0 / M0+?
The ITM block is not part of the ARMv6-M architecture used by Cortex-M0 and M0+. ARM made it optional to reduce gate count and cost for the lowest-tier microcontrollers. The Cortex-M3 (ARMv7-M) and all higher cores include ITM as a standard feature. If your project might eventually move to a Cortex-M3/M4 target, designing your debug output through a thin abstraction layer (e.g., a debug_print() function that calls either ITM or UART depending on a compile-time flag) makes the transition seamless.
What is semihosting and why should I avoid it in production?
Semihosting is an ARM mechanism where special software breakpoint instructions (BKPT 0xAB) are caught by the debugger. When printf generates a semihosting call, the CPU halts, the debugger on your PC services the call (writes the string to its console), and then resumes the CPU. This means every printf call halts the CPU for potentially hundreds of microseconds — the round-trip through USB. It also means the firmware will hang indefinitely if a debugger is not attached (the BKPT causes a fault with no handler). ITM has none of these drawbacks: no CPU halt, no debugger dependency, no timing impact.
Can I log data to a file on my PC using ITM?
Yes. STM32CubeIDE’s SWV console has a “Save to file” option in its toolbar. You can also use the open-source OpenOCD tool which supports SWO capture directly to a file: start OpenOCD with SWO tracing enabled and pipe the output to a log file. Third-party tools like SEGGER J-Link (with a different probe) provide more sophisticated data logging and real-time graphing of ITM data.
Next: The Cortex-M Programmer’s Model

Lecture 4 covers the complete Cortex-M register bank — R0 through R15, the two stack pointers MSP and PSP, the Link Register, Program Counter, and the xPSR. Understanding these registers is the foundation for writing interrupt handlers, context switches, and RTOS code.

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *