#!/usr/bin/env python3 # -*- coding: utf-8 -*- """stlink_flash.py - ST-LINK 命令行烧录/复位/回读校验(纯标准库,可移植 skill 版)。 用法(**项目根目录**下运行,任意 python3): python /scripts/stlink_flash.py list python /scripts/stlink_flash.py flash [镜像.hex|.bin] # 烧录 + -Rst -Run python /scripts/stlink_flash.py cmp [镜像.hex|.bin] [--base 0x08000000] python /scripts/stlink_flash.py reset # 只复位运行 python /scripts/stlink_flash.py hard # nRST 100ms 脉冲(CLI 拒 RM= 时自动降级为系统复位) python /scripts/stlink_flash.py halt | run 镜像缺省取项目档案 "hex"(解析顺序同下)。 ST-LINK_CLI 解析(命令行 > 档案 > 环境 > PATH): --cli > 档案 "stlink_cli" > $DSH_STLINK_CLI > $DTU_STLINK_CLI > PATH (环境变量**显式设了但路径无效 = 立即报错,不静默回退**) 铁律(实测教训,脚本已内置): - 烧录必带 -Rst -Run:裸 -P 在目标核死循环时报 Complete 但内容不落地 - flash 成功判据 = rc==0 且输出含 "Programming Complete" - cmp 成功判据 = 某行以 "No difference found" 开头(原样透传 CLI 输出, 上层 run_boot_test 的 readback_ok 默认值直接可用) - cmp 连接时会**复位目标**(板子重打开机日志)→ 自动闭环里必须排在日志采集之后 - hard 复位 = `RM=Hrst -HardRst PULSE=100`;v3.5.0 CLI 拒 `RM=` → **自动降级为系统复位** (打印 RESET_NOTE,仍按降级结果给 rc);本机 nRST 未接线时这条即唯一可用路径 退出码: 0=OK 1=探针/目标错误 2=CLI 未找到/无效 3=用法错误 """ import argparse import json import os import shutil import subprocess import sys import time from pathlib import Path def find_config_file(): env = os.environ.get("DSH_SERIAL_CONFIG") if env: return Path(env) cwd = Path.cwd() candidates = [cwd / ".serialtest" / "config.json", cwd / ".opencode" / "serial_config.json"] for c in candidates: if c.is_file(): return c return candidates[0] def load_config(): f = find_config_file() if not f.is_file(): return {}, "" try: raw = json.loads(f.read_text(encoding="utf-8-sig")) except (OSError, ValueError) as e: return {}, "cannot parse {}: {}".format(f, e) if not isinstance(raw, dict): return {}, "config root is not an object: {}".format(f) return raw, "" def find_cli(cli_arg, cfg): for label, val in (("--cli", cli_arg), ("config 'stlink_cli'", cfg.get("stlink_cli")), ("$DSH_STLINK_CLI", os.environ.get("DSH_STLINK_CLI")), ("$DTU_STLINK_CLI", os.environ.get("DTU_STLINK_CLI"))): if val: if Path(val).is_file(): return Path(val), label raise SystemExit("ENV_ERROR: {} is set but not a file: {} " "(fix the value; no silent fallback)".format(label, val)) w = shutil.which("ST-LINK_CLI.exe") or shutil.which("ST-LINK_CLI") if w: return Path(w), "PATH" raise SystemExit("ENV_ERROR: ST-LINK_CLI.exe not found " "(--cli / config 'stlink_cli' / $DSH_STLINK_CLI / $DTU_STLINK_CLI / PATH)") def run_cli(cli, args, timeout): t0 = time.monotonic() try: # stdin=DEVNULL 必须:ST-LINK_CLI 编程完成后会停在 "run application to exit" # 等按键,若 stdin 是未关闭的管道/控制台会**永久挂起**(2026-10-10 台架实测: # 编程已成功、板子已复位运行,CLI 却 180s 不退 → 闭环误判 flash rc=-1)。 proc = subprocess.run([str(cli)] + args, capture_output=True, timeout=timeout, stdin=subprocess.DEVNULL) rc = proc.returncode raw = (proc.stdout or b"") + (proc.stderr or b"") except subprocess.TimeoutExpired as e: rc = -1 raw = (e.stdout or b"") + (e.stderr or b"") text = raw.decode("utf-8", errors="replace") for line in text.strip().splitlines(): print(line) print("RC: {} elapsed_ms={}".format(rc, int((time.monotonic() - t0) * 1000))) return rc, text def main(argv=None): for stream in (sys.stdout, sys.stderr): try: stream.reconfigure(encoding="utf-8", errors="replace") except Exception: pass ap = argparse.ArgumentParser(description="ST-LINK CLI flash/reset/readback (pure stdlib)") ap.add_argument("command", choices=["list", "flash", "cmp", "reset", "hard", "halt", "run"]) ap.add_argument("image", nargs="?", default=None, help="image file (.hex/.bin; default: config 'hex')") ap.add_argument("--cli", default=None, help="ST-LINK_CLI.exe path") ap.add_argument("--base", default="0x08000000", help="cmp base address") ap.add_argument("--timeout", type=int, default=None, help="subprocess timeout (default: flash 300 / cmp 180 / other 60)") args = ap.parse_args(argv) cfg, err = load_config() if err: print("ENV_ERROR: {}".format(err)) return 3 cli, src = find_cli(args.cli, cfg) print("CLI: {} ({})".format(cli, src)) if args.command in ("list", "reset", "hard", "halt", "run"): if args.image: print("NOTE: image argument ignored for '{}'".format(args.command)) timeout = args.timeout or 60 if args.command == "list": rc, _ = run_cli(cli, ["-List"], timeout) print("STLINK_RC={}".format(rc)) return 0 if rc == 0 else 1 if args.command == "reset": rc, _ = run_cli(cli, ["-c", "SWD", "-Rst", "-Run", "-Q"], timeout) print("STLINK_RC={}".format(rc)) return 0 if rc == 0 else 1 if args.command == "hard": print("RESET: nRST pulse 100ms (falls back to system reset if CLI lacks RM=)") rc, _ = run_cli(cli, ["-c", "SWD", "RM=Hrst", "-HardRst", "PULSE=100", "-Q"], timeout) if rc != 0: # v3.5.0 CLI 帮助与实现不符:RM= 被拒(Unknown debug protocol or option)。 # 回退 = 与 `reset` 同款的系统复位 + 运行;本机 nRST 未接线时这就是唯一可用路径。 print("RESET_NOTE: this ST-LINK_CLI rejects RM= (v3.5.0 known gap), " "falling back to system reset") rc, _ = run_cli(cli, ["-c", "SWD", "-Rst", "-Run", "-Q"], timeout) print("STLINK_RC={}".format(rc)) return 0 if rc == 0 else 1 if args.command == "halt": rc, _ = run_cli(cli, ["-c", "SWD", "-Rst", "-Halt"], timeout) print("STLINK_RC={}".format(rc)) return 0 if rc == 0 else 1 rc, _ = run_cli(cli, ["-c", "SWD", "-Run"], timeout) print("STLINK_RC={}".format(rc)) return 0 if rc == 0 else 1 # flash / cmp 需要镜像 img = args.image or cfg.get("hex") or "" if not img: print("STLINK_ERROR: no image given and no config 'hex'") return 3 img = Path(img) if not img.is_file(): print("STLINK_ERROR: image not found: {}".format(img)) return 3 if args.command == "flash": print("FLASH: {} then reset and run".format(img)) rc, text = run_cli(cli, ["-c", "SWD", "-P", str(img), "-Rst", "-Run", "-Q"], args.timeout or 300) if rc == 0 and "programming complete" in text.lower(): print("FLASH: OK") print("STLINK_RC=0") return 0 if rc == 0: print("STLINK_ERROR: rc=0 but no 'Programming Complete' in output " "(treat as failure - possible forwarded/hung run)") print("STLINK_RC=1") return 1 print("STLINK_RC=1") return 1 # cmp:原样透传输出("No difference found" 独占行首 = 成功判据) print("CMP: {} vs device flash @ {} (NOTE: connecting resets the target)".format( img, args.base)) rc, text = run_cli(cli, ["-c", "SWD", "-CmpFile", str(img), args.base], args.timeout or 180) ok = any(ln.strip().lower().startswith("no difference found") for ln in text.splitlines()) if ok: print("CMP: OK (No difference found)") print("STLINK_RC=0") return 0 print("STLINK_ERROR: readback mismatch or compare failed") print("STLINK_RC=1") return 1 if __name__ == "__main__": sys.exit(main())