address #3787 reviews: bsp fixes, script hardening, board-note accuracy

Bot findings (Copilot/Codex): no-op board_trace_pinmux stubs for
lpcxpresso18s37/43s67 (TRACE_ETM otherwise broke their build), SAME70
ID_PIOD clock enable, capture-script duplicate BeforeTargetConnect on the
RA references, profile-script support for --no-timestamps itraces.

Deep review (whole branch): same70_xplained board row + caveat restored,
stale pico2 72 MHz claim corrected to the shipped 48, explicit
SetTracePortWidth(4) in the three references that relied on Ozone's
default, coverage-cell guard, median-based SysTick calibration, dead
session flag removed, stale RA8M1 divider comment fixed (0x02 = /4 is the
validated chip max) and the debugger guard indented.

EVKB bench findings: only R1884/D3 remains open (D1/D2 meter-verified);
RT1176 trace width is 1 or 4 only - J-Link arms the CSSYS TPIU and its
own sampler at 4-bit for any width>=2 request; a powered MCU-Link USB
breaks the external probe even with JP4 shorted.
diff --git a/.claude/skills/etm-trace/boards.md b/.claude/skills/etm-trace/boards.md
index ea6c618..ccebefb 100644
--- a/.claude/skills/etm-trace/boards.md
+++ b/.claude/skills/etm-trace/boards.md
@@ -23,11 +23,12 @@
 | 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) | reflow R1882-R1884 (D1-D3 open) → width 4 |
+| 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):
@@ -35,7 +36,7 @@
 - **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 theb
+  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
@@ -76,7 +77,12 @@
   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. FlexSPI apps: ROM bootloader must set SP/PC — the
+  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
@@ -118,7 +124,7 @@
   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 72 MHz from crt0
+  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
@@ -136,5 +142,15 @@
   `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
index 6468be1..af6c1ec 100644
--- a/.claude/skills/etm-trace/scripts/etm_capture.py
+++ b/.claude/skills/etm-trace/scripts/etm_capture.py
@@ -126,6 +126,12 @@
             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":
@@ -472,7 +478,6 @@
                             start_new_session=True)
     profile_out = os.path.join(outdir, "code_profile.txt")
     itrace_out = os.path.join(outdir, "itrace.csv")
-    ok = False
     try:
         ses.connect(20)
         ses.drain(3)  # version banner
@@ -513,7 +518,6 @@
             ses.wait_echo(f'Export.PowerGraphs ("{outdir}/power.csv")', 60)
         ses.send("Debug.Stop", 5)
         ses.send("File.Exit", 2)
-        ok = True
     finally:
         for _ in range(15):
             if proc.poll() is not None:
@@ -551,9 +555,6 @@
         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.")
-    if not ok:
-        sys.exit("error: session did not complete cleanly (see session.log)")
-
     print(f"\ncapture OK: {outdir}")
     print(f"  code_profile.txt  ({os.path.getsize(profile_out)} bytes)")
     if args.trace_csv:
diff --git a/.claude/skills/etm-trace/scripts/etm_profile.py b/.claude/skills/etm-trace/scripts/etm_profile.py
index c5ddf27..b00f6f1 100644
--- a/.claude/skills/etm-trace/scripts/etm_profile.py
+++ b/.claude/skills/etm-trace/scripts/etm_profile.py
@@ -66,7 +66,7 @@
     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:
+        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 name in funcs and m_inst:
@@ -120,14 +120,29 @@
     caller reads unit separately with itrace_unit()."""
     with open(path, newline="", errors="replace") as f:
         rd = csv.reader(f)
-        next(rd, None)
+        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 row[0] == "PC" or len(row) < 2:
+            if not row or len(row) <= addr_i:
                 continue
             try:
-                yield float(row[0]), int(row[1], 16)
+                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):
@@ -144,6 +159,8 @@
     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:
@@ -158,6 +175,8 @@
         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:
@@ -180,6 +199,8 @@
 
     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):
@@ -195,10 +216,12 @@
               f"({len(tick_rows)} rows) - capture with timestamps enabled")
         return
 
-    # rough raw-units-per-1ms from the large mode of consecutive tick deltas
+    # 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 > max(deltas) / 10]
-    raw_ms = statistics.median(big)
+    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
 
@@ -390,11 +413,18 @@
         cshare = {k: v / tc for k, v in cf.items()}
         unit = itrace_unit(itrace)
         print(f"\n## Instruction history (itrace.csv, unit '{unit}')\n")
-        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")
+        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):
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 fdb8b30..80489b6 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", "");
diff --git a/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h b/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h
index 2cf4dbd..1be07c4 100644
--- a/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h
+++ b/hw/bsp/lpc18/boards/lpcxpresso18s37/board.h
@@ -76,6 +76,12 @@
   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/lpc43/boards/lpcxpresso43s67/board.h b/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h
index 4427905..6a317b5 100644
--- a/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h
+++ b/hw/bsp/lpc43/boards/lpcxpresso43s67/board.h
@@ -71,6 +71,12 @@
     {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/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug b/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug
index f0a43bf..927eeda 100644
--- a/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug
+++ b/hw/bsp/ra/boards/ra8m1_ek/ozone/ra8m1.jdebug
@@ -10,6 +10,7 @@
 */
 void OnProjectLoad (void) {
   Project.SetTraceSource ("Trace Pins");
+  Project.SetTracePortWidth (4);
   Project.SetDevice ("R7FA8M1AH");
   Project.SetHostIF ("USB", "");
   Project.SetTargetIF ("SWD");
diff --git a/hw/bsp/ra/family.c b/hw/bsp/ra/family.c
index 3c548fa..3076449 100644
--- a/hw/bsp/ra/family.c
+++ b/hw/bsp/ra/family.c
@@ -103,25 +103,27 @@
   // 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);
+    // TRCKCR is protected by PRCR bit0 register
+    R_SYSTEM->PRCR = (uint16_t) (BSP_PRV_PRCR_KEY | 0x01);
 
-  // TCLK pin = TRCLK/2; set the divider with TRCKEN=0 first (HUM procedure).
-  // Values are the empirical per-board ceilings: one step below the divider
-  // at which the stream dies mid-run.
+    // 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: /8 -> 60 MHz TRCLK, 30 MHz pin (/4 = 60 MHz pin dies)
-  R_SYSTEM->TRCKCR = 0x02;
-  R_SYSTEM->TRCKCR = R_SYSTEM_TRCKCR_TRCKEN_Msk | 0x02;
+    // 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;
+    // 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
 
diff --git a/hw/bsp/same7x/family.c b/hw/bsp/same7x/family.c
index 6a34663..d99c17e 100644
--- a/hw/bsp/same7x/family.c
+++ b/hw/bsp/same7x/family.c
@@ -82,6 +82,7 @@
     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;
diff --git a/hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug b/hw/bsp/stm32h7/boards/stm32h743eval/ozone/stm32h743.jdebug
index a8645c3..f978014 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);