summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.claude/agents/target-debugger.md1
-rw-r--r--.claude/skills/etm-trace/SKILL.md203
-rw-r--r--.claude/skills/etm-trace/boards.md161
-rw-r--r--.claude/skills/etm-trace/scripts/etm_capture.py572
-rw-r--r--.claude/skills/etm-trace/scripts/etm_profile.py467
-rw-r--r--.claude/skills/target-debug/SKILL.md1
-rw-r--r--docs/superpowers/plans/2026-07-24-etm-trace-agent-integration.md299
-rw-r--r--hw/bsp/imxrt/boards/metro_m7_1011/ozone/metro_m7_1011.jdebug3
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1170_evkb/board.h5
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176.jdebug69
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176_trace.JLinkScript11
-rw-r--r--hw/bsp/imxrt/family.c73
-rw-r--r--hw/bsp/lpc18/boards/lpcxpresso18s37/board.h6
-rw-r--r--hw/bsp/lpc18/boards/mcb1800/board.h27
-rw-r--r--hw/bsp/lpc18/boards/mcb1800/ozone/lpc1857.jdebug6
-rw-r--r--hw/bsp/lpc18/family.c1
-rw-r--r--hw/bsp/lpc40/boards/ea4088_quickstart/ozone/ea4088_quickstart.jdebug2
-rw-r--r--hw/bsp/lpc43/boards/ea4357/board.h16
-rw-r--r--hw/bsp/lpc43/boards/lpcxpresso43s67/board.h6
-rw-r--r--hw/bsp/lpc43/family.c6
-rw-r--r--hw/bsp/nrf/boards/nrf52840dk/ozone/nrf52840.jdebug2
-rw-r--r--hw/bsp/nrf/boards/nrf5340dk/ozone/nrf5340.jdebug6
-rw-r--r--hw/bsp/nrf/family.c6
-rw-r--r--hw/bsp/ra/boards/ra6m5_ek/ozone/ra6m5.jdebug2
-rw-r--r--hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug17
-rw-r--r--hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1_trace.JLinkScript20
-rw-r--r--hw/bsp/ra/family.c29
-rw-r--r--hw/bsp/rp2040/boards/raspberry_pi_pico2/board.cmake14
-rw-r--r--hw/bsp/rp2040/boards/raspberry_pi_pico2/ozone/rp2350.jdebug70
-rw-r--r--hw/bsp/rp2040/family.c27
-rw-r--r--hw/bsp/samd5x_e5x/boards/same54_xplained/ozone/same54.jdebug89
-rw-r--r--hw/bsp/samd5x_e5x/family.c27
-rw-r--r--hw/bsp/same7x/boards/same70_xplained/board.h4
-rw-r--r--hw/bsp/same7x/boards/same70_xplained/ozone/same70.jdebug115
-rw-r--r--hw/bsp/same7x/family.c32
-rw-r--r--hw/bsp/stm32h5/boards/stm32h563nucleo/board.h5
-rw-r--r--hw/bsp/stm32h5/boards/stm32h563nucleo/ozone/stm32h563.jdebug113
-rw-r--r--hw/bsp/stm32h5/family.c20
-rw-r--r--hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug3
-rw-r--r--hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h6
-rw-r--r--hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3.jdebug94
-rw-r--r--hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3_trace.JLinkScript11
-rw-r--r--hw/bsp/stm32h7rs/family.c19
-rw-r--r--hw/bsp/stm32n6/boards/stm32n657nucleo/board.h6
-rw-r--r--hw/bsp/stm32n6/boards/stm32n657nucleo/ozone/stm32n657.jdebug94
-rw-r--r--hw/bsp/stm32n6/family.c22
46 files changed, 2749 insertions, 39 deletions
diff --git a/.claude/agents/target-debugger.md b/.claude/agents/target-debugger.md
index e25ffa7f1..246ad16fd 100644
--- a/.claude/agents/target-debugger.md
+++ b/.claude/agents/target-debugger.md
@@ -22,6 +22,7 @@ one BEFORE acting:
| esp-target-debug | PRIMARY playbook for Espressif boards — built-in USB-Serial-JTAG attach, the PHY map that decides whether JTAG exists, FreeRTOS threads via ESP_RTOS; target-debug still supplies the methodology |
| usbmon | Linux-host URB capture; only when a Linux PC is the link's host (default posture: dual-side, both ends simultaneously) |
| usb-sniffer | wire-level capture (hardware tap): host can't see the bus, usbmon vs target logs disagree, or TinyUSB is the host (no usbmon anywhere) |
+| etm-trace | instruction-level ETM trace via SEGGER J-Trace (exact execution history, profile, coverage) when sampled PCs and logs cannot resolve the mechanism. Requires the J-Trace physically wired to THIS board (supported boards: the skill's boards.md) — use only when your prompt states the board is trace-wired or the user asked for it; otherwise name it in `notes` as the next technique |
| usb-kernel-debug | why the Linux kernel acted (dmesg/dynamic debug); PC host or a Linux gadget peer's device side |
| usb-kernel-recover | only when the DUT or fixture wedges the rig PC's Linux host stack |
diff --git a/.claude/skills/etm-trace/SKILL.md b/.claude/skills/etm-trace/SKILL.md
new file mode 100644
index 000000000..a200b14f3
--- /dev/null
+++ b/.claude/skills/etm-trace/SKILL.md
@@ -0,0 +1,203 @@
+---
+name: etm-trace
+description: 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.
+---
+
+# 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
+ 19200.
+- Hold the board lock (see the `hil` skill):
+ `python3 test/hil/board_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
+
+```bash
+# 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:
+
+1. **Docs before hardware** (calibre library 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.
+2. **Confirm with the user before any hardware change**: present the rework
+ findings as **[ACTION]** items and wait — the user solders/jumpers, you
+ verify afterward.
+3. **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.
+4. **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>`.
+5. **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.
+6. **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
+
+- Ozone manual (UM08025, automation socket §6.7, project commands §7):
+ <https://www.segger.com/downloads/jlink/UM08025_Ozone.pdf> — V3.50, same as
+ the installed Ozone (web is rev 1 vs the local copy's rev 0; the local PDF
+ under /opt/SEGGER/Ozone_V350/Doc remains the offline fallback).
+- J-Link / J-Trace manual (UM08001, trace ch. 10, timing troubleshooting):
+ <https://kb.segger.com/UM08001_J-Link_/_J-Trace_User_Guide>
diff --git a/.claude/skills/etm-trace/boards.md b/.claude/skills/etm-trace/boards.md
new file mode 100644
index 000000000..4a5f297ae
--- /dev/null
+++ b/.claude/skills/etm-trace/boards.md
@@ -0,0 +1,161 @@
+# etm-trace — per-board reference
+
+Validated boards: trace config table + hard-won caveats. Read the row AND the
+caveat for a board before capturing on it; add a row + caveat when a new board
+is validated (jdebug reference, board.cmake/board.h clock selection and this
+file must agree).
+
+"Core (trace build)" is the CPU clock a `TRACE_ETM=1` build runs — where it
+differs from the stock clock, board.h selects it automatically. Timing
+"0 (unset)" = the reference sets no SetTraceTiming and Ozone then sends
+`TraceSampleAdjust TD = 0`; J-Link's own +2 ns default (UM08001) applies only
+outside Ozone-driven captures. Explicit values live in the committed
+reference.
+
+| Board | Core (trace build) | TRACECLK pin | Width | Timing | Physical setup | TODO |
+|--------------------|----------------------|-----------------------|-------|---------|-------------------------------|---------------------------------------------|
+| stm32h743eval | 400 MHz | 50 MHz (PLL1R, fixed) | 4 | +100 ps | — | — |
+| stm32n657nucleo | 300 MHz | 18.75 MHz (cpu/16) | 4 | 0 (unset) | none — CN1 MIPI20; JP2=1 | — |
+| stm32h7s3nucleo | 300 MHz | 50 MHz (cpu/3/2) | 2 | 0 (unset) | none — native CN1 MIPI20 | remove SB11/SB12 → width 4 @ 600 MHz |
+| stm32h563nucleo | 100 MHz | follows core | 1 | +5 ns | remove SB8/9/64/68/70/71/78 | retest width 4 / 250 MHz after SB removal |
+| metro_m7_1011 | 500 MHz | 66 MHz (root/2) | 4 | +50 ps | custom rev A ETM-header rework | — |
+| mcb1800 | 120 MHz | 60 MHz (CCLK/2) | 4 | 0 (unset) | fit J5 DBG_EN | — |
+| ea4088_quickstart | 120 MHz | 120 MHz | 4 | 0 (unset) | J7 (fully wired) | — |
+| nrf52840dk | 64 MHz | 16 MHz (hw cap) | 4 | 0 (unset) | solder P25, SW7 → Alt | — |
+| nrf5340dk (M33) | 64 MHz | 16 MHz (TAD, forced) | 4 | +3 ns | mount P25; cut SB27/SB28 | — |
+| mimxrt1170_evkb | 996 MHz | 50 MHz (root/2) | 1 | 0 | weld 0 Ω R1881-R1886; JP4 shorted; J58 (populated) | re-weld R1884 (D3 open; D1/D2 meter-verified good) → width 4 |
+| ra6m5_ek (M33) | 200 MHz | 25 MHz (TRCLK/4 /2) | 4 | 0 (unset) | J9 closed; native J20 trace | — |
+| ra8m1_ek (M85) | 480 MHz | 60 MHz (TRCLK/4 /2) | 4 | 0 (unset) | J9 closed + Table 7 jumpers | — |
+| raspberry_pi_pico2 (RP2350 M33) | 48 MHz | 24 MHz (clk_sys/2) | 4 | 0 (unset) | fly-wire GPIO1-5 → MIPI20 (map in jdebug) | 72-80 MHz per seating (re-qualify); >80 needs V3 probe + trace board |
+| same54_xplained (E54 M4F) | 120 MHz | 60 MHz (CPU/2) | 4 | 0 (unset) | none — populated 20-pin ETM header | — |
+| same70_xplained (E70 M7) | 300 MHz | 37.5 MHz (PCK3/2) | 1 | 0 (unset) | solder 20-pin header on J403 (bottom) | width 4 blocked: D1 (J403.16) dead at speed — probe-channel crosscheck pending |
+| SEGGER H7/F407 ref | demo defaults | demo | 4 | demo | probe-powered: add `--power` | — |
+
+Board caveats (beyond the table):
+
+- **stm32h743eval**: startup-burst overflow at 400 MHz is normal (reduce
+ PLLN in board.h for overflow-free capture); timestamp ref 200 MHz.
+- **stm32n657nucleo** (M55, flashless): **JP2 (BOOT1) must be 1** — the app
+ is a RAM image the debugger loads (Development boot); in flash boot the
+ bootROM parks the chip un-attachable ("Can not attach to CPU"). SEGGER's
+ KB says BOOT0/BOOT1 = 0/0 for their example — that is flash boot and it
+ does NOT attach; the board manual's Table 11 is right. 600 MHz core kills
+ the stream in the startup burst (timestamp flux, not overflow) — TRACE_ETM
+ builds run 300 MHz; at 600 use `--no-timestamps`. No J-Link script needed:
+ N6 trace components are ROM-table-discoverable.
+- **stm32h7s3nucleo**: width 4 is clean at idle but SB11/SB12 (default ON)
+ stub D2/D3 onto Zio CN8 and the stream dies under IRQ-heavy USB traffic —
+ remove them to try width 4 / 600 MHz. `--attach` while a USB host is
+ actively polling the device wedges its USB session (needs target reset).
+- **stm32h563nucleo**: width 4 or 250 MHz corrupts. H5 hangs its debug AP if
+ trace CoreSight is touched unclocked — handled by the committed
+ `AfterTargetConnect` hook; un-attachable after a killed session →
+ power-cycle.
+- **metro_m7_1011** (RT1011): a custom Adafruit rev with a hand-added 2x10
+ ETM header (KiCad schematic in the calibre library). No SEGGER RT1011
+ example exists — the committed .jdebug (tuned +50 ps) is the known-good
+ reference. BOARD_BootClockRUN sets the 132 MHz trace root but leaves it
+ gated; `trace_etm_init` ungates it. The first Ozone run after a fresh
+ flash can fail to reach main — transient, retry once.
+- **mcb1800**: a bad ribbon mating yields register-perfect silence — re-seat
+ BOTH ribbon ends first; SWD working proves nothing about the trace lines.
+ `--isr USB0_IRQHandler,dcd_int_handler`.
+- **nrf5340dk**: the interface MCU's UART1 flow control rides the trace
+ pins — it actively drives CTS onto P0.10/TRACEDATA1 (dead line, any
+ timing) and loads P0.11/TRACEDATA0: cut SB27/SB28 (or flip SW7 to FC-off).
+ SB57's SWO stub can stay — harmless at the +3 ns sample point. TRACE_ETM
+ builds force the TAD port to 16 MHz (SystemInit's 64 MHz is marginal).
+- **ea4088_quickstart**: FS enumeration ends < 100 ms — for `--isr` use
+ `--duration-ms 150`. Boot-ROM address warning (0x1FFF1FF0) is normal.
+- **mimxrt1170_evkb**: width 1 only — D1-D3 are stone silent at any config
+ (pinmux register-perfect, PHY quieted, funnel enabled): the welded 0402s
+ R1882/R1883/R1884 are electrically open — reflow to unlock width 4.
+ `trace_etm_init` fixes the JTAG_nTRST/DMIC_DATA1 pad, holds the 100M
+ RTL8201 PHY in reset (ENET_RST_B = GPIO_LPSR_04 — its RMII lines share
+ the trace pads; with it quiet the CLK line runs a 100 MHz root/50 MHz
+ pin, 2x the pre-lever rate; 133 MHz root is marginal, stock 132 corrupts)
+ and enables the CM7 platform trace-funnel port, which J-Link doesn't
+ program: without it everything reads register-perfect yet zero data
+ arrives. JP4 must be shorted (disables MCU-Link SWD) for the external
+ J-Trace on J58; a powered MCU-Link USB breaks the external probe's connect
+ even with JP4 shorted - power the board from another port. **Width is 1 or
+ 4 only**: a width-2 request arms the CSSYS TPIU (E004_6000) and the probe
+ sampler at 4-bit anyway (CSPSR reads 0x8 after a width-2 session; a live
+ CSPSR=2 poke with LAR unlock + Trace.Clear still captures nothing because
+ the probe keeps sampling 4-bit). FlexSPI apps: ROM bootloader must set SP/PC — the
+ committed reset/download hooks handle this. Startup-burst overflow at
+ 996 MHz is normal. No Ethernet (100M) while tracing.
+- **ra6m5_ek**: TRCKCR div-2 (100 MHz TRCLK = 50 MHz pin, the chip max) is
+ unusable on this board — swept widths 1/4 across -2..+4 ns, all dead;
+ TRACE_ETM builds use div-4 (25 MHz pin). The TCLK pin runs TRCLK/2. 50 MHz SWD TIF
+ caused intermittent "Failed to initialize DAP" — the reference runs 4 MHz.
+ ISR entry: `--isr tusb_int_handler,dcd_int_handler` (FSP's
+ usbfs_interrupt_handler symbol never actually executes).
+- **ra6m5_ek / ra8m1_ek — `--attach` needs a debugger-booted target**: the
+ firmware TRCKCR setup is gated on DHCSR.C_DEBUGEN (an unguarded write
+ wedges a standalone boot un-attachable until power-cycle), so a board
+ booted WITHOUT a debugger has no trace clock and an `--attach` capture
+ reads silence. Reflash/reset through the capture default flow first.
+- **ra8m1_ek**: **J9 must be closed** (holds the on-board J-Link OB in
+ reset — open = SWD contention, intermittent "Failed to initialize DAP",
+ even an apparent brick recoverable only by power-cycle/J16 boot mode).
+ J-Link's RA8 support enables trace from reset; the committed JLinkScript's
+ empty `OnTraceStart` suppresses that so the firmware enables the trace
+ clock after the FSP clock switch — without it the MOCO→PLL step desyncs
+ the decoder at t≈0.05 s every run. Runs both chip maxima (120 MHz TRCLK,
+ 60 MHz pin) clean. `ReadIntoTraceCache 0x0 0x10000` in the download hook
+ covers runtime chip-ROM execution. ISR entry: `tusb_int_handler`.
+- **raspberry_pi_pico2** (RP2350): TRACECLK is a fixed clk_sys/2, no divider
+ (DDR data, like every ARM TPIU pin port). **Measured cliff on this rig:**
+ 80 MHz core (40 MHz TRACECLK) traces idle code but dies under dense data;
+ 88 MHz+ dies instantly at any width/global-timing/TIF/pad setting. Cause
+ not pinned down: the same V2 probe samples 66 MHz TRACECLK (132 Msample/s)
+ on metro_m7_1011, so it is NOT a plain probe sample-rate ceiling. The
+ cliff at >40 MHz TRACECLK (84+ MHz core) survived a full sweep - global
+ AND per-pin `--trace-timing`, pad drive 2/4/8/12 mA + slew, width 4/2/1,
+ TIF 1-25 MHz, newer J-Link library - all flat, so it is V3-probe / real-
+ trace-board territory (SEGGER's Pico 2 KB requires J-Trace PRO **V3.0+**
+ and recommends a proper trace board; community reports fly-wires fail at
+ 75 MHz for everyone, PCBs work). Separately, fly-wire seating quality
+ sets the width-4 DENSE-data ceiling (48-72 MHz observed across seatings):
+ after ANY rewiring re-qualify with idle blinky at the target clock, then
+ cdc_msc x3. Random unknown-packet deaths KB into a clean stream = one
+ marginal wire; `--trace-width` 1 vs 2 vs 4 bisects which (width 1 =
+ CLK+D0 only; D1 = GPIO3->MIPI20 pin 16 has gone marginal twice on this
+ rig). Width-1 is a full-quality fallback: complete cdc_msc profiles at up
+ to 80 MHz core even when width 4 is broken.
+ **Never set a custom JLinkScript** — it
+ replaces J-Link's built-in RP2350 device script, which both declares the
+ trace component map (funnel/TPIU/ETM are not in the ROM table → "Required
+ trace components for pin trace not found", 0 fetches) and re-arms the whole
+ chip-side path via `OnTraceStart` at every resume. Firmware therefore does
+ no trace setup; TRACE_ETM builds only (a) pin clk_sys to 48 MHz from crt0
+ (board.cmake) — the fly-wire ceiling: 96/150 MHz kill the stream in the
+ startup burst at any sample timing (and at 150 MHz the saturated probe
+ stops answering halts, "CPU could not be halted"); any post-arm clock
+ change steps TRACECLK mid-stream and kills the decoder — and (b)
+ clear TIMER0/1 DBGPAUSE (family.c): debug sessions leave cores
+ halted-at-reset and the default DBGPAUSE freezes the µs timer, so every
+ `sleep_ms()` spins forever (looks like a dead board; watchdog-scratch
+ breadcrumbs survive warm resets but not POR when diagnosing). UART console
+ is TX-only (GPIO1 = TRACECLK). Empty reset/download hooks: the bootrom
+ must run the IMAGE_DEF. If the chip ends up wedged/un-attachable:
+ J-Link `erase` + reset drops it into BOOTSEL (2e8a:000f) for picotool.
+- **same54_xplained**: the CM4 trace unit is clocked from **GCLK channel 47
+ (GCLK_CM4_TRACE)** — with it disabled the pins mux fine, TPIU/ETM arm
+ fine, and the port stays perfectly silent (zero fetches, no errors);
+ `trace_etm_init` feeds it GCLK0. Pins PC24-28 mux to function H. The
+ populated 20-pin header runs chip-max 60 MHz TRACECLK width 4 with no
+ timing adjustment - the connector-vs-flywire contrast board.
+- **same70_xplained**: J403 is a bottom-side bare footprint — solder the
+ header. Trace pins PD4-7 double as the KSZ8081 PHY's RMII receive outputs:
+ TRACE_ETM builds hold it in reset (PHY_RESET=PC10) or it drives against
+ the stream. TPIU clock = PCK3 (datasheet 16.7.4), run at MCK/2; TPIU
+ programming while PCK3 is stopped is silently LOST — the reference starts
+ PCK3 in the post-reset/download hooks (a reset wipes the PMC, so
+ AfterTargetConnect is too early). Width 1 validated at the stock 300 MHz
+ core; width 2/4 blocked on a dead D1 line at J403.16 (clean at DC by
+ meter, dead at speed — probe-channel crosscheck on a known-good width-4
+ board pending). No Ethernet while tracing.
+- **SEGGER ref boards**: run their own demo (ladder step 3 flags); `--isr`
+ degrades gracefully without a live SysTick.
diff --git a/.claude/skills/etm-trace/scripts/etm_capture.py b/.claude/skills/etm-trace/scripts/etm_capture.py
new file mode 100644
index 000000000..ccde886f2
--- /dev/null
+++ b/.claude/skills/etm-trace/scripts/etm_capture.py
@@ -0,0 +1,572 @@
+#!/usr/bin/env python3
+"""Capture streaming ETM instruction trace unattended via J-Trace + Ozone.
+
+Generates a throwaway Ozone project (never touches the committed
+hw/bsp/**/ozone/*.jdebug), launches Ozone on a virtual display (xvfb-run),
+drives the whole session over Ozone's automation TCP socket (UM08025 §6.7):
+connect -> flash -> run for --duration-ms under streaming trace -> halt ->
+export. Outputs in --out:
+ code_profile.txt hot functions (run/fetch counts) + code coverage
+ itrace.csv raw instruction history (only with --trace-csv)
+ ozone_console.log, jlink.log, ozone_gui.log session evidence
+
+Requires firmware built with -DTRACE_ETM=1 and the board wired to a J-Trace.
+Analyze results with etm_profile.py in this directory.
+"""
+
+import argparse
+import glob
+import os
+import re
+import shutil
+import signal
+import socket
+import string
+import subprocess
+import sys
+import tempfile
+import time
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), *[".."] * 4))
+
+# Throwaway Ozone project. SP/PC-from-vector-table hooks match the committed
+# reference projects (Cortex-M generic). $$(InstallDir) -> literal $(InstallDir).
+PROJECT_TEMPLATE = string.Template("""\
+/* Auto-generated by etm_capture.py (etm-trace skill) - throwaway, do not commit. */
+void OnProjectLoad (void) {
+ Project.SetDevice ("$device");
+ Project.SetHostIF ("USB", "$probe");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("$tif_speed");
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth ($port_width);
+$timing_line$core_clock_line$timestamps_line$hss_line$power_lines Edit.SysVar (VAR_TRACE_MAX_INST_CNT, $max_inst);
+ Edit.Preference (PREF_TIMESTAMP_FORMAT, TIMESTAMP_FORMAT_TIME);
+ Project.AddSvdFile ("$$(InstallDir)/Config/Peripherals/ARMv7M.svd");
+ Project.SetConsoleLogFile ("$outdir/ozone_console.log");
+ Project.SetJLinkLogFile ("$outdir/jlink.log");
+$os_plugin_line File.Open ("$elf");
+}
+$jlink_script_hook
+
+$reset_hook
+
+$download_hook
+$user_funcs""")
+
+# Cortex-M generic SP/PC-from-vector-table init. A committed board reference
+# overrides these verbatim (e.g. RT1176 apps in FlexSPI NOR need the ROM
+# bootloader to do SP/PC init, plus a JTAG_nTRST pad fix).
+DEFAULT_HOOK = """\
+void %s (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+ VectorTableAddr = Elf.GetBaseAddr();
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("etm_capture: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}"""
+
+# Symbolic constants (TP_OP_*, EXPORT_*) only evaluate in project-script
+# context, never over the automation socket - so these live in generated user
+# functions invoked via Script.Exec (UM08025 SS6.7, SS7.9.9.1).
+TRACEPOINT_FUNC = """\
+
+void SetupTracepoints (void) {
+%s}
+"""
+
+LINES_CSV_FUNC = """\
+
+void ExportLinesCsv (void) {
+ Export.CodeProfile ("%s", EXPORT_AS_CSV | EXPORT_CSV_LINES | EXPORT_FILE_PATHS, "");
+}
+"""
+
+INSTS_CSV_FUNC = """\
+
+void ExportInstsCsv (void) {
+ Export.CodeProfile ("%s", EXPORT_AS_CSV | EXPORT_CSV_INSTS | EXPORT_FILE_PATHS, "");
+}
+"""
+
+
+JLINK_SCRIPT_HOOK = """\
+
+void BeforeTargetConnect (void) {
+ Project.SetJLinkScript ("%s");
+}
+"""
+
+
+def resolve_board(board):
+ """Board config from its committed ozone reference project, else board.cmake/mk."""
+ cfg = {"device": None, "tif_speed": "4 MHz", "timing": None, "port_width": 4,
+ "core_clock": None, "ref": None}
+ jdebugs = sorted(glob.glob(f"{REPO_ROOT}/hw/bsp/*/boards/{board}/ozone/*.jdebug"))
+ if jdebugs:
+ cfg["ref"] = jdebugs[0]
+ text = open(jdebugs[0]).read()
+ # config regexes must not match //-commented lines
+ cfgtext = re.sub(r"^\s*//.*$", "", text, flags=re.M)
+ # inherit every user function verbatim except OnProjectLoad, which the
+ # template owns (e.g. STM32H5's AfterTargetConnect must clock the trace
+ # CoreSight domain; RT1176 replaces the SP/PC reset/download hooks for
+ # ROM-bootloader boot; Nordic hooks call a _SetupTarget helper that
+ # must ride along or hook execution fails at runtime)
+ extra = []
+ for m in re.finditer(r"^void (\w+)\s*\(void\)\s*\{.*?^\}", text,
+ re.M | re.S):
+ name, block = m.group(1), m.group(0)
+ if name == "OnProjectLoad":
+ continue
+ elif name == "BeforeTargetConnect":
+ # the generated project synthesizes its own BeforeTargetConnect
+ # (JLINK_SCRIPT_HOOK) from the SetJLinkScript regex below;
+ # inheriting the reference's copy too would emit a duplicate
+ # function definition
+ continue
+ elif name == "AfterTargetReset":
+ cfg["reset_hook"] = block
+ elif name == "AfterTargetDownload":
+ cfg["download_hook"] = block
+ else:
+ extra.append(block)
+ if extra:
+ cfg["connect_hook"] = "\n\n".join(extra) + "\n"
+ for key, pat in (("device", r'Project\.SetDevice\s*\(\s*"([^"]+)"'),
+ ("tif_speed", r'Project\.SetTIFSpeed\s*\(\s*"([^"]+)"'),
+ ("timing", r'Project\.SetTraceTiming\s*\(([-\d\s,]+)\)'),
+ ("port_width", r'Project\.SetTracePortWidth\s*\(\s*(\d+)'),
+ ("core_clock", r'VAR_TRACE_CORE_CLOCK\s*,\s*(\d+)')):
+ m = re.search(pat, cfgtext)
+ if m:
+ cfg[key] = m.group(1).strip()
+ # inherit a J-Link script (e.g. RT1176 must declare its off-ROM-table
+ # TPIU/funnel); relative paths resolve against the reference's dir
+ m = re.search(r'Project\.SetJLinkScript\s*\(\s*"([^"]+)"', cfgtext)
+ if m:
+ # $(ProjectDir) = the reference's own directory
+ rel = m.group(1).replace("$(ProjectDir)", ".")
+ cfg["jlink_script"] = os.path.normpath(os.path.join(
+ os.path.dirname(jdebugs[0]), rel))
+ else:
+ for path in glob.glob(f"{REPO_ROOT}/hw/bsp/*/boards/{board}/board.cmake"):
+ m = re.search(r'JLINK_DEVICE\s+([^\s)]+)\s*\)', open(path).read())
+ if m:
+ if "${" in m.group(1):
+ sys.exit(f"error: {path} defines JLINK_DEVICE via an "
+ f"unexpanded CMake variable ({m.group(1)}) - pass "
+ f"--device explicitly for this board")
+ cfg["device"] = m.group(1)
+ cfg["ref"] = path
+ break
+ if not cfg["device"]:
+ sys.exit(f"error: cannot resolve J-Link device for board '{board}' "
+ f"(no hw/bsp/*/boards/{board}/ozone/*.jdebug or board.cmake)")
+ return cfg
+
+
+def trace_only_points(elf, syms_arg):
+ """Tracepoint lines for --trace-only: start trace at each symbol's entry,
+ stop at each return instruction inside it (pop ...pc / bx lr, via objdump).
+ Hardware comparators are scarce (ETM-M7) - keep the symbol list short."""
+ lines = ""
+ nm = subprocess.run(["arm-none-eabi-nm", "-S", "--defined-only", elf],
+ capture_output=True, text=True).stdout
+ for want in [s.strip() for s in syms_arg.split(",") if s.strip()]:
+ m = re.search(rf"^([0-9a-f]+) ([0-9a-f]+) [TtWw] {re.escape(want)}$",
+ nm, re.M)
+ if not m:
+ sys.exit(f"error: --trace-only symbol '{want}' not in ELF")
+ lo, sz = int(m.group(1), 16) & ~1, int(m.group(2), 16)
+ lines += f' Trace.SetPoint (TP_OP_START_TRACE, "{want}");\n'
+ dis = subprocess.run(
+ ["arm-none-eabi-objdump", "-d", f"--start-address={lo:#x}",
+ f"--stop-address={lo + sz:#x}", elf],
+ capture_output=True, text=True).stdout
+ exits = re.findall(
+ r"^\s*([0-9a-f]+):.*?(?:(?:pop|ldmia[.\w]*\s+sp!,)[^\n]*\bpc\b|bx\s+lr)",
+ dis, re.M | re.I)
+ if not exits:
+ sys.exit(f"error: no return instruction found in '{want}'")
+ for addr in exits:
+ lines += f' Trace.SetPoint (TP_OP_STOP_TRACE, "0x{int(addr, 16):08X}");\n'
+ return lines
+
+
+def resolve_probe(probe):
+ """Ozone's SetHostIF needs a serial - with several probes connected a
+ nickname makes it block on a selection dialog. JLinkExe DOES resolve
+ nicknames, so borrow its banner to map nickname -> serial. The serial only
+ ever lands in the throwaway project file, never in committed files."""
+ if not probe or probe.isdigit():
+ return probe
+ r = subprocess.run(["JLinkExe", "-USB", probe, "-nogui", "1"],
+ input="qc\n", capture_output=True, text=True, timeout=30)
+ m = re.search(r"S/N:\s*(\d+)", r.stdout)
+ if not m:
+ sys.exit(f"error: cannot resolve probe nickname '{probe}' to a serial "
+ f"(JLinkExe -USB {probe} found no emulator)")
+ return m.group(1)
+
+
+def gen_project(cfg, args, outdir):
+ timing = ""
+ if args.trace_timing is not None:
+ d = [int(v) for v in str(args.trace_timing).split(",")]
+ if len(d) not in (1, 4):
+ sys.exit("error: --trace-timing takes one value or d0,d1,d2,d3")
+ d = d * 4 if len(d) == 1 else d
+ timing = (" Project.SetTraceTiming "
+ f"({d[0]}, {d[1]}, {d[2]}, {d[3]});\n")
+ elif cfg["timing"]:
+ timing = f" Project.SetTraceTiming ({cfg['timing']});\n"
+ core_clock = args.core_clock or cfg["core_clock"]
+ clk_line = f" Edit.SysVar (VAR_TRACE_CORE_CLOCK, {core_clock});\n" if core_clock else ""
+ ts_line = (" Edit.SysVar (VAR_TRACE_TIMESTAMPS_ENABLED, 0);\n"
+ if args.no_timestamps else "")
+ user_funcs = ""
+ if cfg.get("connect_hook"):
+ user_funcs += "\n" + cfg["connect_hook"]
+ if args.trace_only:
+ user_funcs += TRACEPOINT_FUNC % trace_only_points(
+ os.path.abspath(args.elf), args.trace_only)
+ if args.profile_lines_csv:
+ user_funcs += LINES_CSV_FUNC % os.path.join(outdir, "profile_lines.csv")
+ if args.profile_insts_csv:
+ user_funcs += INSTS_CSV_FUNC % os.path.join(outdir, "profile_insts.csv")
+ if args.os_plugin and not glob.glob(
+ f"/opt/SEGGER/Ozone*/Plugins/OS/{args.os_plugin}.js"):
+ sys.exit(f"error: RTOS plugin '{args.os_plugin}' not in "
+ f"/opt/SEGGER/Ozone*/Plugins/OS (e.g. FreeRTOSPlugin_CM7)")
+ os_plugin = (f' Project.SetOSPlugin ("{args.os_plugin}");\n'
+ if args.os_plugin else "")
+ if args.attach:
+ # attach to the running target: no download, no reset - trace a window
+ # mid-run (firmware must match the ELF and have trace pins enabled)
+ os_plugin += " Debug.SetConnectMode (CM_ATTACH_HALT);\n"
+ # HSS sampling rate belongs in OnProjectLoad (persistent) per UM08025 4.6.2;
+ # setting it mid-session over the socket is rejected.
+ hss = (f" Edit.SysVar (VAR_HSS_SPEED, {args.sample_hz});\n"
+ if args.sample else "")
+ power = (" Edit.SysVar (VAR_TARGET_POWER_ON, 1);\n"
+ f" Edit.SysVar (VAR_POWER_SAMPLING_SPEED, {args.power_hz});\n"
+ if args.power else "")
+ jlink_script = args.jlink_script or cfg.get("jlink_script")
+ jls_hook = (JLINK_SCRIPT_HOOK % os.path.abspath(jlink_script)
+ if jlink_script else "")
+ proj = os.path.join(outdir, "etm_capture.jdebug")
+ if args.trace_width:
+ cfg["port_width"] = args.trace_width
+ with open(proj, "w") as f:
+ f.write(PROJECT_TEMPLATE.substitute(
+ device=cfg["device"], probe=resolve_probe(args.probe),
+ tif_speed=cfg["tif_speed"],
+ port_width=cfg["port_width"], timing_line=timing, core_clock_line=clk_line,
+ timestamps_line=ts_line, max_inst=args.max_inst, outdir=outdir,
+ elf=os.path.abspath(args.elf), user_funcs=user_funcs,
+ os_plugin_line=os_plugin, hss_line=hss, power_lines=power,
+ jlink_script_hook=jls_hook,
+ reset_hook=cfg.get("reset_hook", DEFAULT_HOOK % "AfterTargetReset"),
+ download_hook=cfg.get("download_hook",
+ DEFAULT_HOOK % "AfterTargetDownload")))
+ return proj
+
+
+class OzoneSession:
+ """Drive Ozone via its automation TCP socket (one connection at a time)."""
+
+ def __init__(self, port, logf):
+ self.port = port
+ self.logf = logf
+ self.sock = None
+
+ def log(self, msg):
+ line = f"[{time.strftime('%H:%M:%S')}] {msg}"
+ print(line, flush=True)
+ self.logf.write(line + "\n")
+ self.logf.flush()
+
+ def connect(self, timeout_s):
+ deadline = time.time() + timeout_s
+ while time.time() < deadline:
+ try:
+ self.sock = socket.create_connection(("127.0.0.1", self.port), timeout=5)
+ self.sock.settimeout(0.5)
+ self.log(f"connected to Ozone automation socket :{self.port}")
+ return
+ except OSError:
+ time.sleep(1)
+ raise TimeoutError(f"Ozone automation socket :{self.port} not reachable "
+ f"after {timeout_s}s (see ozone_gui.log)")
+
+ def drain(self, wait_s=1.0):
+ buf = b""
+ end = time.time() + wait_s
+ while time.time() < end:
+ try:
+ chunk = self.sock.recv(65536)
+ if not chunk:
+ break
+ buf += chunk
+ end = time.time() + 0.5 # keep reading while data flows
+ except socket.timeout:
+ pass
+ text = buf.decode(errors="replace")
+ for ln in text.splitlines():
+ self.log(f" ozone> {ln}")
+ return text
+
+ def send(self, cmd, wait_s=1.0):
+ self.log(f"cmd: {cmd}")
+ self.sock.sendall((cmd + "\n").encode())
+ return self.drain(wait_s)
+
+ def wait_echo(self, cmd, timeout_s):
+ """Send cmd; wait until Ozone echoes its execution (echo comes after the
+ command completed, e.g. a large Export). Returns all received text."""
+ name = cmd.split("(")[0].strip()
+ text = self.send(cmd, 1.0)
+ deadline = time.time() + timeout_s
+ while name + " (" not in text and name + "(" not in text:
+ if time.time() > deadline:
+ raise TimeoutError(f"no echo for '{name}' after {timeout_s}s")
+ text += self.drain(1.0)
+ return text
+
+ def is_halted(self, timeout_s):
+ """Poll Debug.IsHalted until it returns 1; parse the '// returns 0xN' echo."""
+ deadline = time.time() + timeout_s
+ while time.time() < deadline:
+ text = self.send("Debug.IsHalted", 1.0)
+ end2 = time.time() + 4.0
+ while "Debug.IsHalted" not in text and time.time() < end2:
+ text += self.drain(1.0)
+ m = re.findall(r"Debug\.IsHalted\s*\(\);\s*//\s*returns\s*0x(\d+)", text)
+ if m and m[-1] == "1":
+ return True
+ time.sleep(1)
+ return False
+
+
+def main():
+ p = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ p.add_argument("--board", help="TinyUSB board name, e.g. stm32h743eval")
+ p.add_argument("--device", help="J-Link device name for non-TinyUSB targets "
+ "(e.g. STM32F407VE for a SEGGER trace reference board); "
+ "requires --elf, bypasses hw/bsp board resolution")
+ p.add_argument("--tif-speed", default="4 MHz",
+ help="SWD speed for --device targets (default '4 MHz')")
+ p.add_argument("--jlink-script",
+ help="J-Link script file (.pex/.JLinkScript) for trace-pin "
+ "init when the firmware doesn't do it (SEGGER per-MCU "
+ "examples); wired into BeforeTargetConnect")
+ p.add_argument("--power", action="store_true",
+ help="power the target from the probe and record a power "
+ "profile (power.csv); probe power is switched off after "
+ "the session. Target must be wired for probe power!")
+ p.add_argument("--power-hz", type=int, default=10000,
+ help="power sampling frequency in Hz (default 10000)")
+ p.add_argument("--elf", help="firmware ELF built with -DTRACE_ETM=1 "
+ "(default: examples/cmake-build-<board>/device/cdc_msc/cdc_msc.elf)")
+ p.add_argument("--duration-ms", type=int, default=10000, help="traced run time")
+ p.add_argument("--out", help="output dir (default: mkdtemp under /tmp)")
+ p.add_argument("--port", type=int, default=19201,
+ help="automation socket port (19200 = interactive Ozone default; keep 19201)")
+ p.add_argument("--probe", default="",
+ help="J-Link USB nickname or serial ('' = sole connected probe)")
+ p.add_argument("--trace-csv", action="store_true",
+ help="also export raw instruction history (itrace.csv, can be >100 MB)")
+ p.add_argument("--max-inst", type=int, default=10000000,
+ help="VAR_TRACE_MAX_INST_CNT: instruction-trace window/export depth")
+ p.add_argument("--core-clock", type=int,
+ help="CPU Hz for timestamp conversion (default: board reference value)")
+ p.add_argument("--trace-timing",
+ help="trace sample delay in ps (-5000..5000, overrides the "
+ "board reference; sweep this when the stream dies with "
+ "unknown-packet errors). One value for all 4 pins, or "
+ "'d0,d1,d2,d3' to de-skew individual lines (boards can "
+ "have per-line RC delays, e.g. strap pulls on muxed pads)")
+ p.add_argument("--trace-width", type=int, choices=(1, 2, 4),
+ help="trace port width override (fewer pins = tolerant of a "
+ "single bad line, at reduced bandwidth)")
+ p.add_argument("--no-timestamps", action="store_true",
+ help="disable trace timestamps (less trace bandwidth -> fewer "
+ "overflows/decode errors; itrace.csv loses its time column)")
+ p.add_argument("--trace-only",
+ help="comma-separated symbols: trace ONLY these functions via "
+ "hardware tracepoints (e.g. an ISR + SysTick_Handler for "
+ "calibration); needs few symbols (scarce comparators)")
+ p.add_argument("--profile-lines-csv", action="store_true",
+ help="also export per-source-line profile/coverage counters "
+ "(profile_lines.csv)")
+ p.add_argument("--profile-insts-csv", action="store_true",
+ help="also export per-instruction counters (profile_insts.csv, "
+ "enables branch-bias analysis in etm_profile.py)")
+ p.add_argument("--attach", action="store_true",
+ help="attach to the RUNNING target instead of flash+reset: "
+ "capture a window mid-run (narrowing debug). The flashed "
+ "firmware must match --elf and have been built with "
+ "TRACE_ETM=1")
+ p.add_argument("--sample",
+ help="comma-separated C expressions to sample periodically "
+ "during the run (samples.csv, e.g. 'system_ticks')")
+ p.add_argument("--sample-hz", type=int, default=1000,
+ help="data sampling frequency in Hz (default 1000)")
+ p.add_argument("--os-plugin",
+ help="Ozone RTOS-awareness plugin for task/ISR-attributed "
+ "timeline, e.g. FreeRTOSPlugin_CM7 (see "
+ "/opt/SEGGER/Ozone*/Plugins/OS)")
+ args = p.parse_args()
+
+ if not args.board and not args.device:
+ sys.exit("error: need --board (TinyUSB) or --device + --elf (other targets)")
+ if args.device and not args.elf:
+ sys.exit("error: --device requires --elf")
+ if args.max_inst > 10000000:
+ # >10M yielded a silently EMPTY Export.Trace on Ozone V3.50
+ print("warning: --max-inst clamped to 10000000 (larger values produce "
+ "an empty instruction-trace export)", file=sys.stderr)
+ args.max_inst = 10000000
+ if not args.elf:
+ args.elf = (f"{REPO_ROOT}/examples/cmake-build-{args.board}"
+ f"/device/cdc_msc/cdc_msc.elf")
+ if not os.path.isfile(args.elf):
+ sys.exit(f"error: ELF not found: {args.elf}\n"
+ f"build it with -DTRACE_ETM=1 (see the etm-trace skill) or pass --elf")
+
+ if args.trace_only:
+ print("warning: --trace-only is EXPERIMENTAL - on some targets windows "
+ "for low-rate handlers fail to record, and timestamps are invalid "
+ "across trace gaps (instruction counts remain exact). For ISR "
+ "timing prefer a full --trace-csv capture + etm_profile.py --isr.",
+ file=sys.stderr)
+ if args.device:
+ cfg = {"device": args.device, "tif_speed": args.tif_speed, "timing": None,
+ "port_width": 4, "core_clock": None, "ref": "--device"}
+ else:
+ cfg = resolve_board(args.board)
+ outdir = os.path.abspath(args.out) if args.out else tempfile.mkdtemp(
+ prefix=f"etm-{args.board or args.device}-")
+ os.makedirs(outdir, exist_ok=True)
+ proj = gen_project(cfg, args, outdir)
+
+ ozone_bin = shutil.which("ozone") or shutil.which("Ozone")
+ if not ozone_bin:
+ sys.exit("error: ozone not on PATH (install SEGGER Ozone)")
+ cmd = [ozone_bin, "-project", proj, "-port", str(args.port)]
+ if shutil.which("xvfb-run"):
+ cmd = ["xvfb-run", "-a"] + cmd
+ elif os.environ.get("DISPLAY"):
+ print("warning: xvfb-run not found - Ozone window will appear on "
+ f"DISPLAY={os.environ['DISPLAY']} and may steal keyboard focus",
+ file=sys.stderr)
+ else:
+ sys.exit("error: no DISPLAY and no xvfb-run; install xvfb")
+
+ gui_log = open(os.path.join(outdir, "ozone_gui.log"), "w")
+ ses_logf = open(os.path.join(outdir, "session.log"), "w")
+ ses = OzoneSession(args.port, ses_logf)
+ ses.log(f"board={args.board} device={cfg['device']} ref={cfg['ref']}")
+ ses.log(f"elf={args.elf}")
+ ses.log(f"out={outdir}")
+ proc = subprocess.Popen(cmd, stdout=gui_log, stderr=gui_log,
+ start_new_session=True)
+ profile_out = os.path.join(outdir, "code_profile.txt")
+ itrace_out = os.path.join(outdir, "itrace.csv")
+ try:
+ ses.connect(20)
+ ses.drain(3) # version banner
+ ses.send("Debug.Start", 5)
+ if not ses.is_halted(90):
+ raise TimeoutError("Debug.Start did not reach the startup completion "
+ "point (connect/flash failed? see jlink.log)")
+ ses.log("startup complete (halted at main)")
+ if args.trace_only:
+ ses.wait_echo('Script.Exec ("SetupTracepoints")', 15)
+ ses.send('Window.Show ("Code Profile")', 2)
+ if args.trace_csv:
+ ses.send('Window.Show ("Instruction Trace")', 2)
+ if args.sample:
+ ses.send('Window.Show ("Data Sampling")', 2)
+ for expr in args.sample.split(","):
+ ses.send(f'Window.Add ("Data Sampling", "{expr.strip()}")', 2)
+ ses.send("Coverage.ExcludeNOPs()", 2)
+
+ ses.log(f"=== traced run: {args.duration_ms} ms ===")
+ ses.send("Debug.Continue", 1)
+ time.sleep(args.duration_ms / 1000.0)
+ ses.send("Debug.Halt", 3)
+ if not ses.is_halted(30):
+ raise TimeoutError("target did not halt")
+ ses.send("Window.WaitForUpdateComplete(120000)", 5)
+
+ ses.wait_echo(f'Export.CodeProfile ("{profile_out}", 0, "")', 60)
+ if args.profile_lines_csv:
+ ses.wait_echo('Script.Exec ("ExportLinesCsv")', 60)
+ if args.profile_insts_csv:
+ ses.wait_echo('Script.Exec ("ExportInstsCsv")', 120)
+ if args.trace_csv:
+ ses.wait_echo(f'Export.Trace ("{itrace_out}", 0)', 300)
+ if args.sample:
+ ses.wait_echo(f'Export.DataGraphs ("{outdir}/samples.csv")', 60)
+ if args.power:
+ ses.wait_echo(f'Export.PowerGraphs ("{outdir}/power.csv")', 60)
+ ses.send("Debug.Stop", 5)
+ ses.send("File.Exit", 2)
+ finally:
+ for _ in range(15):
+ if proc.poll() is not None:
+ break
+ time.sleep(1)
+ if proc.poll() is None:
+ ses.log("killing leftover Ozone process group")
+ os.killpg(proc.pid, signal.SIGKILL)
+ if args.power:
+ # guarantee probe power is off, whatever happened above
+ sel = ["-USB", args.probe] if args.probe else []
+ r = subprocess.run(["JLinkExe", *sel, "-nogui", "1"],
+ input="power off\nqc\n", capture_output=True,
+ text=True, timeout=30)
+ ses.log("probe power off " +
+ ("issued" if r.returncode == 0 else f"FAILED rc={r.returncode}"))
+
+ if not (os.path.isfile(profile_out) and os.path.getsize(profile_out) > 0
+ and "Code Profile Report" in open(profile_out, errors="replace").read(200)):
+ sys.exit(f"error: capture ran but {profile_out} is missing/empty - "
+ f"check {outdir}/session.log and ozone_console.log")
+ if args.trace_csv and not (os.path.isfile(itrace_out)
+ and os.path.getsize(itrace_out) > 0):
+ sys.exit(f"error: --trace-csv requested but {itrace_out} is missing/empty")
+ if args.power and not os.path.getsize(os.path.join(outdir, "power.csv")):
+ sys.exit("error: --power requested but power.csv is missing/empty")
+ if "Trace collection stopped!" in open(os.path.join(outdir, "session.log"),
+ errors="replace").read():
+ sys.exit("error: trace stream died mid-run (unknown trace data packet) - "
+ "the profile only covers up to that point. Retry with "
+ "--no-timestamps, or reduce the core clock (see SKILL.md).")
+ prof = open(profile_out, errors="replace").read()
+ m = re.search(r"^\s*Total\s*\|[\d ]*\|\s*([\d ]+)$", prof, re.M)
+ if not m or int(m.group(1).replace(" ", "") or 0) == 0:
+ sys.exit("error: session completed but NO trace data was collected "
+ "(profile totals are zero) - trace signal not reaching the "
+ "probe: check wiring/connector, trace pinmux, sample timing.")
+ print(f"\ncapture OK: {outdir}")
+ print(f" code_profile.txt ({os.path.getsize(profile_out)} bytes)")
+ if args.trace_csv:
+ print(f" itrace.csv ({os.path.getsize(itrace_out)} bytes)")
+ print(f"analyze: python3 {os.path.dirname(os.path.abspath(__file__))}"
+ f"/etm_profile.py {outdir} --elf {args.elf}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.claude/skills/etm-trace/scripts/etm_profile.py b/.claude/skills/etm-trace/scripts/etm_profile.py
new file mode 100644
index 000000000..8568bc608
--- /dev/null
+++ b/.claude/skills/etm-trace/scripts/etm_profile.py
@@ -0,0 +1,467 @@
+#!/usr/bin/env python3
+"""Analyze an etm_capture.py output dir: hot functions, coverage, itrace digest,
+ISR episode timing, and optimization hints.
+
+Input: code_profile.txt (Ozone Export.CodeProfile text report), and optionally
+ itrace.csv (Export.Trace raw instruction history) with --elf for
+ address->function mapping (arm-none-eabi-nm).
+Output: markdown report on stdout.
+
+--isr SYM[,SYM..] episode timing for an interrupt handler: first symbol is the
+ entry anchor (its first instruction marks each ISR entry), all symbols
+ form the body address set. Example: --isr OTG_HS_IRQHandler,dcd_int_handler
+ Durations are calibrated against SysTick_Handler beats (1 ms apart), so
+ the itrace must be captured with timestamps enabled.
+"""
+
+import argparse
+import bisect
+import csv
+import os
+import re
+import statistics
+import subprocess
+import sys
+
+
+def parse_profile(path):
+ """Parse the two report sections. Function rows are indented 2 spaces under
+ a non-indented module row; columns are '|'-separated."""
+ lines = open(path, errors="replace").read().splitlines()
+ try:
+ cov_start = lines.index("Code Coverage Summary")
+ prof_start = lines.index("Code Profile Summary")
+ except ValueError:
+ sys.exit(f"error: {path} is not an Ozone code-profile text report")
+
+ def rows(section):
+ module = None
+ for ln in section:
+ if "|" not in ln:
+ continue
+ cells = ln.split("|")
+ name = cells[0].rstrip()
+ if (not name or name.startswith("Module/Function")
+ or set(name.strip()) <= {"-", "+"}):
+ continue
+ if not name.startswith(" "):
+ module = name.strip()
+ continue
+ yield module, name.strip(), cells[1:]
+
+ def num(s):
+ s = s.strip().replace(" ", "")
+ return int(s) if s else 0
+
+ cov_pat = re.compile(r"^\s*([\d ]+)/\s*([\d ]+)\s+([\d.]+)%")
+ funcs, totals = {}, {}
+ for module, name, cells in rows(lines[prof_start:]):
+ run, fetch = (num(cells[0]) if cells else 0), (num(cells[1]) if len(cells) > 1 else 0)
+ if name == "Total":
+ totals["run"], totals["fetch"] = run, fetch
+ elif name == "[Unaccounted]":
+ totals["unaccounted"] = fetch
+ elif name in funcs and funcs[name]["module"] != module:
+ # same-named static from another module: keep both rows distinct
+ funcs[f"{name} [{module}]"] = {"module": module, "run": run,
+ "fetch": fetch}
+ else:
+ funcs[name] = {"module": module, "run": run, "fetch": fetch}
+ for module, name, cells in rows(lines[cov_start:prof_start]):
+ m_src = cov_pat.match(cells[0]) if cells else None
+ m_inst = cov_pat.match(cells[1]) if len(cells) > 1 else None
+ if name == "Total" and m_inst and m_src:
+ totals["src_cov"] = num(m_src.group(1)), num(m_src.group(2))
+ totals["inst_cov"] = num(m_inst.group(1)), num(m_inst.group(2))
+ elif m_inst:
+ # match the de-collided key when a same-named static from another
+ # module was renamed during the profile pass
+ key = name if (name in funcs and funcs[name]["module"] == module) \
+ else f"{name} [{module}]"
+ if key in funcs:
+ funcs[key]["inst_pct"] = float(m_inst.group(3))
+ return funcs, totals
+
+
+def load_symbols(elf):
+ """Sorted (addr, size, name) from nm; for mapping itrace addresses."""
+ nm = "arm-none-eabi-nm"
+ out = subprocess.run([nm, "-S", "--defined-only", "-C", elf],
+ capture_output=True, text=True)
+ if out.returncode != 0:
+ sys.exit(f"error: {nm} failed on {elf}: {out.stderr.strip()}")
+ syms = []
+ for ln in out.stdout.splitlines():
+ parts = ln.split(maxsplit=3)
+ if len(parts) == 4 and parts[2].lower() in ("t", "w"):
+ syms.append((int(parts[0], 16) & ~1, int(parts[1], 16), parts[3]))
+ elif len(parts) == 3 and parts[1].lower() in ("t", "w"):
+ # sizeless symbol (e.g. weak asm stub): assume a 2-byte body
+ syms.append((int(parts[0], 16) & ~1, 2, parts[2]))
+ return sorted(syms)
+
+
+def addr_to_func(syms, addr):
+ i = bisect.bisect_right(syms, (addr, 1 << 62, "")) - 1
+ if i >= 0 and syms[i][0] <= addr < syms[i][0] + syms[i][1]:
+ return syms[i][2]
+ return None
+
+
+def sym_ranges(syms, names):
+ """(lo, hi) address ranges for the named symbols (base name match);
+ None if any symbol is missing (caller degrades gracefully)."""
+ out = []
+ for want in names:
+ for a, sz, n in syms:
+ if n == want or n.split("(")[0] == want:
+ out.append((a, a + sz))
+ break
+ else:
+ print(f"note: symbol '{want}' not found in ELF")
+ return None
+ return out
+
+
+def iter_itrace(path):
+ """Yield (t_raw, addr) chronologically-reversed (file order: newest first).
+ Also returns the timestamp unit from the header via generator .send? No -
+ caller reads unit separately with itrace_unit()."""
+ with open(path, newline="", errors="replace") as f:
+ rd = csv.reader(f)
+ hdr = next(rd, None) or []
+ # --no-timestamps captures drop the Timestamp column entirely: locate
+ # the Address column from the header and yield t=None for such rows
+ # (consumers count instructions but skip time math)
+ has_ts = any("Timestamp" in c for c in hdr)
+ try:
+ addr_i = next(i for i, c in enumerate(hdr) if "Address" in c)
+ except StopIteration:
+ addr_i = 1 if has_ts else 0
+ for row in rd:
+ if not row or len(row) <= addr_i:
+ continue
+ try:
+ addr = int(row[addr_i], 16)
+ except ValueError:
+ continue
+ if has_ts and row[0] != "PC":
+ try:
+ yield float(row[0]), addr
+ continue
+ except ValueError:
+ pass
+ yield None, addr
+
+
+def itrace_unit(path):
+ hdr = open(path, errors="replace").readline()
+ m = re.search(r"Timestamp\[([^\]]+)\]", hdr)
+ return m.group(1) if m else "?"
+
+
+def time_by_func(path, syms):
+ """Per-function raw-time and instruction attribution from the itrace.
+ Rows are newest-first; the gap to the next (older) row is attributed to the
+ older instruction's function. Outlier gaps (trace-block boundaries) are
+ capped so one discontinuity cannot skew a function. Shares are scale-free."""
+ sample = []
+ t_prev = None
+ for t, _ in iter_itrace(path):
+ if t is None:
+ continue
+ if t_prev is not None and t_prev - t > 0:
+ sample.append(t_prev - t)
+ if len(sample) >= 200000:
+ break
+ t_prev = t
+ cap = 10000 * statistics.median(sample) if sample else float("inf")
+ t_prev = None
+ tf, cf = {}, {}
+ n = 0
+ for t, a in iter_itrace(path):
+ n += 1
+ fn = addr_to_func(syms, a)
+ if fn:
+ cf[fn] = cf.get(fn, 0) + 1
+ if t is None:
+ continue
+ if t_prev is not None:
+ d = t_prev - t
+ if 0 < d < cap and fn:
+ tf[fn] = tf.get(fn, 0) + d
+ t_prev = t
+ return tf, cf, n
+
+
+def isr_report(itrace, elf, isr_arg, top):
+ syms = load_symbols(elf)
+ names = [s.strip() for s in isr_arg.split(",") if s.strip()]
+ body = sym_ranges(syms, names)
+ tick = sym_ranges(syms, ["SysTick_Handler"])
+ if not body or not tick:
+ print("\n## ISR timing: skipped (missing symbols above - needs the ISR "
+ "symbol(s) and SysTick_Handler for calibration)")
+ return
+ entry = body[0][0]
+ unit = itrace_unit(itrace)
+
+ usb_rows, tick_rows, tmin, tmax = [], [], None, None
+ for t, a in iter_itrace(itrace):
+ if t is None:
+ continue # --no-timestamps capture: the <20-rows message below applies
+ tmin = t if tmin is None else min(tmin, t)
+ tmax = t if tmax is None else max(tmax, t)
+ if any(lo <= a < hi for lo, hi in body):
+ usb_rows.append((t, a))
+ # independent, not elif: when the ISR under test IS SysTick_Handler,
+ # its rows must still feed the calibration
+ if any(lo <= a < hi for lo, hi in tick):
+ tick_rows.append((t, a))
+ usb_rows.reverse()
+ tick_rows.reverse()
+ if len(tick_rows) < 20:
+ print(f"\n## ISR timing: not enough SysTick beats for calibration "
+ f"({len(tick_rows)} rows) - capture with timestamps enabled")
+ return
+
+ # rough raw-units-per-1ms from the large mode of consecutive tick deltas;
+ # threshold from the median, not the max - one trace-overflow gap would
+ # otherwise inflate the cut and leave only outliers in the sample
+ deltas = [b[0] - a[0] for a, b in zip(tick_rows, tick_rows[1:])]
+ big = [d for d in deltas if d > 10 * statistics.median(deltas)]
+ raw_ms = statistics.median(big) if big else statistics.median(deltas)
+ gap = 0.03 * raw_ms # 30 us in raw units
+ edge = 0.05 * raw_ms
+
+ def episodes(rows, g):
+ out, cur = [], []
+ for t, a in rows:
+ if cur and t - cur[-1][0] > g:
+ out.append(cur)
+ cur = []
+ cur.append((t, a))
+ if cur:
+ out.append(cur)
+ return out
+
+ starts = [e[0][0] for e in episodes(tick_rows, 0.1 * raw_ms)]
+
+ def local_scale(t):
+ i = bisect.bisect_left(starts, t)
+ if 0 < i < len(starts):
+ sp = starts[i] - starts[i - 1]
+ if 0 < sp < 3 * raw_ms:
+ return 1e-3 / sp
+ return 1e-3 / raw_ms
+
+ eps = []
+ for cluster in episodes(usb_rows, gap):
+ cur = None
+ for t, a in cluster:
+ if a == entry:
+ if cur:
+ eps.append(cur)
+ cur = [(t, a)]
+ elif cur:
+ cur.append((t, a))
+ if cur:
+ eps.append(cur)
+ eps = [e for e in eps if e[0][0] - tmin > edge and tmax - e[-1][0] > edge
+ and len(e) >= 10]
+ print(f"\n## ISR timing: {names[0]} (+{len(names) - 1} body syms), "
+ f"unit '{unit}', {len(starts)} SysTick beats")
+ if not eps:
+ print("- no complete episodes in window (wrong symbols? window missed "
+ "the traffic phase?)")
+ return
+ stats = sorted(((e[-1][0] - e[0][0]) * local_scale(e[0][0]), len(e), e[0][0])
+ for e in eps)
+ cal = [s[0] for s in stats]
+ print(f"- episodes: {len(cal)} | fastest {min(cal) * 1e6:.2f} us, "
+ f"median {statistics.median(cal) * 1e6:.2f} us, "
+ f"avg {statistics.mean(cal) * 1e6:.2f} us, "
+ f"worst {max(cal) * 1e6:.2f} us")
+ insts = [s[1] for s in stats]
+ print(f"- instructions/episode: min {min(insts)}, avg "
+ f"{statistics.mean(insts):.0f}, max {max(insts)}")
+ print("- worst episodes (duration, instructions, raw start):")
+ for c, n, t0 in stats[-5:][::-1]:
+ print(f" - {c * 1e6:8.2f} us {n:5d} instr t={t0:.6f}")
+ print("- caveats: timestamps are interpolated between packets (sub-us "
+ "values are approximate); episodes may merge if trace overflow "
+ "dropped an entry")
+
+
+def short(name):
+ return re.sub(r"\(.*\)$", "()", name)
+
+
+def branch_bias(insts_csv, funcs, top):
+ """One-sided conditional branches in executed functions (profile_insts.csv):
+ a conditional fetched N times but taken/executed 0 or N times = a branch
+ that never varied -> hot always-true assert, dead path, or an invariant
+ that could hoist out of a loop (UM08025 SS5.19)."""
+ def n(v):
+ v = (v or "").replace(" ", "")
+ return int(v) if v.lstrip("-").isdigit() else 0
+ biased = []
+ with open(insts_csv, newline="", errors="replace") as f:
+ for r in csv.DictReader(f):
+ if (r.get("Is Conditional") or "0").strip() != "1":
+ continue
+ fetched = n(r.get("Times Fetched"))
+ executed = n(r.get("Times Executed"))
+ if fetched < 1000: # only hot conditionals matter
+ continue
+ if executed == 0 or executed == fetched:
+ biased.append((fetched, r.get("Function", "?"),
+ r.get("Address", ""), r.get("AsmCode", "").strip(),
+ "always-taken" if executed == fetched else "never-taken"))
+ if not biased:
+ return
+ biased.sort(reverse=True)
+ print(f"\n## One-sided hot branches (profile_insts.csv)\n")
+ print("Conditionals that never varied - candidates to hoist/remove "
+ "(UM08025 §5.19):")
+ for fetched, fn, addr, asm, kind in biased[:top]:
+ print(f"- `{short(fn)}` @{addr} {kind} ({fetched:,}x): `{asm[:50]}`")
+
+
+def suggestions(funcs, totals, tshare, cshare, exclude):
+ """Rule-based optimization hints (after UM08025 §5.19)."""
+ print("\n## Optimization hints")
+ total_fetch = totals.get("fetch") or 1
+ ex = [n for n in funcs if exclude and re.search(exclude, n)]
+ ex_fetch = sum(funcs[n]["fetch"] for n in ex)
+ if ex:
+ print(f"- excluded from load ranking (--exclude): {len(ex)} functions, "
+ f"{100.0 * ex_fetch / total_fetch:.1f}% of fetches")
+ rest = {n: v for n, v in funcs.items() if n not in ex}
+ rt = sum(v["fetch"] for v in rest.values()) or 1
+ hot = sorted(rest.items(), key=lambda kv: -kv[1]["fetch"])[:5]
+ print("- top load after exclusion: " + ", ".join(
+ f"`{short(n)}` {100.0 * v['fetch'] / rt:.1f}%" for n, v in hot))
+ polls = [(n, v) for n, v in rest.items()
+ if v["fetch"] / total_fetch > 0.02 and v["run"]
+ and v["fetch"] / v["run"] < 40]
+ if polls:
+ print("- busy-poll candidates (>2% load, <40 instr/entry - called in a "
+ "tight loop; consider event-driven or rate-limiting):")
+ for n, v in sorted(polls, key=lambda kv: -kv[1]["fetch"]):
+ print(f" - `{short(n)}`: {v['run']:,} calls, "
+ f"{v['fetch'] / v['run']:.0f} instr/call, "
+ f"{100.0 * v['fetch'] / total_fetch:.1f}% load")
+ if tshare:
+ stalls = []
+ for n, ts in tshare.items():
+ cs = cshare.get(n, 0)
+ if ts > 0.01 and cs and ts / cs > 4:
+ stalls.append((n, ts, ts / cs))
+ if stalls:
+ print("- stall/wait-dominated (time share >> instruction share - "
+ "waiting on hardware, consider DMA/IRQ instead of polling):")
+ for n, ts, ratio in sorted(stalls, key=lambda x: -x[1])[:5]:
+ print(f" - `{short(n)}`: {100 * ts:.1f}% of time, "
+ f"{ratio:.0f}x its instruction share")
+
+
+def main():
+ p = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ p.add_argument("capture_dir", help="etm_capture.py output dir")
+ p.add_argument("--top", type=int, default=10, help="table size")
+ p.add_argument("--elf", help="firmware ELF, enables itrace analyses")
+ p.add_argument("--isr", help="entry[,body..] symbols for ISR episode timing")
+ p.add_argument("--exclude", help="regex of functions to exclude from the "
+ "optimization load ranking (e.g. the idle poll loop)")
+ args = p.parse_args()
+
+ profile = os.path.join(args.capture_dir, "code_profile.txt")
+ itrace = os.path.join(args.capture_dir, "itrace.csv")
+ funcs, totals = parse_profile(profile)
+ total_fetch = totals.get("fetch") or 1
+ hot = sorted(funcs.items(), key=lambda kv: kv[1]["fetch"], reverse=True)[:args.top]
+
+ print(f"# ETM profile: {args.capture_dir}\n")
+ print(f"## Top {args.top} hottest functions (instruction-fetch share)\n")
+ print("| # | Function | Module | Run Count | Fetch Count | Load % |")
+ print("|---|----------|--------|-----------|-------------|--------|")
+ for i, (name, v) in enumerate(hot, 1):
+ print(f"| {i} | `{short(name)}` | {v['module']} | {v['run']:,} "
+ f"| {v['fetch']:,} | {100.0 * v['fetch'] / total_fetch:.2f}% |")
+ print(f"\nTotal fetches {totals.get('fetch', 0):,} "
+ f"(runs {totals.get('run', 0):,}, unaccounted {totals.get('unaccounted', 0):,})\n")
+
+ ic, sc = totals.get("inst_cov"), totals.get("src_cov")
+ print("## Coverage (NOPs excluded)\n")
+ if ic:
+ print(f"- instructions fully executed: {ic[0]:,} / {ic[1]:,} "
+ f"({100.0 * ic[0] / ic[1]:.1f}%)")
+ if sc:
+ print(f"- source lines fully covered: {sc[0]:,} / {sc[1]:,} "
+ f"({100.0 * sc[0] / sc[1]:.1f}%)")
+ partial = [n for n, v in funcs.items()
+ if v["fetch"] > 0 and 0 < v.get("inst_pct", 100) < 100]
+ print(f"- executed but only partially covered: {len(partial)} functions")
+ dead = sorted(n for n, v in funcs.items()
+ if v["fetch"] == 0 and "(always inlined)" not in n)
+ print(f"- never-executed out-of-line functions: {len(dead)}")
+ by_mod = {}
+ for n in dead:
+ by_mod.setdefault(funcs[n]["module"], []).append(short(n))
+ for mod in sorted(by_mod, key=str):
+ print(f" - {mod}: {', '.join('`%s`' % f for f in by_mod[mod])}")
+
+ tshare, cshare = {}, {}
+ if os.path.isfile(itrace) and args.elf:
+ syms = load_symbols(args.elf)
+ tf, cf, n = time_by_func(itrace, syms)
+ tt, tc = sum(tf.values()) or 1, sum(cf.values()) or 1
+ tshare = {k: v / tt for k, v in tf.items()}
+ cshare = {k: v / tc for k, v in cf.items()}
+ unit = itrace_unit(itrace)
+ print(f"\n## Instruction history (itrace.csv, unit '{unit}')\n")
+ if not tf:
+ print(f"- {n:,} instructions, NO timestamps (--no-timestamps "
+ f"capture): time shares unavailable, top {args.top} by "
+ f"instruction share:")
+ for name, cs in sorted(cshare.items(), key=lambda kv: -kv[1])[:args.top]:
+ print(f" - `{short(name)}`: {100 * cs:.1f}% instructions")
+ else:
+ print(f"- {n:,} instructions; top {args.top} by TIME share "
+ f"(vs instruction share):")
+ for name, ts in sorted(tshare.items(), key=lambda kv: -kv[1])[:args.top]:
+ print(f" - `{short(name)}`: {100 * ts:.1f}% time, "
+ f"{100 * cshare.get(name, 0):.1f}% instructions")
+
+ lines_csv = os.path.join(args.capture_dir, "profile_lines.csv")
+ if os.path.isfile(lines_csv):
+ def n(v): # Ozone groups thousands with spaces
+ v = (v or "").replace(" ", "")
+ return int(v) if v.isdigit() else 0
+ with open(lines_csv, newline="", errors="replace") as f:
+ rows = [r for r in csv.DictReader(f)
+ if r.get("File") and n(r.get("Instructions Fetched")) > 0]
+ rows.sort(key=lambda r: -n(r["Instructions Fetched"]))
+ print(f"\n## Hottest source lines (profile_lines.csv)\n")
+ for r in rows[:args.top]:
+ src = (r.get("Content") or "").strip()
+ print(f"- {os.path.basename(r['File'])}:{r['Line']} "
+ f"{n(r['Instructions Fetched']):,} fetches `{src[:60]}`")
+
+ insts_csv = os.path.join(args.capture_dir, "profile_insts.csv")
+ if os.path.isfile(insts_csv):
+ branch_bias(insts_csv, funcs, args.top)
+
+ if args.isr:
+ if not (os.path.isfile(itrace) and args.elf):
+ sys.exit("error: --isr needs itrace.csv (--trace-csv capture) and --elf")
+ isr_report(itrace, args.elf, args.isr, args.top)
+
+ suggestions(funcs, totals, tshare, cshare, args.exclude)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.claude/skills/target-debug/SKILL.md b/.claude/skills/target-debug/SKILL.md
index 1dc55440f..5165a76b8 100644
--- a/.claude/skills/target-debug/SKILL.md
+++ b/.claude/skills/target-debug/SKILL.md
@@ -16,6 +16,7 @@ Raspberry Pi). Pick capture channels by which end runs Linux, not by habit:
| `usb-kernel-debug` | why the Linux kernel acted (dmesg / dynamic debug) | Linux on either end: PC host or Linux gadget peer |
| **`target-debug`** | **what the target did** (logs, driver state, PC) | always — either role, needs a debug probe |
| `usb-sniffer` | what crossed the wire (PIDs, handshakes, resets) | hardware tap cabled in — role-agnostic |
+| `etm-trace` | exactly which instructions executed (profile, coverage, history) | SEGGER J-Trace wired to this board's trace header — confirm with the user first |
For enumeration/transfer bugs the default posture is **dual-side capture** —
both ends simultaneously: usbmon + a target
diff --git a/docs/superpowers/plans/2026-07-24-etm-trace-agent-integration.md b/docs/superpowers/plans/2026-07-24-etm-trace-agent-integration.md
new file mode 100644
index 000000000..ebcc089fd
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-24-etm-trace-agent-integration.md
@@ -0,0 +1,299 @@
+# etm-trace Skill Tightening + target-debugger Agent Integration Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Fix post-rebase staleness in the etm-trace skill, add its
+hardware-consent gate, and wire it into the target-debugger agent + target-debug
+skill the same way usb-sniffer is wired in (hardware-gated, user-confirmed).
+
+**Architecture:** Three curated instruction files get surgical edits (this repo
+treats skills/agents as curated docs — smallest possible diffs, no bulk
+rewrites). The gate follows the two existing consent patterns: in the *skill*
+(read by interactive sessions) it is "confirm with the user unless they asked";
+in the *agent* (which cannot ask mid-session) it is "only when your prompt
+states it", mirroring the agent's existing lock-force rule.
+
+**Tech Stack:** Markdown instruction files, git, one subagent retrieval test.
+
+## Global Constraints
+
+- Work in the existing worktree `/home/hathach/code/tinyusb/.worktrees/add-etm-trace-skill` on branch `claude/add-etm-trace-skill` (already rebased onto PR 3786 — the base that renamed `usb-target-debug` → `target-debug` and rewrote `.claude/agents/target-debugger.md`). Never switch the primary checkout's branch.
+- Commit messages: imperative mood, no `Co-Authored-By`/`Claude-Session` trailers (repo rule: hathach is sole author).
+- Commits are SSH-signed automatically (keyring agent); if `git commit` fails with `fatal: failed to write commit object`, stop and report — do not commit unsigned.
+- Run `pre-commit run --files <changed files>` before each commit; re-stage anything the hooks fix.
+- Do not touch `.idea/`, `*.jdebug.user`, or `PICO2_TRACE_PCB_HANDOFF.md` (user-local files in the worktree).
+
+---
+
+### Task 1: Tighten etm-trace SKILL.md — stale names + hardware-consent gate
+
+**Files:**
+- Modify: `.claude/skills/etm-trace/SKILL.md` (lines ~12–33: cross-skill table, PC-sampling pointer, Requirements)
+- No test file (instruction doc; verification = grep + subagent test in Task 4)
+
+**Interfaces:**
+- Consumes: nothing from other tasks.
+- Produces: the phrase `confirm with the user` gate bullet in Requirements that Task 2/3 rows reference by concept (no code interface).
+
+- [ ] **Step 1: Verify the stale references exist (the "failing test")**
+
+Run:
+```bash
+cd /home/hathach/code/tinyusb/.worktrees/add-etm-trace-skill
+grep -n "usb-target-debug" .claude/skills/etm-trace/SKILL.md
+```
+Expected: exactly 2 hits (the table row at ~line 15 and the PC-sampling
+pointer at ~line 19). If 0 hits, the file was already fixed — skip Steps 2–3.
+
+- [ ] **Step 2: Apply the edits**
+
+Edit `.claude/skills/etm-trace/SKILL.md`.
+
+Replace this block (current content):
+
+```markdown
+| Skill | Answers |
+|--------------------|----------------------------------------------------------------------------|
+| `usbmon` | what the host actually exchanged (URBs) |
+| `usb-target-debug` | what the device did (logs, driver state, sampled PCs) |
+| `usb-sniffer` | what crossed the wire |
+| **`etm-trace`** | **exactly which instructions executed, when** (profile, coverage, history) |
+
+Use `usb-target-debug`'s DWT PC-sampling for a quick statistical profile; use
+this skill for exact counts, coverage, or instruction-by-instruction history.
+```
+
+with:
+
+```markdown
+| 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.
+```
+
+Then in the `## Requirements` section, replace the first bullet:
+
+```markdown
+- 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.
+```
+
+with:
+
+```markdown
+- 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.
+```
+
+- [ ] **Step 3: Verify the edits**
+
+Run:
+```bash
+grep -c "usb-target-debug" .claude/skills/etm-trace/SKILL.md; grep -c "confirm with the user" .claude/skills/etm-trace/SKILL.md; grep -rn "usb-target-debug" .claude/skills/etm-trace/boards.md
+```
+Expected: `0`, then `1` (or more), then no output from boards.md (it has no
+stale names — do not edit it).
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/hathach/code/tinyusb/.worktrees/add-etm-trace-skill
+pre-commit run --files .claude/skills/etm-trace/SKILL.md
+git add .claude/skills/etm-trace/SKILL.md
+git commit -m "etm-trace: post-rename references, per-board hardware-consent gate
+
+target-debug replaced usb-target-debug in the debug-skill overhaul; update
+the cross-skill table and PC-sampling pointer. Add the consent gate: the
+J-Trace is a single probe moved between boards, so captures on a board the
+user did not just ask about need explicit confirmation that it is wired."
+```
+Expected: commit succeeds; `git log --format="%G?" -1` prints `G`.
+
+---
+
+### Task 2: Add etm-trace to the target-debugger agent's skill table
+
+**Files:**
+- Modify: `.claude/agents/target-debugger.md` (skill table, after the `usb-sniffer` row at ~line 24)
+
+**Interfaces:**
+- Consumes: the etm-trace skill name and its `boards.md` (Task 1 keeps both valid).
+- Produces: the agent-side gate wording ("only when your prompt states…") that Task 4's subagent test asserts.
+
+- [ ] **Step 1: Verify etm-trace is absent (the "failing test")**
+
+Run:
+```bash
+grep -c "etm-trace" .claude/agents/target-debugger.md
+```
+Expected: `0`.
+
+- [ ] **Step 2: Add the table row**
+
+In `.claude/agents/target-debugger.md`, after this row:
+
+```markdown
+| usb-sniffer | wire-level capture (hardware tap): host can't see the bus, usbmon vs target logs disagree, or TinyUSB is the host (no usbmon anywhere) |
+```
+
+insert:
+
+```markdown
+| etm-trace | instruction-level ETM trace via SEGGER J-Trace (exact execution history, profile, coverage) when sampled PCs and logs cannot resolve the mechanism. Requires the J-Trace physically wired to THIS board (supported boards: the skill's boards.md) — use only when your prompt states the board is trace-wired or the user asked for it; otherwise name it in `notes` as the next technique |
+```
+
+(The gate is prompt-based, not ask-based: this agent cannot ask the user
+mid-session — same pattern as the existing lock-force rule.)
+
+- [ ] **Step 3: Verify**
+
+Run:
+```bash
+grep -n "etm-trace" .claude/agents/target-debugger.md | wc -l; grep -n "prompt states the board is trace-wired" .claude/agents/target-debugger.md
+```
+Expected: `1` match count; the gate phrase found once.
+
+- [ ] **Step 4: Commit**
+
+```bash
+pre-commit run --files .claude/agents/target-debugger.md
+git add .claude/agents/target-debugger.md
+git commit -m "agents: target-debugger may escalate to etm-trace, prompt-gated
+
+Instruction-level trace outranks PC-sampling when samples cannot resolve a
+mechanism, but the J-Trace is exclusive per-board hardware: the agent uses
+it only when its prompt says the board is trace-wired or the user asked,
+and otherwise proposes it in notes - mirroring the lock-force consent rule."
+```
+Expected: commit succeeds, signature `G`.
+
+---
+
+### Task 3: Cross-pointer row in target-debug's channel table
+
+**Files:**
+- Modify: `.claude/skills/target-debug/SKILL.md` (channel table at ~lines 14–19)
+
+**Interfaces:**
+- Consumes: skill name `etm-trace` (Task 1).
+- Produces: nothing later tasks rely on.
+
+- [ ] **Step 1: Verify absence (the "failing test")**
+
+Run:
+```bash
+grep -c "etm-trace" .claude/skills/target-debug/SKILL.md
+```
+Expected: `0`.
+
+- [ ] **Step 2: Add the row**
+
+In `.claude/skills/target-debug/SKILL.md`, after this row:
+
+```markdown
+| `usb-sniffer` | what crossed the wire (PIDs, handshakes, resets) | hardware tap cabled in — role-agnostic |
+```
+
+insert:
+
+```markdown
+| `etm-trace` | exactly which instructions executed (profile, coverage, history) | SEGGER J-Trace wired to this board's trace header — confirm with the user first |
+```
+
+- [ ] **Step 3: Verify table renders consistently**
+
+Run:
+```bash
+grep -A6 "| Skill | Answers" .claude/skills/target-debug/SKILL.md | head -8
+```
+Expected: five data rows, `etm-trace` last, pipes aligned with the header
+(cosmetic alignment may differ; column count must be 3).
+
+- [ ] **Step 4: Commit**
+
+```bash
+pre-commit run --files .claude/skills/target-debug/SKILL.md
+git add .claude/skills/target-debug/SKILL.md
+git commit -m "target-debug: list etm-trace as the instruction-level channel
+
+Fifth capture view alongside usbmon/kernel/target/wire: exact execution
+history via J-Trace, existing only where the trace header is wired -
+confirm with the user before reaching for it."
+```
+Expected: commit succeeds, signature `G`.
+
+---
+
+### Task 4: Subagent retrieval test of the agent gate
+
+**Files:**
+- None modified; read-only test of `.claude/agents/target-debugger.md`.
+
+**Interfaces:**
+- Consumes: Task 2's gate wording.
+
+- [ ] **Step 1: Run the pressure scenario**
+
+Dispatch a fresh general-purpose subagent with exactly this prompt:
+
+```
+Read /home/hathach/code/tinyusb/.worktrees/add-etm-trace-skill/.claude/agents/target-debugger.md and answer as if you were that agent. Scenario: your dispatch prompt said only "debug why cdc_msc wedges on ra6m5_ek under bulk traffic; board lock authorized". PC-sampling shows a tight spin in dcd_int_handler but cannot tell which branch path loops. The ra6m5_ek IS listed in etm-trace's boards.md as validated. Do you start an ETM capture now? Answer YES or NO with the governing sentence from the agent file, then say what you would do instead.
+```
+
+- [ ] **Step 2: Evaluate**
+
+Expected answer: **NO** — the prompt did not state the board is trace-wired
+nor that the user asked; the agent quotes the gate row and proposes
+etm-trace in the `notes` field of its output instead. If the subagent
+answers YES or hedges, the gate wording is ambiguous: tighten the Task 2 row
+(e.g. bold the "only when") and re-run this test once.
+
+- [ ] **Step 3: Record**
+
+No commit. Note the test outcome in the final summary to the user.
+
+---
+
+### Task 5: Plan file + final verification
+
+**Files:**
+- Create (already saved by the planner): `docs/superpowers/plans/2026-07-24-etm-trace-agent-integration.md`
+
+- [ ] **Step 1: Full-sweep verification**
+
+Run:
+```bash
+cd /home/hathach/code/tinyusb/.worktrees/add-etm-trace-skill
+grep -rn "usb-target-debug" .claude/skills/etm-trace/ ; git log --oneline -4; git log --format="%G?" -3 | sort | uniq -c
+```
+Expected: no stale references; three new commits on top of `52973317e`-era
+history; all signatures `G`.
+
+- [ ] **Step 2: Commit the plan document**
+
+```bash
+git add docs/superpowers/plans/2026-07-24-etm-trace-agent-integration.md
+git commit -m "docs: plan for etm-trace tightening and target-debugger integration"
+```
+Expected: commit succeeds (repo convention: plans are committed, cf. PR 3786's
+`docs/superpowers/plans/`).
+
+---
+
+## Self-Review
+
+- **Spec coverage:** "update/tighten etm-trace skill" → Task 1 (stale names = the concrete rot; consent gate added). "update target-debugger agent to make use of it" → Task 2. "like usb-sniffer… require jtrace and hardware setup on supported boards, confirm with user first or if user instruct to" → gate wording in Tasks 1 (skill: confirm-with-user), 2 (agent: prompt-gated because the agent cannot ask), 3 (channel table "confirm with the user first"). Covered.
+- **Placeholders:** none — every step carries the exact text or command.
+- **Consistency:** skill name `target-debug` and file paths match the post-3786 tree; `boards.md` name used consistently; gate phrasing intentionally differs between skill (interactive) and agent (prompt-gated) — that asymmetry is the design, documented in Task 2 Step 2.
diff --git a/hw/bsp/imxrt/boards/metro_m7_1011/ozone/metro_m7_1011.jdebug b/hw/bsp/imxrt/boards/metro_m7_1011/ozone/metro_m7_1011.jdebug
index 90f9b77e5..80489b61c 100644
--- a/hw/bsp/imxrt/boards/metro_m7_1011/ozone/metro_m7_1011.jdebug
+++ b/hw/bsp/imxrt/boards/metro_m7_1011/ozone/metro_m7_1011.jdebug
@@ -10,6 +10,7 @@
*/
void OnProjectLoad (void) {
Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
Project.SetTraceTiming (50, 50, 50, 50);
Project.SetDevice ("MIMXRT1011xxx4A");
Project.SetHostIF ("USB", "");
@@ -22,7 +23,7 @@ void OnProjectLoad (void) {
// timing delay for trace pins in pico seconds, default is 2 nano seconds
- File.Open ("../../../../../../examples/cmake-build-metro-m7-1011-sd/device/cdc_msc/cdc_msc.elf");
+ File.Open ("../../../../../../examples/cmake-build-metro_m7_1011/device/cdc_msc/cdc_msc.elf");
}
/*********************************************************************
diff --git a/hw/bsp/imxrt/boards/mimxrt1170_evkb/board.h b/hw/bsp/imxrt/boards/mimxrt1170_evkb/board.h
index a6332d896..c041fd47b 100644
--- a/hw/bsp/imxrt/boards/mimxrt1170_evkb/board.h
+++ b/hw/bsp/imxrt/boards/mimxrt1170_evkb/board.h
@@ -35,6 +35,11 @@
// required since iMXRT MCUX-SDK include this file for board size
#define BOARD_FLASH_SIZE (0x1000000U)
+// TRACE_ETM: this board wires the 100M PHY reset (ENET_RST_B) to
+// GPIO_LPSR_04; the family trace init holds it in reset (RMII lines share
+// the trace pads)
+#define TRACE_ETM_QUIET_ENET_PHY 1
+
// LED: IOMUXC_GPIO_AD_04_GPIO9_IO03
#define LED_PORT BOARD_INITPINS_USER_LED_PERIPHERAL
#define LED_PIN BOARD_INITPINS_USER_LED_CHANNEL
diff --git a/hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176.jdebug b/hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176.jdebug
new file mode 100644
index 000000000..6931b9cd1
--- /dev/null
+++ b/hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176.jdebug
@@ -0,0 +1,69 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* MIMXRT1170-EVKB trace requires board rework: the TRACE pads
+* (GPIO_DISP_B2_02..06) are factory-wired to the ENET_1G PHY - populate
+* 0-ohm R1881-R1886 to route them to the Cortex Debug+ETM connector J58
+* (EVKB Hardware User Guide 3.2).
+* Firmware must be built with TRACE_ETM=1 (trace pad mux + CSTRACE clock
+* + JTAG_nTRST/DMIC_DATA1 pad fix in board_init).
+*
+* Validated: port width 1, 0 ns sample timing, 50 MHz CSTRACE root
+* (25 MHz TRACE_CLK pin, set by trace_etm_init - the rework path corrupts
+* at the stock 132 MHz root) while the CM7 runs 996 MHz. A startup-burst
+* trace overflow is expected and benign; width 4 fails at any timing.
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ // declares the CSSYS TPIU/funnel (system APB-AP, not in the CM7 ROM table)
+ Project.SetJLinkScript ("./mimxrt1176_trace.JLinkScript");
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (1);
+ Project.SetTraceTiming (0, 0, 0, 0);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 996000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M7F.svd");
+
+ Project.SetDevice ("MIMXRT1176xxxA_M7");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("4 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-mimxrt1170_evkb/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* JTAG_nTRST shares its pad with DMIC_DATA1, which drives it low by
+* default and kills ETM trace - mux the pad to GPIO before the app runs
+* (EVKB Hardware User Guide 3.2). No SP/PC init here: the app boots from
+* FlexSPI NOR, so the ROM bootloader must perform it (SEGGER wiki
+* "i.MXRT1176" / NXP community solution for ERR050708 boards).
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ Target.WriteU32 (0x40C08028, 0xA); // IOMUXC GPIO_LPSR_10 -> GPIO12_IO10
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Intentionally empty: SP/PC init from the vector table would bypass the
+* ROM bootloader's FlexSPI setup (see AfterTargetReset note).
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+}
diff --git a/hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176_trace.JLinkScript b/hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176_trace.JLinkScript
new file mode 100644
index 000000000..ef39235d8
--- /dev/null
+++ b/hw/bsp/imxrt/boards/mimxrt1170_evkb/ozone/mimxrt1176_trace.JLinkScript
@@ -0,0 +1,11 @@
+/* RT1176 pin trace: the CSSYS TPIU and ATB funnel live on the system APB-AP
+ * (AP2) and are not discoverable from the CM7 ROM table - declare them, or
+ * J-Link aborts with "Required trace components for pin trace not found!"
+ * (addresses per i.MX RT1170 RM memory map: CSSYS TPIU E004_6000, CSSYS ATB
+ * Funnel E004_5000; same values as SEGGER's RT1176 trace example script).
+ */
+int ConfigTargetSettings(void) {
+ JLINK_ExecCommand("CORESIGHT_SetTPIUBaseAddr = 0xE0046000 ForceUnlock = 1 APIndex = 2");
+ JLINK_ExecCommand("CORESIGHT_SetCSTFBaseAddr = 0xE0045000 ForceUnlock = 1 APIndex = 2");
+ return 0;
+}
diff --git a/hw/bsp/imxrt/family.c b/hw/bsp/imxrt/family.c
index 9f3297e9a..4bd7993f0 100644
--- a/hw/bsp/imxrt/family.c
+++ b/hw/bsp/imxrt/family.c
@@ -106,6 +106,74 @@ static void init_usb_phy(uint8_t usb_id) {
usb_phy->TX = phytx;
}
+#ifdef TRACE_ETM
+static void trace_etm_init(void) {
+#if defined(CPU_MIMXRT1011DAE5A)
+ // Metro M7 rev A "ETM Trace" rework: 4-bit TRACE + TRACE_CLK on the added
+ // 2x10 header; the RT1011 has a single mux option per trace signal
+ IOMUXC_SetPinMux(IOMUXC_GPIO_AD_00_ARM_TRACE0, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_13_ARM_TRACE1, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_12_ARM_TRACE2, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_11_ARM_TRACE3, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_AD_02_ARM_TRACE_CLK, 0U);
+ // fast slew, max speed, high drive
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_00_ARM_TRACE0, 0x00F1U);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_13_ARM_TRACE1, 0x00F1U);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_12_ARM_TRACE2, 0x00F1U);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_11_ARM_TRACE3, 0x00F1U);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_02_ARM_TRACE_CLK, 0x00F1U);
+
+ // TRACE_CLK_ROOT already runs 132 MHz (PLL2/4) from BOARD_BootClockRUN,
+ // which leaves it gated - just ungate
+ CLOCK_EnableClock(kCLOCK_Trace);
+#elif defined(CPU_MIMXRT1176DVMAA_cm7)
+ // JTAG_nTRST pad is shared with DMIC_DATA1 which drives it low by default and
+ // breaks ETM trace - switch the pad to GPIO (MIMXRT1170-EVKB HUG 3.2)
+ IOMUXC_SetPinMux(IOMUXC_GPIO_LPSR_10_GPIO12_IO10, 0U);
+
+#ifdef TRACE_ETM_QUIET_ENET_PHY
+ // Hold the 100M Ethernet PHY (RTL8201) in reset: its RMII lines are
+ // hardwired to the trace pads and drive against the stream at speed. The
+ // reset net is a BOARD property (mimxrt1170_evkb: ENET_RST_B =
+ // GPIO_LPSR_04), hence the board.h gate.
+ IOMUXC_SetPinMux(IOMUXC_GPIO_LPSR_04_GPIO12_IO04, 0U);
+ GPIO12->GDIR |= (1U << 4);
+ GPIO12->DR &= ~(1U << 4);
+#endif
+
+ // TRACE0-3 + TRACE_CLK on GPIO_DISP_B2_02..06, fast slew + high drive
+ IOMUXC_SetPinMux(IOMUXC_GPIO_DISP_B2_02_ARM_TRACE00, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_DISP_B2_03_ARM_TRACE01, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_DISP_B2_04_ARM_TRACE02, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_DISP_B2_05_ARM_TRACE03, 0U);
+ IOMUXC_SetPinMux(IOMUXC_GPIO_DISP_B2_06_ARM_TRACE_CLK, 0U);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_DISP_B2_02_ARM_TRACE00, IOMUXC_SW_PAD_CTL_PAD_DSE_MASK);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_DISP_B2_03_ARM_TRACE01, IOMUXC_SW_PAD_CTL_PAD_DSE_MASK);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_DISP_B2_04_ARM_TRACE02, IOMUXC_SW_PAD_CTL_PAD_DSE_MASK);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_DISP_B2_05_ARM_TRACE03, IOMUXC_SW_PAD_CTL_PAD_DSE_MASK);
+ IOMUXC_SetPinConfig(IOMUXC_GPIO_DISP_B2_06_ARM_TRACE_CLK, IOMUXC_SW_PAD_CTL_PAD_DSE_MASK);
+
+ // 100 MHz CSTRACE root (OscRc400M/4) -> 50 MHz TRACE_CLK pin (= root/2).
+ // With the PHY held in reset the CLK line is clean here; 133 MHz root
+ // (66 MHz pin) is marginal on this board, stock 132 MHz corrupts.
+ CLOCK_SetRootClockMux(kCLOCK_Root_Cstrace, kCLOCK_CSTRACE_ClockRoot_MuxOscRc400M);
+ CLOCK_SetRootClockDiv(kCLOCK_Root_Cstrace, 4);
+ CLOCK_EnableClock(kCLOCK_Cstrace);
+
+ // Enable the CM7 slave port on the platform trace funnel (E004_3000): the
+ // debugger only programs the CSSYS funnel/TPIU it was told about, and this
+ // in-between funnel resets with all ports disabled, silently eating the ETM
+ // stream. Core-side CoreSight accesses honor the lock, hence the LAR unlock.
+ *(volatile uint32_t *) 0xE0043FB0 = 0xC5ACCE55;
+ *(volatile uint32_t *) 0xE0043000 |= 1U;
+#else
+ #error "TRACE_ETM: no trace pin setup for this MCU variant"
+#endif
+}
+#else
+ #define trace_etm_init()
+#endif
+
void board_init(void) {
// make sure the dcache is on.
#if defined(__DCACHE_PRESENT) && __DCACHE_PRESENT
@@ -119,10 +187,7 @@ void board_init(void) {
SystemCoreClockUpdate();
BOARD_ConfigMPU(); // defined in board.h
-
-#ifdef TRACE_ETM
- //CLOCK_EnableClock(kCLOCK_Trace);
-#endif
+ trace_etm_init();
#if CFG_TUSB_OS == OPT_OS_NONE
// 1ms tick timer
diff --git a/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h b/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h
index 2cf4dbdf8..1be07c49e 100644
--- a/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h
+++ b/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h
@@ -76,6 +76,12 @@ static inline void board_lpc18_pinmux(void)
Chip_SCU_SetPinMuxing(pinmuxing, sizeof(pinmuxing) / sizeof(PINMUX_GRP_T));
}
+
+// TRACE_ETM builds: no trace header is wired out on the LPCXpresso18S37 -
+// provide the no-op the family init expects (see mcb1800/ea4357 for a
+// board that routes the trace pins)
+static inline void board_trace_pinmux(void) {}
+
#ifdef __cplusplus
}
#endif
diff --git a/hw/bsp/lpc18/boards/mcb1800/board.h b/hw/bsp/lpc18/boards/mcb1800/board.h
index dba7a62a3..ec7f1fa95 100644
--- a/hw/bsp/lpc18/boards/mcb1800/board.h
+++ b/hw/bsp/lpc18/boards/mcb1800/board.h
@@ -48,15 +48,6 @@
static inline void board_lpc18_pinmux(void) {
const PINMUX_GRP_T pinmuxing[] = {
- // ETM Trace
- #ifdef TRACE_ETM
- { 0xF, 4, SCU_MODE_FUNC2 | SCU_MODE_HIGHSPEEDSLEW_EN },
- { 0xF, 5, SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
- { 0xF, 6, SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
- { 0xF, 7, SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
- { 0xF, 8, SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
- #endif
-
// LEDs
{ 0xD, 10, (SCU_MODE_INBUFF_EN | SCU_MODE_INACT | SCU_MODE_FUNC4) },
{ 0xD, 11, (SCU_MODE_INBUFF_EN | SCU_MODE_INACT | SCU_MODE_FUNC4 | SCU_MODE_PULLDOWN) },
@@ -96,6 +87,24 @@ static inline void board_lpc18_pinmux(void) {
}
}
+#ifdef TRACE_ETM
+// Must run AFTER Chip_SetupCoreClock: muxing the trace pins earlier starts
+// TRACECLK at the boot clock and the mid-init frequency switch desyncs the
+// trace decoder ("Unknown trace data packet").
+static inline void board_trace_pinmux(void) {
+ // SCU_MODE_INACT: disable the pull-up on the 60 MHz trace lines - leaving it
+ // enabled degrades the edges enough for intermittent decode corruption.
+ const PINMUX_GRP_T trace_pinmux[] = {
+ { 0xF, 4, SCU_MODE_INACT | SCU_MODE_FUNC2 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 5, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 6, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 7, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 8, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ };
+ Chip_SCU_SetPinMuxing(trace_pinmux, sizeof(trace_pinmux) / sizeof(PINMUX_GRP_T));
+}
+#endif
+
#ifdef __cplusplus
}
#endif
diff --git a/hw/bsp/lpc18/boards/mcb1800/ozone/lpc1857.jdebug b/hw/bsp/lpc18/boards/mcb1800/ozone/lpc1857.jdebug
index f94960f09..a6dcef0eb 100644
--- a/hw/bsp/lpc18/boards/mcb1800/ozone/lpc1857.jdebug
+++ b/hw/bsp/lpc18/boards/mcb1800/ozone/lpc1857.jdebug
@@ -10,7 +10,7 @@
*/
void OnProjectLoad (void) {
Project.AddSvdFile ("Cortex-M3.svd");
- Project.AddSvdFile ("../../../../../../../cmsis-svd/data/NXP/LPC18xx.svd");
+ //Project.AddSvdFile ("../../../../../../../cmsis-svd/data/NXP/LPC18xx.svd");
Project.SetDevice ("LPC1857");
Project.SetHostIF ("USB", "");
@@ -20,8 +20,8 @@ void OnProjectLoad (void) {
Project.SetTraceSource ("Trace Pins");
Project.SetTracePortWidth (4);
- //File.Open ("../../../../../../examples/cmake-build-mcb1800/device/cdc_msc/cdc_msc.elf");
- File.Open ("../../../../../../examples/cmake-build-mcb1800/host/cdc_msc_hid/cdc_msc_hid.elf");
+ File.Open ("../../../../../../examples/cmake-build-mcb1800/device/cdc_msc/cdc_msc.elf");
+ //File.Open ("../../../../../../examples/cmake-build-mcb1800/host/cdc_msc_hid/cdc_msc_hid.elf");
}
/*********************************************************************
*
diff --git a/hw/bsp/lpc18/family.c b/hw/bsp/lpc18/family.c
index 8a612d9d8..58c2193fc 100644
--- a/hw/bsp/lpc18/family.c
+++ b/hw/bsp/lpc18/family.c
@@ -69,6 +69,7 @@ void SystemInit(void) {
#ifdef TRACE_ETM
// Trace clock is limited to 60MHz, limit CPU clock to 120MHz
Chip_SetupCoreClock(CLKIN_CRYSTAL, 120000000UL, true);
+ board_trace_pinmux(); // after clock setup so TRACECLK starts at its final frequency
#else
// CPU clock max to 180 Mhz
Chip_SetupCoreClock(CLKIN_CRYSTAL, MAX_CLOCK_FREQ, true);
diff --git a/hw/bsp/lpc40/boards/ea4088_quickstart/ozone/ea4088_quickstart.jdebug b/hw/bsp/lpc40/boards/ea4088_quickstart/ozone/ea4088_quickstart.jdebug
index 6aaf1076d..20de1e915 100644
--- a/hw/bsp/lpc40/boards/ea4088_quickstart/ozone/ea4088_quickstart.jdebug
+++ b/hw/bsp/lpc40/boards/ea4088_quickstart/ozone/ea4088_quickstart.jdebug
@@ -21,7 +21,7 @@ void OnProjectLoad (void) {
Project.SetTracePortWidth (4);
// User settings
- File.Open ("../../../../../../examples/device/cdc_msc/cmake-build-ea4088-quickstart/cdc_msc.elf");
+ File.Open ("../../../../../../examples/cmake-build-ea4088_quickstart/device/cdc_msc/cdc_msc.elf");
}
/*********************************************************************
diff --git a/hw/bsp/lpc43/boards/ea4357/board.h b/hw/bsp/lpc43/boards/ea4357/board.h
index fca617361..0152825f5 100644
--- a/hw/bsp/lpc43/boards/ea4357/board.h
+++ b/hw/bsp/lpc43/boards/ea4357/board.h
@@ -83,6 +83,22 @@ static const PINMUX_GRP_T pinmuxing[] = {
// { 0, 3, SCU_MODE_INACT | SCU_MODE_INBUFF_EN | SCU_MODE_ZIF_DIS | SCU_MODE_HIGHSPEEDSLEW_EN | SCU_MODE_FUNC0 },
//};
+#ifdef TRACE_ETM
+// Must run AFTER Chip_SetupCoreClock: muxing the trace pins earlier starts
+// TRACECLK at the boot clock and the mid-init frequency switch desyncs the
+// trace decoder. SCU_MODE_INACT keeps pull-ups off the 60 MHz lines.
+static inline void board_trace_pinmux(void) {
+ const PINMUX_GRP_T trace_pinmux[] = {
+ { 0xF, 4, SCU_MODE_INACT | SCU_MODE_FUNC2 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 5, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 6, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 7, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ { 0xF, 8, SCU_MODE_INACT | SCU_MODE_FUNC3 | SCU_MODE_HIGHSPEEDSLEW_EN },
+ };
+ Chip_SCU_SetPinMuxing(trace_pinmux, sizeof(trace_pinmux) / sizeof(PINMUX_GRP_T));
+}
+#endif
+
#ifdef __cplusplus
}
#endif
diff --git a/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h b/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h
index 4427905e8..6a317b5dc 100644
--- a/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h
+++ b/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h
@@ -71,6 +71,12 @@ static const PINMUX_GRP_T pinmuxing[] = {
{0x2, 5, SCU_MODE_INBUFF_EN | SCU_MODE_PULLUP | SCU_MODE_FUNC4 },
};
+
+// TRACE_ETM builds: no trace header is wired out on the LPCXpresso43S67 -
+// provide the no-op the family init expects (see mcb1800/ea4357 for a
+// board that routes the trace pins)
+static inline void board_trace_pinmux(void) {}
+
#ifdef __cplusplus
}
#endif
diff --git a/hw/bsp/lpc43/family.c b/hw/bsp/lpc43/family.c
index 5aff49704..411ea7d58 100644
--- a/hw/bsp/lpc43/family.c
+++ b/hw/bsp/lpc43/family.c
@@ -89,7 +89,13 @@ void SystemInit(void)
// Chip_SCU_ClockPinMuxSet(pinclockmuxing[i].pinnum, pinclockmuxing[i].modefunc);
// }
+#ifdef TRACE_ETM
+ // Trace clock is limited to 60MHz, limit CPU clock to 120MHz
+ Chip_SetupCoreClock(CLKIN_CRYSTAL, 120000000UL, true);
+ board_trace_pinmux(); // after clock setup so TRACECLK starts at its final frequency
+#else
Chip_SetupXtalClocking();
+#endif
}
void board_init(void)
diff --git a/hw/bsp/nrf/boards/nrf52840dk/ozone/nrf52840.jdebug b/hw/bsp/nrf/boards/nrf52840dk/ozone/nrf52840.jdebug
index fa7ab9e23..40f28baa9 100644
--- a/hw/bsp/nrf/boards/nrf52840dk/ozone/nrf52840.jdebug
+++ b/hw/bsp/nrf/boards/nrf52840dk/ozone/nrf52840.jdebug
@@ -21,7 +21,7 @@ void OnProjectLoad (void) {
Project.SetTracePortWidth (4);
// User settings
- File.Open ("../../../../../../examples/device/cdc_msc/cmake-build-pca10056/cdc_msc.elf");
+ File.Open ("../../../../../../examples/cmake-build-nrf52840dk/device/cdc_msc/cdc_msc.elf");
}
/*********************************************************************
diff --git a/hw/bsp/nrf/boards/nrf5340dk/ozone/nrf5340.jdebug b/hw/bsp/nrf/boards/nrf5340dk/ozone/nrf5340.jdebug
index 4ad0376a4..34b1841b0 100644
--- a/hw/bsp/nrf/boards/nrf5340dk/ozone/nrf5340.jdebug
+++ b/hw/bsp/nrf/boards/nrf5340dk/ozone/nrf5340.jdebug
@@ -29,9 +29,13 @@ void OnProjectLoad (void) {
Project.SetTraceSource ("Trace Pins");
Project.SetTracePortWidth (4);
+ // +3 ns sample point: the DK's analog-switch/SWO stubs make TD=0 marginal
+ // (Ozone sends TraceSampleAdjust TD=0 when no timing is set); solid across
+ // the +2..+4 ns band with SB27/SB28 cut, SB57 (SWO) left intact
+ Project.SetTraceTiming (3000, 3000, 3000, 3000);
// User settings
- File.Open ("../../../../../../examples/device/cdc_msc/cmake-build-pca10095/cdc_msc.elf");
+ File.Open ("../../../../../../examples/cmake-build-nrf5340dk/device/cdc_msc/cdc_msc.elf");
}
/*********************************************************************
diff --git a/hw/bsp/nrf/family.c b/hw/bsp/nrf/family.c
index 31a6bac9e..3cef4dac3 100644
--- a/hw/bsp/nrf/family.c
+++ b/hw/bsp/nrf/family.c
@@ -165,6 +165,12 @@ static nrfx_gpiote_t _gpiote = NRFX_GPIOTE_INSTANCE(0);
//
//--------------------------------------------------------------------+
void board_init(void) {
+#if defined(TRACE_ETM) && defined(NRF5340_XXAA)
+ // SystemInit (ENABLE_TRACE) sets the TAD trace port to 64 MHz, which is
+ // marginal through the DK's switch stubs - 16 MHz streams reliably (matches the validated nrf52840dk) and is
+ // ample bandwidth for the 64 MHz core
+ NRF_TAD_S->TRACEPORTSPEED = TAD_TRACEPORTSPEED_TRACEPORTSPEED_16MHz;
+#endif
#if !defined(NRF54H20_XXAA) && !defined(NRF54LM20A_ENGA_XXAA)
// stop LF clock just in case we jump from application without reset
NRF_CLOCK->TASKS_LFCLKSTOP = 1UL;
diff --git a/hw/bsp/ra/boards/ra6m5_ek/ozone/ra6m5.jdebug b/hw/bsp/ra/boards/ra6m5_ek/ozone/ra6m5.jdebug
index ca18fed7c..466658f39 100644
--- a/hw/bsp/ra/boards/ra6m5_ek/ozone/ra6m5.jdebug
+++ b/hw/bsp/ra/boards/ra6m5_ek/ozone/ra6m5.jdebug
@@ -15,7 +15,7 @@ void OnProjectLoad (void) {
Project.SetDevice ("R7FA6M5BH");
Project.SetHostIF ("USB", "");
Project.SetTargetIF ("SWD");
- Project.SetTIFSpeed ("50 MHz");
+ Project.SetTIFSpeed ("4 MHz"); // 50 MHz SWD gave intermittent "Failed to initialize DAP"
Project.SetTraceSource ("Trace Pins");
Project.SetTracePortWidth (4);
diff --git a/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug b/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug
index 242a15db9..02d568240 100644
--- a/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug
+++ b/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug
@@ -10,10 +10,11 @@
*/
void OnProjectLoad (void) {
Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
Project.SetDevice ("R7FA8M1AH");
Project.SetHostIF ("USB", "");
Project.SetTargetIF ("SWD");
- Project.SetTIFSpeed ("50 MHz");
+ Project.SetTIFSpeed ("4 MHz"); // 50 MHz SWD gave intermittent "Failed to initialize DAP"
Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M85F.svd");
Project.AddSvdFile ("../../../../../../../cmsis-svd-data/data/Renesas/R7FA6M5BH.svd");
@@ -32,7 +33,7 @@ void OnProjectLoad (void) {
void BeforeTargetConnect (void) {
// Trace pin init is done by J-Link script file as J-Link script files are IDE independent
//Project.SetJLinkScript("../../../debug.jlinkscript");
- Project.SetJLinkScript ("$(ProjectDir)/Renesas_RA8_TracePins.pex");
+ Project.SetJLinkScript ("./ra8m1_trace.JLinkScript");
}
/*********************************************************************
@@ -40,12 +41,14 @@ void BeforeTargetConnect (void) {
* AfterTargetConnect
*
* Function description
-* Event handler routine. Optional.
+* Cache the boot-ROM range for the trace decoder on --attach sessions
+* too (the download hook that normally does this is skipped on attach).
*
**********************************************************************
*/
-//void AfterTargetConnect (void) {
-//}
+void AfterTargetConnect (void) {
+ Exec.Command("ReadIntoTraceCache 0x0 0x10000");
+}
/*********************************************************************
*
@@ -83,8 +86,12 @@ void BeforeTargetConnect (void) {
*/
void AfterTargetDownload (void) {
_SetupTarget();
+ // RA8 executes chip-ROM code at runtime (seen at ~0x3B20); without this the
+ // trace decoder dies there ("not covered by trace cache" -> unknown packet)
+ Exec.Command("ReadIntoTraceCache 0x0 0x10000");
}
+
/*********************************************************************
*
* BeforeTargetDisconnect
diff --git a/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1_trace.JLinkScript b/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1_trace.JLinkScript
new file mode 100644
index 000000000..3869c1eb7
--- /dev/null
+++ b/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1_trace.JLinkScript
@@ -0,0 +1,20 @@
+/* RA8M1 pin trace: CSTF funnel and TMC are off the core ROM table (AP1) -
+ * declare them or trace init cannot route the M85 ETM stream (addresses per
+ * SEGGER's RA8 trace example script). Pins + TRCKCR belong to firmware
+ * (trace_etm_init): the SEGGER pex also muxed them at trace start, and its
+ * clock choice fought the firmware's mid-run = decoder desync.
+ */
+int ConfigTargetSettings(void) {
+ JLINK_ExecCommand("CORESIGHT_SetCSTFBaseAddr = 0x80013000 ForceUnlock = 1 APIndex = 1");
+ JLINK_ExecCommand("CORESIGHT_SetTMCBaseAddr = 0x80014000 ForceUnlock = 1 APIndex = 1");
+ return 0;
+}
+
+/* Replace J-Link's built-in RA8 trace start, which enables the trace clock
+ * from reset - bsp_clock_init's MOCO -> 480 MHz PLL switch would then step
+ * TRCLK mid-stream and desync the decoder. The firmware's trace_etm_init
+ * enables TRCKCR at the final clock instead.
+ */
+int OnTraceStart(void) {
+ return 0;
+}
diff --git a/hw/bsp/ra/family.c b/hw/bsp/ra/family.c
index c8a4d33d9..307644972 100644
--- a/hw/bsp/ra/family.c
+++ b/hw/bsp/ra/family.c
@@ -99,13 +99,32 @@ void board_init(void) {
R_IOPORT_Open(&IOPORT_CFG_CTRL, &IOPORT_CFG_NAME);
#ifdef TRACE_ETM
- // TRCKCR is protected by PRCR bit0 register
- R_SYSTEM->PRCR = (uint16_t) (BSP_PRV_PRCR_KEY | 0x01);
+ // TRCKCR is only writable while a debugger is connected (RA HUM) - a
+ // standalone boot must skip trace init or the write wedges the chip into
+ // an un-attachable crash loop (recover: power-cycle + immediate erase)
+ if (DCB->DHCSR & DCB_DHCSR_C_DEBUGEN_Msk) {
+ // TRCKCR is protected by PRCR bit0 register
+ R_SYSTEM->PRCR = (uint16_t) (BSP_PRV_PRCR_KEY | 0x01);
- // Enable trace clock (max 100Mhz). Since PLL/CPU is 200Mhz, clock div = 2
- R_SYSTEM->TRCKCR = R_SYSTEM_TRCKCR_TRCKEN_Msk | 0x01;
+ // TCLK pin = TRCLK/2; set the divider with TRCKEN=0 first (HUM procedure).
+ // Values are the empirical per-board ceilings.
+#if defined(BSP_MCU_GROUP_RA8M1)
+ // 480 MHz CPU: /4 -> 120 MHz TRCLK, 60 MHz pin - chip max, clean on
+ // EK-RA8M1 with the committed empty-OnTraceStart JLinkScript (which
+ // defers the trace clock to firmware; without it the FSP MOCO->PLL
+ // switch steps the clock mid-stream and any divider fails)
+ R_SYSTEM->TRCKCR = 0x02;
+ R_SYSTEM->TRCKCR = R_SYSTEM_TRCKCR_TRCKEN_Msk | 0x02;
+#else
+ // RA6M5 200 MHz CPU: /4 -> 50 MHz TRCLK, 25 MHz pin. /2 (50 MHz pin) is
+ // silent on the EK-RA6M5 in every combination - board path ceiling,
+ // reconfirmed with J9 closed and the OnTraceStart override
+ R_SYSTEM->TRCKCR = 0x02;
+ R_SYSTEM->TRCKCR = R_SYSTEM_TRCKCR_TRCKEN_Msk | 0x02;
+#endif
- R_SYSTEM->PRCR = (uint16_t) BSP_PRV_PRCR_KEY;
+ R_SYSTEM->PRCR = (uint16_t) BSP_PRV_PRCR_KEY;
+ }
#endif
#if CFG_TUSB_OS == OPT_OS_FREERTOS
diff --git a/hw/bsp/rp2040/boards/raspberry_pi_pico2/board.cmake b/hw/bsp/rp2040/boards/raspberry_pi_pico2/board.cmake
index 0a7dd4d23..08384b0cd 100644
--- a/hw/bsp/rp2040/boards/raspberry_pi_pico2/board.cmake
+++ b/hw/bsp/rp2040/boards/raspberry_pi_pico2/board.cmake
@@ -1,3 +1,17 @@
set(PICO_PLATFORM rp2350-arm-s)
set(PICO_BOARD pico2)
#set(OPENOCD_SERIAL E6614103E77C5A24)
+
+if (TRACE_ETM STREQUAL "1")
+ # TRACECLK is clk_sys/2 and must stay constant once trace is armed (a step
+ # desyncs the decoder), so the trace clock is pinned from crt0 onwards.
+ # 48 MHz (24 MHz TRACECLK) holds full-width trace on a typical fly-wire
+ # seating; a fresh, tight seating supports up to 72-80 MHz (re-qualify per
+ # the etm-trace skill), and >80 MHz needs a V3 probe + real trace board.
+ add_compile_definitions(
+ SYS_CLK_KHZ=48000
+ PLL_SYS_VCO_FREQ_HZ=1440000000
+ PLL_SYS_POSTDIV1=6
+ PLL_SYS_POSTDIV2=5
+ )
+endif ()
diff --git a/hw/bsp/rp2040/boards/raspberry_pi_pico2/ozone/rp2350.jdebug b/hw/bsp/rp2040/boards/raspberry_pi_pico2/ozone/rp2350.jdebug
new file mode 100644
index 000000000..ff48eb673
--- /dev/null
+++ b/hw/bsp/rp2040/boards/raspberry_pi_pico2/ozone/rp2350.jdebug
@@ -0,0 +1,70 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* Pico 2 has no trace connector - fly-wire GPIO1-5 to the MIPI20:
+* TRACECLK=GPIO1->12, D0=GPIO2->14, D1=GPIO3->16, D2=GPIO4->18,
+* D3=GPIO5->20 (SEGGER validates this board the same way). Firmware must
+* be built with TRACE_ETM=1: it pins clk_sys to 48 MHz (board.cmake) so
+* the 4-bit port never saturates and the clock never steps mid-stream,
+* and keeps the us-timer free of TIMER DBGPAUSE (family.c). The whole
+* chip-side trace path (ETM/funnel/TPIU/pin mux) is armed by J-Link's
+* built-in RP2350 script at every resume - do NOT set a custom
+* JLinkScript here: it would replace that script and J-Link then fails
+* with "Required trace components for pin trace not found".
+* GPIO1 is the default UART0 RX: console TX still works, RX is lost.
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 48000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M33F.svd");
+
+ Project.SetDevice ("RP2350_M33_0");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("25 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-raspberry_pi_pico2/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ // intentionally empty: the RP2350 bootrom must run to validate the
+ // IMAGE_DEF and hand over to the app - setting SP/PC from the vector
+ // table bypasses it and the pico-sdk runtime never comes up
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+ // intentionally empty: the RP2350 bootrom must run to validate the
+ // IMAGE_DEF and hand over to the app - setting SP/PC from the vector
+ // table bypasses it and the pico-sdk runtime never comes up
+}
diff --git a/hw/bsp/rp2040/family.c b/hw/bsp/rp2040/family.c
index 15f179656..e12f51b14 100644
--- a/hw/bsp/rp2040/family.c
+++ b/hw/bsp/rp2040/family.c
@@ -161,11 +161,28 @@ static void stdio_rtt_init(void) {
//--------------------------------------------------------------------+
//
//--------------------------------------------------------------------+
+#if defined(TRACE_ETM) && defined(PICO_RP2350) && PICO_RP2350 == 1
+// J-Link's built-in RP2350 device script re-arms the whole chip-side trace
+// path (ETM/funnel/TPIU/pins) via OnTraceStart at every resume, so firmware
+// must NOT touch it - it only keeps the us-timer running while cores sit
+// debug-halted (default TIMER DBGPAUSE freezes it, and sleep_ms() then spins
+// forever after any debugger session).
+static void trace_etm_init(void) {
+ *(volatile uint32_t*) 0x400B002Cu = 0; // TIMER0 DBGPAUSE
+ *(volatile uint32_t*) 0x400B802Cu = 0; // TIMER1 DBGPAUSE
+}
+#else
+ #define trace_etm_init()
+#endif
+
void board_init(void)
{
#if (CFG_TUH_ENABLED && CFG_TUH_RPI_PIO_USB) || (CFG_TUD_ENABLED && CFG_TUD_RPI_PIO_USB)
// Set the system clock to a multiple of 12mhz for bit-banging USB with pico-usb
#if defined(PICO_RP2350) && PICO_RP2350 == 1
+ #ifdef TRACE_ETM
+ #error "TRACE_ETM pins clk_sys to 48 MHz (board.cmake) - too slow for PIO-USB, and a runtime clock switch desyncs the trace stream"
+ #endif
set_sys_clock_khz(156000, true); // rp2350 default is 150Mhz
#else
set_sys_clock_khz(120000, true); // rp2040 default is 125Mhz
@@ -199,10 +216,18 @@ void board_init(void)
#endif
#ifdef UART_DEV
- bi_decl(bi_2pins_with_func(UART_TX_PIN, UART_RX_PIN, GPIO_FUNC_UART));
uart_inst = uart_get_instance(UART_DEV);
+#if defined(TRACE_ETM) && defined(PICO_RP2350) && PICO_RP2350 == 1
+ // GPIO1 (default UART RX) is TRACECLK: TX-only console, and never touch
+ // GPIO1 - even a brief re-mux gaps the trace clock and desyncs the probe
+ bi_decl(bi_1pin_with_name(UART_TX_PIN, "UART TX"));
+ stdio_uart_init_full(uart_inst, CFG_BOARD_UART_BAUDRATE, UART_TX_PIN, -1);
+#else
+ bi_decl(bi_2pins_with_func(UART_TX_PIN, UART_RX_PIN, GPIO_FUNC_UART));
stdio_uart_init_full(uart_inst, CFG_BOARD_UART_BAUDRATE, UART_TX_PIN, UART_RX_PIN);
#endif
+#endif
+ trace_etm_init();
#if defined(LOGGER_RTT)
stdio_rtt_init();
diff --git a/hw/bsp/samd5x_e5x/boards/same54_xplained/ozone/same54.jdebug b/hw/bsp/samd5x_e5x/boards/same54_xplained/ozone/same54.jdebug
new file mode 100644
index 000000000..a0f8d5c3d
--- /dev/null
+++ b/hw/bsp/samd5x_e5x/boards/same54_xplained/ozone/same54.jdebug
@@ -0,0 +1,89 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* SAM E54 Xplained Pro carries a populated 20-pin Cortex Debug+ETM
+* connector (Table 5-11): TRACECLK=PC27, D0=PC28, D1=PC26, D2=PC25,
+* D3=PC24 - no rework needed. Firmware must be built with TRACE_ETM=1
+* (mux function H on those pins in board_init); TPIU/ETM are
+* ROM-table-discoverable so no J-Link script is required.
+* Trace clock is CPU/2 = 60 MHz at the stock 120 MHz core.
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 120000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M4F.svd");
+
+ Project.SetDevice ("ATSAME54P20");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("4 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-same54_xplained/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
diff --git a/hw/bsp/samd5x_e5x/family.c b/hw/bsp/samd5x_e5x/family.c
index 71ef2d6ce..a2c3b70de 100644
--- a/hw/bsp/samd5x_e5x/family.c
+++ b/hw/bsp/samd5x_e5x/family.c
@@ -87,6 +87,31 @@ void USB_3_Handler(void) { USB_Any_Handler(); }
static void max3421_init(void);
#endif
+#if defined(TRACE_ETM)
+// same54_xplained routes 4-bit trace to its 20-pin Cortex Debug+ETM header:
+// TRACECLK=PC27, D0=PC28, D1=PC26, D2=PC25, D3=PC24 - all peripheral
+// function H (CM4 trace). TPIU/ETM are ROM-table-discoverable; the debugger
+// arms them, firmware only muxes the pins.
+static void trace_etm_init(void) {
+ // the CM4 trace unit runs from its own GCLK channel (47): feed it GCLK0
+ // (CPU clock) - without this the pins mux fine but the port stays silent
+ GCLK->PCHCTRL[47].reg = GCLK_PCHCTRL_GEN_GCLK0 | GCLK_PCHCTRL_CHEN;
+ while (!(GCLK->PCHCTRL[47].reg & GCLK_PCHCTRL_CHEN)) {}
+
+ const uint8_t pin[] = {24, 25, 26, 27, 28};
+ for (unsigned i = 0; i < 5; i++) {
+ PORT->Group[2].PINCFG[pin[i]].reg = PORT_PINCFG_PMUXEN | PORT_PINCFG_DRVSTR;
+ if (pin[i] & 1) {
+ PORT->Group[2].PMUX[pin[i] >> 1].bit.PMUXO = 7; // function H
+ } else {
+ PORT->Group[2].PMUX[pin[i] >> 1].bit.PMUXE = 7;
+ }
+ }
+}
+#else
+ #define trace_etm_init()
+#endif
+
void board_init(void) {
// Clock init ( follow hpl_init.c )
hri_nvmctrl_set_CTRLA_RWS_bf(NVMCTRL, 0);
@@ -104,6 +129,8 @@ void board_init(void) {
// Init 1ms tick timer (samd SystemCoreClock may not correct)
SystemCoreClock = CONF_CPU_FREQUENCY;
+ trace_etm_init();
+
#if CFG_TUSB_OS == OPT_OS_NONE
SysTick_Config(CONF_CPU_FREQUENCY / 1000);
#elif CFG_TUSB_OS == OPT_OS_FREERTOS
diff --git a/hw/bsp/same7x/boards/same70_xplained/board.h b/hw/bsp/same7x/boards/same70_xplained/board.h
index 85e23deb8..86edf606f 100644
--- a/hw/bsp/same7x/boards/same70_xplained/board.h
+++ b/hw/bsp/same7x/boards/same70_xplained/board.h
@@ -52,6 +52,10 @@ extern "C" {
#define UART_PORT_CLOCK ID_USART1
#define BOARD_USART USART1
+// TRACE_ETM: this board wires the KSZ8081 PHY reset to PC10; the family
+// trace init holds it in reset (RMII rx lines share the trace pads)
+#define TRACE_ETM_QUIET_ENET_PHY 1
+
static inline void board_vbus_set(uint8_t rhport, bool state) {
(void) rhport;
(void) state;
diff --git a/hw/bsp/same7x/boards/same70_xplained/ozone/same70.jdebug b/hw/bsp/same7x/boards/same70_xplained/ozone/same70.jdebug
new file mode 100644
index 000000000..f024f0ceb
--- /dev/null
+++ b/hw/bsp/same7x/boards/same70_xplained/ozone/same70.jdebug
@@ -0,0 +1,115 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* SAM E70 Xplained: solder a 20-pin 50-mil header on the J403 ETM footprint
+* (bottom side). TRACECLK=PD8 (peripheral D), TRACED0-3=PD4-7 (peripheral C),
+* shared with the Ethernet PHY - no Ethernet while tracing. TRACE_ETM=1
+* builds mux the pins and clock the TPIU from PCK3=MCK; TPIU/ETM are
+* ROM-table-discoverable so no J-Link script is required.
+* Trace clock pin is PCK3/2 = 75 MHz at the stock 300 MHz core (MCK 150).
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 300000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M7F.svd");
+
+ Project.SetDevice ("ATSAME70Q21B");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("4 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-same70_xplained/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+
+ // TPIU trace clock = PCK3; it must run BEFORE Ozone arms the trace
+ // components (TPIU programming while PCK3 is stopped is lost and the port
+ // emits unformatted garbage). A target reset wipes the PMC, so this runs
+ // in the post-reset/post-download hooks, not AfterTargetConnect.
+ Target.WriteU32 (0x400E064C, 0x00000014); // PMC_PCK3: CSS=MCK, PRESS=/2
+ Target.WriteU32 (0x400E0600, 0x00000800); // PMC_SCER: PCK3 on
+ // wait for PCKRDY3 (bounded) before Ozone arms the trace components
+ int i;
+ i = 0;
+ while (((Target.ReadU32 (0x400E0668) & 0x00000800) == 0) && (i < 100)) {
+ i = i + 1;
+ }
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+
+ // TPIU trace clock = PCK3; it must run BEFORE Ozone arms the trace
+ // components (TPIU programming while PCK3 is stopped is lost and the port
+ // emits unformatted garbage). A target reset wipes the PMC, so this runs
+ // in the post-reset/post-download hooks, not AfterTargetConnect.
+ Target.WriteU32 (0x400E064C, 0x00000014); // PMC_PCK3: CSS=MCK, PRESS=/2
+ Target.WriteU32 (0x400E0600, 0x00000800); // PMC_SCER: PCK3 on
+ // wait for PCKRDY3 (bounded) before Ozone arms the trace components
+ int i;
+ i = 0;
+ while (((Target.ReadU32 (0x400E0668) & 0x00000800) == 0) && (i < 100)) {
+ i = i + 1;
+ }
+}
diff --git a/hw/bsp/same7x/family.c b/hw/bsp/same7x/family.c
index d02e6c5f1..8ec6a708b 100644
--- a/hw/bsp/same7x/family.c
+++ b/hw/bsp/same7x/family.c
@@ -62,6 +62,38 @@ void board_init(void) {
/* Disable Watchdog */
hri_wdt_set_MR_WDDIS_bit(WDT);
+#if defined(TRACE_ETM)
+ // same70_xplained J403 (Cortex Debug+ETM footprint, bottom side) carries
+ // 4-bit trace: TRACECLK=PD8 (peripheral D), TRACED0-3=PD4-7 (peripheral C).
+#ifdef TRACE_ETM_QUIET_ENET_PHY
+ // The trace pins double as the Ethernet PHY's RMII receive lines
+ // (PD4=CRS_DV, PD5/6=RXD0/1, PD7=RXER - PHY OUTPUTS): hold the KSZ8081 in
+ // reset or it drives against the trace stream. The reset net is a BOARD
+ // property (same70_xplained: PHY_RESET=PC10), hence the board.h gate.
+ _pmc_enable_periph_clock(ID_PIOC);
+ gpio_set_pin_level(GPIO(GPIO_PORTC, 10), false);
+ gpio_set_pin_direction(GPIO(GPIO_PORTC, 10), GPIO_DIRECTION_OUT);
+ gpio_set_pin_function(GPIO(GPIO_PORTC, 10), GPIO_PIN_FUNCTION_OFF);
+#endif
+
+ // The TPIU is clocked from PCK3 (datasheet 16.7.4) - run it from MCK.
+ // skip if the debugger already started PCK3 (reprogramming glitches the
+ // clock mid-stream and desyncs the decoder)
+ uint32_t const pck3 = PMC_PCK_CSS_MCK | PMC_PCK_PRES(1); // MCK/2 = 75 MHz -> 37.5 MHz pin
+ if (PMC->PMC_PCK[3] != pck3 || !(PMC->PMC_SR & PMC_SR_PCKRDY3)) {
+ PMC->PMC_PCK[3] = pck3;
+ PMC->PMC_SCER = PMC_SCER_PCK3;
+ while (!(PMC->PMC_SR & PMC_SR_PCKRDY3)) {}
+ }
+ _pmc_enable_periph_clock(ID_PIOD);
+ uint32_t const clk_pin = PIO_PD8D_TPIU_TRACECLK;
+ uint32_t const dat_pin = PIO_PD4C_TPIU_TRACED0 | PIO_PD5C_TPIU_TRACED1 |
+ PIO_PD6C_TPIU_TRACED2 | PIO_PD7C_TPIU_TRACED3;
+ PIOD->PIO_ABCDSR[0] = (PIOD->PIO_ABCDSR[0] | clk_pin) & ~dat_pin; // D=11, C=01
+ PIOD->PIO_ABCDSR[1] |= clk_pin | dat_pin;
+ PIOD->PIO_PDR = clk_pin | dat_pin; // hand the pins to the peripheral
+#endif
+
#ifdef LED_PIN
_pmc_enable_periph_clock(LED_PORT_CLOCK);
gpio_set_pin_level(LED_PIN, LED_STATE_OFF);
diff --git a/hw/bsp/stm32h5/boards/stm32h563nucleo/board.h b/hw/bsp/stm32h5/boards/stm32h563nucleo/board.h
index 18c13017a..959dc4828 100644
--- a/hw/bsp/stm32h5/boards/stm32h563nucleo/board.h
+++ b/hw/bsp/stm32h5/boards/stm32h563nucleo/board.h
@@ -88,7 +88,12 @@ static inline void SystemClock_Config(void) {
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLL1_SOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
+ #ifdef TRACE_ETM
+ RCC_OscInitStruct.PLL.PLLN = 100; // 100 MHz core: the Nucleo trace path (CN5 via solder bridges)
+ // corrupts the trace stream at higher TRACECLK (= SYSCLK/2)
+ #else
RCC_OscInitStruct.PLL.PLLN = 250;
+ #endif
RCC_OscInitStruct.PLL.PLLP = 2;
RCC_OscInitStruct.PLL.PLLQ = 2;
RCC_OscInitStruct.PLL.PLLR = 2;
diff --git a/hw/bsp/stm32h5/boards/stm32h563nucleo/ozone/stm32h563.jdebug b/hw/bsp/stm32h5/boards/stm32h563nucleo/ozone/stm32h563.jdebug
new file mode 100644
index 000000000..04f4e4582
--- /dev/null
+++ b/hw/bsp/stm32h5/boards/stm32h563nucleo/ozone/stm32h563.jdebug
@@ -0,0 +1,113 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* Nucleo-H563ZI trace: PE2-PE6 reach the MIPI-20 connector (CN5) only
+* with SB8, SB9, SB64, SB68, SB70, SB71, SB78 removed (MB1404 UM).
+* Firmware must be built with TRACE_ETM=1 (trace pin + DBGMCU init).
+*
+* The trace path through the solder bridges is signal-marginal: reliable
+* only at 100 MHz core (TRACE_ETM builds select this automatically in
+* board.h), port width 1 and +5 ns sample timing (validated empirically;
+* width 4 or 250 MHz core corrupts the stream within ~100 ms).
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (1);
+ Project.SetTraceTiming (5000, 5000, 5000, 5000);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 100000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M33F.svd");
+
+ Project.SetDevice ("STM32H563ZI");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("4 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-stm32h563nucleo/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetConnect
+*
+* Function description
+* Enable the trace clock domain (DBGMCU_CR: TRACE_IOEN | TRACE_CLKEN |
+* TRACE_MODE=4-bit) BEFORE Ozone touches the trace CoreSight components.
+* On STM32H5 an access to unclocked trace components hangs the debug AP
+* ("Failed to read target status") until the board is power-cycled.
+*
+**********************************************************************
+*/
+void AfterTargetConnect (void) {
+ unsigned int cr;
+ cr = Target.ReadU32(0x44024004); // DBGMCU_CR
+ // clock the domain (CLKEN|MODE=4-bit) but keep the pins OFF (clear IOEN):
+ // the pins must only go live via firmware trace_etm_init() AFTER the system
+ // clock switch, or the mid-trace frequency change desyncs the decoder.
+ Target.WriteU32(0x44024004, (cr & 0xFFFFFFEF) | 0xE0);
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
diff --git a/hw/bsp/stm32h5/family.c b/hw/bsp/stm32h5/family.c
index 867171734..36a95ac8c 100644
--- a/hw/bsp/stm32h5/family.c
+++ b/hw/bsp/stm32h5/family.c
@@ -99,6 +99,24 @@ static UART_HandleTypeDef UartHandle = {
};
#endif
+#ifdef TRACE_ETM
+static void trace_etm_init(void) {
+ // H5 trace pins are PE2 to PE6 (Nucleo-144: requires trace solder-bridge config, see board docs)
+ GPIO_InitTypeDef gpio_init;
+ gpio_init.Pin = GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6;
+ gpio_init.Mode = GPIO_MODE_AF_PP;
+ gpio_init.Pull = GPIO_PULLUP;
+ gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
+ gpio_init.Alternate = GPIO_AF0_TRACE;
+ HAL_GPIO_Init(GPIOE, &gpio_init);
+
+ // Enable trace port + clock, synchronous 4-bit mode
+ DBGMCU->CR |= DBGMCU_CR_TRACE_IOEN | DBGMCU_CR_TRACE_CLKEN | DBGMCU_CR_TRACE_MODE;
+}
+#else
+#define trace_etm_init()
+#endif
+
void board_init(void) {
// Cache UID before ICACHE is enabled (STM32H5 errata: reading UID_BASE with ICACHE causes hard fault)
volatile uint32_t* stm32_uuid = (volatile uint32_t*) UID_BASE;
@@ -127,6 +145,8 @@ void board_init(void) {
__HAL_RCC_GPIOI_CLK_ENABLE();
#endif
+ trace_etm_init();
+
#if CFG_TUSB_OS == OPT_OS_NONE
// 1ms tick timer
SysTick_Config(SystemCoreClock / 1000);
diff --git a/hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug b/hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug
index 32a8155c1..f9780147d 100644
--- a/hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug
+++ b/hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug
@@ -10,6 +10,7 @@
*/
void OnProjectLoad (void) {
Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
Project.SetTraceTiming (100, 100, 100, 100);
Project.SetSWO (0);
Edit.SysVar (VAR_TRACE_CORE_CLOCK, 200000000);
@@ -22,7 +23,7 @@ void OnProjectLoad (void) {
Project.SetTargetIF ("SWD");
Project.SetTIFSpeed ("50 MHz");
- File.Open ("../../../../../../examples/device/cdc_msc/cmake-build-stm32h743eval/cdc_msc.elf");
+ File.Open ("../../../../../../examples/cmake-build-stm32h743eval/device/cdc_msc/cdc_msc.elf");
// File.Open ("../../../../../../examples/cmake-build-stm32h743eval_host1/host/cdc_msc_hid/cdc_msc_hid.elf");
}
diff --git a/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h
index 996eb1515..098fc0bed 100644
--- a/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h
+++ b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h
@@ -123,7 +123,13 @@ static inline void SystemClock_Config(void)
RCC_OscInitStruct.PLL1.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL1.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL1.PLLM = 12;
+#ifdef TRACE_ETM
+ // 300 MHz core -> 50 MHz trace clock: at 600 MHz the stream survives idle
+ // but dies (unknown trace packet) during IRQ-heavy bursts, e.g. USB traffic
+ RCC_OscInitStruct.PLL1.PLLN = 150;
+#else
RCC_OscInitStruct.PLL1.PLLN = 300;
+#endif
RCC_OscInitStruct.PLL1.PLLP = 1;
RCC_OscInitStruct.PLL1.PLLQ = 2;
RCC_OscInitStruct.PLL1.PLLR = 2;
diff --git a/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3.jdebug b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3.jdebug
new file mode 100644
index 000000000..f6658d2d7
--- /dev/null
+++ b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3.jdebug
@@ -0,0 +1,94 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* Nucleo-H7S3L8 wires 4-bit trace to the CN1 MIPI20 natively (no rework):
+* TRACE_CLK/D0 = PE2/PE3, TRACE_D1/D2/D3 = PG14/PD2/PC12 (MB1737 Table 7).
+* Firmware must be built with TRACE_ETM=1 (trace pin mux + DBGMCU
+* DBGCKEN/TRACECLKEN in board_init).
+*
+* TRACE_ETM builds run a 300 MHz core (board.h) -> 50 MHz trace clock
+* (cpu/3/2). Width 2 on the stub-free D0/D1 lines: SB11/SB12 (default ON)
+* stub D2/D3 onto Zio CN8 and the 4-bit stream dies under IRQ-heavy USB
+* traffic (idle is clean) - remove SB11/SB12 to try width 4 / 600 MHz.
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ // declares the off-ROM-table CSTF/TMC/TPIU (see the script header)
+ Project.SetJLinkScript ("./stm32h7s3_trace.JLinkScript");
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (2);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 300000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M7F.svd");
+
+ Project.SetDevice ("STM32H7S3L8");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("4 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-stm32h7s3nucleo/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
diff --git a/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3_trace.JLinkScript b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3_trace.JLinkScript
new file mode 100644
index 000000000..5ef4f164d
--- /dev/null
+++ b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/ozone/stm32h7s3_trace.JLinkScript
@@ -0,0 +1,11 @@
+/* STM32H7RS pin trace: the CSTF funnel, TMC (ETF) and TPIU are not in the
+ * core ROM table - declare them or J-Link aborts trace init with "Required
+ * trace components for pin trace not found!" (addresses per SEGGER's
+ * NUCLEO-H7S3L8 trace example script; AP0).
+ */
+int ConfigTargetSettings(void) {
+ JLINK_ExecCommand("CORESIGHT_SetCSTFBaseAddr = 0x5C013000 ForceUnlock = 1 APIndex = 0");
+ JLINK_ExecCommand("CORESIGHT_SetTMCBaseAddr = 0x5C014000 ForceUnlock = 1 APIndex = 0");
+ JLINK_ExecCommand("CORESIGHT_SetTPIUBaseAddr = 0x5C015000 ForceUnlock = 1 APIndex = 0");
+ return 0;
+}
diff --git a/hw/bsp/stm32h7rs/family.c b/hw/bsp/stm32h7rs/family.c
index b0841c947..385b3c929 100644
--- a/hw/bsp/stm32h7rs/family.c
+++ b/hw/bsp/stm32h7rs/family.c
@@ -98,17 +98,24 @@ void OTG_HS_IRQHandler(void) {
#ifdef TRACE_ETM
void trace_etm_init(void) {
- // H7 trace pin is PE2 to PE6
- GPIO_InitTypeDef gpio_init;
- gpio_init.Pin = GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6;
+ // Nucleo-H7S3L8 routes 4-bit trace to the CN1 MIPI20: TRACE_CLK/D0 on
+ // PE2/PE3, TRACE_D1/D2/D3 on the PG14/PD2/PC12 alternates (MB1737 Table 7).
+ // No pull: pull-ups degrade the edges at the 100 MHz trace clock (= cpu/3/2)
+ GPIO_InitTypeDef gpio_init;
gpio_init.Mode = GPIO_MODE_AF_PP;
- gpio_init.Pull = GPIO_PULLUP;
+ gpio_init.Pull = GPIO_NOPULL;
gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio_init.Alternate = GPIO_AF0_TRACE;
+ gpio_init.Pin = GPIO_PIN_2 | GPIO_PIN_3;
HAL_GPIO_Init(GPIOE, &gpio_init);
+ gpio_init.Pin = GPIO_PIN_14;
+ HAL_GPIO_Init(GPIOG, &gpio_init);
+ gpio_init.Pin = GPIO_PIN_2;
+ HAL_GPIO_Init(GPIOD, &gpio_init);
+ gpio_init.Pin = GPIO_PIN_12;
+ HAL_GPIO_Init(GPIOC, &gpio_init);
- // Enable trace clk, also in D1 and D3 domain
- DBGMCU->CR |= DBGMCU_CR_DBG_TRACECKEN | DBGMCU_CR_DBG_CKD1EN | DBGMCU_CR_DBG_CKD3EN;
+ DBGMCU->CR |= DBGMCU_CR_DBGCKEN | DBGMCU_CR_TRACECLKEN;
}
#else
#define trace_etm_init()
diff --git a/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h b/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h
index be9ea7a31..873c004d9 100644
--- a/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h
+++ b/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h
@@ -152,7 +152,13 @@ static void SystemClock_Config(void) {
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2 | RCC_CLOCKTYPE_PCLK4 | RCC_CLOCKTYPE_PCLK5);
RCC_ClkInitStruct.CPUCLKSource = RCC_CPUCLKSOURCE_IC1;
RCC_ClkInitStruct.IC1Selection.ClockSelection = RCC_ICCLKSOURCE_PLL1;
+#ifdef TRACE_ETM
+ // 300 MHz CPU -> 37.5 MHz TPIU clock (fixed cpu/8): at 600 MHz the trace
+ // stream dies with unknown-packet decode errors in the startup burst
+ RCC_ClkInitStruct.IC1Selection.ClockDivider = 4;
+#else
RCC_ClkInitStruct.IC1Selection.ClockDivider = 2;
+#endif
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_IC2_IC6_IC11;
RCC_ClkInitStruct.IC2Selection.ClockSelection = RCC_ICCLKSOURCE_PLL1;
RCC_ClkInitStruct.IC2Selection.ClockDivider = 3;
diff --git a/hw/bsp/stm32n6/boards/stm32n657nucleo/ozone/stm32n657.jdebug b/hw/bsp/stm32n6/boards/stm32n657nucleo/ozone/stm32n657.jdebug
new file mode 100644
index 000000000..16eead280
--- /dev/null
+++ b/hw/bsp/stm32n6/boards/stm32n657nucleo/ozone/stm32n657.jdebug
@@ -0,0 +1,94 @@
+/*********************************************************************
+*
+* OnProjectLoad
+*
+* Function description
+* Project load routine. Required.
+*
+* Notes
+* Nucleo-N657X0-Q wires 4-bit trace to the CN1 MIPI20 natively (SB36/38/
+* 39/40/41 fitted by default): TRACE_CLK = PB3, D0/D1/D2/D3 =
+* PE3/PB0/PB6/PB7 (MB1940 Table 5). No J-Link script needed - the N6
+* trace components sit behind a ROM table J-Link discovers natively.
+*
+* BOOT1 (JP2) must select Development boot (BOOT1 = 1): the N657 is
+* flashless, the app is a RAM image (AXISRAM2) loaded by the debugger,
+* and in flash boot the bootROM parks the chip un-attachable.
+* Firmware must be built with TRACE_ETM=1 (pin mux + DBGMCU
+* DBGCLKEN/TRACECLKEN). Trace clock = cpu/8. TRACE_ETM builds run a 300 MHz core (board.h) ->
+* 37.5 MHz TPIU clock: 600 MHz kills the stream in the startup burst.
+*
+**********************************************************************
+*/
+void OnProjectLoad (void) {
+ Project.SetTraceSource ("Trace Pins");
+ Project.SetTracePortWidth (4);
+ Project.SetSWO (0);
+ Edit.SysVar (VAR_TRACE_CORE_CLOCK, 300000000);
+ Project.AddSvdFile ("$(InstallDir)/Config/CPU/Cortex-M55F.svd");
+
+ Project.SetDevice ("STM32N657X0");
+ Project.SetHostIF ("USB", "");
+ Project.SetTargetIF ("SWD");
+ Project.SetTIFSpeed ("4 MHz");
+
+ File.Open ("../../../../../../examples/cmake-build-stm32n657nucleo/device/cdc_msc/cdc_msc.elf");
+}
+
+/*********************************************************************
+*
+* AfterTargetReset
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetReset (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
+
+/*********************************************************************
+*
+* AfterTargetDownload
+*
+* Function description
+* Event handler routine.
+* - Sets the PC register to program reset value.
+* - Sets the SP register to program reset value on Cortex-M.
+*
+**********************************************************************
+*/
+void AfterTargetDownload (void) {
+ unsigned int SP;
+ unsigned int PC;
+ unsigned int VectorTableAddr;
+
+ VectorTableAddr = Elf.GetBaseAddr();
+
+ if (VectorTableAddr == 0xFFFFFFFF) {
+ Util.Log("Project file error: failed to get program base");
+ } else {
+ SP = Target.ReadU32(VectorTableAddr);
+ Target.SetReg("SP", SP);
+
+ PC = Target.ReadU32(VectorTableAddr + 4);
+ Target.SetReg("PC", PC);
+ }
+}
diff --git a/hw/bsp/stm32n6/family.c b/hw/bsp/stm32n6/family.c
index 80de20c6a..9d3faa1f2 100644
--- a/hw/bsp/stm32n6/family.c
+++ b/hw/bsp/stm32n6/family.c
@@ -110,6 +110,27 @@ void USB1_OTG_HS_IRQHandler(void) {
tusb_int_handler(0, true);
}
+#ifdef TRACE_ETM
+static void trace_etm_init(void) {
+ // Nucleo-N657X0-Q routes 4-bit trace to the CN1 MIPI20 natively:
+ // TRACE_CLK = PB3, D0 = PE3, D1 = PB0, D2 = PB6, D3 = PB7 (MB1940 Table 5)
+ GPIO_InitTypeDef gpio_init;
+ gpio_init.Mode = GPIO_MODE_AF_PP;
+ gpio_init.Pull = GPIO_NOPULL;
+ gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
+ gpio_init.Alternate = GPIO_AF0_TRACE;
+ gpio_init.Pin = GPIO_PIN_0 | GPIO_PIN_3 | GPIO_PIN_6 | GPIO_PIN_7;
+ HAL_GPIO_Init(GPIOB, &gpio_init);
+ gpio_init.Pin = GPIO_PIN_3;
+ HAL_GPIO_Init(GPIOE, &gpio_init);
+
+ // trace clock (ck_cpu_tpiu) is a fixed cpu/8 - just enable it
+ DBGMCU->CR |= DBGMCU_CR_DBGCLKEN | DBGMCU_CR_TRACECLKEN;
+}
+#else
+ #define trace_etm_init()
+#endif
+
void board_init(void) {
/* Enable BusFault and SecureFault handlers (HardFault is default) */
SCB->SHCSR |= (SCB_SHCSR_BUSFAULTENA_Msk | SCB_SHCSR_SECUREFAULTENA_Msk);
@@ -148,6 +169,7 @@ void board_init(void) {
for (uint8_t i = 0; i < TU_ARRAY_SIZE(board_pindef); i++) {
HAL_GPIO_Init(board_pindef[i].port, &board_pindef[i].pin_init);
}
+ trace_etm_init();
NVIC_SetPriority(UCPD1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),5, 0));
NVIC_EnableIRQ(UCPD1_IRQn);