- 新增 FlashTool/ 图形化烧录工具,支持 GD32E230F4/F6/F8/C8 - 支持 DAPLink/CMSIS-DAP、GD-Link、ST-Link、J-Link - 支持 BIN/HEX/ELF 烧录、整片擦除、读取 Flash、复位、补齐与拼接 - 配置按主机名保存于 FlashTool/config/,自动创建并 gitignore - README 调整:GUI 使用说明置顶,命令行工具说明保留在下方
878 lines
34 KiB
Python
878 lines
34 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
DAPLink Flash GUI
|
||
|
||
A small Windows desktop tool for flashing GD32 firmware with OpenOCD over a
|
||
CMSIS-DAP / DAPLink adapter. It also contains the two firmware preparation
|
||
helpers used by the project:
|
||
|
||
* pad - pad a binary to a fixed alignment (1024 bytes for the 1K Y-Modem
|
||
packets used by the GD32E230 bootloader)
|
||
* merge - combine bootloader + application + valid-app flag into one full
|
||
flash image for initial flashing
|
||
|
||
Only the Python standard library is required (tkinter is included with the
|
||
standard Windows Python installer).
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import queue
|
||
import re
|
||
import socket
|
||
import struct
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
import tkinter as tk
|
||
from tkinter import filedialog, messagebox, scrolledtext, ttk
|
||
|
||
|
||
APP_TITLE = "DAPLink Flash GUI"
|
||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
CONFIG_DIR = os.path.join(SCRIPT_DIR, "config")
|
||
CONFIG_NAME = "flash_gui_%s.json" % re.sub(r"[^A-Za-z0-9_.-]", "_", socket.gethostname())
|
||
CONFIG_PATH = os.path.join(CONFIG_DIR, CONFIG_NAME)
|
||
|
||
DEFAULT_OPENOCD = r"D:/toolchain/openocd/bin/openocd.exe"
|
||
DEFAULT_SCRIPTS = r"D:/toolchain/openocd/openocd/scripts"
|
||
|
||
FLASH_BASE_DEFAULT = 0x08000000
|
||
APP_ADDR_DEFAULT = 0x08002000
|
||
FLAG_ADDR_DEFAULT = 0x0800FFFC
|
||
FLAG_VALUE_DEFAULT = 0xEEEEEEEE
|
||
|
||
CHIP_PROFILES = {
|
||
"GD32E230F4": {"target": "target/gd32e23x.cfg", "flash": 16 * 1024, "ram": 4 * 1024},
|
||
"GD32E230F6": {"target": "target/gd32e23x.cfg", "flash": 32 * 1024, "ram": 4 * 1024},
|
||
"GD32E230F8": {"target": "target/gd32e23x.cfg", "flash": 64 * 1024, "ram": 8 * 1024},
|
||
"GD32E230C8": {"target": "target/gd32e23x.cfg", "flash": 64 * 1024, "ram": 8 * 1024},
|
||
}
|
||
|
||
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 _strip_ansi(text):
|
||
return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text)
|
||
|
||
|
||
def _parse_int(text):
|
||
s = text.strip().lower()
|
||
if not s:
|
||
return None
|
||
mult = 1
|
||
if s.endswith("k"):
|
||
mult = 1024
|
||
s = s[:-1]
|
||
elif s.endswith("m"):
|
||
mult = 1024 * 1024
|
||
s = s[:-1]
|
||
try:
|
||
if s.startswith("0x"):
|
||
return int(s, 16) * mult
|
||
return int(s, 10) * mult
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def _tcl_quote(path):
|
||
return '"' + path.replace("\\", "/").replace('"', '\\"') + '"'
|
||
|
||
|
||
def _ensure_cfg_path(value, folder):
|
||
value = value.strip()
|
||
if not value:
|
||
return value
|
||
if "/" in value or "\\" in value or value.lower().endswith(".cfg") is False:
|
||
return value
|
||
return folder + "/" + value
|
||
|
||
|
||
def pad_bin(src_path, dst_path, align, pad_byte=0xFF):
|
||
"""Pad a binary file to a multiple of `align` bytes with `pad_byte`."""
|
||
with open(src_path, "rb") as f:
|
||
data = f.read()
|
||
if align <= 0:
|
||
raise ValueError("align must be positive")
|
||
remainder = len(data) % align
|
||
if remainder:
|
||
data += bytes([pad_byte & 0xFF]) * (align - remainder)
|
||
with open(dst_path, "wb") as f:
|
||
f.write(data)
|
||
return len(data)
|
||
|
||
|
||
def merge_firmware(bootloader_path, app_path, output_path, app_addr, flag_addr,
|
||
flash_size, flag_value=FLAG_VALUE_DEFAULT):
|
||
"""Merge bootloader + app + valid-app flag into one flash image."""
|
||
if app_addr < FLASH_BASE_DEFAULT or app_addr >= FLASH_BASE_DEFAULT + flash_size:
|
||
raise ValueError("APP address is outside the flash range")
|
||
if flag_addr < FLASH_BASE_DEFAULT or flag_addr + 4 > FLASH_BASE_DEFAULT + flash_size:
|
||
raise ValueError("flag address is outside the flash range")
|
||
|
||
with open(bootloader_path, "rb") as f:
|
||
bootloader = f.read()
|
||
with open(app_path, "rb") as f:
|
||
app = f.read()
|
||
|
||
app_offset = app_addr - FLASH_BASE_DEFAULT
|
||
flag_offset = flag_addr - FLASH_BASE_DEFAULT
|
||
|
||
if len(bootloader) > app_offset:
|
||
raise ValueError("bootloader (%d bytes) exceeds APP offset (%d bytes)"
|
||
% (len(bootloader), app_offset))
|
||
if app_offset + len(app) > flag_offset:
|
||
raise ValueError("APP end address exceeds the flag address")
|
||
|
||
image = bytearray(b"\xFF" * flash_size)
|
||
image[0:len(bootloader)] = bootloader
|
||
image[app_offset:app_offset + len(app)] = app
|
||
struct.pack_into("<I", image, flag_offset, flag_value & 0xFFFFFFFF)
|
||
|
||
with open(output_path, "wb") as f:
|
||
f.write(image)
|
||
|
||
return {
|
||
"bootloader_size": len(bootloader),
|
||
"app_size": len(app),
|
||
"output_size": len(image),
|
||
"app_addr": app_addr,
|
||
"flag_addr": flag_addr,
|
||
}
|
||
|
||
|
||
class FlashGui:
|
||
def __init__(self):
|
||
self.root = tk.Tk()
|
||
self.root.title(APP_TITLE)
|
||
self.root.geometry("980x780")
|
||
self.root.minsize(880, 660)
|
||
self._configure_style()
|
||
|
||
self.queue = queue.Queue()
|
||
self.proc = None
|
||
self.action_buttons = []
|
||
|
||
self._create_vars()
|
||
self._build_ui()
|
||
self._ensure_config()
|
||
self._load_config()
|
||
self._on_chip_change()
|
||
self._on_probe_change()
|
||
|
||
self.root.after(100, self._poll_queue)
|
||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||
|
||
def _configure_style(self):
|
||
try:
|
||
self.style = ttk.Style(self.root)
|
||
if "vista" in self.style.theme_names():
|
||
self.style.theme_use("vista")
|
||
style_options = {
|
||
"Title.TLabel": {
|
||
"font": ("Microsoft YaHei UI", 13, "bold"),
|
||
},
|
||
"TLabel": {
|
||
"font": ("Microsoft YaHei UI", 9),
|
||
},
|
||
"TButton": {
|
||
"font": ("Microsoft YaHei UI", 9),
|
||
"padding": (12, 6),
|
||
},
|
||
"TLabelframe.Label": {
|
||
"font": ("Microsoft YaHei UI", 9, "bold"),
|
||
},
|
||
"TNotebook.Tab": {
|
||
"font": ("Microsoft YaHei UI", 9),
|
||
"padding": (14, 6),
|
||
},
|
||
}
|
||
for name, opts in style_options.items():
|
||
try:
|
||
self.style.configure(name, **opts)
|
||
except tk.TclError:
|
||
pass
|
||
except tk.TclError:
|
||
pass
|
||
|
||
def _create_vars(self):
|
||
self.v_openocd = tk.StringVar(value=DEFAULT_OPENOCD)
|
||
self.v_scripts = tk.StringVar(value=DEFAULT_SCRIPTS)
|
||
self.v_speed = tk.StringVar(value="4000")
|
||
self.v_serial = tk.StringVar(value="")
|
||
self.v_extra = tk.StringVar(value="")
|
||
|
||
self.v_chip = tk.StringVar(value="GD32E230F8")
|
||
self.v_probe = tk.StringVar(value="DAPLink / CMSIS-DAP")
|
||
self.v_interface = tk.StringVar(value="interface/cmsis-dap.cfg")
|
||
self.v_target = tk.StringVar(value="target/gd32e23x.cfg")
|
||
self.v_flash_size = tk.StringVar(value="0x10000")
|
||
|
||
self.v_firmware = tk.StringVar(value="")
|
||
self.v_base = tk.StringVar(value="0x08000000")
|
||
self.v_erase = tk.BooleanVar(value=True)
|
||
self.v_verify = tk.BooleanVar(value=True)
|
||
self.v_reset = tk.BooleanVar(value=True)
|
||
|
||
self.v_pad_in = tk.StringVar(value="")
|
||
self.v_pad_out = tk.StringVar(value="")
|
||
self.v_pad_align = tk.StringVar(value="1024")
|
||
self.v_pad_byte = tk.StringVar(value="0xFF")
|
||
self.v_pad_result = tk.StringVar(value="")
|
||
|
||
self.v_bl = tk.StringVar(value="")
|
||
self.v_app = tk.StringVar(value="")
|
||
self.v_merge_out = tk.StringVar(value="")
|
||
self.v_app_addr = tk.StringVar(value="0x08002000")
|
||
self.v_flag_addr = tk.StringVar(value="0x0800FFFC")
|
||
self.v_merge_flash = tk.StringVar(value="0x10000")
|
||
self.v_flag_value = tk.StringVar(value="0xEEEEEEEE")
|
||
self.v_merge_result = tk.StringVar(value="")
|
||
|
||
self.v_status = tk.StringVar(value="就绪")
|
||
|
||
def _build_ui(self):
|
||
root = self.root
|
||
root.grid_rowconfigure(2, weight=1)
|
||
root.grid_columnconfigure(0, weight=1)
|
||
|
||
top = ttk.Frame(root, padding=(14, 8, 14, 4))
|
||
top.grid(row=0, column=0, sticky="ew")
|
||
ttk.Label(top, text=APP_TITLE, style="Title.TLabel").pack(side="left")
|
||
ttk.Label(top, text="OpenOCD 烧录与固件工具", foreground="#666666").pack(
|
||
side="left", padx=(6, 0), pady=(3, 0))
|
||
ttk.Button(top, text="清空日志", command=self._clear_log).pack(
|
||
side="right", padx=(0, 2))
|
||
|
||
ttk.Separator(root, orient="horizontal").grid(
|
||
row=1, column=0, sticky="ew", padx=10, pady=(2, 0))
|
||
|
||
self.notebook = ttk.Notebook(root)
|
||
self.notebook.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 4))
|
||
|
||
self._build_flash_tab()
|
||
self._build_tools_tab()
|
||
|
||
self.log_text = scrolledtext.ScrolledText(
|
||
root, height=7, state="disabled", font=("Consolas", 9),
|
||
relief="solid", borderwidth=1)
|
||
self.log_text.grid(row=3, column=0, sticky="sew", padx=10, pady=(2, 4))
|
||
|
||
status = ttk.Frame(root, padding=(12, 0, 12, 6))
|
||
status.grid(row=4, column=0, sticky="ew")
|
||
ttk.Label(status, textvariable=self.v_status).pack(side="left")
|
||
|
||
def _build_flash_tab(self):
|
||
tab = ttk.Frame(self.notebook, padding=8)
|
||
self.notebook.add(tab, text="烧录")
|
||
tab.grid_columnconfigure(0, weight=1)
|
||
|
||
# Tool / probe paths
|
||
path_frame = ttk.LabelFrame(tab, text="工具与探针", padding=8)
|
||
path_frame.grid(row=0, column=0, sticky="ew")
|
||
path_frame.grid_columnconfigure(0, minsize=132)
|
||
path_frame.grid_columnconfigure(1, weight=1)
|
||
path_frame.grid_columnconfigure(2, minsize=82)
|
||
|
||
self._add_row(path_frame, 0, "OpenOCD 路径", self.v_openocd, self._browse_openocd)
|
||
self._add_row(path_frame, 1, "OpenOCD 脚本目录", self.v_scripts, self._browse_scripts)
|
||
self._add_pair_row(path_frame, 2, "适配器速度 (kHz)", self.v_speed,
|
||
"探针序列号 (可选)", self.v_serial)
|
||
probe_combo, _ = self._add_pair_row(
|
||
path_frame, 3, "烧录器型号", self.v_probe, "Interface 配置", self.v_interface,
|
||
widget1_cls=ttk.Combobox,
|
||
widget1_kwargs={"state": "readonly", "values": list(PROBE_PROFILES.keys())})
|
||
probe_combo.bind("<<ComboboxSelected>>", self._on_probe_change)
|
||
self.probe_combo = probe_combo
|
||
self._add_pair_row(path_frame, 4, "Target 配置", self.v_target,
|
||
"额外初始化命令", self.v_extra)
|
||
|
||
# Target chip / firmware parameters
|
||
chip_frame = ttk.LabelFrame(tab, text="芯片与固件参数", padding=8)
|
||
chip_frame.grid(row=1, column=0, sticky="ew", pady=(6, 0))
|
||
chip_frame.grid_columnconfigure(0, minsize=132)
|
||
chip_frame.grid_columnconfigure(1, weight=1)
|
||
chip_frame.grid_columnconfigure(2, minsize=82)
|
||
|
||
chip_combo, _ = self._add_pair_row(
|
||
chip_frame, 0, "芯片型号", self.v_chip, "Flash 大小", self.v_flash_size,
|
||
widget1_cls=ttk.Combobox,
|
||
widget1_kwargs={"state": "readonly", "values": list(CHIP_PROFILES.keys())})
|
||
chip_combo.bind("<<ComboboxSelected>>", self._on_chip_change)
|
||
self.chip_combo = chip_combo
|
||
self._add_row(chip_frame, 1, "固件文件", self.v_firmware, self._browse_firmware)
|
||
self._add_row(chip_frame, 2, "烧录基地址 (BIN)", self.v_base)
|
||
|
||
ttk.Label(chip_frame, text="HEX/ELF 自动使用文件内地址,仅 BIN 需要填写基地址",
|
||
foreground="#666666").grid(row=3, column=0, columnspan=3,
|
||
sticky="w", padx=0, pady=(0, 4))
|
||
|
||
opt_frame = ttk.Frame(chip_frame)
|
||
opt_frame.grid(row=4, column=0, columnspan=3, sticky="w", pady=(2, 0))
|
||
ttk.Checkbutton(opt_frame, text="烧录前擦除", variable=self.v_erase).pack(
|
||
side="left", padx=(0, 16))
|
||
ttk.Checkbutton(opt_frame, text="烧录后校验", variable=self.v_verify).pack(
|
||
side="left", padx=(0, 16))
|
||
ttk.Checkbutton(opt_frame, text="烧录后复位运行", variable=self.v_reset).pack(
|
||
side="left")
|
||
|
||
# Actions
|
||
act_frame = ttk.LabelFrame(tab, text="操作", padding=8)
|
||
act_frame.grid(row=2, column=0, sticky="ew", pady=(6, 0))
|
||
for i in range(6):
|
||
act_frame.grid_columnconfigure(i, weight=1)
|
||
|
||
self.action_buttons = [
|
||
self._make_action_button(act_frame, 0, "检测设备", self._on_detect),
|
||
self._make_action_button(act_frame, 1, "整片擦除", self._on_erase),
|
||
self._make_action_button(act_frame, 2, "烧录固件", self._on_flash),
|
||
self._make_action_button(act_frame, 3, "读取 Flash", self._on_read),
|
||
self._make_action_button(act_frame, 4, "复位芯片", self._on_reset),
|
||
self._make_action_button(act_frame, 5, "停止", self._on_stop),
|
||
]
|
||
|
||
note = ("提示:BIN 文件需要填写基地址;完整镜像(bootloader+app)基地址为 0x08000000,"
|
||
"单独烧录 APP 时基地址为 0x08002000。")
|
||
ttk.Label(tab, text=note, foreground="#666666").grid(
|
||
row=3, column=0, sticky="w", pady=(6, 0))
|
||
|
||
def _build_tools_tab(self):
|
||
tab = ttk.Frame(self.notebook, padding=12)
|
||
self.notebook.add(tab, text="固件工具")
|
||
tab.grid_columnconfigure(0, weight=1)
|
||
|
||
pad_frame = ttk.LabelFrame(tab, text="补齐 (Pad) - 对齐到 1K,适配 Y-Modem 整包", padding=10)
|
||
pad_frame.grid(row=0, column=0, sticky="ew")
|
||
pad_frame.grid_columnconfigure(0, minsize=132)
|
||
pad_frame.grid_columnconfigure(1, weight=1)
|
||
pad_frame.grid_columnconfigure(2, minsize=82)
|
||
|
||
self._add_row(pad_frame, 0, "输入 BIN", self.v_pad_in, self._browse_pad_in)
|
||
self._add_row(pad_frame, 1, "输出 BIN", self.v_pad_out, self._browse_pad_out)
|
||
|
||
pad_opt = ttk.Frame(pad_frame)
|
||
pad_opt.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(6, 0))
|
||
ttk.Label(pad_opt, text="对齐字节").pack(side="left", padx=(0, 6))
|
||
ttk.Combobox(pad_opt, textvariable=self.v_pad_align, values=["1024", "2048", "4096"],
|
||
width=8, state="readonly").pack(side="left", padx=(0, 16))
|
||
ttk.Label(pad_opt, text="填充字节").pack(side="left", padx=(0, 6))
|
||
ttk.Entry(pad_opt, textvariable=self.v_pad_byte, width=8).pack(
|
||
side="left", padx=(0, 16))
|
||
btn = ttk.Button(pad_opt, text="执行补齐", command=self._on_pad)
|
||
btn.pack(side="left")
|
||
self.action_buttons.append(btn)
|
||
ttk.Label(pad_frame, textvariable=self.v_pad_result, foreground="#1a6fb5").grid(
|
||
row=3, column=0, columnspan=3, sticky="w", pady=(6, 0))
|
||
|
||
merge_frame = ttk.LabelFrame(tab, text="拼接 (Merge) - Bootloader + APP + 有效标志", padding=10)
|
||
merge_frame.grid(row=1, column=0, sticky="ew", pady=(10, 0))
|
||
merge_frame.grid_columnconfigure(0, minsize=132)
|
||
merge_frame.grid_columnconfigure(1, weight=1)
|
||
merge_frame.grid_columnconfigure(2, minsize=82)
|
||
|
||
self._add_row(merge_frame, 0, "Bootloader", self.v_bl, self._browse_bl)
|
||
self._add_row(merge_frame, 1, "Application", self.v_app, self._browse_app)
|
||
self._add_row(merge_frame, 2, "输出文件", self.v_merge_out, self._browse_merge_out)
|
||
|
||
merge_opt = ttk.Frame(merge_frame)
|
||
merge_opt.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(6, 0))
|
||
ttk.Label(merge_opt, text="APP 地址").pack(side="left", padx=(0, 4))
|
||
ttk.Entry(merge_opt, textvariable=self.v_app_addr, width=11).pack(
|
||
side="left", padx=(0, 12))
|
||
ttk.Label(merge_opt, text="标志地址").pack(side="left", padx=(0, 4))
|
||
ttk.Entry(merge_opt, textvariable=self.v_flag_addr, width=11).pack(
|
||
side="left", padx=(0, 12))
|
||
ttk.Label(merge_opt, text="Flash 大小").pack(side="left", padx=(0, 4))
|
||
ttk.Entry(merge_opt, textvariable=self.v_merge_flash, width=11).pack(
|
||
side="left", padx=(0, 12))
|
||
ttk.Label(merge_opt, text="标志值").pack(side="left", padx=(0, 4))
|
||
ttk.Entry(merge_opt, textvariable=self.v_flag_value, width=11).pack(
|
||
side="left", padx=(0, 12))
|
||
btn = ttk.Button(merge_opt, text="执行拼接", command=self._on_merge)
|
||
btn.pack(side="left")
|
||
self.action_buttons.append(btn)
|
||
|
||
ttk.Label(merge_frame, textvariable=self.v_merge_result, foreground="#1a6fb5").grid(
|
||
row=4, column=0, columnspan=3, sticky="w", pady=(6, 0))
|
||
|
||
layout = ("典型布局:0x08000000 Bootloader 8KB | 0x08002000 APP | "
|
||
"0x0800FFFC 有效标志 0xEEEEEEEE | Flash 共 64KB")
|
||
ttk.Label(tab, text=layout, foreground="#666666").grid(row=2, column=0, sticky="w", pady=8)
|
||
|
||
def _add_row(self, parent, row, label, var, browse_cmd=None, widget_cls=ttk.Entry, **kwargs):
|
||
ttk.Label(parent, text=label).grid(
|
||
row=row, column=0, sticky="w", padx=(0, 10), pady=3)
|
||
widget = widget_cls(parent, textvariable=var, **kwargs)
|
||
widget.grid(row=row, column=1, sticky="ew", pady=3)
|
||
if browse_cmd is not None:
|
||
ttk.Button(parent, text="浏览", width=6, command=browse_cmd).grid(
|
||
row=row, column=2, sticky="ew", padx=(8, 0), pady=3)
|
||
return widget
|
||
|
||
def _add_pair_row(self, parent, row, label1, var1, label2, var2,
|
||
widget1_cls=ttk.Entry, widget2_cls=ttk.Entry,
|
||
widget1_kwargs=None, widget2_kwargs=None):
|
||
pair = ttk.Frame(parent)
|
||
pair.grid(row=row, column=0, columnspan=3, sticky="ew", pady=3)
|
||
pair.grid_columnconfigure(0, minsize=132)
|
||
pair.grid_columnconfigure(1, weight=1)
|
||
pair.grid_columnconfigure(2, minsize=12)
|
||
pair.grid_columnconfigure(3, minsize=132)
|
||
pair.grid_columnconfigure(4, weight=1)
|
||
|
||
ttk.Label(pair, text=label1).grid(row=0, column=0, sticky="w", padx=(0, 10))
|
||
widget1 = widget1_cls(pair, textvariable=var1, **(widget1_kwargs or {}))
|
||
widget1.grid(row=0, column=1, sticky="ew")
|
||
|
||
ttk.Label(pair, text=label2).grid(row=0, column=3, sticky="w", padx=(12, 10))
|
||
widget2 = widget2_cls(pair, textvariable=var2, **(widget2_kwargs or {}))
|
||
widget2.grid(row=0, column=4, sticky="ew")
|
||
return widget1, widget2
|
||
|
||
def _make_action_button(self, parent, col, text, command):
|
||
btn = ttk.Button(parent, text=text, command=command)
|
||
btn.grid(row=0, column=col, sticky="ew", padx=3, pady=3)
|
||
return btn
|
||
|
||
def _browse_openocd(self):
|
||
path = filedialog.askopenfilename(
|
||
title="选择 OpenOCD", filetypes=[("OpenOCD", "openocd.exe"), ("所有文件", "*.*")])
|
||
if path:
|
||
self.v_openocd.set(path)
|
||
|
||
def _browse_scripts(self):
|
||
path = filedialog.askdirectory(title="选择 OpenOCD scripts 目录")
|
||
if path:
|
||
self.v_scripts.set(path)
|
||
|
||
def _browse_firmware(self):
|
||
path = filedialog.askopenfilename(
|
||
title="选择固件",
|
||
filetypes=[("固件文件", "*.bin *.hex *.elf"), ("所有文件", "*.*")])
|
||
if path:
|
||
self.v_firmware.set(path)
|
||
if path.lower().endswith(".bin"):
|
||
name, ext = os.path.splitext(os.path.basename(path))
|
||
self.v_merge_out.set(os.path.join(os.path.dirname(path), name + "_BL" + ext))
|
||
|
||
def _browse_pad_in(self):
|
||
path = filedialog.askopenfilename(
|
||
title="选择需要补齐的 BIN", filetypes=[("BIN 文件", "*.bin"), ("所有文件", "*.*")])
|
||
if path:
|
||
self.v_pad_in.set(path)
|
||
name, ext = os.path.splitext(path)
|
||
self.v_pad_out.set(name + "_pad" + ext)
|
||
|
||
def _browse_pad_out(self):
|
||
path = filedialog.asksaveasfilename(
|
||
title="保存补齐后的 BIN", defaultextension=".bin",
|
||
filetypes=[("BIN 文件", "*.bin")])
|
||
if path:
|
||
self.v_pad_out.set(path)
|
||
|
||
def _browse_bl(self):
|
||
path = filedialog.askopenfilename(
|
||
title="选择 Bootloader BIN", filetypes=[("BIN 文件", "*.bin"), ("所有文件", "*.*")])
|
||
if path:
|
||
self.v_bl.set(path)
|
||
self._suggest_merge_output()
|
||
|
||
def _browse_app(self):
|
||
path = filedialog.askopenfilename(
|
||
title="选择 APP BIN", filetypes=[("BIN 文件", "*.bin"), ("所有文件", "*.*")])
|
||
if path:
|
||
self.v_app.set(path)
|
||
self._suggest_merge_output()
|
||
|
||
def _suggest_merge_output(self):
|
||
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))
|
||
|
||
def _browse_merge_out(self):
|
||
path = filedialog.asksaveasfilename(
|
||
title="保存拼接后的镜像", defaultextension=".bin",
|
||
filetypes=[("BIN 文件", "*.bin")])
|
||
if path:
|
||
self.v_merge_out.set(path)
|
||
|
||
def _on_chip_change(self, event=None):
|
||
profile = CHIP_PROFILES.get(self.v_chip.get())
|
||
if profile:
|
||
self.v_flash_size.set("0x%X" % profile["flash"])
|
||
self.v_target.set(profile["target"])
|
||
|
||
def _on_probe_change(self, event=None):
|
||
cfg = PROBE_PROFILES.get(self.v_probe.get(), "")
|
||
if cfg:
|
||
self.v_interface.set(cfg)
|
||
|
||
def _check_tools(self):
|
||
if not os.path.isfile(self.v_openocd.get()):
|
||
messagebox.showerror("缺少工具", "找不到 OpenOCD:%s" % self.v_openocd.get())
|
||
return False
|
||
if not os.path.isdir(self.v_scripts.get()):
|
||
messagebox.showerror("缺少工具", "找不到 OpenOCD scripts 目录:%s" % self.v_scripts.get())
|
||
return False
|
||
return True
|
||
|
||
def _build_openocd_cmd(self, script):
|
||
cmd = [self.v_openocd.get().strip(), "-s", self.v_scripts.get().strip()]
|
||
|
||
interface = _ensure_cfg_path(self.v_interface.get(), "interface")
|
||
target = _ensure_cfg_path(self.v_target.get(), "target")
|
||
cmd += ["-f", interface, "-f", target]
|
||
|
||
speed = self.v_speed.get().strip()
|
||
if speed:
|
||
cmd += ["-c", "adapter speed " + speed]
|
||
|
||
serial = self.v_serial.get().strip()
|
||
if serial and "cmsis-dap" in interface.lower():
|
||
cmd += ["-c", "cmsis_dap_serial " + serial]
|
||
|
||
extra = self.v_extra.get().strip()
|
||
if extra:
|
||
cmd += ["-c", extra]
|
||
|
||
cmd += ["-c", script]
|
||
return cmd
|
||
|
||
def _start_operation(self, title, script):
|
||
if self.proc is not None:
|
||
self._append_log("已有操作正在执行,请先停止。")
|
||
return
|
||
if not self._check_tools():
|
||
return
|
||
|
||
cmd = self._build_openocd_cmd(script)
|
||
self._set_busy(True)
|
||
self._append_log("== %s ==" % title)
|
||
self._append_log("命令: " + " ".join(cmd))
|
||
|
||
thread = threading.Thread(target=self._worker, args=(title, cmd), daemon=True)
|
||
thread.start()
|
||
|
||
def _worker(self, title, cmd):
|
||
creation = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||
try:
|
||
proc = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
bufsize=1,
|
||
creationflags=creation,
|
||
)
|
||
except Exception as exc:
|
||
self.queue.put(("log", "启动 OpenOCD 失败: %s" % exc))
|
||
self.queue.put(("done", (title, -1)))
|
||
return
|
||
|
||
self.proc = proc
|
||
try:
|
||
for line in proc.stdout:
|
||
line = _strip_ansi(line.rstrip("\r\n"))
|
||
if line.strip():
|
||
self.queue.put(("log", line))
|
||
except Exception:
|
||
pass
|
||
|
||
code = proc.wait()
|
||
self.proc = None
|
||
self.queue.put(("done", (title, code)))
|
||
|
||
def _poll_queue(self):
|
||
try:
|
||
while True:
|
||
kind, payload = self.queue.get_nowait()
|
||
if kind == "log":
|
||
self._append_log(payload)
|
||
elif kind == "done":
|
||
title, code = payload
|
||
if code == 0:
|
||
self._append_log("[完成] %s" % title)
|
||
self._set_status("%s 完成" % title)
|
||
else:
|
||
self._append_log("[失败] %s (退出码 %d)" % (title, code))
|
||
self._set_status("%s 失败" % title)
|
||
self._set_busy(False)
|
||
except queue.Empty:
|
||
pass
|
||
self.root.after(100, self._poll_queue)
|
||
|
||
def _on_detect(self):
|
||
self._start_operation("检测设备", "init; reset halt; flash list; exit")
|
||
|
||
def _on_erase(self):
|
||
size = _parse_int(self.v_flash_size.get())
|
||
if size is None or size <= 0:
|
||
messagebox.showerror("参数错误", "Flash 大小无效:%s" % self.v_flash_size.get())
|
||
return
|
||
script = "init; reset halt; flash erase_address 0x%08X 0x%X; reset halt; exit" % (
|
||
FLASH_BASE_DEFAULT, size)
|
||
self._start_operation("整片擦除", script)
|
||
|
||
def _on_flash(self):
|
||
filepath = self.v_firmware.get().strip()
|
||
if not filepath or not os.path.isfile(filepath):
|
||
messagebox.showerror("缺少固件", "请先选择固件文件。")
|
||
return
|
||
|
||
ext = os.path.splitext(filepath)[1].lower()
|
||
script = "init; reset halt; "
|
||
|
||
if ext == ".bin":
|
||
base = _parse_int(self.v_base.get())
|
||
if base is None:
|
||
messagebox.showerror("参数错误", "BIN 基地址无效:%s" % self.v_base.get())
|
||
return
|
||
addr = " 0x%08X" % base
|
||
else:
|
||
addr = ""
|
||
|
||
quoted = _tcl_quote(filepath)
|
||
if self.v_erase.get():
|
||
script += "flash write_image erase %s%s; " % (quoted, addr)
|
||
else:
|
||
script += "flash write_image %s%s; " % (quoted, addr)
|
||
|
||
if self.v_verify.get():
|
||
script += "verify_image %s%s; " % (quoted, addr)
|
||
|
||
script += ("reset run" if self.v_reset.get() else "reset halt") + "; exit"
|
||
self._start_operation("烧录固件", script)
|
||
|
||
def _on_read(self):
|
||
out_path = filedialog.asksaveasfilename(
|
||
title="保存 Flash 内容", defaultextension=".bin",
|
||
initialfile="flash_dump.bin", filetypes=[("BIN 文件", "*.bin")])
|
||
if not out_path:
|
||
return
|
||
size = _parse_int(self.v_flash_size.get())
|
||
if size is None or size <= 0:
|
||
messagebox.showerror("参数错误", "Flash 大小无效:%s" % self.v_flash_size.get())
|
||
return
|
||
script = "init; reset halt; dump_image %s 0x%08X 0x%X; exit" % (
|
||
_tcl_quote(out_path), FLASH_BASE_DEFAULT, size)
|
||
self._start_operation("读取 Flash", script)
|
||
|
||
def _on_reset(self):
|
||
self._start_operation("复位芯片", "init; reset run; exit")
|
||
|
||
def _on_stop(self):
|
||
if self.proc is not None:
|
||
try:
|
||
self.proc.terminate()
|
||
self._append_log("已发送停止请求。")
|
||
except Exception as exc:
|
||
self._append_log("停止失败: %s" % exc)
|
||
else:
|
||
self._append_log("当前没有正在执行的操作。")
|
||
|
||
def _on_pad(self):
|
||
src = self.v_pad_in.get().strip()
|
||
dst = self.v_pad_out.get().strip()
|
||
align = _parse_int(self.v_pad_align.get())
|
||
pad_byte = _parse_int(self.v_pad_byte.get())
|
||
if not src or not os.path.isfile(src):
|
||
messagebox.showerror("缺少文件", "请选择需要补齐的 BIN。")
|
||
return
|
||
if not dst:
|
||
messagebox.showerror("缺少文件", "请设置输出文件。")
|
||
return
|
||
if align is None or align <= 0:
|
||
messagebox.showerror("参数错误", "对齐字节无效。")
|
||
return
|
||
if pad_byte is None:
|
||
pad_byte = 0xFF
|
||
|
||
try:
|
||
original = os.path.getsize(src)
|
||
padded = pad_bin(src, dst, align, pad_byte)
|
||
self.v_pad_result.set("原始 %d 字节 -> 补齐后 %d 字节 (+%d),对齐 %d 字节"
|
||
% (original, padded, padded - original, align))
|
||
self._append_log("补齐完成: %s -> %s" % (src, dst))
|
||
except Exception as exc:
|
||
messagebox.showerror("补齐失败", str(exc))
|
||
self._append_log("补齐失败: %s" % exc)
|
||
|
||
def _on_merge(self):
|
||
bl = self.v_bl.get().strip()
|
||
app = self.v_app.get().strip()
|
||
out = self.v_merge_out.get().strip()
|
||
app_addr = _parse_int(self.v_app_addr.get())
|
||
flag_addr = _parse_int(self.v_flag_addr.get())
|
||
flash_size = _parse_int(self.v_merge_flash.get())
|
||
flag_value = _parse_int(self.v_flag_value.get())
|
||
|
||
if not bl or not os.path.isfile(bl):
|
||
messagebox.showerror("缺少文件", "请选择 Bootloader BIN。")
|
||
return
|
||
if not app or not os.path.isfile(app):
|
||
messagebox.showerror("缺少文件", "请选择 APP BIN。")
|
||
return
|
||
if not out:
|
||
messagebox.showerror("缺少文件", "请设置输出文件。")
|
||
return
|
||
if app_addr is None or flag_addr is None or flash_size is None:
|
||
messagebox.showerror("参数错误", "地址或 Flash 大小无效。")
|
||
return
|
||
if flag_value is None:
|
||
flag_value = FLAG_VALUE_DEFAULT
|
||
|
||
try:
|
||
info = merge_firmware(bl, app, out, app_addr, flag_addr, flash_size, flag_value)
|
||
self.v_merge_result.set(
|
||
"Bootloader %d 字节 @0x%08X | APP %d 字节 @0x%08X | 镜像 %d 字节 | 标志 @0x%08X"
|
||
% (info["bootloader_size"], FLASH_BASE_DEFAULT, info["app_size"],
|
||
info["app_addr"], info["output_size"], info["flag_addr"]))
|
||
self._append_log("拼接完成: %s" % out)
|
||
except Exception as exc:
|
||
messagebox.showerror("拼接失败", str(exc))
|
||
self._append_log("拼接失败: %s" % exc)
|
||
|
||
def _set_busy(self, busy):
|
||
for btn in self.action_buttons:
|
||
btn.config(state=("disabled" if busy else "normal"))
|
||
if busy:
|
||
self._set_status("正在执行...")
|
||
else:
|
||
self._set_status("就绪")
|
||
|
||
def _set_status(self, text):
|
||
self.v_status.set("状态: " + text)
|
||
|
||
def _append_log(self, text):
|
||
stamp = time.strftime("%H:%M:%S")
|
||
self.log_text.config(state="normal")
|
||
self.log_text.insert("end", "[%s] %s\n" % (stamp, text))
|
||
self.log_text.see("end")
|
||
self.log_text.config(state="disabled")
|
||
|
||
def _clear_log(self):
|
||
self.log_text.config(state="normal")
|
||
self.log_text.delete("1.0", "end")
|
||
self.log_text.config(state="disabled")
|
||
|
||
def _load_config(self):
|
||
try:
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
cfg = json.load(f)
|
||
except (OSError, ValueError):
|
||
return
|
||
|
||
mapping = {
|
||
"openocd": self.v_openocd,
|
||
"scripts": self.v_scripts,
|
||
"speed": self.v_speed,
|
||
"serial": self.v_serial,
|
||
"extra": self.v_extra,
|
||
"chip": self.v_chip,
|
||
"probe": self.v_probe,
|
||
"interface": self.v_interface,
|
||
"target": self.v_target,
|
||
"flash_size": self.v_flash_size,
|
||
"firmware": self.v_firmware,
|
||
"base": self.v_base,
|
||
"erase": self.v_erase,
|
||
"verify": self.v_verify,
|
||
"reset": self.v_reset,
|
||
"pad_in": self.v_pad_in,
|
||
"pad_out": self.v_pad_out,
|
||
"pad_align": self.v_pad_align,
|
||
"pad_byte": self.v_pad_byte,
|
||
"bootloader": self.v_bl,
|
||
"app": self.v_app,
|
||
"merge_out": self.v_merge_out,
|
||
"app_addr": self.v_app_addr,
|
||
"flag_addr": self.v_flag_addr,
|
||
"merge_flash": self.v_merge_flash,
|
||
"flag_value": self.v_flag_value,
|
||
}
|
||
for key, var in mapping.items():
|
||
if key in cfg:
|
||
try:
|
||
var.set(cfg[key])
|
||
except tk.TclError:
|
||
pass
|
||
|
||
def _ensure_config(self):
|
||
try:
|
||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||
except OSError as exc:
|
||
self._append_log("创建配置目录失败: %s" % exc)
|
||
return
|
||
if not os.path.isfile(CONFIG_PATH):
|
||
self._save_config()
|
||
|
||
def _save_config(self):
|
||
try:
|
||
os.makedirs(CONFIG_DIR, exist_ok=True)
|
||
except OSError as exc:
|
||
self._append_log("创建配置目录失败: %s" % exc)
|
||
return
|
||
data = {
|
||
"openocd": self.v_openocd.get(),
|
||
"scripts": self.v_scripts.get(),
|
||
"speed": self.v_speed.get(),
|
||
"serial": self.v_serial.get(),
|
||
"extra": self.v_extra.get(),
|
||
"chip": self.v_chip.get(),
|
||
"probe": self.v_probe.get(),
|
||
"interface": self.v_interface.get(),
|
||
"target": self.v_target.get(),
|
||
"flash_size": self.v_flash_size.get(),
|
||
"firmware": self.v_firmware.get(),
|
||
"base": self.v_base.get(),
|
||
"erase": bool(self.v_erase.get()),
|
||
"verify": bool(self.v_verify.get()),
|
||
"reset": bool(self.v_reset.get()),
|
||
"pad_in": self.v_pad_in.get(),
|
||
"pad_out": self.v_pad_out.get(),
|
||
"pad_align": self.v_pad_align.get(),
|
||
"pad_byte": self.v_pad_byte.get(),
|
||
"bootloader": self.v_bl.get(),
|
||
"app": self.v_app.get(),
|
||
"merge_out": self.v_merge_out.get(),
|
||
"app_addr": self.v_app_addr.get(),
|
||
"flag_addr": self.v_flag_addr.get(),
|
||
"merge_flash": self.v_merge_flash.get(),
|
||
"flag_value": self.v_flag_value.get(),
|
||
}
|
||
try:
|
||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
except OSError as exc:
|
||
self._append_log("保存配置失败: %s" % exc)
|
||
|
||
def _on_close(self):
|
||
if self.proc is not None:
|
||
if not messagebox.askyesno("确认退出", "烧录操作仍在进行,是否停止并退出?"):
|
||
return
|
||
try:
|
||
self.proc.terminate()
|
||
except Exception:
|
||
pass
|
||
self._save_config()
|
||
self.root.destroy()
|
||
|
||
|
||
def main():
|
||
app = FlashGui()
|
||
app.root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|