5009a844a9
stlink_flash.py 新增 hard 子命令(-c SWD RM=Hrst -HardRst PULSE=100), RM= 被 CLI 拒绝时自动降级为系统复位并打印 RESET_NOTE;至此 reset.bat 的 全部子命令(复位/hard/halt/run/list/flash/cmp)均已由 skill 覆盖。 删除 MDK-ARM/reset.bat(git rm)。@build/@download 两条旧批处理入口至此 全部收敛到 .dsh/skills/ 下的 skill,一个能力一个实现。 同步更新引用:AGENTS.md、README、MDK-ARM/README、7 篇 docs、4 个 skill (stlink-flash/serial-boot-test/new-machine-setup 及 config.example.json)。 真机验证:hard 子命令 RM= 被拒 → RESET_NOTE → 回退系统复位 → STLINK_RC=0。
204 lines
8.5 KiB
Python
204 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""stlink_flash.py - ST-LINK 命令行烧录/复位/回读校验(纯标准库,可移植 skill 版)。
|
||
|
||
用法(**项目根目录**下运行,任意 python3):
|
||
python <skill>/scripts/stlink_flash.py list
|
||
python <skill>/scripts/stlink_flash.py flash [镜像.hex|.bin] # 烧录 + -Rst -Run
|
||
python <skill>/scripts/stlink_flash.py cmp [镜像.hex|.bin] [--base 0x08000000]
|
||
python <skill>/scripts/stlink_flash.py reset # 只复位运行
|
||
python <skill>/scripts/stlink_flash.py hard # nRST 100ms 脉冲(CLI 拒 RM= 时自动降级为系统复位)
|
||
python <skill>/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())
|