From d776979a5a45c89bcdd9800e59ccd07e1add95e1 Mon Sep 17 00:00:00 2001 From: hulk Date: Wed, 16 Sep 2026 19:49:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(flash-gui):=20cfgs=20=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=20cfg=20=E6=9C=BA=E5=88=B6=20+=20=E5=85=A8=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=20SRST=20=E8=BF=9E=E6=8E=A5=E5=BA=8F=E5=88=97=20+=20?= =?UTF-8?q?=E6=95=B4=E5=8C=85=200.11=20=E5=B7=A5=E5=85=B7=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FlashTool/README.md | 7 +- FlashTool/cfgs/openocd_gdlink_gd32e23x.cfg | 72 ++++++++++++++ FlashTool/src/app_gui.py | 94 ++++++++++++++---- FlashTool/src/config.py | 8 +- FlashTool/src/flash_gui.py | 10 +- FlashTool/src/openocd_runner.py | 106 +++++++++++++++++---- FlashTool/src/package_release.py | 67 +++++++++---- 7 files changed, 307 insertions(+), 57 deletions(-) create mode 100644 FlashTool/cfgs/openocd_gdlink_gd32e23x.cfg diff --git a/FlashTool/README.md b/FlashTool/README.md index 2fde743..b127405 100644 --- a/FlashTool/README.md +++ b/FlashTool/README.md @@ -69,12 +69,15 @@ DAPLinkFlash/ "自动定位"可重新探测;也可手动浏览指定 `openocd.exe` 与 scripts 目录; 2. **选芯片与烧录器**:芯片型号下拉自动带出 Flash 大小和 target;烧录器下拉 自动带出 interface cfg;多探针时填序列号; + **自定义 cfg**:把自己的 OpenOCD cfg 文件(如工程定制的 target 配置)放进 + 程序旁的 `cfgs/` 目录,"Target 配置"/"Interface 配置"下拉框即列出 + `cfgs/<文件名>` 供选用;`cfgs/` 会随发布包分发; 3. **选固件**:点"浏览"选 BIN/HEX/ELF;BIN 确认基地址(完整镜像 `0x08000000`, 单独 APP `0x08002000`); 4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区; 5. **固件工具**(顶栏按钮弹出的独立窗口,非模态): - - 补齐:选输入 BIN 后输出名自动带出(`*_pad.bin`),对齐默认 1K,点"执行补齐"; - - 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`),地址按典型布局 + - 补齐:选输入 BIN 后输出名自动带出(`*_update.bin`,Y-Modem 升级包),对齐默认 1K,点"执行补齐"; + - 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`,整片镜像),地址按典型布局 预填,点"执行拼接"。 典型布局:`0x08000000` Bootloader 8KB | `0x08002000` APP | `0x0800FFFC` diff --git a/FlashTool/cfgs/openocd_gdlink_gd32e23x.cfg b/FlashTool/cfgs/openocd_gdlink_gd32e23x.cfg new file mode 100644 index 0000000..6252c06 --- /dev/null +++ b/FlashTool/cfgs/openocd_gdlink_gd32e23x.cfg @@ -0,0 +1,72 @@ +# +# GigaDevice GD32E23x target +# + +adapter driver cmsis-dap + +# adapter driver jlink + +source [find target/swj-dp.tcl] +source [find mem_helper.tcl] + +if { [info exists CHIPNAME] } { + set _CHIPNAME $CHIPNAME +} else { + set _CHIPNAME gd32e23x +} + +# Work-area is a space in RAM used for flash programming +# By default use 4kB (as found on some GD32E230s) +if { [info exists WORKAREASIZE] } { + set _WORKAREASIZE $WORKAREASIZE +} else { + set _WORKAREASIZE 0x1000 +} + +# Allow overriding the Flash bank size +if { [info exists FLASH_SIZE] } { + set _FLASH_SIZE $FLASH_SIZE +} else { + # autodetect size + set _FLASH_SIZE 0 +} + +#jtag scan chain +if { [info exists CPUTAPID] } { + set _CPUTAPID $CPUTAPID +} else { + # this is the SW-DP tap id not the jtag tap id + set _CPUTAPID 0x0BF11477 +} + +swj_newdap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID +dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu + +set _TARGETNAME $_CHIPNAME.cpu +target create $_TARGETNAME cortex_m -dap $_CHIPNAME.dap + +$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size $_WORKAREASIZE -work-area-backup 0 + +# flash size will be probed +set _FLASHNAME $_CHIPNAME.flash +flash bank $_FLASHNAME gd32e23x 0x08000000 $_FLASH_SIZE 0 0 $_TARGETNAME + +# SWD speed (may be updated to higher value in board config file) +adapter speed 1000 + +# use the hardware reset line (DAPLink RST pin -> GD32 NRST); +# connect_assert_srst: hold the chip in reset while connecting, so SWD pins +# (shared with LED in firmware) revert to SWD function before any access +adapter srst delay 100 +reset_config srst_only srst_nogate connect_assert_srst + +if {![using_hla]} { + # if srst is not fitted use SYSRESETREQ to + # perform a soft reset + cortex_m reset_config sysresetreq +} + +$_TARGETNAME configure -event examine-end { + mmw 0x40021018 0x00400000 0 + mmw 0x40015804 0x00000307 0 +} diff --git a/FlashTool/src/app_gui.py b/FlashTool/src/app_gui.py index d92eed7..2a80b89 100644 --- a/FlashTool/src/app_gui.py +++ b/FlashTool/src/app_gui.py @@ -33,7 +33,7 @@ BOLD = ('Microsoft YaHei UI', 13, 'bold') TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold') APP_NAME = 'DAPLink Flash' -APP_VERSION = '2.2.0' +APP_VERSION = '2.5.2' REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool' OK_BLUE = '#3B8ED0' @@ -200,18 +200,31 @@ class App(ctk.CTk): card.grid(row=0, column=0, sticky='ew') self._field(c, 'OpenOCD 路径', self.v['openocd'], self._browse_openocd) self._field(c, '脚本目录', self.v['scripts'], self._browse_scripts) - probe, _w = self._field_pair( + custom = ['cfgs/' + n for n in openocd_runner.list_custom_cfgs()] + probe, iface_cbo = self._field_pair( c, '烧录器', self.v['probe'], 'Interface', self.v['interface'], cls1=(ctk.CTkComboBox, {'state': 'readonly', 'values': list(openocd_runner.PROBE_PROFILES)}), - cls2=(ctk.CTkComboBox, - {'values': sorted(set(openocd_runner.PROBE_PROFILES.values()) - {''})})) + cls2=(ctk.CTkComboBox, {'values': custom})) probe.configure(command=lambda _=None: self._on_probe_change()) + self.probe_combo = probe + self.cbo_interface = iface_cbo self._field_pair(c, '速度 (kHz)', self.v['speed'], '探针序列号', self.v['serial'], width2=150) - self._field_pair( + target_cbo, _extra = self._field_pair( c, 'Target', self.v['target'], '额外命令', self.v['extra'], - cls1=(ctk.CTkComboBox, {'values': ['target/gd32e23x.cfg']})) + cls1=(ctk.CTkComboBox, {'values': custom})) + self.cbo_target = target_cbo + row = ctk.CTkFrame(c, fg_color='transparent') + row.pack(fill='x', pady=(4, 0)) + ctk.CTkButton(row, text='打开 cfgs 目录', width=110, height=24, + fg_color='transparent', border_width=1, + border_color=('gray55', 'gray35'), + text_color='gray60', hover_color=('gray82', 'gray22'), + command=self._open_cfgs_dir).pack(side='left') + ctk.CTkLabel(row, text='自定义 cfg 放 cfgs/(下拉自动列出); 换工具链后 Target/Interface 列表自动跟随其 scripts 目录', + text_color='gray50').pack(side='left', padx=(8, 0)) + self._refresh_cfg_options() card, c = self._card(col, '芯片与固件') card.grid(row=1, column=0, sticky='ew', pady=(10, 0)) @@ -228,13 +241,27 @@ class App(ctk.CTk): '单独烧 APP 0x08002000', text_color='gray50', justify='left', anchor='w', ).pack(fill='x', pady=(2, 0)) - opts = ctk.CTkFrame(c, fg_color='transparent') - opts.pack(fill='x', pady=(6, 0)) - for key, name in (('erase', '烧录前擦除'), ('verify', '烧录后校验'), - ('reset', '烧录后复位运行')): - ctk.CTkCheckBox(opts, text=name, variable=self.v[key], - checkbox_width=18, checkbox_height=18).pack( - side='left', padx=(0, 14)) + opts1 = ctk.CTkFrame(c, fg_color='transparent') + opts1.pack(fill='x', pady=(6, 0)) + ctk.CTkCheckBox(opts1, text='烧录前硬件复位', variable=self.v['pre_reset'], + checkbox_width=18, checkbox_height=18).pack( + side='left', padx=(0, 14)) + ctk.CTkCheckBox(opts1, text='烧录前擦除', variable=self.v['erase'], + checkbox_width=18, checkbox_height=18).pack( + side='left', padx=(0, 14)) + ctk.CTkCheckBox(opts1, text='烧录后校验', variable=self.v['verify'], + checkbox_width=18, checkbox_height=18).pack( + side='left', padx=(0, 14)) + opts2 = ctk.CTkFrame(c, fg_color='transparent') + opts2.pack(fill='x', pady=(4, 0)) + ctk.CTkCheckBox(opts2, text='烧录后硬件复位', variable=self.v['post_reset'], + checkbox_width=18, checkbox_height=18).pack( + side='left', padx=(0, 14)) + ctk.CTkCheckBox(opts2, text='烧录后复位运行', variable=self.v['reset'], + checkbox_width=18, checkbox_height=18).pack( + side='left', padx=(0, 14)) + ctk.CTkLabel(opts2, text='硬件复位 = 复位前先释放 SRST 并等待 200ms', + text_color='gray50').pack(side='left') card, c = self._card(col, '运行日志 — OpenOCD 输出与固件工具结果', button='清空', command=self._clear_log) @@ -376,11 +403,38 @@ class App(ctk.CTk): title='选择 OpenOCD', filetypes=[('OpenOCD', 'openocd.exe'), ('所有文件', '*.*')]) if path: self.v['openocd'].set(path) + # 换工具链: scripts 目录与 cfg 下拉跟随当前版本 + root = os.path.dirname(os.path.dirname(path)) + scripts = openocd_runner._find_scripts(root) + if scripts: + self.v['scripts'].set(scripts) + self._refresh_cfg_options() def _browse_scripts(self): path = filedialog.askdirectory(title='选择 OpenOCD scripts 目录') if path: self.v['scripts'].set(path) + self._refresh_cfg_options() + + def _refresh_cfg_options(self): + """按当前 scripts 目录重建 Target/Interface 下拉列表(自定义 cfgs/ 优先在前)""" + custom = ['cfgs/' + n for n in openocd_runner.list_custom_cfgs()] + scripts = self.v['scripts'].get() + targets = openocd_runner.scan_cfgs(scripts, 'target') + interfaces = openocd_runner.scan_cfgs(scripts, 'interface') + cur_t, cur_i = self.v['target'].get(), self.v['interface'].get() + self.cbo_target.configure(values=custom + targets) + self.cbo_interface.configure(values=custom + interfaces) + self.v['target'].set(cur_t) + self.v['interface'].set(cur_i) + + def _open_cfgs_dir(self): + """创建并打开自定义 cfg 目录, 方便直接丢 cfg 文件进来""" + import subprocess + d = openocd_runner.custom_cfg_dir() + os.makedirs(d, exist_ok=True) + subprocess.Popen(['explorer', os.path.normpath(d)]) + self._refresh_cfg_options() def _relocate_toolchain(self): exe, scripts, source = openocd_runner.locate_toolchain() @@ -390,6 +444,7 @@ class App(ctk.CTk): self.v_toolchain.set('工具链: ' + source) ver = openocd_runner.version(exe) self._log('工具链定位: %s (%s)%s' % (exe, source, ' ' + ver if ver else '')) + self._refresh_cfg_options() def _mark_custom_toolchain(self, *_a): if self._toolchain_source != '自定义': @@ -409,7 +464,7 @@ class App(ctk.CTk): if path: self.v['pad_in'].set(path) name, ext = os.path.splitext(path) - self.v['pad_out'].set(name + '_pad' + ext) + self.v['pad_out'].set(name + '_update' + ext) def _browse_pad_out(self): path = filedialog.asksaveasfilename( @@ -436,7 +491,8 @@ class App(ctk.CTk): if self.v['app'].get(): name, ext = os.path.splitext(os.path.basename(self.v['app'].get())) self.v['merge_out'].set( - os.path.join(os.path.dirname(self.v['app'].get()), name + '_BL' + ext)) + os.path.join(os.path.dirname(self.v['app'].get()), + name + '_BL' + ext)) def _browse_merge_out(self): path = filedialog.asksaveasfilename( @@ -450,6 +506,10 @@ class App(ctk.CTk): self.v['target'].set(openocd_runner.chip_target(self.v['chip'].get())) def _on_probe_change(self, *_a): + if self.v['probe'].get() == 'Custom': + # Custom: interface 留空(配合 cfgs/ 里自带 adapter driver 的 cfg) + self.v['interface'].set('') + return cfg = openocd_runner.PROBE_PROFILES.get(self.v['probe'].get(), '') if cfg: self.v['interface'].set(cfg) @@ -511,7 +571,9 @@ class App(ctk.CTk): script = openocd_runner.script_flash( filepath, self.v['base'].get(), erase=self.v['erase'].get(), verify=self.v['verify'].get(), - reset=self.v['reset'].get()) + reset=self.v['reset'].get(), + pre_reset=self.v['pre_reset'].get(), + post_reset=self.v['post_reset'].get()) except ValueError as exc: messagebox.showerror('参数错误', str(exc)) return diff --git a/FlashTool/src/config.py b/FlashTool/src/config.py index a2c960c..03119ab 100644 --- a/FlashTool/src/config.py +++ b/FlashTool/src/config.py @@ -30,19 +30,21 @@ SETTINGS_PATH = os.path.join(BASE_DIR, SETTINGS_NAME) DEFAULTS = { "openocd": "", # 空 = 启动时按 locate_toolchain() 自动定位 "scripts": "", - "speed": "4000", + "speed": "2000", # kHz; 空则用 cfg 内的值 "serial": "", "extra": "", "chip": "GD32E230F8", "probe": "DAPLink / CMSIS-DAP", - "interface": "interface/cmsis-dap.cfg", - "target": "target/gd32e23x.cfg", + "interface": "interface/cmsis-dap.cfg", # 与 cfgs/ 定制 cfg 内重复声明 driver 无害 + "target": "cfgs/openocd_gdlink_gd32e23x.cfg", # 随包分发的工程定制 cfg "flash_size": "0x10000", "firmware": "", "base": "0x08000000", "erase": True, "verify": True, "reset": True, + "pre_reset": True, + "post_reset": True, "pad_in": "", "pad_out": "", "pad_align": "1024", diff --git a/FlashTool/src/flash_gui.py b/FlashTool/src/flash_gui.py index 240f736..eba187c 100644 --- a/FlashTool/src/flash_gui.py +++ b/FlashTool/src/flash_gui.py @@ -87,7 +87,7 @@ def run_pad(args): if pad_byte is None: pad_byte = 0xFF stem, ext = os.path.splitext(args.pad) - out = args.output or stem + '_pad' + ext + out = args.output or stem + '_update' + ext _ensure_parent(out) original = os.path.getsize(args.pad) padded = pad_bin(args.pad, out, align, pad_byte) @@ -162,6 +162,10 @@ def main(): ap.add_argument('--no-erase', action='store_true', help='烧录前不擦除') ap.add_argument('--no-verify', action='store_true', help='烧录后不校验') ap.add_argument('--no-reset', action='store_true', help='烧录后不复位运行') + ap.add_argument('--no-pre-reset', action='store_true', + help='烧录前不做硬件复位(SRST 释放 + halt)') + ap.add_argument('--no-post-reset', action='store_true', + help='烧录后复位前不释放 SRST(仅软件复位)') ap.add_argument('--speed', type=int, help='适配器速度 kHz(默认取配置 4000)') ap.add_argument('--serial', help='探针序列号(多探针时用)') ap.add_argument('--interface', help='interface cfg(默认 interface/cmsis-dap.cfg)') @@ -187,7 +191,9 @@ def main(): sys.exit(run_openocd(args, '烧录固件', openocd_runner.script_flash( args.flash, args.base, erase=not args.no_erase, verify=not args.no_verify, - reset=not args.no_reset))) + reset=not args.no_reset, + pre_reset=not args.no_pre_reset, + post_reset=not args.no_post_reset))) if args.read: sys.exit(run_openocd(args, '读取 Flash', openocd_runner.script_read(args.read, args.flash_size))) diff --git a/FlashTool/src/openocd_runner.py b/FlashTool/src/openocd_runner.py index f89e63c..23d3f47 100644 --- a/FlashTool/src/openocd_runner.py +++ b/FlashTool/src/openocd_runner.py @@ -43,15 +43,54 @@ def _app_base(): 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""" - exe = os.path.join(_app_base(), "toolchain", "openocd", "bin", "openocd.exe") + root = os.path.join(_app_base(), "toolchain", "openocd") + exe = os.path.join(root, "bin", "openocd.exe") if os.path.isfile(exe): - scripts = os.path.join(_app_base(), "toolchain", "openocd", "openocd", "scripts") - return exe, scripts if os.path.isdir(scripts) else DEFAULT_SCRIPTS + 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() @@ -83,6 +122,20 @@ def ensure_cfg_path(value, folder): return folder + "/" + value +def resolve_cfg(value, folder): + """界面里的 cfg 引用 → OpenOCD -f 参数。 + + cfgs/.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: @@ -99,12 +152,14 @@ def version(exe): # 烧录脚本构建(GUI 与 CLI 共用) # ============================================================ def build_cmd(openocd, scripts, interface, target, speed="", serial="", extra="", script=""): - """拼 OpenOCD 命令行: -s 搜索目录 + interface/target cfg + -c 选项""" + """拼 OpenOCD 命令行: -s 搜索目录 + interface/target cfg + -c 选项。 + interface 留空时跳过(配合自带 adapter driver 的自定义 target cfg 使用)""" cmd = [openocd.strip()] if scripts and scripts.strip(): cmd += ["-s", scripts.strip()] - cmd += ["-f", ensure_cfg_path(interface, "interface"), - "-f", ensure_cfg_path(target, "target")] + 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(): @@ -117,21 +172,38 @@ def build_cmd(openocd, scripts, interface, target, speed="", serial="", extra="" def script_detect(): - return "init; reset halt; flash list; exit" + 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; reset halt; flash erase_address 0x%08X 0x%X; reset halt; exit" - % (FLASH_BASE_DEFAULT, size)) + return ("init; %s; flash erase_address 0x%08X 0x%X; reset halt; exit" + % (CONNECT_SEQ, FLASH_BASE_DEFAULT, size)) -def script_flash(firmware, base_text="", erase=True, verify=True, reset=True): - """HEX/ELF 用文件内地址(base_text 留空), BIN 必须给基地址""" +# 复刻 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; reset halt; " + script = "init; " + script += (CONNECT_SEQ if pre_reset else "reset halt") + "; " addr = "" if ext == ".bin": base = parse_int(base_text) @@ -143,7 +215,8 @@ def script_flash(firmware, base_text="", erase=True, verify=True, reset=True): else "flash write_image %s%s; ") % (quoted, addr) if verify: script += "verify_image %s%s; " % (quoted, addr) - script += ("reset run" if reset else "reset halt") + "; exit" + tail = "reset run" if reset else "reset halt" + script += (POST_RESET_PREFIX + tail if post_reset else tail) + "; exit" return script @@ -151,12 +224,13 @@ 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; reset halt; dump_image %s 0x%08X 0x%X; exit" % ( - tcl_quote(out_path), FLASH_BASE_DEFAULT, size) + return "init; %s; dump_image %s 0x%08X 0x%X; exit" % ( + CONNECT_SEQ, tcl_quote(out_path), FLASH_BASE_DEFAULT, size) def script_reset(): - return "init; reset run; exit" + """对齐 VSCode Reset 任务: 释放 SRST 后复位运行""" + return "init; adapter deassert srst; sleep 200; reset; exit" # ============================================================ diff --git a/FlashTool/src/package_release.py b/FlashTool/src/package_release.py index 3713a9f..3bf1578 100644 --- a/FlashTool/src/package_release.py +++ b/FlashTool/src/package_release.py @@ -15,8 +15,11 @@ zip 解压后根目录为 DAPLinkFlash/, 与 exe 运行目录结构一致: OpenOCD 来源(--openocd-src > 环境变量 OPENOCD_SRC > 自动探测): 1. FlashTool/toolchain/openocd (随仓库自带, 若已 vendor) - 2. D:/toolchain/openocd (开发机本机安装) -目录布局: /bin/openocd.exe + /openocd/scripts/ + 2. D:/toolchain/xpack-openocd-0.11.0-3 + 3. D:/toolchain/openocd +整个工具链目录原样打包(不挑文件); scripts 布局自动兼容 +xpack 0.11(scripts/) 与 0.12(openocd/scripts/)两种。 +程序目录还会带一个空的 cfgs/ 目录(放自定义 OpenOCD cfg, 界面下拉可选)。 用法: python package_release.py 版本号自动取最近 git tag(DAPLinkFlash-v*) @@ -52,37 +55,52 @@ def detect_version(): def resolve_openocd_src(explicit): - """定位 OpenOCD 工具链源目录, 校验布局并返回路径""" + """定位 OpenOCD 工具链源目录(bin/openocd.exe 必在, scripts 布局兼容两种)""" candidates = [explicit, os.environ.get('OPENOCD_SRC'), os.path.join(BASE, 'toolchain', 'openocd'), + r'D:/toolchain/xpack-openocd-0.11.0-3', r'D:/toolchain/openocd'] for cand in candidates: if not cand: continue - exe = os.path.join(cand, 'bin', 'openocd.exe') + if not os.path.isfile(os.path.join(cand, 'bin', 'openocd.exe')): + continue scripts = os.path.join(cand, 'openocd', 'scripts') - if os.path.isfile(exe) and os.path.isdir(scripts): + if not os.path.isdir(scripts): + scripts = os.path.join(cand, 'scripts') + if os.path.isdir(scripts): print(f'OpenOCD 工具链来源: {os.path.abspath(cand)}') return os.path.abspath(cand) - sys.exit('未找到 OpenOCD 工具链(需 bin/openocd.exe + openocd/scripts/):\n' - ' python package_release.py <版本> --openocd-src \n' + sys.exit('未找到 OpenOCD 工具链(需 bin/openocd.exe + scripts/):\n' + ' python package_release.py <版本> --openocd-src \n' ' 或设置环境变量 OPENOCD_SRC') def copy_toolchain(openocd_src): - """复制 bin(openocd.exe + 运行 DLL)与 openocd/scripts 到程序目录 toolchain/""" + """整个工具链目录原样复制到程序目录 toolchain/openocd(不挑文件)""" dst_root = os.path.join(APP_DIR, 'toolchain', 'openocd') if os.path.isdir(dst_root): shutil.rmtree(dst_root) - count = 0 - for sub in ('bin', os.path.join('openocd', 'scripts')): - src = os.path.join(openocd_src, sub) - dst = os.path.join(dst_root, sub) - os.makedirs(os.path.dirname(dst), exist_ok=True) - shutil.copytree(src, dst) - for _root, _dirs, files in os.walk(dst): - count += len(files) - return count + shutil.copytree(openocd_src, dst_root) + return sum(len(fs) for _r, _d, fs in os.walk(dst_root)) + + +CFGS_README = ( + "把自定义 OpenOCD cfg 文件放到本目录(任意文件名.cfg)。\n" + "DAPLink Flash 的 \"Target 配置\"/\"Interface 配置\" 下拉框会自动列出\n" + "cfgs/<文件名>, 选中即用; 选择会被记住。\n" +) + + +def ensure_cfgs_dir(): + """程序目录建 cfgs/ + 说明文件""" + d = os.path.join(APP_DIR, 'cfgs') + os.makedirs(d, exist_ok=True) + readme = os.path.join(d, '说明-把cfg放这里.txt') + if not os.path.exists(readme): + with open(readme, 'w', encoding='utf-8') as f: + f.write(CFGS_README) + return d def main(): @@ -103,7 +121,18 @@ def main(): openocd_src = resolve_openocd_src(args.openocd_src) n_files = copy_toolchain(openocd_src) + ensure_cfgs_dir() shutil.copy2(readme, os.path.join(APP_DIR, 'README.md')) + # 用户自定义 cfg 目录(存在才打包): 界面 Target/Interface 下拉可选用 + src_cfgs = os.path.join(BASE, 'cfgs') + n_cfgs = 0 + if os.path.isdir(src_cfgs): + dst_cfgs = os.path.join(APP_DIR, 'cfgs') + if os.path.isdir(dst_cfgs): + shutil.rmtree(dst_cfgs) + shutil.copytree(src_cfgs, dst_cfgs) + n_cfgs = sum(len(fs) for _r, _d, fs in os.walk(dst_cfgs)) + print(f'自定义 cfg: 复制 {n_cfgs} 个到发布包 cfgs/') zip_name = f'{APP_NAME}-v{version}-win64.zip' zip_path = os.path.join(RELEASE, zip_name) @@ -116,7 +145,9 @@ def main(): z.write(full, os.path.join(APP_NAME, os.path.relpath(full, APP_DIR))) zip_mb = os.path.getsize(zip_path) / 1024 / 1024 - print(f'发布文件夹: {APP_DIR} (exe + _internal + 内置工具链 {n_files} 个文件, 平铺即用)') + print(f'发布文件夹: {APP_DIR} (exe + _internal + 内置工具链 {n_files} 个文件' + + (f' + 自定义 cfg {n_cfgs} 个' if n_cfgs else '') + + ', 平铺即用)') print(f'发布包: {zip_path} ({zip_mb:.1f} MB)') print('下一步上传: python publish_gitea.py', zip_name)