refactor(flash-gui): 对齐 DLPilot 重构 GUI 并内置 OpenOCD 发布链

This commit is contained in:
2026-09-14 18:54:20 +08:00
parent e8eb194fb8
commit 4c6cdc3bf8
16 changed files with 1896 additions and 933 deletions
+202
View File
@@ -0,0 +1,202 @@
#!/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 --version 打印 OpenOCD 版本
"""
import argparse
import os
import subprocess
import sys
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)
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 + '_pad' + 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 _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 → 整片镜像')
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 输出路径(默认自动命名)')
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('--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.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)))
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)
import app_gui
app_gui.run_gui(args.seconds)
if __name__ == '__main__':
main()