Enabling ITM Trace
nucleus trace decodes ARM CoreSight ITM/SWO packets and streams them to the
trace dashboard. Getting data flowing end-to-end needs two pieces wired
together: a few lines of C in your firmware that configure the ITM/TPIU
peripherals and write to stimulus ports, and an OpenOCD session that captures
the resulting SWO byte stream and forwards it to nucleus trace.
Firmware (ITM) --SWO--> ST-Link --USB--> OpenOCD --TCP--> nucleus trace --WebSocket--> dashboard
Firmware side: the CoreSight register setup
CMSIS headers (pulled in by stm32f4xx_hal.h) define CoreDebug, TPI, and
ITM as memory-mapped structs. Enabling SWO output is six register writes:
#include "stm32f4xx_hal.h"
/* Configure SWO output. `core_hz` is [device].clock_hz; `swo_hz` must match
* [trace].swo_freq in stm32.toml (and the value passed to
* `nucleus trace --openocd`). */
void itm_init(uint32_t core_hz, uint32_t swo_hz)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* 1. enable tracing */
TPI->SPPR = 2; /* 2. SWO protocol = NRZ/UART */
TPI->ACPR = (core_hz / swo_hz) - 1; /* 3. SWO baud rate divisor */
ITM->LAR = 0xC5ACCE55; /* 4. unlock the ITM registers */
ITM->TCR |= ITM_TCR_ITMENA_Msk; /* 5. enable the ITM */
ITM->TER |= 1UL; /* 6. enable stimulus port 0 (log text) */
}
Call itm_init(...) once, after SystemClock_Config(), alongside the
generated Nucleus_Init().
Logging text (port 0)
nucleus-trace reassembles single-byte writes to stimulus port 0 into UTF-8
log lines, splitting on \n. Write one byte at a time, blocking while the
port's FIFO is full:
void itm_log(const char *s)
{
while (*s) {
while (ITM->PORT[0].u32 == 0) { } /* wait for FIFO space */
ITM->PORT[0].u8 = (uint8_t)*s;
s++;
}
}
A trailing \n flushes the line to the dashboard's log panel:
itm_log("system init complete\n");
Tracing typed variables (ports 1-7)
Each [[trace.variables]] entry in stm32.toml names a port and a type
(f32, u16, u32, or i32). The access width of the write to
ITM->PORT[n] determines the packet size nucleus-itm reports, so match the
width to the type: 4 bytes for f32/u32/i32, 2 bytes for u16.
static inline void itm_write32(uint8_t port, uint32_t value)
{
if (!(ITM->TCR & ITM_TCR_ITMENA_Msk) || !(ITM->TER & (1UL << port)))
return;
while (ITM->PORT[port].u32 == 0) { }
ITM->PORT[port].u32 = value;
}
static inline void itm_write16(uint8_t port, uint16_t value)
{
if (!(ITM->TCR & ITM_TCR_ITMENA_Msk) || !(ITM->TER & (1UL << port)))
return;
while (ITM->PORT[port].u16 == 0) { }
ITM->PORT[port].u16 = value;
}
To trace an f32 on port 1, write its bits as u32:
float temperature = read_temperature();
uint32_t bits;
memcpy(&bits, &temperature, sizeof(bits));
itm_write32(1, bits);
Remember to enable each port you use — add it to itm_init():
ITM->TER |= (1UL << 1); /* enable port 1 for the temperature trace */
OpenOCD side
OpenOCD captures SWO from the ST-Link and forwards it to a TCP port via
tpiu config internal. nucleus trace --openocd <telnet_addr> sends this
sequence for you over OpenOCD's telnet console (default port 4444):
tpiu config internal :<trace_port> uart off <core_hz> <swo_hz>
itm ports on
<trace_port> is the TCP port nucleus trace --trace-tcp connects to, and
<core_hz>/<swo_hz> must match the values passed to itm_init() in
firmware. If you'd rather configure OpenOCD by hand (e.g. to debug a version
mismatch), connect with telnet localhost 4444 and run the same two commands.
Putting it together
# stm32.toml
[trace]
enabled = true
swo_freq = 2_000_000
[[trace.variables]]
name = "temperature"
port = 1
type = "f32"
nucleus trace --trace-tcp 127.0.0.1:3344 --openocd 127.0.0.1:4444 \
--config stm32.toml
See CLI Usage for the full flag reference, and open
the dashboard via the VS Code command Nucleus: Open Trace Dashboard (or
extension/dist/index.html standalone) to see the log lines and temperature
chart update live.