#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 — 入口与命令行模式 ================================================================ 配合 GD32E230 + DAPLink/CMSIS-DAP(或 ST-Link/J-Link) 使用, OpenOCD 烧录; 内含固件补齐(pad)与镜像拼接(merge)工具。发布包内置 OpenOCD 工具链。 模块划分(重构自原单文件 daplink_flash_gui.py, 界面/打包方式对齐 DLPilot): app_gui.py CustomTkinter 主界面(双栏仪表盘) openocd_runner.py 工具链定位 / OpenOCD 命令构建与子进程 fw_image.py 补齐(pad)与拼接(merge) + 芯片参数表 config.py 运行目录与 flash_gui_settings.json 配置 依赖: 全部纯标准库 + customtkinter(py -m pip install customtkinter); 内置工具链不存在时需本机已装 OpenOCD 或在界面/参数里指定路径。 命令行模式(自动化脚本用): python flash_gui.py 启动 GUI python flash_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试) python flash_gui.py --detect 检测烧录器与目标芯片 python flash_gui.py --flash app.bin 烧录(BIN 默认按基地址 0x08000000) python flash_gui.py --flash app.bin --base 0x08002000 --no-reset python flash_gui.py --erase 整片擦除(按配置的 Flash 大小) python flash_gui.py --read dump.bin 读取整片 Flash python flash_gui.py --reset 复位运行 python flash_gui.py --pad app.bin 补齐到 1K 边界(app_pad.bin) python flash_gui.py --merge bl.bin app.bin 拼接整片镜像(*_BL.bin) python flash_gui.py --pack a.bin b.bin --pack-bl bl.bin v2 双固件打包(UPDATE/FULL/FACTORY+MD5) python flash_gui.py --version 打印 OpenOCD 版本 """ import argparse import os import subprocess import sys import time import config import openocd_runner from fw_image import (APP_ADDR_DEFAULT, FLASH_BASE_DEFAULT, FLAG_ADDR_DEFAULT, FLAG_VALUE_DEFAULT, merge_firmware, pad_bin, parse_int) # fw_pack (v2 双固件打包公共库) 位于仓库根目录: 源码模式补 sys.path, # 打包模式(PyInstaller --paths+hiddenimport)由运行库提供 _REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) if os.path.isfile(os.path.join(_REPO_ROOT, 'fw_pack.py')) \ and _REPO_ROOT not in sys.path: sys.path.insert(0, _REPO_ROOT) import fw_pack def _toolchain(args): """命令行工具链解析: 参数 > 配置文件 > 自动定位(内置/本机/PATH)""" saved = config.load_settings() exe = args.openocd or (saved['openocd'] if os.path.isfile(saved['openocd']) else '') scripts = args.scripts or (saved['scripts'] if exe == saved['openocd'] else '') if not exe: exe, scripts2, source = openocd_runner.locate_toolchain() scripts = scripts or scripts2 print(f'工具链: {exe} ({source})') if not os.path.isfile(exe): sys.exit(f'找不到 OpenOCD: {exe}\n' f'可用 --openocd 指定, 或把内置工具链放在 exe 旁 toolchain/openocd/') if not scripts or not os.path.isdir(scripts): sys.exit(f'找不到 OpenOCD scripts 目录: {scripts or "(空)"}\n可用 --scripts 指定') return exe, scripts def _ocd_cmd(args, script): saved = config.load_settings() exe, scripts = _toolchain(args) return openocd_runner.build_cmd( exe, scripts, args.interface or saved['interface'], args.target or saved['target'], str(args.speed) if args.speed else saved['speed'], args.serial or saved['serial'], saved['extra'], script) def run_openocd(args, title, script): """跑一条烧录命令, 实时回显输出, 返回退出码""" cmd = _ocd_cmd(args, script) print('== %s ==' % title) print('命令: ' + subprocess.list2cmdline(cmd)) code, _lines = openocd_runner.run_sync(cmd) return code def run_pad(args): if not os.path.isfile(args.pad): sys.exit(f'找不到输入文件: {args.pad}') align = parse_int(str(args.align)) if align is None or align <= 0: sys.exit(f'对齐字节无效: {args.align}') pad_byte = parse_int(args.pad_byte) if pad_byte is None: pad_byte = 0xFF stem, ext = os.path.splitext(args.pad) 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) print(f'补齐完成: {out}') print(f' 原始 {original} B → 补齐后 {padded} B (+{padded - original}), 对齐 {align} B') return 0 def run_merge(args): bootloader, app = args.merge for p in (bootloader, app): if not os.path.isfile(p): sys.exit(f'找不到文件: {p}') flash_size = parse_int(str(args.flash_size)) app_addr = parse_int(args.app_addr) flag_addr = parse_int(args.flag_addr) flag_value = parse_int(args.flag_value) if flag_value is None: flag_value = FLAG_VALUE_DEFAULT out = args.output or os.path.splitext(app)[0] + '_BL.bin' _ensure_parent(out) try: info = merge_firmware(bootloader, app, out, app_addr, flag_addr, flash_size, flag_value) except ValueError as exc: sys.exit(f'拼接失败: {exc}') print(f'拼接完成: {out}') print(f' Bootloader {info["bootloader_size"]} B @0x{FLASH_BASE_DEFAULT:08X}') print(f' Application {info["app_size"]} B @0x{info["app_addr"]:08X}') print(f' Flag @0x{info["flag_addr"]:08X} 镜像 {info["output_size"]} B') return 0 def run_pack(args): """v2 双固件打包(GUI '打包 v2' 的命令行形态, 共用 fw_pack.pack_all)""" try: fw_pack.self_test() profile = fw_pack.load_profile(args.pack_profile) except fw_pack.FwPackError as exc: sys.exit(f'{exc}') found = {} for path in args.pack: if not os.path.isfile(path): sys.exit(f'找不到文件: {path}') try: slot = fw_pack.detect_slot(open(path, 'rb').read(), profile) except fw_pack.FwPackError as exc: sys.exit(f'{path}: {exc}') if slot in found: sys.exit(f'{path} 与 {found[slot]} 都被识别为 {slot.upper()} 包, ' f'一个槽只能有一个输入') found[slot] = path a, b = found.get('a'), found.get('b') if not a and not b: sys.exit('未提供可识别的固件输入 (A 包或 B 包)') if args.output and os.path.splitext(args.output)[1].lower() == '.bin': sys.exit('--pack 的 -o 是输出目录(产物自动命名), 不能是 .bin 文件路径') try: fw_pack.pack_all(args.pack_profile, a, b_path=b, bl_path=args.pack_bl or None, out_dir=args.output, make_full=not args.pack_no_full, make_factory=not args.pack_no_factory) except fw_pack.FwPackError as exc: sys.exit(f'打包失败: {exc}') return 0 def _ensure_parent(path): parent = os.path.dirname(os.path.abspath(path)) if parent: os.makedirs(parent, exist_ok=True) def main(): ap = argparse.ArgumentParser( description='DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 ' '(无参数则启动 GUI)') ap.add_argument('--seconds', type=int, default=0, help='GUI 自动退出秒数(冒烟测试)') # -- 烧录操作(互斥, 按下述顺序判优) -- op = ap.add_mutually_exclusive_group() op.add_argument('--detect', action='store_true', help='检测烧录器与目标芯片') op.add_argument('--flash', metavar='FILE', help='烧录固件 (BIN/HEX/ELF)') op.add_argument('--erase', action='store_true', help='整片擦除') op.add_argument('--read', metavar='OUT', help='读取整片 Flash 到 OUT') op.add_argument('--reset', action='store_true', help='复位芯片并运行') op.add_argument('--version', action='store_true', help='打印 OpenOCD 版本') # -- 固件工具 -- op.add_argument('--pad', metavar='FILE', dest='pad', help='补齐 BIN 到页边界') op.add_argument('--merge', nargs=2, metavar=('BL', 'APP'), help='拼接 Bootloader + APP → 整片镜像') op.add_argument('--pack', nargs='+', metavar='BIN', help='v2 双固件打包: A/B 包自动识别 → UPDATE/FULL/FACTORY+MD5') ap.add_argument('--pack-profile', default='CeramixNano_MB_Dual_FW', help='--pack 布局 profile(内置名或 JSON 路径)') ap.add_argument('--pack-bl', default=None, metavar='BIN', help='--pack 使用的 Bootloader bin') ap.add_argument('--pack-no-full', action='store_true', help='--pack 不拼 FULL 整片镜像') ap.add_argument('--pack-no-factory', action='store_true', help='--pack 不出 FACTORY 工厂镜像') ap.add_argument('--align', default='1024', help='--pad 对齐字节(默认 1024)') ap.add_argument('--pad-byte', default='0xFF', help='--pad 填充字节(默认 0xFF)') ap.add_argument('-o', '--output', help='输出路径: --pad/--merge 为文件, --pack 为目录(默认自动命名)') ap.add_argument('--app-addr', default='0x%08X' % APP_ADDR_DEFAULT, help='--merge APP 地址(默认 0x08002000)') ap.add_argument('--flag-addr', default='0x%08X' % FLAG_ADDR_DEFAULT, help='--merge 标志地址(默认 0x0800FFFC)') ap.add_argument('--flag-value', default='0x%08X' % FLAG_VALUE_DEFAULT, help='--merge 标志值(默认 0xEEEEEEEE)') ap.add_argument('--flash-size', default='0x10000', help='Flash 大小(默认 0x10000, --erase/--read/--merge 用)') # -- 烧录参数 -- ap.add_argument('--base', default='0x%08X' % FLASH_BASE_DEFAULT, help='BIN 烧录基地址(默认 0x08000000)') 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)') ap.add_argument('--target', help='target cfg(默认 target/gd32e23x.cfg)') ap.add_argument('--openocd', help='OpenOCD 可执行文件路径(默认自动定位)') ap.add_argument('--scripts', help='OpenOCD scripts 目录(默认自动定位)') args = ap.parse_args() if args.pack: sys.exit(run_pack(args)) if args.pad: sys.exit(run_pad(args)) if args.merge: sys.exit(run_merge(args)) if args.version: exe, _scripts = _toolchain(args) print(openocd_runner.version(exe) or '无法获取版本') sys.exit(0) if args.detect: sys.exit(run_openocd(args, '检测设备', openocd_runner.script_detect())) if args.erase: sys.exit(run_openocd(args, '整片擦除', openocd_runner.script_erase(args.flash_size))) if args.flash: 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, 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))) if args.reset: sys.exit(run_openocd(args, '复位芯片', openocd_runner.script_reset())) # 无操作参数 → 启动 GUI(实现在 app_gui.py) try: import app_gui app_gui.run_gui(args.seconds) except SystemExit: raise except Exception: # --noconsole 打包后异常无处可见, 落一份 crash.log 到程序目录便于远程定位 import traceback try: with open(os.path.join(config.BASE_DIR, 'crash.log'), 'w', encoding='utf-8') as f: f.write(time.strftime('%Y-%m-%d %H:%M:%S\n') + traceback.format_exc()) except OSError: pass raise if __name__ == '__main__': main()