etm-trace — unattended ETM instruction trace via J-Trace + Ozone
Streams full instruction (ETM) trace from a board wired to a SEGGER J-Trace,
headlessly: no GUI, scripted end to end. Produces hot-function profile, code
coverage, and optionally the raw instruction history.
| Skill |
Answers |
usbmon |
what the host actually exchanged (URBs) |
target-debug |
what the target did (logs, driver state, sampled PCs) |
usb-sniffer |
what crossed the wire |
etm-trace |
exactly which instructions executed, when (profile, coverage, history) |
Use target-debug's DWT PC-sampling for a quick statistical profile; use
this skill for exact counts, coverage, or instruction-by-instruction history.
Requirements
- J-Trace on USB (
lsusb -d 1366:1020) wired to the board's trace header;
select it by J-Link USB nickname (this rig: jtrace) — never commit
serials.
- Physical setup is per-board and exclusive (one J-Trace, moved between
boards; some rigs are fly-wired): unless the user just asked for trace on
this board or your task states it is wired, confirm with the user that
the J-Trace is connected to the target before flashing or capturing.
ozone on PATH (≥ V3.38 for the automation socket) and xvfb-run.
- Firmware built with
-DTRACE_ETM=1 (BSP trace-pin + trace-clock init).
- Boards with a reference
hw/bsp/*/boards/<board>/ozone/*.jdebug work out of
the box (ls that glob for the list); others fall back to JLINK_DEVICE
from board.cmake + default trace config. Verified boards: boards.md in
this skill directory.
Rig discipline
- One probe, one client: quit interactive Ozone/JLinkExe/GDB on the probe
first. Kill only processes you started — if it's held by someone else's
session (check
fuser /dev/bus/usb/<bus>/<dev>), surface it and ask. The
capture script uses automation port 19201, never an interactive Ozone's
- Hold the board lock (see the
hil skill):
python3 test/hil/helper/hil_lock.py hold <board> --reason "etm capture".
- Committed
hw/bsp/**/ozone/*.jdebug are the maintainer's interactive
projects — automation never opens them (Ozone rewrites project files); the
script generates a throwaway project.
- The default capture reflashes and resets the target (
--attach doesn't).
Capture and analyze
# 1. Build with trace support:
cd examples && cmake -B cmake-build-<board> -DBOARD=<board> -G Ninja \
-DCMAKE_BUILD_TYPE=MinSizeRel -DTRACE_ETM=1 . \
&& cmake --build cmake-build-<board> --target <example>
# 2. Capture (all options + defaults: etm_capture.py --help):
python3 .claude/skills/etm-trace/scripts/etm_capture.py \
--board <board> --probe jtrace --duration-ms 10000 --out <dir>
# 3. Analyze (hot functions, coverage, hottest lines, optimization hints):
python3 .claude/skills/etm-trace/scripts/etm_profile.py <dir> --elf <elf>
Every capture — TinyUSB firmware or vendor demo — goes through
etm_capture.py; extend it when a board needs something new, never
hand-roll Ozone drivers.
Choosing capture flags (semantics in --help):
- fresh-boot profile/coverage: defaults (flash + reset + trace from startup)
- narrowing debug on a LIVE target:
--attach — no reflash/reset (flashed
firmware must match --elf and be TRACE_ETM-built)
- raw history:
--trace-csv (~80 MB/1M instructions) — when sequence/timing
matters, e.g. feeding --isr
- stream dies (overflow/unknown-packet):
--no-timestamps, then reduce the
core clock (boards.md); marginal wiring: sweep --trace-timing,
isolate lines with --trace-width. "capture OK" requires nonzero profile
totals — silence (no trace at all) fails with its own error
- deeper data:
--profile-lines-csv (hottest lines), --profile-insts-csv
(branch bias), --sample "expr,.." (data sampling), --power (probe-powered
targets only), --os-plugin (RTOS timeline), --trace-only (experimental,
see Warnings)
- non-TinyUSB targets:
--device + --elf, plus --jlink-script when the
firmware doesn't init the trace pins
First trace on a board — or after any rewiring — is a bring-up, not a plain
capture: follow "Adding a new board" below (vendor example first).
Analyzer: --isr ENTRY[,BODY..] gives ISR min/median/avg/worst from a
--trace-csv capture with timestamps (fast-enumerating boards need a short
no-eviction run); --exclude REGEX drops idle/poll loops from the load
ranking.
Outputs in <dir>: code_profile.txt (run/fetch counts + coverage); on
request itrace.csv, profile_lines.csv, profile_insts.csv, samples.csv,
power.csv; session.log / ozone_console.log / jlink.log as evidence.
Reading results
- Load % = share of instruction fetches — Ozone has no per-function
time; time comes only from itrace timestamps (
--isr, time-share table).
- ISR timing: sub-µs values are approximate (interpolated timestamps — hence
the SysTick calibration); instruction counts are exact. Time-share ≫
instruction-share = stalled/waiting (e.g. slave-mode FIFO at wire pace).
- "Fully covered" needs both branch directions — 100% is not expected from an
idle run.
- itrace timestamps scale by
VAR_TRACE_CORE_CLOCK (from the board reference;
--core-clock overrides): ordering is exact, absolute times approximate.
- A ms+ "largest gap" or
Trace overflow detected beyond the startup burst =
lost packets — reduce the core clock or trace a quieter phase.
- One
Invalid trace timestamp line at Debug.Halt is a normal decoder
artifact.
Unknown trace data packet … Trace collection stopped! = stream dead from
that point (the script exits non-zero): retry with --no-timestamps, then
reduce the core clock.
Timing
- Capture ≈
--duration-ms + 15 s overhead; add ~5 s per 1M instructions with
--trace-csv. Bash timeout: duration + 120000 ms.
- Analyzer: < 5 s for a 2M-row itrace.csv.
Warnings
- Trace starts at
trace_etm_init(), not at reset: earlier board_init()
code shows as never-executed and Ozone logs No trace clock present — both
expected. Trace-from-reset needs a SEGGER J-Link script (.pex) instead of
firmware init.
- Never commit capture output (
itrace.csv can exceed 100 MB) — keep --out
in scratchpad//tmp; *.jdebug.user files stay untracked.
- The automation socket can't evaluate symbolic constants (
EXPORT_AS_CSV):
the scripts send numeric/plain commands only — keep it that way when
extending them (UM08025 §6.7).
- Without
xvfb-run, Ozone opens on DISPLAY and steals keyboard focus.
Ozone has no --help/--version — any such probe opens the GUI; check
with which ozone only.
--trace-only is experimental: ETM start/stop comparators are scarce
and erratic — low-rate handler windows may silently not record, adjacent
instructions leak in, timestamps are invalid across gaps, and the profile
becomes share-of-traced-stream. Use only for instruction-exact inventories
of high-rate symbols; for ISR timing use full trace + --isr.
Adding a new board
Bring-up ladder — each step gates the next:
- Docs before hardware (
read-doc skill first, then vendor site): board
manual, schematics, MCU reference manual. Establish the trace clock
source and max — chip side and probe side (J-Trace PRO Cortex-M tops out
at a 150 MHz trace clock) — the pins carrying TRACE_CLK/D0-D3 (read the board's
debug-connector table — boards often route trace on alternate pins), and
required rework (jumpers, solder bridges, 0 Ω resistors to add/remove).
Hunt shared-net hazards: PHYs or other active drivers on trace nets,
boot straps, connector stubs.
- Confirm with the user before any hardware change: present the rework
findings as [ACTION] items and wait — the user solders/jumpers, you
verify afterward.
- Vendor example before TinyUSB: fetch SEGGER's trace example for the
same/similar MCU
(https://www.segger.com/products/debug-probes/j-trace/technology/tested-devices/)
and run it with
--device <MCU> --elf <demo ELF> --jlink-script <demo .pex>. Streaming proves the physical path — and only that: demo
firmware often runs reset-default clocks (the RA6M5 one traces at a few
MHz), so its success says nothing about your target's trace rate. The
example may target a different board (the LPC4357 one is tested on a
Keil MCB4300), so silence isn't final proof — but its J-Link
script/config is often borrowable.
- TinyUSB support:
trace_etm_init() in the family BSP — mux trace
pins AFTER the final core-clock switch, enable the trace clock, enable
any funnel between ETM and TPIU; committed ozone/*.jdebug reference,
plus a .JLinkScript declaring off-ROM-table CSTF/TMC/TPIU (addresses
from the vendor demo's script); build with TRACE_ETM=1, validate with
--board <board>.
- Still silent or corrupt? In order: chip-side register audit (pinmux,
TPIU, ETM, DEMCR — and EVERY funnel in the path; an unprogrammed funnel
reads register-perfect and eats the stream), physically re-seat both
connector ends, then SEGGER's procedure (UM08001): find a stable
--trace-timing at --trace-width 1, step up to 2, then 4 (sampling
default is +2 ns) — then search the
MCU vendor's application notes and community forums for the chip's trace
recipe: more than one board's fix lived only in a forum thread.
- Board note: add the table row (core clock, TRACECLK pin + max,
width, timing, physical setup, TODO for anything left unvalidated) plus
a caveat bullet — both in
boards.md.
Per-board notes
Every validated board has a row (config: core clock, TRACECLK, width, timing,
physical setup, TODO) and a caveat entry in boards.md (same directory) —
read a board's row and caveat before capturing on it; new validations add
both. Timing semantics and clock columns are explained at the top of that file.
References
1---2name: etm-trace3description: Use when you need instruction-level execution data from real hardware via a SEGGER J-Trace — cycle-accurate hot-function profiling, on-target code coverage, or raw instruction history (e.g. what ran right before a fault/hang) — beyond what logs, GDB, or DWT PC-sampling can answer. Covers unattended (headless) capture and analysis on ETM-capable TinyUSB boards.4---56# etm-trace — unattended ETM instruction trace via J-Trace + Ozone78Streams full instruction (ETM) trace from a board wired to a SEGGER J-Trace,9headlessly: no GUI, scripted end to end. Produces hot-function profile, code10coverage, and optionally the raw instruction history.1112| Skill | Answers |13|-----------------|----------------------------------------------------------------------------|14| `usbmon` | what the host actually exchanged (URBs) |15| `target-debug` | what the target did (logs, driver state, sampled PCs) |16| `usb-sniffer` | what crossed the wire |17| **`etm-trace`** | **exactly which instructions executed, when** (profile, coverage, history) |1819Use `target-debug`'s DWT PC-sampling for a quick statistical profile; use20this skill for exact counts, coverage, or instruction-by-instruction history.2122## Requirements2324- J-Trace on USB (`lsusb -d 1366:1020`) wired to the board's trace header;25 select it by J-Link USB **nickname** (this rig: `jtrace`) — never commit26 serials.27- **Physical setup is per-board and exclusive** (one J-Trace, moved between28 boards; some rigs are fly-wired): unless the user just asked for trace on29 this board or your task states it is wired, **confirm with the user** that30 the J-Trace is connected to the target before flashing or capturing.31- `ozone` on PATH (≥ V3.38 for the automation socket) and `xvfb-run`.32- Firmware built with **`-DTRACE_ETM=1`** (BSP trace-pin + trace-clock init).33- Boards with a reference `hw/bsp/*/boards/<board>/ozone/*.jdebug` work out of34 the box (`ls` that glob for the list); others fall back to `JLINK_DEVICE`35 from `board.cmake` + default trace config. Verified boards: `boards.md` in36 this skill directory.3738## Rig discipline3940- One probe, one client: quit interactive Ozone/JLinkExe/GDB on the probe41 first. Kill only processes you started — if it's held by someone else's42 session (check `fuser /dev/bus/usb/<bus>/<dev>`), surface it and ask. The43 capture script uses automation port **19201**, never an interactive Ozone's44 19200.45- Hold the board lock (see the `hil` skill):46 `python3 test/hil/helper/hil_lock.py hold <board> --reason "etm capture"`.47- Committed `hw/bsp/**/ozone/*.jdebug` are the maintainer's interactive48 projects — automation never opens them (Ozone rewrites project files); the49 script generates a throwaway project.50- The default capture reflashes and resets the target (`--attach` doesn't).5152## Capture and analyze5354```bash55# 1. Build with trace support:56cd examples && cmake -B cmake-build-<board> -DBOARD=<board> -G Ninja \57 -DCMAKE_BUILD_TYPE=MinSizeRel -DTRACE_ETM=1 . \58 && cmake --build cmake-build-<board> --target <example>5960# 2. Capture (all options + defaults: etm_capture.py --help):61python3 .claude/skills/etm-trace/scripts/etm_capture.py \62 --board <board> --probe jtrace --duration-ms 10000 --out <dir>6364# 3. Analyze (hot functions, coverage, hottest lines, optimization hints):65python3 .claude/skills/etm-trace/scripts/etm_profile.py <dir> --elf <elf>66```6768Every capture — TinyUSB firmware or vendor demo — goes through69`etm_capture.py`; extend it when a board needs something new, never70hand-roll Ozone drivers.7172Choosing capture flags (semantics in `--help`):73- fresh-boot profile/coverage: defaults (flash + reset + trace from startup)74- narrowing debug on a LIVE target: `--attach` — no reflash/reset (flashed75 firmware must match `--elf` and be TRACE_ETM-built)76- raw history: `--trace-csv` (~80 MB/1M instructions) — when sequence/timing77 matters, e.g. feeding `--isr`78- stream dies (overflow/unknown-packet): `--no-timestamps`, then reduce the79 core clock (`boards.md`); marginal wiring: sweep `--trace-timing`,80 isolate lines with `--trace-width`. "capture OK" requires nonzero profile81 totals — silence (no trace at all) fails with its own error82- deeper data: `--profile-lines-csv` (hottest lines), `--profile-insts-csv`83 (branch bias), `--sample "expr,.."` (data sampling), `--power` (probe-powered84 targets only), `--os-plugin` (RTOS timeline), `--trace-only` (experimental,85 see Warnings)86- non-TinyUSB targets: `--device` + `--elf`, plus `--jlink-script` when the87 firmware doesn't init the trace pins8889First trace on a board — or after any rewiring — is a bring-up, not a plain90capture: follow "Adding a new board" below (vendor example first).9192Analyzer: `--isr ENTRY[,BODY..]` gives ISR min/median/avg/worst from a93`--trace-csv` capture with timestamps (fast-enumerating boards need a short94no-eviction run); `--exclude REGEX` drops idle/poll loops from the load95ranking.9697Outputs in `<dir>`: `code_profile.txt` (run/fetch counts + coverage); on98request `itrace.csv`, `profile_lines.csv`, `profile_insts.csv`, `samples.csv`,99`power.csv`; `session.log` / `ozone_console.log` / `jlink.log` as evidence.100101## Reading results102103- **Load %** = share of instruction **fetches** — Ozone has no per-function104 time; time comes only from itrace timestamps (`--isr`, time-share table).105- ISR timing: sub-µs values are approximate (interpolated timestamps — hence106 the SysTick calibration); instruction counts are exact. Time-share ≫107 instruction-share = stalled/waiting (e.g. slave-mode FIFO at wire pace).108- "Fully covered" needs both branch directions — 100% is not expected from an109 idle run.110- itrace timestamps scale by `VAR_TRACE_CORE_CLOCK` (from the board reference;111 `--core-clock` overrides): ordering is exact, absolute times approximate.112- A ms+ "largest gap" or `Trace overflow detected` beyond the startup burst =113 lost packets — reduce the core clock or trace a quieter phase.114- One `Invalid trace timestamp` line at `Debug.Halt` is a normal decoder115 artifact.116- `Unknown trace data packet … Trace collection stopped!` = stream dead from117 that point (the script exits non-zero): retry with `--no-timestamps`, then118 reduce the core clock.119120## Timing121122- Capture ≈ `--duration-ms` + 15 s overhead; add ~5 s per 1M instructions with123 `--trace-csv`. Bash timeout: duration + 120000 ms.124- Analyzer: < 5 s for a 2M-row itrace.csv.125126## Warnings127128- **Trace starts at `trace_etm_init()`**, not at reset: earlier `board_init()`129 code shows as never-executed and Ozone logs `No trace clock present` — both130 expected. Trace-from-reset needs a SEGGER J-Link script (`.pex`) instead of131 firmware init.132- Never commit capture output (`itrace.csv` can exceed 100 MB) — keep `--out`133 in scratchpad/`/tmp`; `*.jdebug.user` files stay untracked.134- The automation socket can't evaluate symbolic constants (`EXPORT_AS_CSV`):135 the scripts send numeric/plain commands only — keep it that way when136 extending them (UM08025 §6.7).137- Without `xvfb-run`, Ozone opens on `DISPLAY` and steals keyboard focus.138 Ozone has no `--help`/`--version` — any such probe opens the GUI; check139 with `which ozone` only.140- **`--trace-only` is experimental**: ETM start/stop comparators are scarce141 and erratic — low-rate handler windows may silently not record, adjacent142 instructions leak in, timestamps are invalid across gaps, and the profile143 becomes share-of-traced-stream. Use only for instruction-exact inventories144 of high-rate symbols; for ISR timing use full trace + `--isr`.145146## Adding a new board147148Bring-up ladder — each step gates the next:1491501. **Docs before hardware** (`read-doc` skill first, then vendor site): board151 manual, schematics, MCU reference manual. Establish the trace clock152 source and max — chip side and probe side (J-Trace PRO Cortex-M tops out153 at a 150 MHz trace clock) — the pins carrying TRACE_CLK/D0-D3 (read the board's154 debug-connector table — boards often route trace on alternate pins), and155 required rework (jumpers, solder bridges, 0 Ω resistors to add/remove).156 Hunt shared-net hazards: PHYs or other active drivers on trace nets,157 boot straps, connector stubs.1582. **Confirm with the user before any hardware change**: present the rework159 findings as **[ACTION]** items and wait — the user solders/jumpers, you160 verify afterward.1613. **Vendor example before TinyUSB**: fetch SEGGER's trace example for the162 same/similar MCU163 (<https://www.segger.com/products/debug-probes/j-trace/technology/tested-devices/>)164 and run it with `--device <MCU> --elf <demo ELF> --jlink-script165 <demo .pex>`. Streaming proves the physical path — and only that: demo166 firmware often runs reset-default clocks (the RA6M5 one traces at a few167 MHz), so its success says nothing about your target's trace rate. The168 example may target a different board (the LPC4357 one is tested on a169 Keil MCB4300), so silence isn't final proof — but its J-Link170 script/config is often borrowable.1714. **TinyUSB support**: `trace_etm_init()` in the family BSP — mux trace172 pins AFTER the final core-clock switch, enable the trace clock, enable173 any funnel between ETM and TPIU; committed `ozone/*.jdebug` reference,174 plus a `.JLinkScript` declaring off-ROM-table CSTF/TMC/TPIU (addresses175 from the vendor demo's script); build with `TRACE_ETM=1`, validate with176 `--board <board>`.1775. **Still silent or corrupt?** In order: chip-side register audit (pinmux,178 TPIU, ETM, DEMCR — and EVERY funnel in the path; an unprogrammed funnel179 reads register-perfect and eats the stream), physically re-seat both180 connector ends, then SEGGER's procedure (UM08001): find a stable181 `--trace-timing` at `--trace-width 1`, step up to 2, then 4 (sampling182 default is +2 ns) — then search the183 MCU vendor's application notes and community forums for the chip's trace184 recipe: more than one board's fix lived only in a forum thread.1856. **Board note**: add the table row (core clock, TRACECLK pin + max,186 width, timing, physical setup, TODO for anything left unvalidated) plus187 a caveat bullet — both in `boards.md`.188189## Per-board notes190191Every validated board has a row (config: core clock, TRACECLK, width, timing,192physical setup, TODO) and a caveat entry in `boards.md` (same directory) —193**read a board's row and caveat before capturing on it**; new validations add194both. Timing semantics and clock columns are explained at the top of that file.195196## References197198- Ozone manual (UM08025, automation socket §6.7, project commands §7):199 <https://www.segger.com/downloads/jlink/UM08025_Ozone.pdf>; the installed200 Ozone ships its own revision of it under its `Doc/` directory (offline201 fallback — section numbers can differ between revisions).202- J-Link / J-Trace manual (UM08001, trace ch. 10, timing troubleshooting):203 <https://kb.segger.com/UM08001_J-Link_/_J-Trace_User_Guide>