diff options
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); |
