Files
gd32e230f8_firmware_merge_tool/FlashTool/daplink_flash_gui.py
T
hulk e8eb194fb8 feat(flash-gui): 新增 DAPLink 烧录 GUI 并整理固件工具说明
- 新增 FlashTool/ 图形化烧录工具,支持 GD32E230F4/F6/F8/C8
- 支持 DAPLink/CMSIS-DAP、GD-Link、ST-Link、J-Link
- 支持 BIN/HEX/ELF 烧录、整片擦除、读取 Flash、复位、补齐与拼接
- 配置按主机名保存于 FlashTool/config/,自动创建并 gitignore
- README 调整:GUI 使用说明置顶,命令行工具说明保留在下方
2026-08-01 00:09:24 +08:00

878 lines
34 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.
# -*- 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()