From 44a6db5eb4162fdf72fae96a94fb9a3d649cecce Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 3 May 2026 21:03:35 +0200 Subject: [PATCH 1/5] silicon: NUCLEO-G474RE anchor protocol scaffolding (board-prep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI = Renode (deterministic, parallel-safe). Silicon captures are manual, periodic, and shared across one board per architecture. Recorded captures live in the repo as immutable evidence, citeable from any blog post via stable git URLs. This commit is the scaffolding — protocol doc, build wrapper, board overlay, capture script — that makes a silicon capture a flash-and-go operation the moment hardware is in hand. Files: silicon/README.md Protocol: why we silicon-anchor, the recorded-run-in-git convention, the capture procedure for the NUCLEO-G474RE, the comparison workflow against Renode CI, anchor cadence, and the don't-do-this list (overwriting, mixing pre/post-overhead- compensation captures, claiming WCET). silicon/capture.sh Build + flash + capture + tag + manifest, in one invocation. --board nucleo_g474re --variant {baseline,gale} [--sweep ...]. Auto-detects the serial port on macOS / Linux. Refuses to overwrite an existing dated dir. silicon/capture.py Cross-platform pyserial UART capture. Reads until '=== END ===', times out at the wall clock, writes the raw stream to a file. silicon/boards/nucleo_g474re/{README.md,prj.conf} Board notes + (currently empty) Kconfig overlay. Cortex-M4F + FPU @ 170 MHz, ST-Link/V3E with VCP at 115200, DWT_CYCCNT works identically to stm32f4_disco. Closest production-shape silicon to our existing Renode target. silicon/runs/.gitkeep Placeholder; first dated capture goes in here. Each captured run will commit: - output.csv (raw firmware UART) - events.csv (tagged through tag_events.py) - firmware.elf + firmware.elf.sha256 - manifest.txt (board, MCU, gale_sha, rustc, west, zephyr_sha, ELF sha256, capture timestamp, port, timeout) Manual flow only — no CI changes. README updated to point at silicon/ from the methodology section. Co-Authored-By: Claude Opus 4.7 (1M context) --- benches/engine_control/README.md | 16 ++ benches/engine_control/silicon/README.md | 145 +++++++++++++ .../silicon/boards/nucleo_g474re/README.md | 72 +++++++ .../silicon/boards/nucleo_g474re/prj.conf | 15 ++ benches/engine_control/silicon/capture.py | 91 ++++++++ benches/engine_control/silicon/capture.sh | 198 ++++++++++++++++++ benches/engine_control/silicon/runs/.gitkeep | 0 7 files changed, 537 insertions(+) create mode 100644 benches/engine_control/silicon/README.md create mode 100644 benches/engine_control/silicon/boards/nucleo_g474re/README.md create mode 100644 benches/engine_control/silicon/boards/nucleo_g474re/prj.conf create mode 100755 benches/engine_control/silicon/capture.py create mode 100755 benches/engine_control/silicon/capture.sh create mode 100644 benches/engine_control/silicon/runs/.gitkeep diff --git a/benches/engine_control/README.md b/benches/engine_control/README.md index 2049879..fc48f91 100644 --- a/benches/engine_control/README.md +++ b/benches/engine_control/README.md @@ -86,6 +86,22 @@ measures and what it does NOT measure. That file is the source of truth for any downstream copy (blog posts, reports). Do not embed scope claims in published copy without first updating SCOPE.md. +## Silicon-anchor protocol + +Renode is the CI workhorse; **silicon captures are manual**, periodic, +and recorded directly into the repo as immutable evidence. See +[`silicon/README.md`](silicon/README.md) for the procedure, board +notes, and the `capture.sh` wrapper. Per-board configs live under +`silicon/boards/`; recorded captures land under `silicon/runs//` +with a manifest, the firmware ELF, and the tagged events CSV. + +The first supported board is the NUCLEO-G474RE (STM32G474, Cortex-M4F ++ FPU, 170 MHz) — closest production-shape silicon to the +`stm32f4_disco` Renode target. The ratio `silicon_median / +renode_median` per RPM step is what the anchor establishes; once +consistent across multiple captures it can be cited as the +Renode-silicon multiplier. + ## Building ```sh diff --git a/benches/engine_control/silicon/README.md b/benches/engine_control/silicon/README.md new file mode 100644 index 0000000..3428ad9 --- /dev/null +++ b/benches/engine_control/silicon/README.md @@ -0,0 +1,145 @@ +# Silicon-anchor protocol — engine_control + +CI runs Renode (deterministic, parallel-safe). **Silicon runs are +manual**, periodic, and hand-driven on a single shared board. +This directory contains the protocol for taking a silicon capture, +recording it as immutable evidence in the repo, and citing it as +the anchor for Renode-headlined published numbers. + +## Why + +Renode is per-translated-block instruction-cost simulation, not +microarchitectural simulation: no cache, no memory contention, no +pipeline modeling. The cross-Renode A/B (1.16.0 vs nightly = 0.0% +drift) ruled out simulator-version drift but did NOT rule out +Renode being systematically off vs real silicon by a fixed +multiplier. The silicon anchor settles that. + +The relationship `silicon_cycles / renode_cycles = R` is what the +silicon anchor establishes. Once `R` is consistent across +multiple silicon captures over time, it can be cited as the +Renode-silicon multiplier for that bench/board combination. + +## Recorded-run-in-git protocol + +Every silicon run lives in `silicon/runs/---/` +and contains: + +- `output.csv` — the raw UART capture (firmware-emitted) +- `events.csv` — same data, tagged through `tag_events.py` +- `manifest.txt` — board, MCU, clock, rustc/cargo versions, gale + commit SHA, ELF sha256, capture timestamp +- `firmware.elf` — the exact binary that produced the capture +- `firmware.elf.sha256` — checksum file + +These directories are **immutable** once committed. To re-run the +same capture, create a new dated directory; never overwrite an +existing one. This makes any silicon citation in a blog post or +report point to a stable git URL. + +CSV row counts are small (~50–500 KB per run, ~7,750 rows long +sweep). At one capture per board per major bench-relevant commit, +the repo growth is modest. + +## Boards + +| Board | Status | Anchors | +|---|---|---| +| `nucleo_g474re` (STM32G474RE, Cortex-M4F, 170 MHz) | scaffold ready | the existing Renode `stm32f4_disco` Cortex-M numbers | +| `esp32c3_devkit_rust1` (ESP32-C3, RV32IMC, 160 MHz) | not started | the *future* RISC-V Renode lane (separate work) | + +## Capture procedure (NUCLEO-G474RE) + +Hardware: +- Hardware: STMicroelectronics NUCLEO-G474RE +- Connection: USB to host (ST-Link integrated, virtual COM port at 115200 8N1) +- Programming: `west flash` via OpenOCD or pyOCD (ST-Link backend) + +Host setup (one-time): +- Zephyr SDK with `arm-zephyr-eabi` toolchain +- OpenOCD or pyOCD installed (`brew install open-ocd` on macOS, or `apt install openocd`) +- Python with `pyserial` for the capture script: `pip3 install pyserial` + +To take a baseline capture (stock Zephyr): + +```sh +cd $GALE_ROOT +bash benches/engine_control/silicon/capture.sh \ + --board nucleo_g474re \ + --variant baseline \ + --sweep long +``` + +To take a gale capture: + +```sh +bash benches/engine_control/silicon/capture.sh \ + --board nucleo_g474re \ + --variant gale \ + --sweep long +``` + +Both invocations: + +1. Build the firmware locally (no Bazel; `west build -b `). +2. Compute the firmware ELF sha256. +3. Flash via `west flash`. +4. Open the board's USB CDC serial port and read until `=== END ===` + (default timeout: 30 minutes for `--sweep long`). +5. Generate `manifest.txt` from the build environment + capture + metadata. +6. Tag the raw output through `tag_events.py` (run-id auto-derived + from the date + board). +7. Write everything into a new `silicon/runs//`. + +The capture script does not commit. After both variants are +captured and you've eyeballed `output.csv` for sanity, commit: + +```sh +git add benches/engine_control/silicon/runs/-nucleo_g474re-*-{baseline,gale}/ +git commit -m "silicon: NUCLEO-G474RE anchor at gale@" +``` + +## Comparing silicon vs Renode + +Once `silicon/runs/-{baseline,gale}/` exist, run: + +```sh +python3 benches/engine_control/analyze.py \ + --baseline silicon/runs//events.csv \ + --gale silicon/runs//events.csv \ + --runs 1 \ + > /tmp/silicon-comparison.md +``` + +The analyzer renders the same baseline-vs-gale tables as for +Renode, but the metadata in the report header carries through the +silicon-run identifiers. Compare side-by-side with the Renode CI +output for the same gale SHA — the **ratio** `silicon_median / +renode_median` per RPM step is the calibration data. + +If you want a single-call Renode-vs-silicon side-by-side rendering, +that's a planned analyzer extension (`--silicon-anchor `) +to be added once the first capture exists to test against. + +## Anchor cadence + +- One silicon capture per board per major bench-relevant gale + commit (e.g., when overhead compensation lands, when synth + pipeline changes, when a primitive's hot-path is rewritten). +- Each Renode-headlined publication cites the most recent matching + anchor by stable git URL. +- Three to four anchor points per board per year is enough to + claim the Renode-silicon relationship is monotonic. + +## Don't + +- Don't overwrite an existing `runs//` — start a new one. +- Don't combine pre-overhead-compensation and post-overhead- + compensation captures in the same comparison table; they're + different measurements (see `../SCOPE.md`). +- Don't claim WCET from silicon captures. Worst-case-observed is + not WCET. Same rule as the synthetic bench (see `../SCOPE.md`). +- Don't run silicon captures from a branch that isn't reproducible + (uncommitted changes). The manifest captures the working-tree + state, not just HEAD. diff --git a/benches/engine_control/silicon/boards/nucleo_g474re/README.md b/benches/engine_control/silicon/boards/nucleo_g474re/README.md new file mode 100644 index 0000000..a5df2ba --- /dev/null +++ b/benches/engine_control/silicon/boards/nucleo_g474re/README.md @@ -0,0 +1,72 @@ +# NUCLEO-G474RE — silicon-anchor board notes + +## Hardware + +- **Board:** STMicroelectronics NUCLEO-G474RE +- **MCU:** STM32G474RET6 (Cortex-M4F + FPU + DSP, 170 MHz) +- **Memory:** 512 KB Flash, 128 KB RAM +- **Cycle counter:** DWT_CYCCNT (same as Cortex-M4F on `stm32f4_disco`) +- **Programmer:** integrated ST-Link/V3E over USB; exposes virtual + COM port for stdout +- **Upstream Zephyr support:** `nucleo_g474re` (already in the tree) + +## Why this board for the anchor + +Cortex-M4F + FPU at 170 MHz is the closest production-shape silicon +to the simulated `stm32f4_disco` (also Cortex-M4F + FPU at 168 MHz). +The architectural variables held constant between the synthetic and +silicon measurements are: + +- ARMv7E-M instruction set (Thumb-2) +- DWT_CYCCNT cycle counter (same width, same definition) +- 3-stage in-order pipeline +- Single-cycle MUL, hardware DIV, single-precision FPU + +What differs: + +- Real cache effects (none on Cortex-M4 — no D-cache; flash + prefetch buffer behavior visible) +- Real bus arbitration with non-existent peripherals on this bench + (negligible — no DMA, no peripheral activity) +- Clock 170 vs 168 MHz (1.2% — accountable directly) + +So the cycle ratio `silicon / renode` for `algo` and `handoff` +should be near 1.0 in steady state. Anything materially off is +information about Renode's cycle model, not about the silicon. + +## Connection + +USB cable from NUCLEO USB connector (CN1) to host. The ST-Link +virtual COM port appears as: + +- macOS: `/dev/cu.usbmodem*` +- Linux: `/dev/ttyACM0` + +Zephyr's default for this board uses LPUART1 for stdout, exposed +through ST-Link. + +## Programming + +`west flash` from a build directory works out of the box: + +```sh +west flash -d /tmp/eng-nucleo-baseline +``` + +Default backend is OpenOCD. To force pyOCD: + +```sh +west flash -d /tmp/eng-nucleo-baseline --runner pyocd +``` + +## Clock / cycle counter notes + +On the G4 family, `k_cycle_get_32()` returns `SCB_DWT->CYCCNT` +directly, same as on F4. `sys_clock_hw_cycles_per_sec()` returns +the bus clock the cycle counter ticks at — verify this matches +170 MHz at runtime by reading the boot banner before relying on +absolute ns conversions. + +## Known issues + +None yet — populate as captures happen. diff --git a/benches/engine_control/silicon/boards/nucleo_g474re/prj.conf b/benches/engine_control/silicon/boards/nucleo_g474re/prj.conf new file mode 100644 index 0000000..1cb76bc --- /dev/null +++ b/benches/engine_control/silicon/boards/nucleo_g474re/prj.conf @@ -0,0 +1,15 @@ +# NUCLEO-G474RE — engine_control bench overlay +# +# Empty for now: Zephyr's nucleo_g474re defaults give us: +# - 170 MHz HCLK (PLL'd up) +# - LPUART1 console at 115200 8N1 via ST-Link VCP +# - DWT_CYCCNT enabled (Cortex-M4 default in Zephyr) +# +# Add overlay options here only if a future capture exposes a +# default that biases the measurement (e.g. interrupt priority of +# a peripheral we don't use; tickless idle behavior; etc.). +# +# Anything board-specific that *must* be on for the silicon +# measurement to be valid goes here. Anything project-wide +# (gale module enable, sweep size) stays in the main prj.conf +# overlay or the CMake invocation. diff --git a/benches/engine_control/silicon/capture.py b/benches/engine_control/silicon/capture.py new file mode 100755 index 0000000..2e59421 --- /dev/null +++ b/benches/engine_control/silicon/capture.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Cross-platform UART capture for the silicon-anchor protocol. + +Reads lines from a serial port until either a sentinel line +(default '=== END ===') appears, the byte budget is exhausted, or +the wall-clock timeout fires. Writes the raw stream to stdout (or +to a file with --out). + +Designed to be invoked by capture.sh — keep this script's +dependencies minimal: stdlib + pyserial. + +Usage: + capture.py --port /dev/cu.usbmodem11403 --baud 115200 \\ + --sentinel '=== END ===' --timeout 1800 \\ + --out output.csv +""" +from __future__ import annotations + +import argparse +import sys +import time + +try: + import serial # type: ignore +except ImportError: + sys.stderr.write( + "ERROR: pyserial not installed. Run: pip3 install pyserial\n") + sys.exit(2) + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--port", required=True, + help="serial device path (e.g. /dev/cu.usbmodem11403)") + p.add_argument("--baud", type=int, default=115200, + help="baud rate (default 115200)") + p.add_argument("--sentinel", default="=== END ===", + help="line marking end-of-capture") + p.add_argument("--timeout", type=int, default=1800, + help="wall-clock timeout in seconds (default 1800)") + p.add_argument("--out", default="-", + help="output path or '-' for stdout (default '-')") + p.add_argument("--max-bytes", type=int, default=64 * 1024 * 1024, + help="byte-budget ceiling (default 64 MiB)") + args = p.parse_args() + + out = sys.stdout if args.out == "-" else open(args.out, "w") + deadline = time.monotonic() + args.timeout + bytes_written = 0 + sentinel_seen = False + + try: + # serial timeout = 1s so we wake periodically to check the + # wall-clock budget even if the firmware is silent. + ser = serial.Serial(args.port, args.baud, timeout=1) + except serial.SerialException as e: + sys.stderr.write(f"ERROR opening {args.port}: {e}\n") + return 3 + + try: + while time.monotonic() < deadline and bytes_written < args.max_bytes: + line_bytes = ser.readline() + if not line_bytes: + continue # serial timeout, loop back to check deadline + try: + line = line_bytes.decode("utf-8", errors="replace") + except Exception: + line = line_bytes.decode("latin-1", errors="replace") + out.write(line) + out.flush() + bytes_written += len(line_bytes) + if line.rstrip("\r\n") == args.sentinel: + sentinel_seen = True + break + finally: + ser.close() + if out is not sys.stdout: + out.close() + + if not sentinel_seen: + sys.stderr.write( + f"WARN: sentinel '{args.sentinel}' not seen " + f"(timeout={args.timeout}s, bytes={bytes_written})\n") + return 1 + sys.stderr.write( + f"OK: sentinel seen at {bytes_written} bytes\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benches/engine_control/silicon/capture.sh b/benches/engine_control/silicon/capture.sh new file mode 100755 index 0000000..e8b7560 --- /dev/null +++ b/benches/engine_control/silicon/capture.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# Silicon-anchor capture wrapper for engine_control. +# +# Builds, flashes, and captures one variant on a real board, then +# writes the result + manifest into a dated directory under runs/. +# Manual flow — not invoked from CI. +# +# Usage: +# capture.sh --board nucleo_g474re --variant {baseline,gale} \ +# [--sweep {short,long}] [--port /dev/cu.usbmodem11403] +# +# Defaults: +# --sweep short (use --sweep long for the publication-grade run) +# --port: auto-detect first /dev/cu.usbmodem* (macOS) or +# /dev/ttyACM0 (Linux). Override if multiple boards present. + +set -euo pipefail + +# --------------------------------------------------------------------- args +BOARD="" +VARIANT="" +SWEEP="short" +PORT="" +SILICON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GALE_ROOT="$(cd "${SILICON_DIR}/../../.." && pwd)" + +while [[ $# -gt 0 ]]; do + case "$1" in + --board) BOARD="$2"; shift 2 ;; + --variant) VARIANT="$2"; shift 2 ;; + --sweep) SWEEP="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + -h|--help) + sed -n '1,/^set -/p' "$0" | sed -n 's/^# \?//p'; exit 0 ;; + *) + echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +[[ -z "$BOARD" ]] && { echo "missing --board" >&2; exit 2; } +[[ -z "$VARIANT" ]] && { echo "missing --variant" >&2; exit 2; } +case "$VARIANT" in baseline|gale) ;; *) + echo "--variant must be 'baseline' or 'gale'" >&2; exit 2 ;; +esac +case "$SWEEP" in short|long) ;; *) + echo "--sweep must be 'short' or 'long'" >&2; exit 2 ;; +esac + +# Verify board overlay exists in our silicon/boards/ tree +BOARD_DIR="${SILICON_DIR}/boards/${BOARD}" +if [[ ! -d "$BOARD_DIR" ]]; then + echo "no silicon overlay for board '$BOARD' at $BOARD_DIR" >&2 + echo "supported: $(ls "${SILICON_DIR}/boards/" 2>/dev/null | tr '\n' ' ')" >&2 + exit 2 +fi + +# --------------------------------------------------------------------- env +: "${ZEPHYR_BASE:?need ZEPHYR_BASE in env}" +: "${ZEPHYR_SDK_INSTALL_DIR:=}" # optional; west picks one up if unset +GALE_SHA_FULL="$(git -C "$GALE_ROOT" rev-parse HEAD)" +GALE_SHA="${GALE_SHA_FULL:0:8}" +DATE="$(date -u +%Y-%m-%d)" +RUNS_DIR_BASE="${SILICON_DIR}/runs" +RUN_DIR="${RUNS_DIR_BASE}/${DATE}-${BOARD}-${GALE_SHA}-${VARIANT}" +BUILD_DIR="/tmp/silicon-${BOARD}-${VARIANT}" + +if [[ -d "$RUN_DIR" ]]; then + echo "ERROR: run dir already exists: $RUN_DIR" >&2 + echo "Per protocol, never overwrite. Start a new dated dir or delete the old one." >&2 + exit 3 +fi + +# --------------------------------------------------------------------- port autodetect +if [[ -z "$PORT" ]]; then + case "$(uname -s)" in + Darwin) + PORT="$(ls /dev/cu.usbmodem* 2>/dev/null | head -1 || true)" ;; + Linux) + PORT="$(ls /dev/ttyACM* 2>/dev/null | head -1 || true)" ;; + esac + [[ -z "$PORT" ]] && { + echo "could not auto-detect serial port; pass --port" >&2; exit 2; + } + echo "auto-detected port: $PORT" +fi + +# --------------------------------------------------------------------- build +echo "==> Building $VARIANT for $BOARD (sweep=$SWEEP)" +WEST_ARGS=( -b "$BOARD" -d "$BUILD_DIR" -s "${GALE_ROOT}/benches/engine_control" ) +WEST_DEFINES=( -DENGINE_BENCH_SWEEP="$SWEEP" ) + +# Layer the board's silicon-overlay if it has anything. +BOARD_OVERLAY="${BOARD_DIR}/prj.conf" +if [[ -s "$BOARD_OVERLAY" ]]; then + # If gale variant, append after the gale overlay; if baseline, this + # is the only overlay. + if [[ "$VARIANT" == "gale" ]]; then + WEST_DEFINES+=( + -DZEPHYR_EXTRA_MODULES="$GALE_ROOT" + -DOVERLAY_CONFIG="${GALE_ROOT}/benches/engine_control/prj-gale.conf;${BOARD_OVERLAY}" + ) + else + WEST_DEFINES+=( -DOVERLAY_CONFIG="${BOARD_OVERLAY}" ) + fi +elif [[ "$VARIANT" == "gale" ]]; then + WEST_DEFINES+=( + -DZEPHYR_EXTRA_MODULES="$GALE_ROOT" + -DOVERLAY_CONFIG="${GALE_ROOT}/benches/engine_control/prj-gale.conf" + ) +fi + +rm -rf "$BUILD_DIR" +( cd "$GALE_ROOT/.." && west build -p auto "${WEST_ARGS[@]}" -- "${WEST_DEFINES[@]}" ) + +ELF="${BUILD_DIR}/zephyr/zephyr.elf" +[[ ! -f "$ELF" ]] && { echo "build did not produce $ELF" >&2; exit 4; } + +# --------------------------------------------------------------------- record +mkdir -p "$RUN_DIR" +cp "$ELF" "$RUN_DIR/firmware.elf" + +if command -v sha256sum >/dev/null 2>&1; then + ELF_SHA="$(sha256sum "$ELF" | awk '{print $1}')" +else + ELF_SHA="$(shasum -a 256 "$ELF" | awk '{print $1}')" # macOS fallback +fi +echo "$ELF_SHA firmware.elf" > "$RUN_DIR/firmware.elf.sha256" + +# --------------------------------------------------------------------- flash +echo "==> Flashing" +( cd "$GALE_ROOT/.." && west flash -d "$BUILD_DIR" ) + +# --------------------------------------------------------------------- capture +# Long sweep can take a few minutes wall-time at 168 MHz; short ~10s. +TIMEOUT=1800 # 30 min +[[ "$SWEEP" == "short" ]] && TIMEOUT=120 + +echo "==> Capturing from $PORT (timeout ${TIMEOUT}s)" +python3 "${SILICON_DIR}/capture.py" \ + --port "$PORT" --baud 115200 \ + --sentinel "=== END ===" \ + --timeout "$TIMEOUT" \ + --out "$RUN_DIR/output.csv" + +# --------------------------------------------------------------------- tag +RUN_ID="silicon-${DATE}" # deterministic per-day-per-board; tag_events + # prefixes with R, so this becomes R-silicon-... +python3 "${GALE_ROOT}/benches/engine_control/tag_events.py" \ + "$RUN_DIR/output.csv" "$RUN_ID" "$VARIANT" \ + > "$RUN_DIR/events.csv" + +# --------------------------------------------------------------------- manifest +MANIFEST="$RUN_DIR/manifest.txt" +{ + echo "# Silicon-anchor manifest" + echo "# Produced by benches/engine_control/silicon/capture.sh" + echo "captured_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "board: ${BOARD}" + echo "variant: ${VARIANT}" + echo "sweep: ${SWEEP}" + echo "gale_sha: ${GALE_SHA_FULL}" + echo "gale_status: $(cd "$GALE_ROOT" && git status --porcelain | wc -l | tr -d ' ') uncommitted file(s)" + echo "host: $(uname -srm)" + echo "rustc: $(rustc --version 2>&1 | head -1)" + echo "cargo: $(cargo --version 2>&1 | head -1)" + echo "west: $(west --version 2>&1 | head -1)" + echo "zephyr_base: ${ZEPHYR_BASE}" + echo "zephyr_sha: $(git -C "$ZEPHYR_BASE" rev-parse HEAD 2>/dev/null || echo unknown)" + echo "sdk_dir: ${ZEPHYR_SDK_INSTALL_DIR:-auto-detected by west}" + echo "elf_sha256: ${ELF_SHA}" + echo "csv_sha256: $(sha256sum "$RUN_DIR/output.csv" 2>/dev/null \ + || shasum -a 256 "$RUN_DIR/output.csv") | awk '{print $1}'" + echo "csv_bytes: $(wc -c < "$RUN_DIR/output.csv" | tr -d ' ')" + echo "csv_event_lines: $(grep -c '^E,' "$RUN_DIR/output.csv" || echo 0)" + echo "serial_port: ${PORT}" + echo "capture_timeout_s: ${TIMEOUT}" +} > "$MANIFEST" + +# --------------------------------------------------------------------- summary +echo +echo "==========================================================" +echo " Silicon capture complete" +echo " board: $BOARD" +echo " variant: $VARIANT" +echo " sweep: $SWEEP" +echo " events: $(grep -c '^E,' "$RUN_DIR/output.csv" || echo 0)" +echo " manifest: $MANIFEST" +echo " events.csv: $RUN_DIR/events.csv" +echo "==========================================================" +echo +echo "Next steps:" +echo " 1) sanity-check the output: head -20 $RUN_DIR/output.csv" +echo " 2) commit the run dir:" +echo " git add benches/engine_control/silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-${VARIANT}" +echo " 3) (after both variants captured) compare against the matching Renode CI:" +echo " python3 benches/engine_control/analyze.py \\" +echo " --baseline silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-baseline/events.csv \\" +echo " --gale silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-gale/events.csv" diff --git a/benches/engine_control/silicon/runs/.gitkeep b/benches/engine_control/silicon/runs/.gitkeep new file mode 100644 index 0000000..e69de29 From 8670b11d49f51e525a3386feb02f25f72c151c33 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 9 May 2026 17:58:33 +0200 Subject: [PATCH 2/5] fix(silicon): repair capture.sh --help and manifest csv_sha256 escaping Two follow-on fixups on the silicon-anchor capture wrapper, surfaced while preparing first-capture for the NUCLEO-G474RE on macOS: 1. --help printed nothing on macOS. The sed extractor used GNU-only `\?` for "0 or 1 space"; on BSD sed the pattern is treated as a literal `?` and never matches. Replaced with a portable awk one-liner that also skips the shebang line. 2. The manifest's `csv_sha256:` line had `| awk '{print $1}'` outside the `$(...)` command-substitution, so the manifest got the literal pipeline text instead of the hash. Wrapped the `||` group in `{ ...; }` so the pipe applies to either branch. Both are cosmetic but block automated parsing of the manifest and discovering the script's own usage. --- benches/engine_control/silicon/capture.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benches/engine_control/silicon/capture.sh b/benches/engine_control/silicon/capture.sh index e8b7560..73caa7e 100755 --- a/benches/engine_control/silicon/capture.sh +++ b/benches/engine_control/silicon/capture.sh @@ -31,7 +31,7 @@ while [[ $# -gt 0 ]]; do --sweep) SWEEP="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; -h|--help) - sed -n '1,/^set -/p' "$0" | sed -n 's/^# \?//p'; exit 0 ;; + awk '/^set -/{exit} NR>1{sub(/^# ?/, ""); print}' "$0"; exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac @@ -168,8 +168,8 @@ MANIFEST="$RUN_DIR/manifest.txt" echo "zephyr_sha: $(git -C "$ZEPHYR_BASE" rev-parse HEAD 2>/dev/null || echo unknown)" echo "sdk_dir: ${ZEPHYR_SDK_INSTALL_DIR:-auto-detected by west}" echo "elf_sha256: ${ELF_SHA}" - echo "csv_sha256: $(sha256sum "$RUN_DIR/output.csv" 2>/dev/null \ - || shasum -a 256 "$RUN_DIR/output.csv") | awk '{print $1}'" + echo "csv_sha256: $({ sha256sum "$RUN_DIR/output.csv" 2>/dev/null \ + || shasum -a 256 "$RUN_DIR/output.csv"; } | awk '{print $1}')" echo "csv_bytes: $(wc -c < "$RUN_DIR/output.csv" | tr -d ' ')" echo "csv_event_lines: $(grep -c '^E,' "$RUN_DIR/output.csv" || echo 0)" echo "serial_port: ${PORT}" From 5690d52256f9088d3810734612e77baa38442a12 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 9 May 2026 18:53:40 +0200 Subject: [PATCH 3/5] silicon: add tick-source axis (systick / lptim) for 4-run anchor matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A publication-grade silicon-anchor capture is the matrix variant ∈ {baseline, gale} × tick_source ∈ {systick, lptim} not just two variants — LPTIM has different jitter and ISR-overhead characteristics than the Cortex-M default SysTick, so the silicon / renode multiplier must be reported per tick_source to be meaningful. Changes: - capture.sh - new --tick-source {systick,lptim} flag (default: systick) - OVERLAY_CONFIG composed from up to 3 ordered layers: 1. gale overlay (when --variant gale) 2. board silicon overlay (silicon/boards//prj.conf) 3. tick-source overlay (silicon/boards//prj-tick-.conf) - tick_source embedded in BUILD_DIR and RUN_DIR so 4 runs don't collide - manifest gains `tick_source:` field - summary block + post-capture commit hint reflect the 4-run protocol - silicon/boards/nucleo_g474re/prj-tick-lptim.conf - new overlay enabling STM32_LPTIM_TIMER and disabling CORTEX_M_SYSTICK - documented clock-source caveat: LSE-clocked LPTIM cannot sustain the bench's 100 kHz tick; a DT overlay layering LPTIM1 onto PCLK1 is needed for apples-to-apples vs SysTick — flagged in the board README as a follow-up - silicon/README.md - run-dir naming now includes tick_source - capture procedure shows the 4-run loop - smoke-run instruction added (drop --sweep long, omit --tick-source) - commit hint updated to grab all 4 dirs at once - silicon/boards/nucleo_g474re/README.md - new "Kernel tick sources" section with the per-source overlay table and the LPTIM clock-source caveat No firmware code touched (still consistent with PR #37's stated scope). Smart-data emission (DWT counters + STM32 self-monitoring) is the follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- benches/engine_control/silicon/README.md | 49 +++++++----- .../silicon/boards/nucleo_g474re/README.md | 27 +++++++ .../boards/nucleo_g474re/prj-tick-lptim.conf | 23 ++++++ benches/engine_control/silicon/capture.sh | 77 ++++++++++++------- 4 files changed, 131 insertions(+), 45 deletions(-) create mode 100644 benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf diff --git a/benches/engine_control/silicon/README.md b/benches/engine_control/silicon/README.md index 3428ad9..97556a7 100644 --- a/benches/engine_control/silicon/README.md +++ b/benches/engine_control/silicon/README.md @@ -22,7 +22,8 @@ Renode-silicon multiplier for that bench/board combination. ## Recorded-run-in-git protocol -Every silicon run lives in `silicon/runs/---/` +Every silicon run lives in +`silicon/runs/----/` and contains: - `output.csv` — the raw UART capture (firmware-emitted) @@ -60,25 +61,35 @@ Host setup (one-time): - OpenOCD or pyOCD installed (`brew install open-ocd` on macOS, or `apt install openocd`) - Python with `pyserial` for the capture script: `pip3 install pyserial` -To take a baseline capture (stock Zephyr): +A publication-grade anchor on a given board is the **4-run matrix**: -```sh -cd $GALE_ROOT -bash benches/engine_control/silicon/capture.sh \ - --board nucleo_g474re \ - --variant baseline \ - --sweep long -``` +| variant | tick_source | command | +|---|---|---| +| baseline | systick | `--variant baseline --tick-source systick` | +| baseline | lptim | `--variant baseline --tick-source lptim` | +| gale | systick | `--variant gale --tick-source systick` | +| gale | lptim | `--variant gale --tick-source lptim` | -To take a gale capture: +The two tick-source variants exist because LPTIM has different jitter +and ISR-overhead characteristics than the Cortex-M default SysTick; +the `silicon / renode` multiplier is reported per `tick_source`. ```sh -bash benches/engine_control/silicon/capture.sh \ - --board nucleo_g474re \ - --variant gale \ - --sweep long +cd $GALE_ROOT +for V in baseline gale; do + for T in systick lptim; do + bash benches/engine_control/silicon/capture.sh \ + --board nucleo_g474re \ + --variant "$V" \ + --tick-source "$T" \ + --sweep long + done +done ``` +For a smoke run (does the board even talk?), drop `--sweep long`, +omit `--tick-source` (defaults to `systick`), and pick one variant. + Both invocations: 1. Build the firmware locally (no Bazel; `west build -b `). @@ -92,12 +103,14 @@ Both invocations: from the date + board). 7. Write everything into a new `silicon/runs//`. -The capture script does not commit. After both variants are -captured and you've eyeballed `output.csv` for sanity, commit: +The capture script does not commit. After all four runs are +captured and you've eyeballed `output.csv` for sanity, commit +the whole 4-run set together so analyze.py can compute the +matrix in one pass: ```sh -git add benches/engine_control/silicon/runs/-nucleo_g474re-*-{baseline,gale}/ -git commit -m "silicon: NUCLEO-G474RE anchor at gale@" +git add benches/engine_control/silicon/runs/-nucleo_g474re-*-{baseline,gale}-{systick,lptim}/ +git commit -m "silicon: NUCLEO-G474RE 4-run anchor at gale@" ``` ## Comparing silicon vs Renode diff --git a/benches/engine_control/silicon/boards/nucleo_g474re/README.md b/benches/engine_control/silicon/boards/nucleo_g474re/README.md index a5df2ba..d90dd2d 100644 --- a/benches/engine_control/silicon/boards/nucleo_g474re/README.md +++ b/benches/engine_control/silicon/boards/nucleo_g474re/README.md @@ -67,6 +67,33 @@ the bus clock the cycle counter ticks at — verify this matches 170 MHz at runtime by reading the boot banner before relying on absolute ns conversions. +## Kernel tick sources + +The silicon-anchor protocol captures both Cortex-M SysTick and STM32 +LPTIM as kernel-tick sources, since each has a different jitter, +drift, and ISR-overhead profile that the published `silicon / renode` +multiplier may be sensitive to. + +| `--tick-source` | Overlay file | Notes | +|---|---|---| +| `systick` (default) | none — Cortex-M default | DWT_CYCCNT-aligned tick, ~1700 cycles per 10 µs at 170 MHz | +| `lptim` | `prj-tick-lptim.conf` | STM32 LPTIM-based tick. See clock-source caveat below. | + +### LPTIM clock-source caveat + +Zephyr's default LPTIM clock is LSE (32.768 kHz). The bench's +`CONFIG_SYS_CLOCK_TICKS_PER_SEC=100000` (10 µs granularity) cannot +run on a 32.768 kHz timer. To make the LPTIM variant apples-to-apples +with SysTick, layer a device-tree overlay that switches LPTIM1 onto +PCLK1 (170 MHz / prescaler). + +A starter `tick-lptim.overlay` is **not** committed yet — the exact +G4 device-tree binding for the `clocks` property needs verification +against `dts/arm/st/g4/stm32g474Xe.dtsi` before it ships. Until that +overlay lands, the LPTIM variant runs at LSE-derived rates and the +manifest's `tick_source: lptim` field is the user's signal that the +two captures are not numerically comparable. + ## Known issues None yet — populate as captures happen. diff --git a/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf b/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf new file mode 100644 index 0000000..0a213ec --- /dev/null +++ b/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf @@ -0,0 +1,23 @@ +# NUCLEO-G474RE — alternate kernel-tick source: STM32 LPTIM +# +# Layered onto the bench prj.conf when capture.sh is invoked with +# `--tick-source lptim`. Compared against the SysTick variant +# (the Cortex-M default) to determine whether the silicon-anchor +# multiplier is sensitive to tick-source choice. +# +# CAVEAT — clock source. +# Zephyr's default LPTIM clock is LSE (32.768 kHz). The engine_control +# bench sets CONFIG_SYS_CLOCK_TICKS_PER_SEC=100000 (10 µs granularity), +# which exceeds what LSE-clocked LPTIM can sustain. To run the bench's +# 100 kHz tick rate on LPTIM, layer a device-tree overlay that switches +# LPTIM1 to PCLK1 (170 MHz / prescaler). A starter overlay lives next +# to this file at `tick-lptim.overlay` (pending DT verification on G4 +# bindings — see board README). +# +# If the DT overlay isn't applied, this build will use LSE-clocked +# LPTIM and Zephyr's tick-rate negotiation will silently cap the tick +# rate; the captured run will NOT be apples-to-apples with the SysTick +# variant. Manifest field `tick_source: lptim` makes that visible. + +CONFIG_CORTEX_M_SYSTICK=n +CONFIG_STM32_LPTIM_TIMER=y diff --git a/benches/engine_control/silicon/capture.sh b/benches/engine_control/silicon/capture.sh index 73caa7e..caeeb09 100755 --- a/benches/engine_control/silicon/capture.sh +++ b/benches/engine_control/silicon/capture.sh @@ -7,12 +7,19 @@ # # Usage: # capture.sh --board nucleo_g474re --variant {baseline,gale} \ +# [--tick-source {systick,lptim}] \ # [--sweep {short,long}] [--port /dev/cu.usbmodem11403] # # Defaults: +# --tick-source systick (Cortex-M default; lptim selects the STM32 +# LPTIM-based kernel tick — see board README +# for the clock-source caveat) # --sweep short (use --sweep long for the publication-grade run) # --port: auto-detect first /dev/cu.usbmodem* (macOS) or # /dev/ttyACM0 (Linux). Override if multiple boards present. +# +# A publication-grade anchor on a given board is the 4-run matrix: +# variant ∈ {baseline, gale} × tick_source ∈ {systick, lptim}. set -euo pipefail @@ -21,15 +28,17 @@ BOARD="" VARIANT="" SWEEP="short" PORT="" +TICK_SOURCE="systick" SILICON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GALE_ROOT="$(cd "${SILICON_DIR}/../../.." && pwd)" while [[ $# -gt 0 ]]; do case "$1" in - --board) BOARD="$2"; shift 2 ;; - --variant) VARIANT="$2"; shift 2 ;; - --sweep) SWEEP="$2"; shift 2 ;; - --port) PORT="$2"; shift 2 ;; + --board) BOARD="$2"; shift 2 ;; + --variant) VARIANT="$2"; shift 2 ;; + --sweep) SWEEP="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --tick-source) TICK_SOURCE="$2"; shift 2 ;; -h|--help) awk '/^set -/{exit} NR>1{sub(/^# ?/, ""); print}' "$0"; exit 0 ;; *) @@ -45,6 +54,9 @@ esac case "$SWEEP" in short|long) ;; *) echo "--sweep must be 'short' or 'long'" >&2; exit 2 ;; esac +case "$TICK_SOURCE" in systick|lptim) ;; *) + echo "--tick-source must be 'systick' or 'lptim'" >&2; exit 2 ;; +esac # Verify board overlay exists in our silicon/boards/ tree BOARD_DIR="${SILICON_DIR}/boards/${BOARD}" @@ -61,8 +73,8 @@ GALE_SHA_FULL="$(git -C "$GALE_ROOT" rev-parse HEAD)" GALE_SHA="${GALE_SHA_FULL:0:8}" DATE="$(date -u +%Y-%m-%d)" RUNS_DIR_BASE="${SILICON_DIR}/runs" -RUN_DIR="${RUNS_DIR_BASE}/${DATE}-${BOARD}-${GALE_SHA}-${VARIANT}" -BUILD_DIR="/tmp/silicon-${BOARD}-${VARIANT}" +RUN_DIR="${RUNS_DIR_BASE}/${DATE}-${BOARD}-${GALE_SHA}-${VARIANT}-${TICK_SOURCE}" +BUILD_DIR="/tmp/silicon-${BOARD}-${VARIANT}-${TICK_SOURCE}" if [[ -d "$RUN_DIR" ]]; then echo "ERROR: run dir already exists: $RUN_DIR" >&2 @@ -85,28 +97,36 @@ if [[ -z "$PORT" ]]; then fi # --------------------------------------------------------------------- build -echo "==> Building $VARIANT for $BOARD (sweep=$SWEEP)" +echo "==> Building $VARIANT for $BOARD (sweep=$SWEEP, tick=$TICK_SOURCE)" WEST_ARGS=( -b "$BOARD" -d "$BUILD_DIR" -s "${GALE_ROOT}/benches/engine_control" ) WEST_DEFINES=( -DENGINE_BENCH_SWEEP="$SWEEP" ) -# Layer the board's silicon-overlay if it has anything. +# Compose OVERLAY_CONFIG from up to three layers, in deterministic order: +# 1. gale primitive overlay (only when --variant gale) +# 2. board silicon-overlay (board-specific defaults) +# 3. tick-source overlay (only when not the board's default tick) +# Zephyr semantics: later overlays override earlier ones. +OVERLAYS=() +[[ "$VARIANT" == "gale" ]] && OVERLAYS+=("${GALE_ROOT}/benches/engine_control/prj-gale.conf") + BOARD_OVERLAY="${BOARD_DIR}/prj.conf" -if [[ -s "$BOARD_OVERLAY" ]]; then - # If gale variant, append after the gale overlay; if baseline, this - # is the only overlay. - if [[ "$VARIANT" == "gale" ]]; then - WEST_DEFINES+=( - -DZEPHYR_EXTRA_MODULES="$GALE_ROOT" - -DOVERLAY_CONFIG="${GALE_ROOT}/benches/engine_control/prj-gale.conf;${BOARD_OVERLAY}" - ) - else - WEST_DEFINES+=( -DOVERLAY_CONFIG="${BOARD_OVERLAY}" ) +[[ -s "$BOARD_OVERLAY" ]] && OVERLAYS+=("$BOARD_OVERLAY") + +# Tick-source overlay: only layered when the user picked a non-default +# tick. SysTick is the Cortex-M default so its overlay (if any) is opt-in. +TICK_OVERLAY="${BOARD_DIR}/prj-tick-${TICK_SOURCE}.conf" +if [[ "$TICK_SOURCE" != "systick" ]]; then + if [[ ! -s "$TICK_OVERLAY" ]]; then + echo "no tick-source overlay for '$TICK_SOURCE' at $TICK_OVERLAY" >&2 + exit 2 fi -elif [[ "$VARIANT" == "gale" ]]; then - WEST_DEFINES+=( - -DZEPHYR_EXTRA_MODULES="$GALE_ROOT" - -DOVERLAY_CONFIG="${GALE_ROOT}/benches/engine_control/prj-gale.conf" - ) + OVERLAYS+=("$TICK_OVERLAY") +fi + +[[ "$VARIANT" == "gale" ]] && WEST_DEFINES+=( -DZEPHYR_EXTRA_MODULES="$GALE_ROOT" ) +if [[ ${#OVERLAYS[@]} -gt 0 ]]; then + IFS=';' WEST_DEFINES+=( -DOVERLAY_CONFIG="${OVERLAYS[*]}" ) + unset IFS fi rm -rf "$BUILD_DIR" @@ -157,6 +177,7 @@ MANIFEST="$RUN_DIR/manifest.txt" echo "captured_at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "board: ${BOARD}" echo "variant: ${VARIANT}" + echo "tick_source: ${TICK_SOURCE}" echo "sweep: ${SWEEP}" echo "gale_sha: ${GALE_SHA_FULL}" echo "gale_status: $(cd "$GALE_ROOT" && git status --porcelain | wc -l | tr -d ' ') uncommitted file(s)" @@ -182,6 +203,7 @@ echo "==========================================================" echo " Silicon capture complete" echo " board: $BOARD" echo " variant: $VARIANT" +echo " tick_source: $TICK_SOURCE" echo " sweep: $SWEEP" echo " events: $(grep -c '^E,' "$RUN_DIR/output.csv" || echo 0)" echo " manifest: $MANIFEST" @@ -191,8 +213,9 @@ echo echo "Next steps:" echo " 1) sanity-check the output: head -20 $RUN_DIR/output.csv" echo " 2) commit the run dir:" -echo " git add benches/engine_control/silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-${VARIANT}" -echo " 3) (after both variants captured) compare against the matching Renode CI:" +echo " git add benches/engine_control/silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-${VARIANT}-${TICK_SOURCE}" +echo " 3) (after all 4 variant×tick_source runs captured) compare against" +echo " the matching Renode CI:" echo " python3 benches/engine_control/analyze.py \\" -echo " --baseline silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-baseline/events.csv \\" -echo " --gale silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-gale/events.csv" +echo " --baseline silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-baseline-${TICK_SOURCE}/events.csv \\" +echo " --gale silicon/runs/${DATE}-${BOARD}-${GALE_SHA}-gale-${TICK_SOURCE}/events.csv" From ea911ee47f576d5d88aaa591155f4cd2401fdf07 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 9 May 2026 20:46:38 +0200 Subject: [PATCH 4/5] fix(silicon): LPTIM kernel-tick needs CONFIG_PM=y, not CORTEX_M_SYSTICK=n alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local smoke build of the silicon-anchor scaffolding on real Zephyr SDK 1.0.1 + arm-zephyr-eabi-gcc 14.3.0 against the actual Zephyr workspace revealed the original `prj-tick-lptim.conf` doesn't actually switch the kernel tick to LPTIM. Both `baseline/lptim` and `gale/lptim` built configurations failed to link with: zephyr/kernel/libkernel.a(timeout.c.obj): in function `elapsed': timeout.c:70: undefined reference to `sys_clock_elapsed' zephyr/kernel/libkernel.a(busy_wait.c.obj): misc.h:26: undefined reference to `sys_clock_cycle_get_32' …meaning *no* tick driver was being compiled in. Setting `CONFIG_STM32_LPTIM_TIMER=y` was being silently ignored by Kconfig because of unmet dependencies in `zephyr/drivers/timer/Kconfig.stm32_lptim`: depends on dt_nodelabel_exists(stm32_lp_tick_source) ← OK on G4 depends on DT_HAS_ST_STM32_LPTIM_ENABLED ← OK on G4 depends on CLOCK_CONTROL && PM ← MISSING select TICKLESS_CAPABLE Upstream `nucleo_g474re.dts` already labels `&lptim1` as the `stm32_lp_tick_source` and sets `status="okay"` with LSI clocks, so the DT side is fine — the only piece missing was `CONFIG_PM=y`, which lets `STM32_LPTIM_TIMER`'s `default y` fire and the driver source actually compile. Replaces `CONFIG_STM32_LPTIM_TIMER=y` (redundant once PM enables it via default) with `CONFIG_PM=y`. Keeps `CONFIG_CORTEX_M_SYSTICK=n` so the SysTick driver doesn't compile in parallel and race with LPTIM for the system-clock-driver init slot. Comment block reframed to explain the real Kconfig dependency chain rather than the speculative DT-overlay caveat. Verified locally: all 4 variants (baseline/gale × systick/lptim) now link cleanly. The lptim variant carries the PM subsystem (~120 KB ELF growth, 1% extra flash, ~600 B extra RAM) — that's the cost of using LPTIM as the kernel tick on this part. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../boards/nucleo_g474re/prj-tick-lptim.conf | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf b/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf index 0a213ec..28fd26b 100644 --- a/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf +++ b/benches/engine_control/silicon/boards/nucleo_g474re/prj-tick-lptim.conf @@ -5,19 +5,29 @@ # (the Cortex-M default) to determine whether the silicon-anchor # multiplier is sensitive to tick-source choice. # -# CAVEAT — clock source. -# Zephyr's default LPTIM clock is LSE (32.768 kHz). The engine_control -# bench sets CONFIG_SYS_CLOCK_TICKS_PER_SEC=100000 (10 µs granularity), -# which exceeds what LSE-clocked LPTIM can sustain. To run the bench's -# 100 kHz tick rate on LPTIM, layer a device-tree overlay that switches -# LPTIM1 to PCLK1 (170 MHz / prescaler). A starter overlay lives next -# to this file at `tick-lptim.overlay` (pending DT verification on G4 -# bindings — see board README). +# Upstream nucleo_g474re.dts already labels &lptim1 as the +# `stm32_lp_tick_source` and sets status="okay" with LSI as its +# clock — no DT overlay needed. # -# If the DT overlay isn't applied, this build will use LSE-clocked -# LPTIM and Zephyr's tick-rate negotiation will silently cap the tick -# rate; the captured run will NOT be apples-to-apples with the SysTick -# variant. Manifest field `tick_source: lptim` makes that visible. +# Per zephyr/drivers/timer/Kconfig.stm32_lptim, CONFIG_STM32_LPTIM_TIMER: +# depends on dt_nodelabel_exists(stm32_lp_tick_source) ← board DTS sets this +# depends on DT_HAS_ST_STM32_LPTIM_ENABLED ← board DTS sets status=okay +# depends on CLOCK_CONTROL && PM ← needs CONFIG_PM=y +# select TICKLESS_CAPABLE +# …so the only Kconfig fragment we actually need is `CONFIG_PM=y` (which +# auto-enables STM32_LPTIM_TIMER via its `default y`). We still set +# CORTEX_M_SYSTICK=n so the SysTick driver isn't compiled in alongside +# and racing for the system-clock-driver init slot. +# +# CAVEAT — tick rate. +# The bench prj.conf sets CONFIG_SYS_CLOCK_TICKS_PER_SEC=100000 (10 µs +# granularity). LPTIM-on-LSI runs at 32 kHz, well below 100 kHz, so +# Zephyr's tick subsystem will cap the achieved rate — the LPTIM +# variant is NOT apples-to-apples with SysTick at the bench's stated +# tick rate. The manifest's `tick_source: lptim` field is the user's +# signal that the two captures are not directly comparable; what they +# *do* show is how each tick-source's overhead/jitter profile differs +# in absolute cycles. +CONFIG_PM=y CONFIG_CORTEX_M_SYSTICK=n -CONFIG_STM32_LPTIM_TIMER=y From ace0c3a0e93cbcf76fc1d14122b621724ee7760b Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 10 May 2026 08:46:30 +0200 Subject: [PATCH 5/5] =?UTF-8?q?silicon:=20capture.sh=20=E2=80=94=20default?= =?UTF-8?q?=20--runner=20openocd,=20explicit=20post-flash=20reset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes surfaced when running capture.sh on the bench for real: 1. west flash on nucleo_g474re defaults to the stm32cubeprogrammer runner, which requires ST's proprietary STM32CubeProgrammer.app that most Linux/macOS dev setups don't have installed. The board also configures the openocd runner (which is brew-installable on macOS, package-managed on Linux), but it's not the default. Add a --runner flag to capture.sh, default openocd, with pass-through to `west flash`. Include the choice in the manifest. 2. Even with the openocd runner, west flash via Zephyr 4.4.0-rc3 on STM32G4 + CONFIG_PM=y leaves the chip *halted* after writing the image — no implicit reset+run is issued, so the firmware never starts and the UART stays silent. Add an explicit openocd init reset run sleep 200 exit step between flash and the serial capture. NB: do NOT pipe openocd through head/grep — SIGPIPE on early close kills openocd before it processes `reset run`, leaving the chip halted just the same. Capture full openocd output to /tmp/silicon-reset-.log instead, with a 0.5s grace before opening the serial port so the sentinel-search window aligns cleanly with the bench's CSV stream. Co-Authored-By: Claude Opus 4.7 (1M context) --- benches/engine_control/silicon/capture.sh | 35 +++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/benches/engine_control/silicon/capture.sh b/benches/engine_control/silicon/capture.sh index caeeb09..42a938b 100755 --- a/benches/engine_control/silicon/capture.sh +++ b/benches/engine_control/silicon/capture.sh @@ -8,7 +8,8 @@ # Usage: # capture.sh --board nucleo_g474re --variant {baseline,gale} \ # [--tick-source {systick,lptim}] \ -# [--sweep {short,long}] [--port /dev/cu.usbmodem11403] +# [--sweep {short,long}] [--port /dev/cu.usbmodem11403] \ +# [--runner {openocd,stm32cubeprogrammer,pyocd,jlink}] # # Defaults: # --tick-source systick (Cortex-M default; lptim selects the STM32 @@ -17,6 +18,10 @@ # --sweep short (use --sweep long for the publication-grade run) # --port: auto-detect first /dev/cu.usbmodem* (macOS) or # /dev/ttyACM0 (Linux). Override if multiple boards present. +# --runner openocd (the brew-installable open-source flasher). +# stm32cubeprogrammer is the upstream Zephyr default +# for nucleo_g474re but needs ST's proprietary app; +# openocd works out of the box with a brew install. # # A publication-grade anchor on a given board is the 4-run matrix: # variant ∈ {baseline, gale} × tick_source ∈ {systick, lptim}. @@ -29,6 +34,7 @@ VARIANT="" SWEEP="short" PORT="" TICK_SOURCE="systick" +RUNNER="openocd" SILICON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" GALE_ROOT="$(cd "${SILICON_DIR}/../../.." && pwd)" @@ -39,6 +45,7 @@ while [[ $# -gt 0 ]]; do --sweep) SWEEP="$2"; shift 2 ;; --port) PORT="$2"; shift 2 ;; --tick-source) TICK_SOURCE="$2"; shift 2 ;; + --runner) RUNNER="$2"; shift 2 ;; -h|--help) awk '/^set -/{exit} NR>1{sub(/^# ?/, ""); print}' "$0"; exit 0 ;; *) @@ -147,8 +154,29 @@ fi echo "$ELF_SHA firmware.elf" > "$RUN_DIR/firmware.elf.sha256" # --------------------------------------------------------------------- flash -echo "==> Flashing" -( cd "$GALE_ROOT/.." && west flash -d "$BUILD_DIR" ) +echo "==> Flashing (runner=$RUNNER)" +( cd "$GALE_ROOT/.." && west flash -d "$BUILD_DIR" --runner "$RUNNER" ) + +# Explicitly reset+run the target after flashing. west flash via the openocd +# runner does NOT reliably issue `reset run` after writing on every Zephyr + +# board combo (specifically observed silent post-flash on STM32G4 LPTIM +# builds with CONFIG_PM=y — the chip stays halted, no UART output ever +# reaches the VCP). This 0.5s reset-run handshake fixes that without +# disturbing builds where the runner already does the right thing. +if [[ "$RUNNER" == "openocd" ]]; then + echo "==> Reset+run (post-flash sanity)" + # NB: do NOT pipe openocd through head/grep — SIGPIPE on early close + # kills openocd before it processes `reset run`, leaving the chip + # halted. Capture full output to a temp log instead. + openocd -f interface/stlink.cfg -f target/stm32g4x.cfg \ + -c "init; reset run; sleep 200; exit" \ + > /tmp/silicon-reset-${BOARD}.log 2>&1 || true + tail -3 /tmp/silicon-reset-${BOARD}.log + # Give the chip 500 ms to clear the post-reset UART noise / boot banner + # printk before capture.py starts reading, so the sentinel-search + # window aligns cleanly with the bench's CSV stream. + sleep 0.5 +fi # --------------------------------------------------------------------- capture # Long sweep can take a few minutes wall-time at 168 MHz; short ~10s. @@ -179,6 +207,7 @@ MANIFEST="$RUN_DIR/manifest.txt" echo "variant: ${VARIANT}" echo "tick_source: ${TICK_SOURCE}" echo "sweep: ${SWEEP}" + echo "flash_runner: ${RUNNER}" echo "gale_sha: ${GALE_SHA_FULL}" echo "gale_status: $(cd "$GALE_ROOT" && git status --porcelain | wc -l | tr -d ' ') uncommitted file(s)" echo "host: $(uname -srm)"