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 使用说明置顶,命令行工具说明保留在下方
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# DAPLink Flash GUI
|
||||
|
||||
一个面向 GD32 日常烧录的 Windows 小工具,不依赖 VSCode,也不用记 OpenOCD 命令。
|
||||
双击 `start_flash_gui.bat` 即可启动。
|
||||
|
||||
## 功能
|
||||
|
||||
### 烧录页
|
||||
|
||||
- 芯片型号:GD32E230F4 / F6 / F8 / C8,自动带出 Flash 大小和 OpenOCD target 配置
|
||||
- 烧录器型号:DAPLink / CMSIS-DAP、GD-Link、ST-Link、J-Link、CMSIS-DAP (TCP)
|
||||
- OpenOCD 路径和 scripts 目录可配置,默认使用 `D:/toolchain` 下同一套工具
|
||||
- BIN 文件可指定烧录基地址,HEX/ELF 使用文件内地址
|
||||
- 常用烧录参数:适配器速度、探针序列号、烧录前擦除、烧录后校验、复位运行
|
||||
- 操作:检测设备、整片擦除、烧录固件、读取 Flash、复位芯片、停止当前操作
|
||||
|
||||
### 固件工具页
|
||||
|
||||
- 补齐:把 BIN 填充到 1K(或 2K/4K)整数倍,默认填充 `0xFF`。
|
||||
因为 bootloader 的 Y-Modem 只按 1K 整包写 Flash,最后一包不满 1K 时
|
||||
必须先在发送侧补齐。
|
||||
- 拼接:把 Bootloader + APP + 有效标志合成一个完整 Flash 镜像,
|
||||
逻辑与 `gd32e230f8_firmware_merge_tool/merge_bin.py` 一致。
|
||||
|
||||
## 典型布局
|
||||
|
||||
```text
|
||||
0x08000000 Bootloader 8KB
|
||||
0x08002000 Application
|
||||
0x0800FFFC 有效标志 0xEEEEEEEE
|
||||
0x08010000 Flash 结束 (64KB)
|
||||
```
|
||||
|
||||
完整镜像烧录基地址为 `0x08000000`;只烧 APP 时基地址为 `0x08002000`。
|
||||
|
||||
## 使用
|
||||
|
||||
1. 连接 DAPLink 和目标板 SWD(SWDIO / SWCLK / GND)。
|
||||
2. 双击 `start_flash_gui.bat`,或执行:
|
||||
|
||||
```bat
|
||||
python daplink_flash_gui.py
|
||||
```
|
||||
|
||||
3. 选择芯片型号、烧录器型号、固件文件,点击“烧录固件”。
|
||||
|
||||
运行参数会自动保存到 `FlashTool/config/flash_gui_<主机名>.json`。首次打开会自动创建
|
||||
`config` 目录并生成当前电脑的配置;下次打开检测到就加载,没有就重新创建。
|
||||
每台电脑使用独立文件名,同步仓库时不会互相覆盖。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Windows
|
||||
- Python 3.10+(标准安装自带 tkinter,无需第三方库)
|
||||
- OpenOCD(默认 `D:/toolchain/openocd/bin/openocd.exe`,可在界面中修改)
|
||||
@@ -0,0 +1,877 @@
|
||||
# -*- 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()
|
||||
@@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
|
||||
where pyw >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
start "" pyw -3 "daplink_flash_gui.py"
|
||||
exit /b
|
||||
)
|
||||
|
||||
where pythonw >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
start "" pythonw "daplink_flash_gui.py"
|
||||
exit /b
|
||||
)
|
||||
|
||||
echo Python GUI launcher not found. Run manually: python daplink_flash_gui.py
|
||||
pause
|
||||
Reference in New Issue
Block a user