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:
+4
-1
@@ -18,4 +18,7 @@ venv/
|
|||||||
|
|
||||||
# Binary files
|
# Binary files
|
||||||
*.bin
|
*.bin
|
||||||
!gd32e230f8_bootloader_hulk.bin
|
!gd32e230f8_bootloader_hulk.bin
|
||||||
|
# DAPLink Flash GUI local config
|
||||||
|
FlashTool/config/
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
# GD32E230 固件工具
|
# 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 布局
|
## Flash 布局
|
||||||
|
|
||||||
```
|
```text
|
||||||
0x08000000 ┌──────────────────┐
|
0x08000000 ┌──────────────────┐
|
||||||
│ Bootloader │ 8KB
|
│ Bootloader │ 8KB
|
||||||
0x08002000 ├──────────────────┤
|
0x08002000 ├──────────────────┤
|
||||||
@@ -24,11 +26,55 @@ Bootloader + Application 固件处理工具,支持合成整片烧录镜像和
|
|||||||
- 标志位 `0xEEEE` 告诉 Bootloader:APP 有效,直接跳转
|
- 标志位 `0xEEEE` 告诉 Bootloader:APP 有效,直接跳转
|
||||||
- 校准页当前为预留空间
|
- 校准页当前为预留空间
|
||||||
|
|
||||||
## 环境要求
|
## 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 等)到目标板 SWD:SWDIO、SWCLK、GND
|
||||||
|
2. 启动 GUI,选择芯片型号和烧录器型号
|
||||||
|
3. 选择固件文件并确认地址:
|
||||||
|
- 完整镜像(bootloader + app):`0x08000000`
|
||||||
|
- 单独烧录 APP:`0x08002000`
|
||||||
|
4. 点击“检测设备”确认连接正常
|
||||||
|
5. 点击“烧录固件”
|
||||||
|
|
||||||
|
> Bootloader 的 Y-Modem 只按 1K 整包接收,OTA 升级包发送前请先用“补齐”功能处理。
|
||||||
|
|
||||||
|
## 命令行固件工具
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 合成完整镜像(整片烧录用)
|
# 合成完整镜像(整片烧录用)
|
||||||
@@ -38,9 +84,9 @@ python gd32e230_firmware_tool.py -m app.bin
|
|||||||
python gd32e230_firmware_tool.py -f app.bin
|
python gd32e230_firmware_tool.py -f app.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
## 模式说明
|
### 模式说明
|
||||||
|
|
||||||
### `-m` / `--merge` — 合成完整镜像
|
#### `-m` / `--merge` — 合成完整镜像
|
||||||
|
|
||||||
将 Bootloader + APP + FLAG 合并为一个 Flash 镜像,直接整片烧录。**不补齐**(整片烧录无需考虑页对齐)。
|
将 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`
|
输出:`CeramixNano-MB_V1.0.9_BL.bin`
|
||||||
|
|
||||||
### `-f` / `--fill` — 补齐升级包
|
#### `-f` / `--fill` — 补齐升级包
|
||||||
|
|
||||||
将 APP 尾部补 `0xFF` 到 1KB 页边界,供 Bootloader 的 Ymodem 接收端使用。补齐后自动检查是否超过芯片 APP 容量上限。
|
将 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`
|
输出:`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` 互斥**,每次只能选一种模式。
|
> **`-m` 和 `-f` 互斥**,每次只能选一种模式。
|
||||||
|
|
||||||
## 使用示例
|
### 使用示例
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# F8 合成镜像(默认)
|
# 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
|
python gd32e230_firmware_tool.py -m app.bin -b my_bootloader.bin -o release.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
## 输出示例
|
### 输出示例
|
||||||
|
|
||||||
**merge 模式:**
|
**merge 模式:**
|
||||||
|
|
||||||
```
|
```text
|
||||||
芯片: GD32E230F8 (64KB)
|
芯片: GD32E230F8 (64KB)
|
||||||
|
|
||||||
合成完成: CeramixNano-MB_V1.0.9_BL.bin
|
合成完成: 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 模式:**
|
**fill 模式:**
|
||||||
|
|
||||||
```
|
```text
|
||||||
芯片: GD32E230F8 (64KB)
|
芯片: GD32E230F8 (64KB)
|
||||||
补齐完成: app_UPDATE.bin
|
补齐完成: app_UPDATE.bin
|
||||||
原始大小: 25000B
|
原始大小: 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)
|
错误:APP (60000B / 58.6KB) 超过最大容量 (55296B / 54KB)
|
||||||
芯片 64KB: 64KB - 8KB(BL) - 1KB(校准) - 1KB(Flag) = 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
|
```bash
|
||||||
# OpenOCD
|
# 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
|
JFlash -openprj project.jflash -open merged.bin,0x08000000 -auto -exit
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安全校验
|
### 安全校验
|
||||||
|
|
||||||
- Bootloader 体积不超过 APP 起始偏移(8KB)
|
- Bootloader 体积不超过 APP 起始偏移(8KB)
|
||||||
- APP 大小不超过芯片可用空间
|
- APP 大小不超过芯片可用空间
|
||||||
|
|||||||
Reference in New Issue
Block a user