Files
gd32e230f8_firmware_merge_tool/FlashTool/src/openocd_runner.py
T

324 lines
11 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 -*-
"""
OpenOCD 子进程封装: 工具链定位 / 烧录脚本构建 / 后台运行
================================================================
工具链定位顺序(locate_toolchain):
1. 内置 <程序目录>/toolchain/openocd/bin/openocd.exe
(发布包自带; PyInstaller frozen 取 exe 旁, 源码模式取 FlashTool/ 下)
2. 默认 D:/toolchain/openocd(开发机本机安装)
3. PATH 里的 openocd
返回值带来源标记, GUI 在界面上显示"工具链: 内置/本机/PATH"。
GUI 与命令行共用 build_cmd/script_* 这组函数, 保证两边生成的命令一致。
"""
import os
import re
import shutil
import subprocess
import sys
import threading
from fw_image import CHIP_PROFILES, FLASH_BASE_DEFAULT, parse_int
DEFAULT_OPENOCD = r"D:/toolchain/openocd/bin/openocd.exe"
DEFAULT_SCRIPTS = r"D:/toolchain/openocd/openocd/scripts"
# 探针型号 → OpenOCD interface cfg(空白 = 自定义, 不改手填值)
PROBE_PROFILES = {
"DAPLink / CMSIS-DAP": "interface/cmsis-dap.cfg",
"GD-Link (CMSIS-DAP)": "interface/cmsis-dap.cfg",
"ST-Link": "interface/stlink.cfg",
"J-Link": "interface/jlink.cfg",
"CMSIS-DAP (TCP)": "interface/cmsis-dap-tcp.cfg",
"Custom": "",
}
def _app_base():
"""程序所在目录: frozen 取 exe 旁, 源码模式取 FlashTool/"""
if getattr(sys, "frozen", False):
return os.path.dirname(os.path.abspath(sys.executable))
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _find_scripts(root):
"""在工具链根目录下找 scripts 目录, 兼容两种布局:
xpack 0.11(脚本在根: scripts/) 与 0.12(脚本在 openocd/scripts/)"""
for sub in ("openocd/scripts", "scripts"):
p = os.path.join(root, sub)
if os.path.isdir(p):
return p
return None
def bundled_toolchain():
"""内置工具链路径, 不存在返回 None"""
root = os.path.join(_app_base(), "toolchain", "openocd")
exe = os.path.join(root, "bin", "openocd.exe")
if os.path.isfile(exe):
scripts = _find_scripts(root)
return exe, (scripts or DEFAULT_SCRIPTS)
return None
def custom_cfg_dir():
"""用户自定义 cfg 目录: 程序目录旁 cfgs/(exe 模式在 exe 旁, 源码模式在 FlashTool/)"""
return os.path.join(_app_base(), "cfgs")
def list_custom_cfgs():
"""cfgs/ 下的 *.cfg 文件名列表(供界面下拉选择), 目录不存在返回空表"""
d = custom_cfg_dir()
if not os.path.isdir(d):
return []
return sorted(f for f in os.listdir(d)
if f.lower().endswith(".cfg") and os.path.isfile(os.path.join(d, f)))
def scan_cfgs(scripts_dir, kind):
"""扫描 scripts 目录下 kind(target/interface) 子目录里的 cfg 文件名。
返回 ['target/gd32e23x.cfg', ...]; scripts 无效/子目录不存在返回空表。
"""
if not scripts_dir or not os.path.isdir(scripts_dir):
return []
d = os.path.join(scripts_dir, kind)
if not os.path.isdir(d):
return []
return sorted(kind + "/" + f for f in os.listdir(d)
if f.lower().endswith(".cfg") and os.path.isfile(os.path.join(d, f)))
def locate_toolchain():
"""返回 (openocd_exe, scripts_dir, source),source ∈ 内置/本机/PATH"""
bundled = bundled_toolchain()
if bundled:
return bundled[0], bundled[1], "内置"
if os.path.isfile(DEFAULT_OPENOCD):
return DEFAULT_OPENOCD, DEFAULT_SCRIPTS, "本机"
which = shutil.which("openocd")
if which:
return which, "", "PATH"
return DEFAULT_OPENOCD, DEFAULT_SCRIPTS, "本机"
def strip_ansi(text):
return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text)
def tcl_quote(path):
return '"' + path.replace("\\", "/").replace('"', '\\"') + '"'
def ensure_cfg_path(value, folder):
"""裸 cfg 文件名(如 gd32e23x.cfg)补上所属目录前缀"""
value = value.strip()
if not value:
return value
if "/" in value or "\\" in value or not value.lower().endswith(".cfg"):
return value
return folder + "/" + value
def resolve_cfg(value, folder):
"""界面里的 cfg 引用 → OpenOCD -f 参数。
cfgs/<name>.cfg 是自定义 cfg, 转成绝对路径(不依赖 cwd);
其余(裸名/带路径/绝对路径)按原逻辑处理。
"""
value = value.strip().replace("\\", "/")
if value.startswith("cfgs/"):
p = os.path.join(custom_cfg_dir(), os.path.basename(value))
if os.path.isfile(p):
return p
return ensure_cfg_path(value, folder)
def version(exe):
"""openocd --version 首行(如 Open On-Chip Debugger 0.12.0), 失败返回 None"""
try:
out = subprocess.run([exe, "--version"], capture_output=True, text=True,
timeout=5, creationflags=subprocess.CREATE_NO_WINDOW
if os.name == "nt" else 0)
line = (out.stdout or out.stderr or "").strip().splitlines()
return line[0].strip() if line else None
except Exception:
return None
# ============================================================
# 烧录脚本构建(GUI 与 CLI 共用)
# ============================================================
def build_cmd(openocd, scripts, interface, target, speed="", serial="", extra="", script=""):
"""拼 OpenOCD 命令行: -s 搜索目录 + interface/target cfg + -c 选项。
interface 留空时跳过(配合自带 adapter driver 的自定义 target cfg 使用)"""
cmd = [openocd.strip()]
if scripts and scripts.strip():
cmd += ["-s", scripts.strip()]
if interface and interface.strip():
cmd += ["-f", resolve_cfg(interface, "interface")]
cmd += ["-f", resolve_cfg(target, "target")]
if speed and speed.strip():
cmd += ["-c", "adapter speed " + speed.strip()]
if serial and serial.strip() and "cmsis-dap" in interface.lower():
cmd += ["-c", "cmsis_dap_serial " + serial.strip()]
if extra and extra.strip():
cmd += ["-c", extra.strip()]
if script:
cmd += ["-c", script]
return cmd
def script_detect():
return "init; %s; flash list; exit" % CONNECT_SEQ
def script_erase(flash_size_text):
size = parse_int(flash_size_text)
if size is None or size <= 0:
raise ValueError("Flash 大小无效: %s" % flash_size_text)
return ("init; %s; flash erase_address 0x%08X 0x%X; reset halt; exit"
% (CONNECT_SEQ, FLASH_BASE_DEFAULT, size))
# 复刻 VSCode tasks.json 的复位序列(0.11 语法, 与内置 xpack 0.11.0-3 匹配):
# init 连接(connect_assert_srst 由 cfg 声明)后显式释放 SRST 并等复位完成再 halt。
# 注意: 0.11 只认 "adapter deassert srst"; 0.12 的 "adapter srst deassert"
# 在 0.11 上是无效命令, 包 catch 会静默吞掉 → SRST 一直按住 → cannot read IDR。
# 全部操作(检测/烧录/擦除/读取/复位)都必须带本序列: 本目标必须按住复位连接,
# 否则 exam 失败(PARTNO 0x0), 后续任何访问都会 "Target not examined" 失败。
CONNECT_SEQ = "adapter deassert srst; sleep 200; catch {reset halt}; reset halt"
POST_RESET_PREFIX = "adapter deassert srst; sleep 200; "
def script_flash(firmware, base_text="", erase=True, verify=True, reset=True,
pre_reset=True, post_reset=True):
"""HEX/ELF 用文件内地址(base_text 留空), BIN 必须给基地址。
pre_reset - 烧录前硬件复位序列(SRST 释放 + halt), 复刻 VSCode 任务链
post_reset - 烧录后复位前先释放 SRST(硬件复位), 否则仅软件 reset
reset - 复位后运行(reset run); 不勾则复位后保持暂停(reset halt)
"""
ext = os.path.splitext(firmware)[1].lower()
script = "init; "
script += (CONNECT_SEQ if pre_reset else "reset halt") + "; "
addr = ""
if ext == ".bin":
base = parse_int(base_text)
if base is None:
raise ValueError("BIN 基地址无效: %s" % base_text)
addr = " 0x%08X" % base
quoted = tcl_quote(firmware)
script += ("flash write_image erase %s%s; " if erase
else "flash write_image %s%s; ") % (quoted, addr)
if verify:
script += "verify_image %s%s; " % (quoted, addr)
tail = "reset run" if reset else "reset halt"
script += (POST_RESET_PREFIX + tail if post_reset else tail) + "; exit"
return script
def script_read(out_path, flash_size_text):
size = parse_int(flash_size_text)
if size is None or size <= 0:
raise ValueError("Flash 大小无效: %s" % flash_size_text)
return "init; %s; dump_image %s 0x%08X 0x%X; exit" % (
CONNECT_SEQ, tcl_quote(out_path), FLASH_BASE_DEFAULT, size)
def script_reset():
"""对齐 VSCode Reset 任务: 释放 SRST 后复位运行"""
return "init; adapter deassert srst; sleep 200; reset; exit"
# ============================================================
# 进程运行
# ============================================================
class OcdProcess:
"""后台跑一条 openocd 命令。
on_line(line)/on_done(code) 在工作线程里回调, GUI 侧自行经队列
投递回主线程; CLI 可直接在回调里 print。
"""
def __init__(self, cmd, on_line, on_done):
self.cmd = cmd
self._on_line = on_line
self._on_done = on_done
self.proc = None
self._thread = None
@property
def running(self):
return self.proc is not None
def start(self):
self._thread = threading.Thread(target=self._worker, daemon=True)
self._thread.start()
def terminate(self):
if self.proc is not None:
try:
self.proc.terminate()
except Exception:
pass
def _worker(self):
creation = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
try:
proc = subprocess.Popen(
self.cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
bufsize=1,
creationflags=creation,
)
except Exception as exc:
self._on_line("启动 OpenOCD 失败: %s" % exc)
self._on_done(-1)
return
self.proc = proc
try:
for line in proc.stdout:
line = strip_ansi(line.rstrip("\r\n"))
if line.strip():
self._on_line(line)
except Exception:
pass
code = proc.wait()
self.proc = None
self._on_done(code)
def run_sync(cmd):
"""阻塞运行一条 openocd 命令, 返回 (exit_code, 输出行)"""
lines = []
done = []
def on_line(line):
lines.append(line)
def on_done(code):
done.append(code)
job = OcdProcess(cmd, on_line, on_done)
job.start()
job._thread.join()
return (done[0] if done else -1), lines
def chip_target(chip_name):
return CHIP_PROFILES.get(chip_name, {}).get("target", "target/gd32e23x.cfg")
def chip_flash_size(chip_name):
profile = CHIP_PROFILES.get(chip_name)
return "0x%X" % profile["flash"] if profile else "0x10000"