refactor(flash-gui): 对齐 DLPilot 重构 GUI 并内置 OpenOCD 发布链
This commit is contained in:
@@ -21,4 +21,9 @@ venv/
|
||||
!gd32e230f8_bootloader_hulk.bin
|
||||
# DAPLink Flash GUI local config
|
||||
FlashTool/config/
|
||||
FlashTool/flash_gui_settings.json
|
||||
# DAPLink Flash 构建与发布产物 + 内置工具链(不入库)
|
||||
FlashTool/release/
|
||||
FlashTool/src/build/
|
||||
FlashTool/toolchain/
|
||||
|
||||
|
||||
+138
-39
@@ -1,55 +1,154 @@
|
||||
# DAPLink Flash GUI
|
||||
# DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接
|
||||
|
||||
一个面向 GD32 日常烧录的 Windows 小工具,不依赖 VSCode,也不用记 OpenOCD 命令。
|
||||
双击 `start_flash_gui.bat` 即可启动。
|
||||
> GD32E230 系列的桌面烧录/固件处理工具:**OpenOCD 烧录**(DAPLink / GD-Link /
|
||||
> ST-Link / J-Link)+ **补齐 Pad**(Y-Modem 页对齐)+ **拼接 Merge**
|
||||
> (Bootloader + APP + 有效标志 → 整片镜像)。
|
||||
>
|
||||
> - **只想用工具**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases)
|
||||
> 下载 `DAPLinkFlash-vX.Y.Z-win64.zip`,解压双击 `DAPLinkFlash.exe`
|
||||
> (Windows 10/11 x64,免安装、无需 Python,**内置 OpenOCD 工具链**)。
|
||||
> - **要改代码/重新打包**:见 §5、§6。
|
||||
>
|
||||
> 界面与构建方式对齐 DLPilot 项目(CustomTkinter 深色双栏仪表盘 +
|
||||
> PyInstaller onefile + Gitea Release 发布)。更新:2026-09-14
|
||||
|
||||
## 功能
|
||||
## 1. 功能总览
|
||||
|
||||
### 烧录页
|
||||
| 功能 | 说明 |
|
||||
|---|---|
|
||||
| OpenOCD 烧录 | 检测设备 / 烧录固件 / 整片擦除 / 读取 Flash / 复位芯片 / 停止,OpenOCD 输出实时进日志区 |
|
||||
| 固件格式 | BIN / HEX / ELF——HEX/ELF 自动用文件内地址,仅 BIN 需填基地址(完整镜像 `0x08000000`,单独 APP `0x08002000`) |
|
||||
| 芯片型号 | GD32E230F4 / F6 / F8 / C8,自动带出 Flash 大小与 target 配置 |
|
||||
| 烧录器 | DAPLink / CMSIS-DAP、GD-Link、ST-Link、J-Link、CMSIS-DAP(TCP)、Custom;支持适配器速度与探针序列号 |
|
||||
| 补齐 Pad | BIN 尾部补 `0xFF` 到 1K/2K/4K 页边界;bootloader 的 Y-Modem 只按 1K 整包写 Flash,最后一包不满必须先补齐 |
|
||||
| 拼接 Merge | Bootloader + APP + 有效标志(`0xEEEEEEEE` @`0x0800FFFC`) → 整片 Flash 镜像,逻辑与 `merge_bin.py` 一致 |
|
||||
| 内置工具链 | 发布包自带 OpenOCD(`toolchain/openocd/`),GUI 自动定位,免安装免配置 |
|
||||
| 命令行模式 | 全部功能可脚本化调用(`--flash / --pad / --merge ...`,见 §4) |
|
||||
| 外观 | 跟随系统深/浅色,顶栏可切换 Dark / Light / System |
|
||||
|
||||
- 芯片型号: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、复位芯片、停止当前操作
|
||||
## 2. 下载与安装
|
||||
|
||||
### 固件工具页
|
||||
1. 打开 [Releases 页面](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases),
|
||||
取最新的 `DAPLinkFlash-vX.Y.Z-win64.zip`;
|
||||
2. 解压到**任意可写目录**(配置 `flash_gui_settings.json` 写在 exe 旁边);
|
||||
3. 双击 `DAPLinkFlash.exe`。首次启动比脚本慢一两秒(onefile 解压),属正常现象。
|
||||
|
||||
- 补齐:把 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)
|
||||
```
|
||||
DAPLinkFlash/
|
||||
├── DAPLinkFlash.exe 主程序(单文件自包含)
|
||||
├── toolchain/openocd/ 内置 OpenOCD(GUI 自动定位)
|
||||
│ ├── bin/openocd.exe + libftdi1.dll / libusb-1.0.dll
|
||||
│ └── openocd/scripts/ interface / target 配置脚本
|
||||
└── README.md 本说明
|
||||
```
|
||||
|
||||
完整镜像烧录基地址为 `0x08000000`;只烧 APP 时基地址为 `0x08002000`。
|
||||
## 3. GUI 使用
|
||||
|
||||
## 使用
|
||||
主窗口为双栏仪表盘:
|
||||
|
||||
1. 连接 DAPLink 和目标板 SWD(SWDIO / SWCLK / GND)。
|
||||
2. 双击 `start_flash_gui.bat`,或执行:
|
||||
```
|
||||
左列 右列
|
||||
┌ 探针与工具链 [自动定位] ┐ ┌ 补齐 (Pad) — Y-Modem 对齐 ┐
|
||||
├ 芯片与固件(型号/固件/基地址)┤ ├ 拼接 (Merge) — BL+APP+标志 ┤
|
||||
└ 操作: 检测/烧录/擦除/读取/复位/停止 ┘ └ 运行日志 — OpenOCD 输出 ┘
|
||||
```
|
||||
|
||||
```bat
|
||||
python daplink_flash_gui.py
|
||||
```
|
||||
1. **选工具链**:默认自动定位(内置 → 本机 `D:/toolchain` → PATH),左卡右上
|
||||
"自动定位"可重新探测;也可手动浏览指定 `openocd.exe` 与 scripts 目录;
|
||||
2. **选芯片与烧录器**:芯片型号下拉自动带出 Flash 大小和 target;烧录器下拉
|
||||
自动带出 interface cfg;多探针时填序列号;
|
||||
3. **选固件**:点"浏览"选 BIN/HEX/ELF;BIN 确认基地址(完整镜像 `0x08000000`,
|
||||
单独 APP `0x08002000`);
|
||||
4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区;
|
||||
5. **固件工具**(右列):
|
||||
- 补齐:选输入 BIN 后输出名自动带出(`*_pad.bin`),对齐默认 1K,点"执行补齐";
|
||||
- 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`),地址按典型布局
|
||||
预填,点"执行拼接"。
|
||||
|
||||
3. 选择芯片型号、烧录器型号、固件文件,点击“烧录固件”。
|
||||
典型布局:`0x08000000` Bootloader 8KB | `0x08002000` APP | `0x0800FFFC`
|
||||
有效标志 `0xEEEEEEEE` | `0x08010000` Flash 结束 (64KB)。
|
||||
|
||||
运行参数会自动保存到 `FlashTool/config/flash_gui_<主机名>.json`。首次打开会自动创建
|
||||
`config` 目录并生成当前电脑的配置;下次打开检测到就加载,没有就重新创建。
|
||||
每台电脑使用独立文件名,同步仓库时不会互相覆盖。
|
||||
配置(全部输入框的值)自动保存在 exe 旁的 `flash_gui_settings.json`,下次启动
|
||||
自动恢复;旧版 `config/flash_gui_<主机名>.json` 在首次启动时自动迁移。
|
||||
|
||||
## 环境要求
|
||||
## 4. 命令行模式
|
||||
|
||||
- Windows
|
||||
- Python 3.10+(标准安装自带 tkinter,无需第三方库)
|
||||
- OpenOCD(默认 `D:/toolchain/openocd/bin/openocd.exe`,可在界面中修改)
|
||||
exe 版无控制台,脚本化请用源码方式(或 `build_exe.py --debug` 打的带控制台变体):
|
||||
|
||||
```
|
||||
python src/flash_gui.py 启动 GUI
|
||||
python src/flash_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试)
|
||||
python src/flash_gui.py --version 打印 OpenOCD 版本(验证工具链定位)
|
||||
python src/flash_gui.py --detect 检测烧录器与目标芯片
|
||||
python src/flash_gui.py --flash app.bin 烧录(BIN 默认基地址 0x08000000)
|
||||
python src/flash_gui.py --flash app.bin --base 0x08002000 --no-reset
|
||||
python src/flash_gui.py --erase 整片擦除
|
||||
python src/flash_gui.py --read dump.bin 读取整片 Flash
|
||||
python src/flash_gui.py --reset 复位运行
|
||||
python src/flash_gui.py --pad app.bin 补齐到 1K → app_pad.bin
|
||||
python src/flash_gui.py --merge bl.bin app.bin 拼接 → app_BL.bin
|
||||
--align N / --pad-byte 0xFF / -o OUT pad 参数
|
||||
--app-addr / --flag-addr / --flash-size / --flag-value merge 参数
|
||||
--speed 4000 / --serial SN / --interface cfg / --target cfg
|
||||
--openocd PATH / --scripts DIR 手动指定工具链(默认自动定位)
|
||||
```
|
||||
|
||||
退出码:OpenOCD 操作非 0 即失败,可直接用于 CI/脚本串联。
|
||||
|
||||
## 5. 目录结构与从源码运行
|
||||
|
||||
```
|
||||
FlashTool/
|
||||
├── src/ # 源代码(人工维护区)
|
||||
│ ├── flash_gui.py # 入口与命令行模式(argparse 分发)
|
||||
│ ├── app_gui.py # CustomTkinter 主界面(双栏仪表盘)
|
||||
│ ├── openocd_runner.py # 工具链定位 / OpenOCD 命令构建与子进程
|
||||
│ ├── fw_image.py # 补齐(pad)/拼接(merge) + 芯片参数表
|
||||
│ ├── config.py # 运行目录与 flash_gui_settings.json 配置
|
||||
│ ├── build_exe.py # PyInstaller 打包 → release/
|
||||
│ ├── package_release.py # 组装发布 zip(exe + 内置 OpenOCD 工具链)
|
||||
│ ├── publish_gitea.py # 上传到 Gitea Release
|
||||
│ └── logos/ # 图标源(make_ico.py + png/ico)
|
||||
├── release/ # 构建与发布产物(机器生成区, 不手改/不入库)
|
||||
│ ├── DAPLinkFlash.exe # 打包单文件版
|
||||
│ ├── toolchain/openocd/ # package_release.py 复制的内置工具链
|
||||
│ └── *.zip # 发布包(上传 Gitea Release)
|
||||
├── toolchain/openocd/ # (可选) vendor 进仓库的工具链, 不入库
|
||||
├── start_flash_gui.bat # 源码模式双击启动器
|
||||
└── README.md # 本说明
|
||||
```
|
||||
|
||||
从源码运行:`python src/flash_gui.py` 或双击 `start_flash_gui.bat`,
|
||||
需 Python ≥3.10 + customtkinter(`py -m pip install customtkinter`);
|
||||
OpenOCD 用本机安装(默认找 `D:/toolchain/openocd`)或先跑一次
|
||||
`package_release.py` 把工具链放进 `release/toolchain/`。
|
||||
|
||||
## 6. 构建与发布
|
||||
|
||||
```powershell
|
||||
cd src
|
||||
python logos/make_ico.py # (可选) 改图标后重新生成 png/ico, 需 Pillow
|
||||
python build_exe.py # 打包 → ../release/DAPLinkFlash.exe(约 19MB;
|
||||
# 自动装 PyInstaller, CTk 主题数据自动收集)
|
||||
python build_exe.py --debug # 额外出一个带控制台的调试变体(命令行模式可见输出)
|
||||
python package_release.py 0.1.0 # 组装 release/DAPLinkFlash-v0.1.0-win64.zip
|
||||
# 含内置 OpenOCD 工具链(来源见脚本说明)
|
||||
python publish_gitea.py DAPLinkFlash-v0.1.0-win64.zip --insecure
|
||||
# 上传 Gitea Release(需环境变量 GITEA_TOKEN)
|
||||
```
|
||||
|
||||
版本号默认从最近的 `DAPLinkFlash-v*` git tag 推断,无 tag 时手动指定。
|
||||
|
||||
## 7. 注意事项 / FAQ
|
||||
|
||||
- **"找不到 OpenOCD"**:点"自动定位";发布包用户确认解压后 `toolchain/openocd/`
|
||||
目录和 exe 在一起;
|
||||
- **烧录失败排查**:①"检测设备"先验链路;② 探针序列号留空可排除序列号问题;
|
||||
③ 速度过高可降到 1000 kHz;④ DAPLink 固件过旧换 GD-Link 固件试试;
|
||||
- **exe 放只读目录**:`flash_gui_settings.json` 写在 exe 旁边,只读目录存不了配置;
|
||||
- **重新打包后图标没变**:Windows 图标缓存,`ie4uinit -show` 或改个名即可;
|
||||
- **残留进程**:修改源码重新打包前先结束残留的 DAPLinkFlash 进程
|
||||
(`build_exe.py` 会自动 taskkill);
|
||||
- **配置互不影响**:exe 与源码脚本各自按所在目录保存 `flash_gui_settings.json`。
|
||||
|
||||
@@ -1,877 +0,0 @@
|
||||
# -*- 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,664 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DAPLink Flash 主界面 — CustomTkinter 双栏仪表盘
|
||||
================================================================
|
||||
布局:
|
||||
左列 探针与工具链 | 芯片与固件 | 操作(检测/烧录/擦除/读取/复位/停止)
|
||||
右列 补齐 Pad(Y-Modem 对齐) | 拼接 Merge(BL+APP+标志) | 运行日志
|
||||
默认跟随系统深/浅色外观, 顶栏可切换 Dark/Light/System。
|
||||
|
||||
本模块只管界面: OpenOCD 子进程与命令构建在 openocd_runner,
|
||||
固件补齐/拼接在 fw_image, 运行配置在 config。
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
import config
|
||||
import openocd_runner
|
||||
from fw_image import (CHIP_PROFILES, FLAG_VALUE_DEFAULT, FLASH_BASE_DEFAULT,
|
||||
merge_firmware, pad_bin, parse_int)
|
||||
|
||||
MONO = ('Consolas', 12)
|
||||
BOLD = ('Microsoft YaHei UI', 13, 'bold')
|
||||
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
|
||||
|
||||
APP_NAME = 'DAPLink Flash'
|
||||
APP_VERSION = '2.0.0'
|
||||
REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool'
|
||||
|
||||
OK_BLUE = '#3B8ED0'
|
||||
|
||||
|
||||
def get_icon_path():
|
||||
"""工具图标: 源码模式取 logos/ 下, 打包模式取 exe 内嵌资源(_MEIPASS)"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
p = os.path.join(getattr(sys, '_MEIPASS', ''), 'logo_flash.ico')
|
||||
else:
|
||||
p = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'logos', 'logo_flash.ico')
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
def _apply_dropdown_theme():
|
||||
"""CTk 默认下拉菜单是白底, 在深色界面里风格突兀; 改为随外观自适应。
|
||||
在创建任何控件前调用, 对全部 CTkComboBox/菜单生效。"""
|
||||
ctk.ThemeManager.theme['DropdownMenu']['fg_color'] = ('#F2F2F2', '#2B2B2B')
|
||||
ctk.ThemeManager.theme['DropdownMenu']['hover_color'] = ('#D8D8D8', '#3C3C3C')
|
||||
ctk.ThemeManager.theme['DropdownMenu']['text_color'] = ('#1A1A1A', '#DADADA')
|
||||
|
||||
|
||||
def run_gui(auto_close=0):
|
||||
"""入口: 配置外观并启动主窗口"""
|
||||
ctk.set_appearance_mode('System')
|
||||
_apply_dropdown_theme()
|
||||
App(auto_close).mainloop()
|
||||
|
||||
|
||||
class App(ctk.CTk):
|
||||
def __init__(self, auto_close=0):
|
||||
super().__init__()
|
||||
self.title(f'{APP_NAME} v{APP_VERSION} — GD32 固件烧录 · 补齐 · 镜像拼接')
|
||||
try: # 任务栏独立分组
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
||||
'hulk.DAPLinkFlash')
|
||||
except Exception:
|
||||
pass
|
||||
self.geometry('1360x880')
|
||||
self.minsize(1200, 780)
|
||||
ic = get_icon_path()
|
||||
if ic:
|
||||
try:
|
||||
self.iconbitmap(ic) # 窗口标题栏图标
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.cfg = config.load_settings()
|
||||
self._toolchain_source = '已配置'
|
||||
if not os.path.isfile(self.cfg['openocd']):
|
||||
# 未配置或配置指向的路径不存在 → 自动定位(内置/本机/PATH)
|
||||
exe, scripts, self._toolchain_source = openocd_runner.locate_toolchain()
|
||||
self.cfg['openocd'], self.cfg['scripts'] = exe, scripts
|
||||
self.v = {}
|
||||
for key, val in self.cfg.items():
|
||||
self.v[key] = tk.BooleanVar(value=val) if isinstance(val, bool) \
|
||||
else tk.StringVar(value=val)
|
||||
for key in ('openocd', 'scripts'):
|
||||
self.v[key].trace_add('write', lambda *_a: self._mark_custom_toolchain())
|
||||
|
||||
self._log_q = queue.Queue()
|
||||
self.proc = None
|
||||
self._poll_job = None
|
||||
self._op_buttons = []
|
||||
|
||||
self._build()
|
||||
self._log(f'{APP_NAME} v{APP_VERSION} — OpenOCD 烧录 · 固件补齐 · 镜像拼接')
|
||||
ver = openocd_runner.version(self.cfg['openocd'])
|
||||
self._log('工具链: %s (%s)%s' % (self.cfg['openocd'], self._toolchain_source,
|
||||
' ' + ver if ver else ''))
|
||||
self._poll()
|
||||
self.protocol('WM_DELETE_WINDOW', self._on_close)
|
||||
if auto_close > 0:
|
||||
self.after(auto_close * 1000, self._on_close)
|
||||
|
||||
# ============================================================
|
||||
# 布局
|
||||
# ============================================================
|
||||
def _set_appearance(self, mode):
|
||||
ctk.set_appearance_mode(mode)
|
||||
|
||||
def _card(self, parent, title, button=None, command=None, right_label=None):
|
||||
"""卡片容器: 圆角边框 + 标题行(可带右侧小按钮/右对齐标签)。
|
||||
卡片自身用 grid 参与列布局; 卡片内部一律用 pack。"""
|
||||
card = ctk.CTkFrame(parent, corner_radius=10, border_width=1,
|
||||
border_color=('gray70', 'gray30'))
|
||||
head = ctk.CTkFrame(card, fg_color='transparent')
|
||||
head.pack(fill='x', padx=12, pady=(8, 0))
|
||||
ctk.CTkLabel(head, text=title, font=BOLD, anchor='w').pack(side='left', fill='y')
|
||||
if right_label is not None:
|
||||
ctk.CTkLabel(head, textvariable=right_label,
|
||||
text_color='gray60').pack(side='right', fill='y')
|
||||
if button:
|
||||
ctk.CTkButton(head, text=button, width=72,
|
||||
command=command).pack(side='right', fill='y')
|
||||
content = ctk.CTkFrame(card, fg_color='transparent')
|
||||
content.pack(fill='both', expand=True, padx=12, pady=(2, 10))
|
||||
return card, content
|
||||
|
||||
def _build(self):
|
||||
top = ctk.CTkFrame(self, fg_color='transparent')
|
||||
top.pack(fill='x', padx=12, pady=(10, 2))
|
||||
ctk.CTkLabel(top, text=APP_NAME, font=TITLE_FONT).pack(side='left')
|
||||
ctk.CTkLabel(top, text='GD32 固件烧录 · 补齐 · 镜像拼接',
|
||||
text_color='gray60').pack(side='left', padx=10, pady=(6, 0))
|
||||
seg = ctk.CTkSegmentedButton(top, values=['Dark', 'Light', 'System'],
|
||||
command=self._set_appearance)
|
||||
seg.set('System')
|
||||
seg.pack(side='right')
|
||||
ctk.CTkButton(top, text='关于', width=48, height=24,
|
||||
fg_color='transparent', border_width=1,
|
||||
border_color=('gray55', 'gray35'),
|
||||
text_color='gray60', hover_color=('gray82', 'gray22'),
|
||||
command=self._show_about).pack(side='right', padx=(0, 12))
|
||||
|
||||
body = ctk.CTkFrame(self, fg_color='transparent')
|
||||
body.pack(fill='both', expand=True, padx=12, pady=(4, 12))
|
||||
body.grid_columnconfigure(0, weight=1, uniform='col')
|
||||
body.grid_columnconfigure(1, weight=1, uniform='col')
|
||||
body.grid_rowconfigure(0, weight=1)
|
||||
left = ctk.CTkFrame(body, fg_color='transparent')
|
||||
left.grid(row=0, column=0, sticky='nsew')
|
||||
right = ctk.CTkFrame(body, fg_color='transparent')
|
||||
right.grid(row=0, column=1, sticky='nsew', padx=(10, 0))
|
||||
self._build_left(left)
|
||||
self._build_right(right)
|
||||
|
||||
status = ctk.CTkFrame(self, fg_color='transparent')
|
||||
status.pack(fill='x', padx=14, pady=(0, 8))
|
||||
self.v_status = tk.StringVar(value='状态: 就绪')
|
||||
ctk.CTkLabel(status, textvariable=self.v_status, anchor='w').pack(side='left')
|
||||
ctk.CTkLabel(status, text=f'v{APP_VERSION}',
|
||||
text_color='gray60').pack(side='right')
|
||||
|
||||
# -- 左列: 探针与工具链 / 芯片与固件 / 操作 --
|
||||
def _build_left(self, col):
|
||||
col.grid_rowconfigure(2, weight=1) # 操作卡片吸底
|
||||
col.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.v_toolchain = tk.StringVar(
|
||||
value='工具链: ' + self._toolchain_source)
|
||||
card, c = self._card(col, '探针与工具链', button='自动定位',
|
||||
command=self._relocate_toolchain,
|
||||
right_label=self.v_toolchain)
|
||||
card.grid(row=0, column=0, sticky='ew')
|
||||
self._field(c, 'OpenOCD 路径', self.v['openocd'], self._browse_openocd)
|
||||
self._field(c, '脚本目录', self.v['scripts'], self._browse_scripts)
|
||||
probe, _w = self._field_pair(
|
||||
c, '烧录器', self.v['probe'], 'Interface', self.v['interface'],
|
||||
cls1=(ctk.CTkComboBox, {'state': 'readonly',
|
||||
'values': list(openocd_runner.PROBE_PROFILES)}),
|
||||
cls2=(ctk.CTkComboBox,
|
||||
{'values': sorted(set(openocd_runner.PROBE_PROFILES.values()) - {''})}))
|
||||
probe.configure(command=lambda _=None: self._on_probe_change())
|
||||
self._field_pair(c, '速度 (kHz)', self.v['speed'], '探针序列号',
|
||||
self.v['serial'], width2=150)
|
||||
self._field_pair(
|
||||
c, 'Target', self.v['target'], '额外命令', self.v['extra'],
|
||||
cls1=(ctk.CTkComboBox, {'values': ['target/gd32e23x.cfg']}))
|
||||
|
||||
card, c = self._card(col, '芯片与固件')
|
||||
card.grid(row=1, column=0, sticky='ew', pady=(10, 0))
|
||||
chip, _flash = self._field_pair(
|
||||
c, '芯片型号', self.v['chip'], 'Flash 大小', self.v['flash_size'],
|
||||
cls1=(ctk.CTkComboBox, {'state': 'readonly',
|
||||
'values': list(CHIP_PROFILES)}),
|
||||
width2=100)
|
||||
chip.configure(command=lambda _=None: self._on_chip_change())
|
||||
_flash.configure(state='disabled')
|
||||
self._field(c, '固件文件', self.v['firmware'], self._browse_firmware)
|
||||
self._field(c, '基地址 (仅 BIN)', self.v['base'])
|
||||
ctk.CTkLabel(c, text='HEX/ELF 自动使用文件内地址; 完整镜像 0x08000000, '
|
||||
'单独烧 APP 0x08002000',
|
||||
text_color='gray50', justify='left', anchor='w',
|
||||
).pack(fill='x', pady=(2, 0))
|
||||
opts = ctk.CTkFrame(c, fg_color='transparent')
|
||||
opts.pack(fill='x', pady=(6, 0))
|
||||
for key, name in (('erase', '烧录前擦除'), ('verify', '烧录后校验'),
|
||||
('reset', '烧录后复位运行')):
|
||||
ctk.CTkCheckBox(opts, text=name, variable=self.v[key],
|
||||
checkbox_width=18, checkbox_height=18).pack(
|
||||
side='left', padx=(0, 14))
|
||||
|
||||
card, c = self._card(col, '操作')
|
||||
card.grid(row=2, column=0, sticky='sew', pady=(10, 0))
|
||||
grid = ctk.CTkFrame(c, fg_color='transparent')
|
||||
grid.pack(fill='x')
|
||||
grid.grid_columnconfigure((0, 1, 2), weight=1, uniform='op')
|
||||
for i, (name, cmd) in enumerate((
|
||||
('检测设备', self._on_detect), ('烧录固件', self._on_flash),
|
||||
('整片擦除', self._on_erase), ('读取 Flash', self._on_read),
|
||||
('复位芯片', self._on_reset))):
|
||||
btn = ctk.CTkButton(grid, text=name, height=34, command=cmd)
|
||||
btn.grid(row=i // 3, column=i % 3, sticky='ew', padx=3, pady=3)
|
||||
self._op_buttons.append(btn)
|
||||
self.btn_stop = ctk.CTkButton(grid, text='停止', height=34,
|
||||
fg_color='gray40', hover_color='gray25',
|
||||
command=self._on_stop, state='disabled')
|
||||
self.btn_stop.grid(row=1, column=2, sticky='ew', padx=3, pady=3)
|
||||
|
||||
# -- 右列: 补齐 / 拼接 / 日志 --
|
||||
def _build_right(self, col):
|
||||
col.grid_rowconfigure(2, weight=1) # 日志卡片吃掉剩余高度
|
||||
col.grid_columnconfigure(0, weight=1)
|
||||
|
||||
card, c = self._card(col, '补齐 (Pad) — 尾部补 0xFF 到页边界, '
|
||||
'适配 Bootloader Y-Modem 整包接收')
|
||||
card.grid(row=0, column=0, sticky='ew')
|
||||
self._field(c, '输入 BIN', self.v['pad_in'], self._browse_pad_in)
|
||||
self._field(c, '输出 BIN', self.v['pad_out'], self._browse_pad_out)
|
||||
row = ctk.CTkFrame(c, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(6, 0))
|
||||
ctk.CTkLabel(row, text='对齐:', width=40, anchor='e').pack(side='left')
|
||||
ctk.CTkComboBox(row, width=86, state='readonly',
|
||||
values=['1024', '2048', '4096'],
|
||||
variable=self.v['pad_align']).pack(side='left', padx=(4, 12))
|
||||
ctk.CTkLabel(row, text='填充字节:', width=64, anchor='e').pack(side='left')
|
||||
ctk.CTkEntry(row, width=76, textvariable=self.v['pad_byte']
|
||||
).pack(side='left', padx=(4, 12))
|
||||
ctk.CTkButton(row, text='执行补齐', width=96,
|
||||
command=self._on_pad).pack(side='left')
|
||||
self.v_pad_result = tk.StringVar(value='')
|
||||
ctk.CTkLabel(c, textvariable=self.v_pad_result, text_color=OK_BLUE,
|
||||
justify='left', anchor='w').pack(fill='x', pady=(4, 0))
|
||||
|
||||
card, c = self._card(col, '拼接 (Merge) — Bootloader + APP + 有效标志 → 整片镜像')
|
||||
card.grid(row=1, column=0, sticky='ew', pady=(10, 0))
|
||||
self._field(c, 'Bootloader', self.v['bootloader'], self._browse_bl)
|
||||
self._field(c, 'Application', self.v['app'], self._browse_app)
|
||||
self._field(c, '输出文件', self.v['merge_out'], self._browse_merge_out)
|
||||
row = ctk.CTkFrame(c, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(6, 0))
|
||||
for label, key, w in (('APP 地址', 'app_addr', 104),
|
||||
('标志地址', 'flag_addr', 104),
|
||||
('Flash', 'merge_flash', 84),
|
||||
('标志值', 'flag_value', 104)):
|
||||
ctk.CTkLabel(row, text=label + ':', anchor='e').pack(side='left')
|
||||
ctk.CTkEntry(row, width=w, textvariable=self.v[key]
|
||||
).pack(side='left', padx=(4, 10))
|
||||
ctk.CTkButton(row, text='执行拼接', width=96,
|
||||
command=self._on_merge).pack(side='left')
|
||||
self.v_merge_result = tk.StringVar(value='')
|
||||
ctk.CTkLabel(c, textvariable=self.v_merge_result, text_color=OK_BLUE,
|
||||
justify='left', anchor='w').pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(c, text='典型布局: 0x08000000 Bootloader 8KB | 0x08002000 APP | '
|
||||
'0x0800FFFC 有效标志 0xEEEEEEEE | Flash 64KB',
|
||||
text_color='gray50', justify='left', anchor='w',
|
||||
).pack(fill='x', pady=(2, 0))
|
||||
|
||||
card, c = self._card(col, '运行日志 — OpenOCD 输出与固件工具结果',
|
||||
button='清空', command=self._clear_log)
|
||||
card.grid(row=2, column=0, sticky='nsew', pady=(10, 0))
|
||||
self.txt_log = ctk.CTkTextbox(c, font=MONO)
|
||||
self.txt_log.pack(fill='both', expand=True)
|
||||
self.txt_log.configure(state='disabled')
|
||||
|
||||
# ============================================================
|
||||
# 输入行辅助
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def _mk_widget(parent, cls_info, var):
|
||||
"""按控件类绑定变量: CTkComboBox 用 variable, 其余用 textvariable"""
|
||||
cls, kw = cls_info
|
||||
if issubclass(cls, ctk.CTkComboBox):
|
||||
return cls(parent, variable=var, **kw)
|
||||
return cls(parent, textvariable=var, **kw)
|
||||
|
||||
def _field(self, parent, label, var, browse=None):
|
||||
"""一行: 标签 + 输入框(+ 浏览按钮), pack 布局"""
|
||||
row = ctk.CTkFrame(parent, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(row, text=label, width=150, anchor='w').pack(side='left')
|
||||
ctk.CTkEntry(row, textvariable=var).pack(side='left', fill='x', expand=True)
|
||||
if browse:
|
||||
ctk.CTkButton(row, text='浏览', width=56,
|
||||
command=browse).pack(side='left', padx=(6, 0))
|
||||
return row
|
||||
|
||||
def _field_pair(self, parent, label1, var1, label2, var2,
|
||||
cls1=(ctk.CTkEntry, {}), cls2=(ctk.CTkEntry, {}),
|
||||
width2=None):
|
||||
"""一行放两个字段: label1[控件1] label2[控件2(可定宽)]"""
|
||||
row = ctk.CTkFrame(parent, fg_color='transparent')
|
||||
row.pack(fill='x', pady=(4, 0))
|
||||
ctk.CTkLabel(row, text=label1, width=150, anchor='w').pack(side='left')
|
||||
w1 = self._mk_widget(row, cls1, var1)
|
||||
w1.pack(side='left', fill='x', expand=True)
|
||||
ctk.CTkLabel(row, text=label2, width=90, anchor='e').pack(side='left')
|
||||
if width2:
|
||||
cls2 = (cls2[0], {**cls2[1], 'width': width2})
|
||||
w2 = self._mk_widget(row, cls2, var2)
|
||||
w2.pack(side='left', padx=(4, 0))
|
||||
return w1, w2
|
||||
|
||||
# ============================================================
|
||||
# 路径选择与联动
|
||||
# ============================================================
|
||||
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 _relocate_toolchain(self):
|
||||
exe, scripts, source = openocd_runner.locate_toolchain()
|
||||
self.v['openocd'].set(exe)
|
||||
self.v['scripts'].set(scripts)
|
||||
self._toolchain_source = source
|
||||
self.v_toolchain.set('工具链: ' + source)
|
||||
ver = openocd_runner.version(exe)
|
||||
self._log('工具链定位: %s (%s)%s' % (exe, source, ' ' + ver if ver else ''))
|
||||
|
||||
def _mark_custom_toolchain(self, *_a):
|
||||
if self._toolchain_source != '自定义':
|
||||
self._toolchain_source = '自定义'
|
||||
self.v_toolchain.set('工具链: 自定义')
|
||||
|
||||
def _browse_firmware(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title='选择固件',
|
||||
filetypes=[('固件文件', '*.bin *.hex *.elf'), ('所有文件', '*.*')])
|
||||
if path:
|
||||
self.v['firmware'].set(path)
|
||||
|
||||
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['bootloader'].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, *_a):
|
||||
self.v['flash_size'].set(openocd_runner.chip_flash_size(self.v['chip'].get()))
|
||||
self.v['target'].set(openocd_runner.chip_target(self.v['chip'].get()))
|
||||
|
||||
def _on_probe_change(self, *_a):
|
||||
cfg = openocd_runner.PROBE_PROFILES.get(self.v['probe'].get(), '')
|
||||
if cfg:
|
||||
self.v['interface'].set(cfg)
|
||||
|
||||
# ============================================================
|
||||
# OpenOCD 烧录操作
|
||||
# ============================================================
|
||||
def _check_tools(self):
|
||||
if not os.path.isfile(self.v['openocd'].get()):
|
||||
messagebox.showerror('缺少工具',
|
||||
'找不到 OpenOCD:\n%s\n\n'
|
||||
'可点"自动定位"使用内置/本机工具链。'
|
||||
% self.v['openocd'].get())
|
||||
return False
|
||||
if not os.path.isdir(self.v['scripts'].get()):
|
||||
messagebox.showerror('缺少工具',
|
||||
'找不到 OpenOCD scripts 目录:\n%s' % self.v['scripts'].get())
|
||||
return False
|
||||
return True
|
||||
|
||||
def _build_cmd(self, script):
|
||||
return openocd_runner.build_cmd(
|
||||
self.v['openocd'].get(), self.v['scripts'].get(),
|
||||
self.v['interface'].get(), self.v['target'].get(),
|
||||
self.v['speed'].get(), self.v['serial'].get(),
|
||||
self.v['extra'].get(), script)
|
||||
|
||||
def _start(self, title, script):
|
||||
if self.proc is not None:
|
||||
self._log('已有操作正在执行, 请先停止。')
|
||||
return
|
||||
if not self._check_tools():
|
||||
return
|
||||
cmd = self._build_cmd(script)
|
||||
self._busy(True)
|
||||
self._log('== %s ==' % title)
|
||||
self._log('命令: ' + subprocess.list2cmdline(cmd))
|
||||
self.proc = openocd_runner.OcdProcess(
|
||||
cmd,
|
||||
on_line=lambda line: self._log_q.put(('log', line)),
|
||||
on_done=lambda code: self._log_q.put(('done', (title, code))))
|
||||
self.proc.start()
|
||||
|
||||
def _on_detect(self):
|
||||
self._start('检测设备', openocd_runner.script_detect())
|
||||
|
||||
def _on_erase(self):
|
||||
try:
|
||||
self._start('整片擦除', openocd_runner.script_erase(self.v['flash_size'].get()))
|
||||
except ValueError as exc:
|
||||
messagebox.showerror('参数错误', str(exc))
|
||||
|
||||
def _on_flash(self):
|
||||
filepath = self.v['firmware'].get().strip()
|
||||
if not filepath or not os.path.isfile(filepath):
|
||||
messagebox.showerror('缺少固件', '请先选择固件文件。')
|
||||
return
|
||||
try:
|
||||
script = openocd_runner.script_flash(
|
||||
filepath, self.v['base'].get(),
|
||||
erase=self.v['erase'].get(), verify=self.v['verify'].get(),
|
||||
reset=self.v['reset'].get())
|
||||
except ValueError as exc:
|
||||
messagebox.showerror('参数错误', str(exc))
|
||||
return
|
||||
self._start('烧录固件', 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
|
||||
try:
|
||||
self._start('读取 Flash',
|
||||
openocd_runner.script_read(out_path, self.v['flash_size'].get()))
|
||||
except ValueError as exc:
|
||||
messagebox.showerror('参数错误', str(exc))
|
||||
|
||||
def _on_reset(self):
|
||||
self._start('复位芯片', openocd_runner.script_reset())
|
||||
|
||||
def _on_stop(self):
|
||||
if self.proc is not None:
|
||||
self.proc.terminate()
|
||||
self._log('已发送停止请求。')
|
||||
else:
|
||||
self._log('当前没有正在执行的操作。')
|
||||
|
||||
def _poll(self):
|
||||
"""工作线程 → GUI 主线程: 80ms 轮询日志队列并同步忙碌状态"""
|
||||
try:
|
||||
while True:
|
||||
kind, payload = self._log_q.get_nowait()
|
||||
if kind == 'log':
|
||||
self._log(payload)
|
||||
elif kind == 'done':
|
||||
title, code = payload
|
||||
if code == 0:
|
||||
self._log('[完成] %s' % title)
|
||||
self._set_status('%s 完成' % title)
|
||||
else:
|
||||
self._log('[失败] %s (退出码 %d)' % (title, code))
|
||||
self._set_status('%s 失败' % title)
|
||||
self._busy(False)
|
||||
self._save()
|
||||
except queue.Empty:
|
||||
pass
|
||||
self._poll_job = self.after(80, self._poll)
|
||||
|
||||
def _busy(self, busy):
|
||||
state = 'disabled' if busy else 'normal'
|
||||
for btn in self._op_buttons:
|
||||
btn.configure(state=state)
|
||||
self.btn_stop.configure(state='normal' if busy else 'disabled')
|
||||
if busy:
|
||||
self._set_status('正在执行...')
|
||||
else:
|
||||
self._set_status('就绪')
|
||||
|
||||
# ============================================================
|
||||
# 固件工具: 补齐 / 拼接
|
||||
# ============================================================
|
||||
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 B → 补齐后 %d B (+%d), 对齐 %d B'
|
||||
% (original, padded, padded - original, align))
|
||||
self._log('补齐完成: %s → %s' % (src, dst))
|
||||
self._set_status('补齐完成')
|
||||
self._save()
|
||||
except Exception as exc:
|
||||
messagebox.showerror('补齐失败', str(exc))
|
||||
self._log('补齐失败: %s' % exc)
|
||||
|
||||
def _on_merge(self):
|
||||
bl = self.v['bootloader'].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(
|
||||
'BL %d B @0x%08X | APP %d B @0x%08X | 镜像 %d B | 标志 @0x%08X'
|
||||
% (info['bootloader_size'], FLASH_BASE_DEFAULT, info['app_size'],
|
||||
info['app_addr'], info['output_size'], info['flag_addr']))
|
||||
self._log('拼接完成: %s' % out)
|
||||
self._set_status('拼接完成')
|
||||
self._save()
|
||||
except Exception as exc:
|
||||
messagebox.showerror('拼接失败', str(exc))
|
||||
self._log('拼接失败: %s' % exc)
|
||||
|
||||
# ============================================================
|
||||
# 日志 / 状态 / 配置 / 退出
|
||||
# ============================================================
|
||||
def _log(self, line):
|
||||
self.txt_log.configure(state='normal')
|
||||
self.txt_log.insert('end', time_stamp() + line + '\n')
|
||||
if int(self.txt_log.index('end-1c').split('.')[0]) > 800:
|
||||
self.txt_log.delete('1.0', '2.0') # 只保留最近约 800 行
|
||||
self.txt_log.see('end')
|
||||
self.txt_log.configure(state='disabled')
|
||||
|
||||
def _clear_log(self):
|
||||
self.txt_log.configure(state='normal')
|
||||
self.txt_log.delete('1.0', 'end')
|
||||
self.txt_log.configure(state='disabled')
|
||||
|
||||
def _set_status(self, text):
|
||||
self.v_status.set('状态: ' + text)
|
||||
|
||||
def _save(self):
|
||||
for key, var in self.v.items():
|
||||
self.cfg[key] = var.get()
|
||||
self.cfg['openocd'] = self.v['openocd'].get()
|
||||
config.save_settings(self.cfg)
|
||||
|
||||
def _show_about(self):
|
||||
dlg = ctk.CTkToplevel(self)
|
||||
dlg.title(f'关于 {APP_NAME}')
|
||||
dlg.resizable(False, False)
|
||||
dlg.after(150, dlg.grab_set())
|
||||
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
||||
box.pack(padx=26, pady=20)
|
||||
ctk.CTkLabel(box, text=APP_NAME, font=TITLE_FONT, anchor='w').pack(anchor='w')
|
||||
ctk.CTkLabel(box, text=f'v{APP_VERSION} — GD32E230 固件烧录与打包工具',
|
||||
text_color='gray60', anchor='w').pack(anchor='w', pady=(0, 12))
|
||||
for line in ('OpenOCD 烧录: 检测 / 烧录 / 擦除 / 读取 / 复位 (DAPLink·GD-Link·ST-Link·J-Link)',
|
||||
'补齐 Pad: BIN 尾部补 0xFF 对齐页边界, 适配 Bootloader Y-Modem',
|
||||
'拼接 Merge: Bootloader + APP + 有效标志 → 整片 Flash 镜像',
|
||||
'发布包内置 OpenOCD 工具链, 免安装解压即用',
|
||||
'作者: Hulk'):
|
||||
ctk.CTkLabel(box, text='· ' + line, anchor='w').pack(anchor='w', pady=1)
|
||||
ctk.CTkLabel(box, text='仓库: ' + REPO_URL, anchor='w',
|
||||
text_color=OK_BLUE).pack(anchor='w', pady=(12, 0))
|
||||
ctk.CTkButton(box, text='关闭', width=84,
|
||||
command=dlg.destroy).pack(anchor='e', pady=(14, 0))
|
||||
|
||||
def _on_close(self):
|
||||
if self.proc is not None:
|
||||
if not messagebox.askyesno('确认退出', '烧录操作仍在进行, 是否停止并退出?'):
|
||||
return
|
||||
self.proc.terminate()
|
||||
if self._poll_job:
|
||||
try:
|
||||
self.after_cancel(self._poll_job)
|
||||
except Exception:
|
||||
pass
|
||||
self._poll_job = None
|
||||
self._save()
|
||||
self.destroy()
|
||||
|
||||
|
||||
def time_stamp():
|
||||
return time.strftime('%H:%M:%S ')
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
把 flash_gui.py 打包成单文件 exe(工具名: DAPLinkFlash)
|
||||
================================================================
|
||||
使用 PyInstaller --onefile --noconsole: Python 解释器 + 标准库 + tkinter +
|
||||
customtkinter 全部打进一个 exe, 目标机器无需安装任何依赖。
|
||||
OpenOCD 工具链不进 exe, 由 package_release.py 以 toolchain/ 目录随包分发
|
||||
(scripts 目录含 1000+ 文件, 进 onefile 会拖慢每次启动的解压)。
|
||||
|
||||
用法:
|
||||
python build_exe.py 自动安装 PyInstaller(如缺)并打包
|
||||
python build_exe.py --debug 额外打一个带控制台的变体(命令行模式可见输出)
|
||||
|
||||
产物: release/DAPLinkFlash.exe(及 --debug 时的 DAPLinkFlash-console.exe)
|
||||
首次运行 onefile exe 会先解压到临时目录, 启动比脚本慢一两秒, 属正常现象。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
# 本脚本位于 src/, 打包产物输出到 FlashTool/release/
|
||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE = os.path.dirname(SRC)
|
||||
DIST = os.path.join(BASE, 'release')
|
||||
WORK = os.path.join(SRC, 'build')
|
||||
ICO = os.path.join(SRC, 'logos', 'logo_flash.ico')
|
||||
|
||||
|
||||
def ensure_pyinstaller():
|
||||
try:
|
||||
import PyInstaller # noqa: F401
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
print('PyInstaller 未安装, 正在安装...')
|
||||
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pyinstaller'])
|
||||
|
||||
|
||||
def extra_collect_args():
|
||||
"""CustomTkinter 的主题/字体等数据文件必须显式收集, 否则打包后 GUI 启动报错"""
|
||||
try:
|
||||
import customtkinter # noqa: F401
|
||||
return ['--collect-all', 'customtkinter']
|
||||
except ImportError:
|
||||
print('警告: 未安装 customtkinter, 打包产物将无法启动 GUI!')
|
||||
return []
|
||||
|
||||
|
||||
def remove_stale(exe_path):
|
||||
"""删除旧 exe; 若被占用(上次验证的进程还在), 先结束同名进程再重试"""
|
||||
if not os.path.exists(exe_path):
|
||||
return
|
||||
try:
|
||||
os.remove(exe_path)
|
||||
return
|
||||
except PermissionError:
|
||||
pass
|
||||
print(f'{exe_path} 被占用, 结束残留进程后重试...')
|
||||
subprocess.run(['taskkill', '/F', '/IM', os.path.basename(exe_path)],
|
||||
capture_output=True)
|
||||
time.sleep(1.0)
|
||||
os.remove(exe_path)
|
||||
|
||||
|
||||
def build(name, extra_args):
|
||||
remove_stale(os.path.join(DIST, name + '.exe'))
|
||||
cmd = [sys.executable, '-m', 'PyInstaller',
|
||||
'--onefile', '--clean', '--name', name,
|
||||
'--distpath', DIST, '--workpath', WORK, '--specpath', WORK,
|
||||
] + extra_args + extra_collect_args() + [os.path.join(SRC, 'flash_gui.py')]
|
||||
if os.path.exists(ICO):
|
||||
# exe 文件图标 + 运行时窗口图标资源(从 _MEIPASS 读取)
|
||||
cmd += ['--icon', ICO, '--add-data', f'{ICO};.']
|
||||
else:
|
||||
print('警告: 未找到 logos/logo_flash.ico(先运行 logos/make_ico.py), '
|
||||
'本次打包不带图标')
|
||||
print('\n>>', ' '.join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
out = os.path.join(DIST, name + '.exe')
|
||||
print(f'完成: {out} ({os.path.getsize(out) / 1024 / 1024:.1f} MB)')
|
||||
|
||||
|
||||
def main():
|
||||
build_console = '--debug' in sys.argv
|
||||
ensure_pyinstaller()
|
||||
# 常规版: 双击即用, 无黑框(GUI 与烧录操作全部可用, 命令行模式输出不可见)
|
||||
build('DAPLinkFlash', ['--noconsole'])
|
||||
if build_console:
|
||||
# 调试版: 带控制台, 命令行模式/异常栈可见
|
||||
build('DAPLinkFlash-console', [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
运行配置: flash_gui_settings.json(工具链/芯片/固件路径记忆)
|
||||
================================================================
|
||||
配置落在程序目录: frozen 取 exe 旁, 源码模式取 FlashTool/ 根。
|
||||
(参考 DLPilot/config.py: PyInstaller onefile 打包后 __file__ 指向临时解压
|
||||
目录, 配置必须用 sys.executable 定位到 exe 旁边)
|
||||
|
||||
兼容迁移: 旧版按主机名分文件的 config/flash_gui_<主机名>.json 在首次
|
||||
启动且新配置不存在时自动导入, 之后不再读取旧文件。
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(sys.executable))
|
||||
else:
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
SETTINGS_NAME = "flash_gui_settings.json"
|
||||
SETTINGS_PATH = os.path.join(BASE_DIR, SETTINGS_NAME)
|
||||
|
||||
# 键与旧版 config/flash_gui_<主机名>.json 完全一致, 旧配置值可原样搬过来
|
||||
DEFAULTS = {
|
||||
"openocd": "", # 空 = 启动时按 locate_toolchain() 自动定位
|
||||
"scripts": "",
|
||||
"speed": "4000",
|
||||
"serial": "",
|
||||
"extra": "",
|
||||
"chip": "GD32E230F8",
|
||||
"probe": "DAPLink / CMSIS-DAP",
|
||||
"interface": "interface/cmsis-dap.cfg",
|
||||
"target": "target/gd32e23x.cfg",
|
||||
"flash_size": "0x10000",
|
||||
"firmware": "",
|
||||
"base": "0x08000000",
|
||||
"erase": True,
|
||||
"verify": True,
|
||||
"reset": True,
|
||||
"pad_in": "",
|
||||
"pad_out": "",
|
||||
"pad_align": "1024",
|
||||
"pad_byte": "0xFF",
|
||||
"bootloader": "",
|
||||
"app": "",
|
||||
"merge_out": "",
|
||||
"app_addr": "0x08002000",
|
||||
"flag_addr": "0x0800FFFC",
|
||||
"merge_flash": "0x10000",
|
||||
"flag_value": "0xEEEEEEEE",
|
||||
}
|
||||
|
||||
|
||||
def _legacy_settings():
|
||||
"""旧版 config/flash_gui_<主机名>.json 的路径, 优先本机名, 找不到取目录里
|
||||
任意一份; 没有旧配置返回 None"""
|
||||
cfg_dir = os.path.join(BASE_DIR, "config")
|
||||
if not os.path.isdir(cfg_dir):
|
||||
return None
|
||||
host = "flash_gui_%s.json" % re.sub(r"[^A-Za-z0-9_.-]", "_", socket.gethostname())
|
||||
path = os.path.join(cfg_dir, host)
|
||||
if not os.path.isfile(path):
|
||||
found = sorted(glob.glob(os.path.join(cfg_dir, "flash_gui_*.json")))
|
||||
path = found[0] if found else None
|
||||
return path
|
||||
|
||||
|
||||
def load_settings():
|
||||
"""读配置, 带默认值兜底; 新配置不存在时尝试从旧版 per-host 配置迁移"""
|
||||
data = dict(DEFAULTS)
|
||||
for src in (_legacy_settings() if not os.path.isfile(SETTINGS_PATH) else None,
|
||||
SETTINGS_PATH):
|
||||
if not src or not os.path.isfile(src):
|
||||
continue
|
||||
try:
|
||||
with open(src, "r", encoding="utf-8") as f:
|
||||
stored = json.load(f)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if not isinstance(stored, dict):
|
||||
continue
|
||||
for key in DEFAULTS:
|
||||
if key in stored:
|
||||
data[key] = stored[key]
|
||||
return data
|
||||
|
||||
|
||||
def save_settings(data):
|
||||
"""只保存 DEFAULTS 里有的键, 防止把界面临时状态写进去"""
|
||||
out = {key: data.get(key, DEFAULTS[key]) for key in DEFAULTS}
|
||||
try:
|
||||
with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 — 入口与命令行模式
|
||||
================================================================
|
||||
配合 GD32E230 + DAPLink/CMSIS-DAP(或 ST-Link/J-Link) 使用, OpenOCD 烧录;
|
||||
内含固件补齐(pad)与镜像拼接(merge)工具。发布包内置 OpenOCD 工具链。
|
||||
|
||||
模块划分(重构自原单文件 daplink_flash_gui.py, 界面/打包方式对齐 DLPilot):
|
||||
app_gui.py CustomTkinter 主界面(双栏仪表盘)
|
||||
openocd_runner.py 工具链定位 / OpenOCD 命令构建与子进程
|
||||
fw_image.py 补齐(pad)与拼接(merge) + 芯片参数表
|
||||
config.py 运行目录与 flash_gui_settings.json 配置
|
||||
|
||||
依赖: 全部纯标准库 + customtkinter(py -m pip install customtkinter);
|
||||
内置工具链不存在时需本机已装 OpenOCD 或在界面/参数里指定路径。
|
||||
|
||||
命令行模式(自动化脚本用):
|
||||
python flash_gui.py 启动 GUI
|
||||
python flash_gui.py --seconds 5 GUI 5 秒自动关闭(冒烟测试)
|
||||
python flash_gui.py --detect 检测烧录器与目标芯片
|
||||
python flash_gui.py --flash app.bin 烧录(BIN 默认按基地址 0x08000000)
|
||||
python flash_gui.py --flash app.bin --base 0x08002000 --no-reset
|
||||
python flash_gui.py --erase 整片擦除(按配置的 Flash 大小)
|
||||
python flash_gui.py --read dump.bin 读取整片 Flash
|
||||
python flash_gui.py --reset 复位运行
|
||||
python flash_gui.py --pad app.bin 补齐到 1K 边界(app_pad.bin)
|
||||
python flash_gui.py --merge bl.bin app.bin 拼接整片镜像(*_BL.bin)
|
||||
python flash_gui.py --version 打印 OpenOCD 版本
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import config
|
||||
import openocd_runner
|
||||
from fw_image import (APP_ADDR_DEFAULT, FLASH_BASE_DEFAULT, FLAG_ADDR_DEFAULT,
|
||||
FLAG_VALUE_DEFAULT, merge_firmware, pad_bin, parse_int)
|
||||
|
||||
|
||||
def _toolchain(args):
|
||||
"""命令行工具链解析: 参数 > 配置文件 > 自动定位(内置/本机/PATH)"""
|
||||
saved = config.load_settings()
|
||||
exe = args.openocd or (saved['openocd'] if os.path.isfile(saved['openocd']) else '')
|
||||
scripts = args.scripts or (saved['scripts'] if exe == saved['openocd'] else '')
|
||||
if not exe:
|
||||
exe, scripts2, source = openocd_runner.locate_toolchain()
|
||||
scripts = scripts or scripts2
|
||||
print(f'工具链: {exe} ({source})')
|
||||
if not os.path.isfile(exe):
|
||||
sys.exit(f'找不到 OpenOCD: {exe}\n'
|
||||
f'可用 --openocd 指定, 或把内置工具链放在 exe 旁 toolchain/openocd/')
|
||||
if not scripts or not os.path.isdir(scripts):
|
||||
sys.exit(f'找不到 OpenOCD scripts 目录: {scripts or "(空)"}\n可用 --scripts 指定')
|
||||
return exe, scripts
|
||||
|
||||
|
||||
def _ocd_cmd(args, script):
|
||||
saved = config.load_settings()
|
||||
exe, scripts = _toolchain(args)
|
||||
return openocd_runner.build_cmd(
|
||||
exe, scripts, args.interface or saved['interface'],
|
||||
args.target or saved['target'],
|
||||
str(args.speed) if args.speed else saved['speed'],
|
||||
args.serial or saved['serial'], saved['extra'], script)
|
||||
|
||||
|
||||
def run_openocd(args, title, script):
|
||||
"""跑一条烧录命令, 实时回显输出, 返回退出码"""
|
||||
cmd = _ocd_cmd(args, script)
|
||||
print('== %s ==' % title)
|
||||
print('命令: ' + subprocess.list2cmdline(cmd))
|
||||
code, _lines = openocd_runner.run_sync(cmd)
|
||||
return code
|
||||
|
||||
|
||||
def run_pad(args):
|
||||
if not os.path.isfile(args.pad):
|
||||
sys.exit(f'找不到输入文件: {args.pad}')
|
||||
align = parse_int(str(args.align))
|
||||
if align is None or align <= 0:
|
||||
sys.exit(f'对齐字节无效: {args.align}')
|
||||
pad_byte = parse_int(args.pad_byte)
|
||||
if pad_byte is None:
|
||||
pad_byte = 0xFF
|
||||
stem, ext = os.path.splitext(args.pad)
|
||||
out = args.output or stem + '_pad' + ext
|
||||
_ensure_parent(out)
|
||||
original = os.path.getsize(args.pad)
|
||||
padded = pad_bin(args.pad, out, align, pad_byte)
|
||||
print(f'补齐完成: {out}')
|
||||
print(f' 原始 {original} B → 补齐后 {padded} B (+{padded - original}), 对齐 {align} B')
|
||||
return 0
|
||||
|
||||
|
||||
def run_merge(args):
|
||||
bootloader, app = args.merge
|
||||
for p in (bootloader, app):
|
||||
if not os.path.isfile(p):
|
||||
sys.exit(f'找不到文件: {p}')
|
||||
flash_size = parse_int(str(args.flash_size))
|
||||
app_addr = parse_int(args.app_addr)
|
||||
flag_addr = parse_int(args.flag_addr)
|
||||
flag_value = parse_int(args.flag_value)
|
||||
if flag_value is None:
|
||||
flag_value = FLAG_VALUE_DEFAULT
|
||||
out = args.output or os.path.splitext(app)[0] + '_BL.bin'
|
||||
_ensure_parent(out)
|
||||
try:
|
||||
info = merge_firmware(bootloader, app, out,
|
||||
app_addr, flag_addr, flash_size, flag_value)
|
||||
except ValueError as exc:
|
||||
sys.exit(f'拼接失败: {exc}')
|
||||
print(f'拼接完成: {out}')
|
||||
print(f' Bootloader {info["bootloader_size"]} B @0x{FLASH_BASE_DEFAULT:08X}')
|
||||
print(f' Application {info["app_size"]} B @0x{info["app_addr"]:08X}')
|
||||
print(f' Flag @0x{info["flag_addr"]:08X} 镜像 {info["output_size"]} B')
|
||||
return 0
|
||||
|
||||
|
||||
def _ensure_parent(path):
|
||||
parent = os.path.dirname(os.path.abspath(path))
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description='DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 '
|
||||
'(无参数则启动 GUI)')
|
||||
ap.add_argument('--seconds', type=int, default=0,
|
||||
help='GUI 自动退出秒数(冒烟测试)')
|
||||
# -- 烧录操作(互斥, 按下述顺序判优) --
|
||||
op = ap.add_mutually_exclusive_group()
|
||||
op.add_argument('--detect', action='store_true', help='检测烧录器与目标芯片')
|
||||
op.add_argument('--flash', metavar='FILE', help='烧录固件 (BIN/HEX/ELF)')
|
||||
op.add_argument('--erase', action='store_true', help='整片擦除')
|
||||
op.add_argument('--read', metavar='OUT', help='读取整片 Flash 到 OUT')
|
||||
op.add_argument('--reset', action='store_true', help='复位芯片并运行')
|
||||
op.add_argument('--version', action='store_true', help='打印 OpenOCD 版本')
|
||||
# -- 固件工具 --
|
||||
op.add_argument('--pad', metavar='FILE', dest='pad', help='补齐 BIN 到页边界')
|
||||
op.add_argument('--merge', nargs=2, metavar=('BL', 'APP'),
|
||||
help='拼接 Bootloader + APP → 整片镜像')
|
||||
ap.add_argument('--align', default='1024', help='--pad 对齐字节(默认 1024)')
|
||||
ap.add_argument('--pad-byte', default='0xFF', help='--pad 填充字节(默认 0xFF)')
|
||||
ap.add_argument('-o', '--output', help='--pad/--merge 输出路径(默认自动命名)')
|
||||
ap.add_argument('--app-addr', default='0x%08X' % APP_ADDR_DEFAULT,
|
||||
help='--merge APP 地址(默认 0x08002000)')
|
||||
ap.add_argument('--flag-addr', default='0x%08X' % FLAG_ADDR_DEFAULT,
|
||||
help='--merge 标志地址(默认 0x0800FFFC)')
|
||||
ap.add_argument('--flag-value', default='0x%08X' % FLAG_VALUE_DEFAULT,
|
||||
help='--merge 标志值(默认 0xEEEEEEEE)')
|
||||
ap.add_argument('--flash-size', default='0x10000',
|
||||
help='Flash 大小(默认 0x10000, --erase/--read/--merge 用)')
|
||||
# -- 烧录参数 --
|
||||
ap.add_argument('--base', default='0x%08X' % FLASH_BASE_DEFAULT,
|
||||
help='BIN 烧录基地址(默认 0x08000000)')
|
||||
ap.add_argument('--no-erase', action='store_true', help='烧录前不擦除')
|
||||
ap.add_argument('--no-verify', action='store_true', help='烧录后不校验')
|
||||
ap.add_argument('--no-reset', action='store_true', help='烧录后不复位运行')
|
||||
ap.add_argument('--speed', type=int, help='适配器速度 kHz(默认取配置 4000)')
|
||||
ap.add_argument('--serial', help='探针序列号(多探针时用)')
|
||||
ap.add_argument('--interface', help='interface cfg(默认 interface/cmsis-dap.cfg)')
|
||||
ap.add_argument('--target', help='target cfg(默认 target/gd32e23x.cfg)')
|
||||
ap.add_argument('--openocd', help='OpenOCD 可执行文件路径(默认自动定位)')
|
||||
ap.add_argument('--scripts', help='OpenOCD scripts 目录(默认自动定位)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.pad:
|
||||
sys.exit(run_pad(args))
|
||||
if args.merge:
|
||||
sys.exit(run_merge(args))
|
||||
if args.version:
|
||||
exe, _scripts = _toolchain(args)
|
||||
print(openocd_runner.version(exe) or '无法获取版本')
|
||||
sys.exit(0)
|
||||
if args.detect:
|
||||
sys.exit(run_openocd(args, '检测设备', openocd_runner.script_detect()))
|
||||
if args.erase:
|
||||
sys.exit(run_openocd(args, '整片擦除',
|
||||
openocd_runner.script_erase(args.flash_size)))
|
||||
if args.flash:
|
||||
sys.exit(run_openocd(args, '烧录固件', openocd_runner.script_flash(
|
||||
args.flash, args.base,
|
||||
erase=not args.no_erase, verify=not args.no_verify,
|
||||
reset=not args.no_reset)))
|
||||
if args.read:
|
||||
sys.exit(run_openocd(args, '读取 Flash',
|
||||
openocd_runner.script_read(args.read, args.flash_size)))
|
||||
if args.reset:
|
||||
sys.exit(run_openocd(args, '复位芯片', openocd_runner.script_reset()))
|
||||
|
||||
# 无操作参数 → 启动 GUI(实现在 app_gui.py)
|
||||
import app_gui
|
||||
app_gui.run_gui(args.seconds)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
固件镜像处理: 补齐(pad)与拼接(merge) — 纯标准库, GUI 与命令行共用
|
||||
================================================================
|
||||
pad - BIN 尾部补 pad_byte 到指定对齐(bootloader 的 Y-Modem 按 1K 整包接收)
|
||||
merge - Bootloader + APP + 有效标志 拼成一整片 Flash 镜像(初次烧录用)
|
||||
|
||||
芯片参数表(CHIP_PROFILES)同时供烧录页(target cfg/Flash 大小)与补齐/拼接
|
||||
的容量校验使用。
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
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},
|
||||
}
|
||||
|
||||
|
||||
def parse_int(text):
|
||||
"""解析 '0x1C' / '4096' / '1k' / '2m' 形式的数值, 非法返回 None"""
|
||||
s = str(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 pad_bin(src_path, dst_path, align, pad_byte=0xFF):
|
||||
"""把 BIN 补齐到 align 的整数倍, 返回补齐后大小"""
|
||||
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):
|
||||
"""Bootloader + APP + 有效标志 → 整片 Flash 镜像, 返回尺寸信息 dict"""
|
||||
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,
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DAPLink Flash 图标栅格化: 黑底 512, 绿色圆角矩形内 FLASH 黑字, 下方 Link 白字
|
||||
================================================================
|
||||
风格对齐 DLPilot(黑底 + 圆角色块 + 两行字), 绿色区分烧录工具定位。
|
||||
|
||||
输出: logos/logo_flash.png(512, 预览/复用)
|
||||
logos/logo_flash.ico(16~256 七档, exe 图标用)
|
||||
|
||||
用法: python make_ico.py (需 Pillow)
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
FONT = r'C:\Windows\Fonts\arialbd.ttf' # Arial Bold, Windows 标配
|
||||
|
||||
|
||||
def render(size=512):
|
||||
s = size / 512.0
|
||||
img = Image.new('RGB', (size, size), (0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rounded_rectangle([38 * s, 64 * s, (38 + 436) * s, (64 + 186) * s],
|
||||
radius=34 * s, fill=(0, 200, 120))
|
||||
if not os.path.exists(FONT):
|
||||
raise SystemExit(f'未找到字体 {FONT}, 无法栅格化')
|
||||
f = ImageFont.truetype(FONT, max(8, round(146 * s)))
|
||||
cx = size / 2
|
||||
# FLASH: 居中于绿色框
|
||||
d.text((cx, 64 * s + 186 * s / 2), 'FLASH', font=f, fill=(0, 0, 0), anchor='mm')
|
||||
# Link: 与 DLPilot 同款基线(中心 ~368)
|
||||
d.text((cx, 368 * s), 'Link', font=f, fill=(255, 255, 255), anchor='mm')
|
||||
return img
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
img = render(512)
|
||||
png = os.path.join(BASE, 'logo_flash.png')
|
||||
ico = os.path.join(BASE, 'logo_flash.ico')
|
||||
img.save(png)
|
||||
img.save(ico, sizes=[(16, 16), (24, 24), (32, 32), (48, 48),
|
||||
(64, 64), (128, 128), (256, 256)])
|
||||
print(f'写出 {png}')
|
||||
print(f'写出 {ico}')
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OpenOCD 子进程封装: 工具链定位 / 烧录脚本构建 / 后台运行
|
||||
================================================================
|
||||
工具链定位顺序(locate_toolchain):
|
||||
1. 内置 <程序目录>/toolchain/openocd/bin/openocd.exe
|
||||
(发布包自带; PyInstaller frozen 取 exe 旁, 源码模式取 FlashTool/ 下)
|
||||
2. 默认 D:/toolchain/openocd(开发机本机安装)
|
||||
3. PATH 里的 openocd
|
||||
返回值带来源标记, GUI 在界面上显示"工具链: 内置/本机/PATH"。
|
||||
|
||||
GUI 与命令行共用 build_cmd/script_* 这组函数, 保证两边生成的命令一致。
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from fw_image import CHIP_PROFILES, FLASH_BASE_DEFAULT, parse_int
|
||||
|
||||
DEFAULT_OPENOCD = r"D:/toolchain/openocd/bin/openocd.exe"
|
||||
DEFAULT_SCRIPTS = r"D:/toolchain/openocd/openocd/scripts"
|
||||
|
||||
# 探针型号 → OpenOCD interface cfg(空白 = 自定义, 不改手填值)
|
||||
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 _app_base():
|
||||
"""程序所在目录: frozen 取 exe 旁, 源码模式取 FlashTool/"""
|
||||
if getattr(sys, "frozen", False):
|
||||
return os.path.dirname(os.path.abspath(sys.executable))
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def bundled_toolchain():
|
||||
"""内置工具链路径, 不存在返回 None"""
|
||||
exe = os.path.join(_app_base(), "toolchain", "openocd", "bin", "openocd.exe")
|
||||
if os.path.isfile(exe):
|
||||
scripts = os.path.join(_app_base(), "toolchain", "openocd", "openocd", "scripts")
|
||||
return exe, scripts if os.path.isdir(scripts) else DEFAULT_SCRIPTS
|
||||
return None
|
||||
|
||||
|
||||
def locate_toolchain():
|
||||
"""返回 (openocd_exe, scripts_dir, source),source ∈ 内置/本机/PATH"""
|
||||
bundled = bundled_toolchain()
|
||||
if bundled:
|
||||
return bundled[0], bundled[1], "内置"
|
||||
if os.path.isfile(DEFAULT_OPENOCD):
|
||||
return DEFAULT_OPENOCD, DEFAULT_SCRIPTS, "本机"
|
||||
which = shutil.which("openocd")
|
||||
if which:
|
||||
return which, "", "PATH"
|
||||
return DEFAULT_OPENOCD, DEFAULT_SCRIPTS, "本机"
|
||||
|
||||
|
||||
def strip_ansi(text):
|
||||
return re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text)
|
||||
|
||||
|
||||
def tcl_quote(path):
|
||||
return '"' + path.replace("\\", "/").replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def ensure_cfg_path(value, folder):
|
||||
"""裸 cfg 文件名(如 gd32e23x.cfg)补上所属目录前缀"""
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return value
|
||||
if "/" in value or "\\" in value or not value.lower().endswith(".cfg"):
|
||||
return value
|
||||
return folder + "/" + value
|
||||
|
||||
|
||||
def version(exe):
|
||||
"""openocd --version 首行(如 Open On-Chip Debugger 0.12.0), 失败返回 None"""
|
||||
try:
|
||||
out = subprocess.run([exe, "--version"], capture_output=True, text=True,
|
||||
timeout=10, creationflags=subprocess.CREATE_NO_WINDOW
|
||||
if os.name == "nt" else 0)
|
||||
line = (out.stdout or out.stderr or "").strip().splitlines()
|
||||
return line[0].strip() if line else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 烧录脚本构建(GUI 与 CLI 共用)
|
||||
# ============================================================
|
||||
def build_cmd(openocd, scripts, interface, target, speed="", serial="", extra="", script=""):
|
||||
"""拼 OpenOCD 命令行: -s 搜索目录 + interface/target cfg + -c 选项"""
|
||||
cmd = [openocd.strip()]
|
||||
if scripts and scripts.strip():
|
||||
cmd += ["-s", scripts.strip()]
|
||||
cmd += ["-f", ensure_cfg_path(interface, "interface"),
|
||||
"-f", ensure_cfg_path(target, "target")]
|
||||
if speed and speed.strip():
|
||||
cmd += ["-c", "adapter speed " + speed.strip()]
|
||||
if serial and serial.strip() and "cmsis-dap" in interface.lower():
|
||||
cmd += ["-c", "cmsis_dap_serial " + serial.strip()]
|
||||
if extra and extra.strip():
|
||||
cmd += ["-c", extra.strip()]
|
||||
if script:
|
||||
cmd += ["-c", script]
|
||||
return cmd
|
||||
|
||||
|
||||
def script_detect():
|
||||
return "init; reset halt; flash list; exit"
|
||||
|
||||
|
||||
def script_erase(flash_size_text):
|
||||
size = parse_int(flash_size_text)
|
||||
if size is None or size <= 0:
|
||||
raise ValueError("Flash 大小无效: %s" % flash_size_text)
|
||||
return ("init; reset halt; flash erase_address 0x%08X 0x%X; reset halt; exit"
|
||||
% (FLASH_BASE_DEFAULT, size))
|
||||
|
||||
|
||||
def script_flash(firmware, base_text="", erase=True, verify=True, reset=True):
|
||||
"""HEX/ELF 用文件内地址(base_text 留空), BIN 必须给基地址"""
|
||||
ext = os.path.splitext(firmware)[1].lower()
|
||||
script = "init; reset halt; "
|
||||
addr = ""
|
||||
if ext == ".bin":
|
||||
base = parse_int(base_text)
|
||||
if base is None:
|
||||
raise ValueError("BIN 基地址无效: %s" % base_text)
|
||||
addr = " 0x%08X" % base
|
||||
quoted = tcl_quote(firmware)
|
||||
script += ("flash write_image erase %s%s; " if erase
|
||||
else "flash write_image %s%s; ") % (quoted, addr)
|
||||
if verify:
|
||||
script += "verify_image %s%s; " % (quoted, addr)
|
||||
script += ("reset run" if reset else "reset halt") + "; exit"
|
||||
return script
|
||||
|
||||
|
||||
def script_read(out_path, flash_size_text):
|
||||
size = parse_int(flash_size_text)
|
||||
if size is None or size <= 0:
|
||||
raise ValueError("Flash 大小无效: %s" % flash_size_text)
|
||||
return "init; reset halt; dump_image %s 0x%08X 0x%X; exit" % (
|
||||
tcl_quote(out_path), FLASH_BASE_DEFAULT, size)
|
||||
|
||||
|
||||
def script_reset():
|
||||
return "init; reset run; exit"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 进程运行
|
||||
# ============================================================
|
||||
class OcdProcess:
|
||||
"""后台跑一条 openocd 命令。
|
||||
|
||||
on_line(line)/on_done(code) 在工作线程里回调, GUI 侧自行经队列
|
||||
投递回主线程; CLI 可直接在回调里 print。
|
||||
"""
|
||||
|
||||
def __init__(self, cmd, on_line, on_done):
|
||||
self.cmd = cmd
|
||||
self._on_line = on_line
|
||||
self._on_done = on_done
|
||||
self.proc = None
|
||||
self._thread = None
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
return self.proc is not None
|
||||
|
||||
def start(self):
|
||||
self._thread = threading.Thread(target=self._worker, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def terminate(self):
|
||||
if self.proc is not None:
|
||||
try:
|
||||
self.proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _worker(self):
|
||||
creation = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
self.cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
bufsize=1,
|
||||
creationflags=creation,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._on_line("启动 OpenOCD 失败: %s" % exc)
|
||||
self._on_done(-1)
|
||||
return
|
||||
|
||||
self.proc = proc
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
line = strip_ansi(line.rstrip("\r\n"))
|
||||
if line.strip():
|
||||
self._on_line(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
code = proc.wait()
|
||||
self.proc = None
|
||||
self._on_done(code)
|
||||
|
||||
|
||||
def run_sync(cmd):
|
||||
"""阻塞运行一条 openocd 命令, 返回 (exit_code, 输出行)"""
|
||||
lines = []
|
||||
done = []
|
||||
|
||||
def on_line(line):
|
||||
lines.append(line)
|
||||
|
||||
def on_done(code):
|
||||
done.append(code)
|
||||
|
||||
job = OcdProcess(cmd, on_line, on_done)
|
||||
job.start()
|
||||
job._thread.join()
|
||||
return (done[0] if done else -1), lines
|
||||
|
||||
|
||||
def chip_target(chip_name):
|
||||
return CHIP_PROFILES.get(chip_name, {}).get("target", "target/gd32e23x.cfg")
|
||||
|
||||
|
||||
def chip_flash_size(chip_name):
|
||||
profile = CHIP_PROFILES.get(chip_name)
|
||||
return "0x%X" % profile["flash"] if profile else "0x10000"
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
组装 DAPLink Flash 发布包: exe + 内置 OpenOCD 工具链 → 文件夹 + zip(解压即用)
|
||||
================================================================
|
||||
release/ 本身就是可直接使用的完整文件夹(exe + toolchain/ + 说明),
|
||||
再原样压成 zip —— 两者内容始终一致。
|
||||
|
||||
zip 解压后根目录为 DAPLinkFlash/, 与 exe 运行目录结构一致:
|
||||
DAPLinkFlash/
|
||||
├── DAPLinkFlash.exe 主程序(先运行 build_exe.py 打包)
|
||||
├── toolchain/openocd/ 内置 OpenOCD(bin + scripts, GUI 自动定位)
|
||||
└── README.md 使用说明
|
||||
|
||||
OpenOCD 来源(--openocd-src > 环境变量 OPENOCD_SRC > 自动探测):
|
||||
1. FlashTool/toolchain/openocd (随仓库自带, 若已 vendor)
|
||||
2. D:/toolchain/openocd (开发机本机安装)
|
||||
目录布局: <src>/bin/openocd.exe + <src>/openocd/scripts/
|
||||
|
||||
用法:
|
||||
python package_release.py 版本号自动取最近 git tag(DAPLinkFlash-v*)
|
||||
python package_release.py 0.1.0 手动指定版本号
|
||||
python package_release.py 0.1.0 --openocd-src D:/toolchain/openocd
|
||||
产物: release/DAPLinkFlash-v<版本>-win64.zip(zip 不入库, 上传到 Gitea Release)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
SRC = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE = os.path.dirname(SRC)
|
||||
RELEASE = os.path.join(BASE, 'release')
|
||||
APP_NAME = 'DAPLinkFlash'
|
||||
|
||||
|
||||
def detect_version():
|
||||
"""从最近的 DAPLinkFlash-v* tag 推断版本号, 无 tag 则报错提示手动指定"""
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
['git', 'describe', '--tags', '--abbrev=0', '--match', 'DAPLinkFlash-v*'],
|
||||
cwd=BASE, stderr=subprocess.DEVNULL)
|
||||
return out.decode().strip().removeprefix(APP_NAME + '-v')
|
||||
except Exception:
|
||||
sys.exit('未能从 git tag 推断版本号, 请手动指定: '
|
||||
'python package_release.py <版本号>')
|
||||
|
||||
|
||||
def resolve_openocd_src(explicit):
|
||||
"""定位 OpenOCD 工具链源目录, 校验布局并返回路径"""
|
||||
candidates = [explicit, os.environ.get('OPENOCD_SRC'),
|
||||
os.path.join(BASE, 'toolchain', 'openocd'),
|
||||
r'D:/toolchain/openocd']
|
||||
for cand in candidates:
|
||||
if not cand:
|
||||
continue
|
||||
exe = os.path.join(cand, 'bin', 'openocd.exe')
|
||||
scripts = os.path.join(cand, 'openocd', 'scripts')
|
||||
if os.path.isfile(exe) and os.path.isdir(scripts):
|
||||
print(f'OpenOCD 工具链来源: {os.path.abspath(cand)}')
|
||||
return os.path.abspath(cand)
|
||||
sys.exit('未找到 OpenOCD 工具链(需 bin/openocd.exe + openocd/scripts/):\n'
|
||||
' python package_release.py <版本> --openocd-src <toolchain/openocd 目录>\n'
|
||||
' 或设置环境变量 OPENOCD_SRC')
|
||||
|
||||
|
||||
def copy_toolchain(openocd_src):
|
||||
"""复制 bin(openocd.exe + 运行 DLL)与 openocd/scripts 到 release/toolchain/"""
|
||||
dst_root = os.path.join(RELEASE, 'toolchain', 'openocd')
|
||||
if os.path.isdir(dst_root):
|
||||
shutil.rmtree(dst_root)
|
||||
count = 0
|
||||
for sub in ('bin', os.path.join('openocd', 'scripts')):
|
||||
src = os.path.join(openocd_src, sub)
|
||||
dst = os.path.join(dst_root, sub)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copytree(src, dst)
|
||||
for _root, _dirs, files in os.walk(dst):
|
||||
count += len(files)
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('version', nargs='?', default=None,
|
||||
help='版本号, 默认取最近 DAPLinkFlash-v* git tag')
|
||||
ap.add_argument('--openocd-src', default=None,
|
||||
help='OpenOCD 工具链目录(含 bin/ 与 openocd/scripts/)')
|
||||
args = ap.parse_args()
|
||||
|
||||
version = (args.version or detect_version()).lstrip('v')
|
||||
exe = os.path.join(RELEASE, APP_NAME + '.exe')
|
||||
readme = os.path.join(BASE, 'README.md')
|
||||
if not os.path.exists(exe):
|
||||
sys.exit(f'缺少 release/{APP_NAME}.exe, 先运行: python build_exe.py')
|
||||
if not os.path.exists(readme):
|
||||
sys.exit(f'缺少 {readme}')
|
||||
openocd_src = resolve_openocd_src(args.openocd_src)
|
||||
|
||||
n_files = copy_toolchain(openocd_src)
|
||||
shutil.copy2(readme, os.path.join(RELEASE, 'README.md'))
|
||||
|
||||
zip_name = f'{APP_NAME}-v{version}-win64.zip'
|
||||
zip_path = os.path.join(RELEASE, zip_name)
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
z.write(exe, f'{APP_NAME}/{APP_NAME}.exe')
|
||||
z.write(os.path.join(RELEASE, 'README.md'), f'{APP_NAME}/README.md')
|
||||
tool_root = os.path.join(RELEASE, 'toolchain')
|
||||
for root, _dirs, files in os.walk(tool_root):
|
||||
for f in files:
|
||||
full = os.path.join(root, f)
|
||||
z.write(full, os.path.join(APP_NAME, os.path.relpath(full, RELEASE)))
|
||||
|
||||
exe_mb = os.path.getsize(exe) / 1024 / 1024
|
||||
zip_mb = os.path.getsize(zip_path) / 1024 / 1024
|
||||
print(f'发布文件夹: {RELEASE} (exe {exe_mb:.1f} MB + 内置工具链 {n_files} 个文件, 平铺即用)')
|
||||
print(f'发布包: {zip_path} ({zip_mb:.1f} MB)')
|
||||
print('下一步上传: python publish_gitea.py', zip_name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
创建/更新 Gitea Release 并上传发布 zip(纯标准库, 无需安装依赖)
|
||||
================================================================
|
||||
用法:
|
||||
python publish_gitea.py DAPLinkFlash-v0.1.0-win64.zip
|
||||
python publish_gitea.py <zip> --notes-file release/notes-v0.1.0.md
|
||||
python publish_gitea.py <zip> --tag DAPLinkFlash-v0.1.0 --replace
|
||||
|
||||
行为:
|
||||
- tag 与版本默认取 zip 文件名(DAPLinkFlash-vX.Y.Z-win64.zip → DAPLinkFlash-vX.Y.Z)
|
||||
- release 不存在则创建(tag 不存在时 Gitea 在默认分支头自动建 tag);
|
||||
已存在则复用, 仅上传/更新资产
|
||||
- release 上已有同名资产时: 默认跳过, --replace 删旧再传
|
||||
- API token: --token 参数或环境变量 GITEA_TOKEN(需该仓库写权限)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
APP_NAME = 'DAPLinkFlash'
|
||||
|
||||
DEFAULT_NOTES = """## DAPLink Flash {version}
|
||||
|
||||
GD32E230 固件烧录与打包工具: OpenOCD 烧录(检测/烧录/擦除/读取/复位)、
|
||||
固件补齐(Pad, Y-Modem 对齐)、镜像拼接(Merge, Bootloader+APP+有效标志)。
|
||||
|
||||
### 安装
|
||||
下载 `{app}-v{version}-win64.zip`, 解压后双击 `{app}.exe`(Windows 10/11 x64, 免安装)。
|
||||
|
||||
### 包内容
|
||||
- `{app}.exe` — 主程序(单文件自包含)
|
||||
- `toolchain/openocd/` — 内置 OpenOCD 工具链(openocd.exe + scripts, GUI 自动定位)
|
||||
- `README.md` — 完整使用说明
|
||||
"""
|
||||
|
||||
|
||||
def git_remote_info():
|
||||
"""从 origin 远程地址解析 API base 与 owner/repo(ssh 端口不影响 https API)"""
|
||||
url = subprocess.check_output(['git', 'remote', 'get-url', 'origin']).decode().strip()
|
||||
m = re.match(r'(?:ssh://git@|git@)([\w.-]+)(?::\d+)?[:/]([\w.-]+)/([\w.-]+?)(?:\.git)?$', url)
|
||||
if not m:
|
||||
sys.exit(f'无法从 origin 解析 Gitea 地址: {url}')
|
||||
host, owner, repo = m.groups()
|
||||
return f'https://{host}', owner, repo
|
||||
|
||||
|
||||
class Api:
|
||||
def __init__(self, base, owner, repo, token, insecure):
|
||||
self.api = f'{base}/api/v1/repos/{owner}/{repo}'
|
||||
self.headers = {'Authorization': f'token {token}'}
|
||||
self.ctx = ssl._create_unverified_context() if insecure else None
|
||||
|
||||
def request(self, method, path, data=None, headers=None, retries=4):
|
||||
"""发请求; 实例 TLS 间歇性抽风(SSL UNEXPECTED_EOF), 连接级错误自动重试"""
|
||||
last = None
|
||||
for attempt in range(retries):
|
||||
req = urllib.request.Request(self.api + path, data=data, method=method,
|
||||
headers={**self.headers, **(headers or {})})
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=self.ctx) as resp:
|
||||
body = resp.read()
|
||||
return resp.status, json.loads(body) if body else None
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read() or b'{}')
|
||||
except (urllib.error.URLError, ssl.SSLError, OSError) as e:
|
||||
last = e
|
||||
time.sleep(1.0 + attempt)
|
||||
raise RuntimeError(f'Gitea 请求连续 {retries} 次失败: {last}')
|
||||
|
||||
def get_json(self, path):
|
||||
status, data = self.request('GET', path)
|
||||
return data if status == 200 else None
|
||||
|
||||
def upload_asset(self, release_id, filepath, replace):
|
||||
name = os.path.basename(filepath)
|
||||
status, rel = self.request('GET', f'/releases/{release_id}')
|
||||
if status != 200:
|
||||
sys.exit(f'读取 release {release_id} 失败: {status} {rel}')
|
||||
# Gitea 版本差异: assets 可能是 {items:[...]} 或纯数组
|
||||
assets = rel.get('assets') or []
|
||||
if isinstance(assets, dict):
|
||||
assets = assets.get('items', [])
|
||||
for asset in assets:
|
||||
if asset['name'] == name:
|
||||
if not replace:
|
||||
print(f'资产 {name} 已存在, 跳过(覆盖请加 --replace)')
|
||||
return
|
||||
self.request('DELETE', f"/releases/{release_id}/assets/{asset['id']}")
|
||||
print(f'已删除旧资产 {name}')
|
||||
boundary = uuid.uuid4().hex
|
||||
payload = open(filepath, 'rb').read()
|
||||
body = (f'--{boundary}\r\n'
|
||||
f'Content-Disposition: form-data; name="attachment"; filename="{name}"\r\n'
|
||||
f'Content-Type: application/octet-stream\r\n\r\n').encode() + payload + \
|
||||
f'\r\n--{boundary}--\r\n'.encode()
|
||||
status, data = self.request(
|
||||
'POST', f'/releases/{release_id}/assets?name={name}', body,
|
||||
{'Content-Type': f'multipart/form-data; boundary={boundary}'})
|
||||
if status != 201:
|
||||
sys.exit(f'上传资产失败: {status} {data}')
|
||||
print(f'已上传资产: {name} ({len(payload) / 1024 / 1024:.1f} MB)')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('zipfile_path', help='发布 zip(release/ 下)')
|
||||
ap.add_argument('--tag', help=f'release 标签, 默认取 zip 文件名去掉 -win64.zip')
|
||||
ap.add_argument('--notes-file', help='release 说明 markdown 文件, 默认用内置模板')
|
||||
ap.add_argument('--token', default=os.environ.get('GITEA_TOKEN'), help='API token')
|
||||
ap.add_argument('--replace', action='store_true', help='覆盖已存在的同名资产')
|
||||
ap.add_argument('--insecure', action='store_true', help='跳过 TLS 证书校验(自签证书)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(args.zipfile_path):
|
||||
sys.exit(f'找不到 {args.zipfile_path}')
|
||||
if not args.token:
|
||||
sys.exit('缺少 API token: 传 --token 或设环境变量 GITEA_TOKEN')
|
||||
|
||||
tag = args.tag or re.sub(r'-win64\.zip$', '', os.path.basename(args.zipfile_path))
|
||||
version = tag.removeprefix(APP_NAME + '-v')
|
||||
notes = open(args.notes_file, encoding='utf-8').read() if args.notes_file \
|
||||
else DEFAULT_NOTES.format(version=version, app=APP_NAME)
|
||||
|
||||
base, owner, repo = git_remote_info()
|
||||
api = Api(base, owner, repo, args.token, args.insecure)
|
||||
print(f'目标: {base}/{owner}/{repo} tag={tag}')
|
||||
|
||||
rel = api.get_json(f'/releases/tags/{tag}')
|
||||
if rel:
|
||||
print(f'release 已存在(id={rel["id"]}), 复用')
|
||||
else:
|
||||
status, rel = api.request('POST', '/releases', json.dumps({
|
||||
'tag_name': tag,
|
||||
'name': f'{APP_NAME} v{version}',
|
||||
'body': notes,
|
||||
}).encode(), {'Content-Type': 'application/json'})
|
||||
if status != 201:
|
||||
sys.exit(f'创建 release 失败: {status} {rel}')
|
||||
print(f'release 已创建(id={rel["id"]})')
|
||||
api.upload_asset(rel['id'], args.zipfile_path, args.replace)
|
||||
print(f'完成: {rel.get("html_url", "") or f"{base}/{owner}/{repo}/releases/tag/{tag}"}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -3,15 +3,15 @@ cd /d "%~dp0"
|
||||
|
||||
where pyw >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
start "" pyw -3 "daplink_flash_gui.py"
|
||||
start "" pyw -3 "src\flash_gui.py"
|
||||
exit /b
|
||||
)
|
||||
|
||||
where pythonw >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
start "" pythonw "daplink_flash_gui.py"
|
||||
start "" pythonw "src\flash_gui.py"
|
||||
exit /b
|
||||
)
|
||||
|
||||
echo Python GUI launcher not found. Run manually: python daplink_flash_gui.py
|
||||
echo Python GUI launcher not found. Run manually: python src\flash_gui.py
|
||||
pause
|
||||
|
||||
@@ -30,17 +30,14 @@
|
||||
|
||||
日常烧录优先使用图形界面工具,不需要打开 VSCode,也不需要记忆 OpenOCD 命令。
|
||||
|
||||
### 启动
|
||||
- **免安装版**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases)
|
||||
下载 `DAPLinkFlash-vX.Y.Z-win64.zip`,解压双击 `DAPLinkFlash.exe` 即可,
|
||||
**包内自带 OpenOCD 工具链**(`toolchain/openocd/`),目标机器无需任何环境。
|
||||
- **源码运行**:`python FlashTool/src/flash_gui.py` 或双击 `FlashTool\start_flash_gui.bat`
|
||||
(需 Python 3.10+ 与 `pip install customtkinter`;OpenOCD 默认找 `D:/toolchain`)。
|
||||
|
||||
```bat
|
||||
双击 FlashTool\start_flash_gui.bat
|
||||
```
|
||||
|
||||
或手动运行:
|
||||
|
||||
```bat
|
||||
python FlashTool\daplink_flash_gui.py
|
||||
```
|
||||
详细说明见 [FlashTool/README.md](FlashTool/README.md)(GUI 截图说明、命令行模式、
|
||||
构建与发布流程)。下面是旧单文件版说明保留的要点:
|
||||
|
||||
### 功能
|
||||
|
||||
@@ -52,13 +49,13 @@ python FlashTool\daplink_flash_gui.py
|
||||
- 烧录器:DAPLink / CMSIS-DAP、GD-Link、ST-Link、J-Link
|
||||
- 烧录参数:适配器速度、探针序列号、烧录前擦除、烧录后校验、烧录后复位运行
|
||||
- 固件处理:补齐(1K / 2K / 4K)、拼接 Bootloader + APP + 有效标志
|
||||
- 工具链自动定位:内置(exe 旁 `toolchain/openocd`)→ 本机 `D:/toolchain` → PATH
|
||||
|
||||
### 配置
|
||||
|
||||
- 配置文件按主机名分开:`FlashTool/config/flash_gui_<主机名>.json`
|
||||
- 首次打开自动创建 `config` 目录和当前电脑的配置
|
||||
- 之后打开自动加载;每台电脑互不影响
|
||||
- `FlashTool/config/` 已加入 `.gitignore`,不会提交
|
||||
- 配置文件:`flash_gui_settings.json`,保存在程序目录(exe 旁 / FlashTool 根),
|
||||
自动保存与恢复
|
||||
- 旧版 `config/flash_gui_<主机名>.json` 在新版首次启动时自动迁移
|
||||
|
||||
### 使用步骤
|
||||
|
||||
|
||||
Reference in New Issue
Block a user