Files
STM32F4-Base/.dsh/skills/stlink-flash/scripts/stlink_flash.py
T
wandering1 5009a844a9
CI / L0 静态门禁 (pre-commit) (push) Has been cancelled
CI / L0.5 主机侧单测 (MSVC) (push) Has been cancelled
收敛 reset.bat 到 stlink-flash skill 并删除该批处理
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。
2026-10-10 16:41:46 +08:00

204 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())