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:
2026-08-01 00:09:24 +08:00
parent 49f84396bd
commit e8eb194fb8
5 changed files with 1020 additions and 18 deletions
+4 -1
View File
@@ -18,4 +18,7 @@ venv/
# Binary files
*.bin
!gd32e230f8_bootloader_hulk.bin
!gd32e230f8_bootloader_hulk.bin
# DAPLink Flash GUI local config
FlashTool/config/
+55
View File
@@ -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 和目标板 SWDSWDIO / 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`,可在界面中修改)
+877
View File
@@ -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()
+17
View File
@@ -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
+67 -17
View File
@@ -1,12 +1,14 @@
# GD32E230 固件工具
Bootloader + Application 固件处理工具,支持合成整片烧录镜像和 Ymodem 升级包补齐。
本仓库提供 GD32E230 系列固件处理与烧录工具:
> **旧工具** `merge_bin.py` / `fill_bin.py` 仍保留(→ [README_old.md](README_old.md)),本仓库推荐使用新工具。
- **DAPLink Flash GUI(推荐)**:图形界面烧录工具,位于 `FlashTool/`
- **命令行固件工具**`gd32e230_firmware_tool.py`(合成 + 补齐二合一)
- **旧版脚本**`merge_bin.py``fill_bin.py`(保留兼容)
## Flash 布局
```
```text
0x08000000 ┌──────────────────┐
│ Bootloader │ 8KB
0x08002000 ├──────────────────┤
@@ -24,11 +26,55 @@ Bootloader + Application 固件处理工具,支持合成整片烧录镜像和
- 标志位 `0xEEEE` 告诉 BootloaderAPP 有效,直接跳转
- 校准页当前为预留空间
## 环境要求
## DAPLink Flash GUI(推荐)
- Python 3.x,无第三方依赖
日常烧录优先使用图形界面工具,不需要打开 VSCode,也不需要记忆 OpenOCD 命令。
## 快速开始
### 启动
```bat
双击 FlashTool\start_flash_gui.bat
```
或手动运行:
```bat
python FlashTool\daplink_flash_gui.py
```
### 功能
- 烧录操作:检测设备、整片擦除、烧录固件、读取 Flash、复位芯片、停止
- 固件格式:BIN / HEX / ELF
- BIN:手动填写烧录基地址
- HEX / ELF:自动使用文件内地址
- 芯片型号:GD32E230F4 / F6 / F8 / C8,自动带出 Flash 大小和 target 配置
- 烧录器:DAPLink / CMSIS-DAP、GD-Link、ST-Link、J-Link
- 烧录参数:适配器速度、探针序列号、烧录前擦除、烧录后校验、烧录后复位运行
- 固件处理:补齐(1K / 2K / 4K)、拼接 Bootloader + APP + 有效标志
### 配置
- 配置文件按主机名分开:`FlashTool/config/flash_gui_<主机名>.json`
- 首次打开自动创建 `config` 目录和当前电脑的配置
- 之后打开自动加载;每台电脑互不影响
- `FlashTool/config/` 已加入 `.gitignore`,不会提交
### 使用步骤
1. 连接烧录器(DAPLink / GD-Link 等)到目标板 SWDSWDIO、SWCLK、GND
2. 启动 GUI,选择芯片型号和烧录器型号
3. 选择固件文件并确认地址:
- 完整镜像(bootloader + app):`0x08000000`
- 单独烧录 APP`0x08002000`
4. 点击“检测设备”确认连接正常
5. 点击“烧录固件”
> Bootloader 的 Y-Modem 只按 1K 整包接收,OTA 升级包发送前请先用“补齐”功能处理。
## 命令行固件工具
### 快速开始
```bash
# 合成完整镜像(整片烧录用)
@@ -38,9 +84,9 @@ python gd32e230_firmware_tool.py -m app.bin
python gd32e230_firmware_tool.py -f app.bin
```
## 模式说明
### 模式说明
### `-m` / `--merge` — 合成完整镜像
#### `-m` / `--merge` — 合成完整镜像
将 Bootloader + APP + FLAG 合并为一个 Flash 镜像,直接整片烧录。**不补齐**(整片烧录无需考虑页对齐)。
@@ -50,7 +96,7 @@ python gd32e230_firmware_tool.py -m CeramixNano-MB_V1.0.9.bin
输出:`CeramixNano-MB_V1.0.9_BL.bin`
### `-f` / `--fill` — 补齐升级包
#### `-f` / `--fill` — 补齐升级包
将 APP 尾部补 `0xFF` 到 1KB 页边界,供 Bootloader 的 Ymodem 接收端使用。补齐后自动检查是否超过芯片 APP 容量上限。
@@ -60,7 +106,7 @@ python gd32e230_firmware_tool.py -f CeramixNano-MB_V1.0.9.bin
输出:`CeramixNano-MB_V1.0.9_UPDATE.bin`
## 参数
### 参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
@@ -74,7 +120,7 @@ python gd32e230_firmware_tool.py -f CeramixNano-MB_V1.0.9.bin
> **`-m` 和 `-f` 互斥**,每次只能选一种模式。
## 使用示例
### 使用示例
```bash
# F8 合成镜像(默认)
@@ -93,11 +139,11 @@ python gd32e230_firmware_tool.py -f app.bin -s F4
python gd32e230_firmware_tool.py -m app.bin -b my_bootloader.bin -o release.bin
```
## 输出示例
### 输出示例
**merge 模式:**
```
```text
芯片: GD32E230F8 (64KB)
合成完成: CeramixNano-MB_V1.0.9_BL.bin
@@ -112,7 +158,7 @@ python gd32e230_firmware_tool.py -m app.bin -b my_bootloader.bin -o release.bin
**fill 模式:**
```
```text
芯片: GD32E230F8 (64KB)
补齐完成: app_UPDATE.bin
原始大小: 25000B
@@ -124,12 +170,16 @@ python gd32e230_firmware_tool.py -m app.bin -b my_bootloader.bin -o release.bin
**超限拦截:**
```
```text
错误:APP (60000B / 58.6KB) 超过最大容量 (55296B / 54KB)
芯片 64KB: 64KB - 8KB(BL) - 1KB(校准) - 1KB(Flag) = 54KB
```
## 烧录
### 旧版脚本
`merge_bin.py` / `fill_bin.py` 为旧版命令行脚本,功能已合并进 `gd32e230_firmware_tool.py`,历史说明见 [README_old.md](README_old.md)。
### 烧录
```bash
# OpenOCD
@@ -140,7 +190,7 @@ openocd -f interface/cmsis-dap.cfg -f target/gd32e23x.cfg \
JFlash -openprj project.jflash -open merged.bin,0x08000000 -auto -exit
```
## 安全校验
### 安全校验
- Bootloader 体积不超过 APP 起始偏移(8KB)
- APP 大小不超过芯片可用空间