feat(pack): v2 双固件 profile 打包 — fw_pack 公共库 + CLI 重构 + GUI 打包弹窗

This commit is contained in:
2026-10-22 00:53:52 +08:00
parent d776979a5a
commit 031ee4efdf
12 changed files with 2345 additions and 292 deletions
+39 -15
View File
@@ -1,8 +1,9 @@
# DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接
# DAPLink Flash — GD32 固件烧录 · 双固件打包 · 补齐 · 拼接
> GD32E230 系列的桌面烧录/固件处理工具:**OpenOCD 烧录**(DAPLink / GD-Link /
> ST-Link / J-Link)+ **补齐 Pad**(Y-Modem 页对齐)+ **拼接 Merge**
> (Bootloader + APP + 有效标志 → 整片镜像)。
> ST-Link / J-Link)+ **打包 v2**(双固件 profile 布局 → UPDATE / B / FULL 镜像
> + MD5 清单,与命令行工具共用 `fw_pack.py` 公共库)+ **补齐 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`
@@ -10,7 +11,7 @@
> - **要改代码/重新打包**:见 §5、§6。
>
> 界面与构建方式对齐 DLPilot 项目(CustomTkinter 深色卡片仪表盘 +
> PyInstaller onefile + Gitea Release 发布)。更新:2026-09-14
> PyInstaller onedir + Gitea Release 发布)。更新:2026-10-22
## 1. 功能总览
@@ -20,10 +21,11 @@
| 固件格式 | 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;支持适配器速度与探针序列号 |
| 打包 v2 | 双固件 profile 布局(默认 `ceramixnano_mb_v51`):A/B 包向量表自动识别、补页+trailer/满槽+记录、CRC-16/XMODEM、FULL 整片镜像、MD5 清单、写盘回读复核 |
| 补齐 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) |
| 命令行模式 | 全部功能可脚本化调用(`--flash / --pack / --pad / --merge ...`,见 §4) |
| 外观 | 跟随系统深/浅色,顶栏可切换 Dark / Light / System |
## 2. 下载与安装
@@ -54,15 +56,15 @@ DAPLinkFlash/
## 3. GUI 使用
主窗口为单列仪表盘,补齐/拼接经顶栏按钮弹出独立窗口:
主窗口为单列仪表盘,打包/补齐/拼接经顶栏按钮弹出独立窗口:
```
顶栏 DAPLink Flash [补齐 (Pad)] [拼接 (Merge)] 关于 Dark/Light/System
顶栏 DAPLink Flash [打包 (v2)] [补齐 (Pad)] [拼接 (Merge)] 关于 Dark/Light/System
┌ 探针与工具链 [自动定位] ┐
├ 芯片与固件(型号/固件/基地址) ┤
├ 运行日志 — OpenOCD 输出 ┤
├ 运行日志 — OpenOCD 输出 / 打包校验 ┤
└ 操作: 检测/烧录/擦除/读取/复位/停止 ┘
弹窗 补齐 (Pad) / 拼接 (Merge) — 非模态, 配置与主窗口共享
弹窗 打包 (v2) / 补齐 (Pad) / 拼接 (Merge) — 非模态, 配置与主窗口共享
```
1. **选工具链**:默认自动定位(内置 → 本机 `D:/toolchain` → PATH),左卡右上
@@ -76,12 +78,16 @@ DAPLinkFlash/
单独 APP `0x08002000`);
4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区;
5. **固件工具**(顶栏按钮弹出的独立窗口,非模态):
- 打包 v2:选 profile(内置下拉或浏览 JSON)+ BL/A 包/B 包 bin(至少一个)+
输出目录,点"执行打包"→ `*_UPDATE.bin` / `*_B.bin` / `*_FULL.bin` + MD5 清单;
A/B 槽按向量表自动校验,逐项字节校验与回读复核结果流进主日志;
- 补齐:选输入 BIN 后输出名自动带出(`*_update.bin`,Y-Modem 升级包),对齐默认 1K,点"执行补齐";
- 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`,整片镜像),地址按典型布局
预填,点"执行拼接"。
典型布局:`0x08000000` Bootloader 8KB | `0x08002000` APP | `0x0800FFFC`
有效标志 `0xEEEEEEEE` | `0x08010000` Flash 结束 (64KB)。
v2 双固件布局(默认 profile `ceramixnano_mb_v51`)与打包规则详见仓库根
[README.md](../README.md);legacy 单槽布局:`0x08000000` Bootloader 8KB |
`0x08002000` APP | `0x0800FFFC` 有效标志 `0xEEEEEEEE`。
配置(全部输入框的值)自动保存在 exe 旁的 `flash_gui_settings.json`,下次启动
自动恢复;旧版 `config/flash_gui_<主机名>.json` 在首次启动时自动迁移。
@@ -102,8 +108,11 @@ 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 参数
python src/flash_gui.py --pack a.bin b.bin --pack-bl bl.bin -o outdir
v2 双固件打包 → UPDATE/B/FULL+MD5
--align N / --pad-byte 0xFF / -o OUT pad 参数(-o 对 pack 为目录)
--app-addr / --flag-addr / --flash-size / --flag-value merge 参数
--pack-profile ceramixnano_mb_v51|JSON / --pack-no-full pack 参数
--speed 4000 / --serial SN / --interface cfg / --target cfg
--openocd PATH / --scripts DIR 手动指定工具链(默认自动定位)
```
@@ -116,7 +125,7 @@ python src/flash_gui.py --merge bl.bin app.bin 拼接 → app_BL.bin
FlashTool/
├── src/ # 源代码(人工维护区)
│ ├── flash_gui.py # 入口与命令行模式(argparse 分发)
│ ├── app_gui.py # CustomTkinter 主界面(双栏仪表盘)
│ ├── app_gui.py # CustomTkinter 主界面(单列仪表盘 + 打包/补齐/拼接弹窗)
│ ├── openocd_runner.py # 工具链定位 / OpenOCD 命令构建与子进程
│ ├── fw_image.py # 补齐(pad)/拼接(merge) + 芯片参数表
│ ├── config.py # 运行目录与 flash_gui_settings.json 配置
@@ -134,8 +143,13 @@ FlashTool/
从源码运行:`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/`。
OpenOCD 默认找本机 `D:/toolchain/openocd`,要 vendor 的话把工具链放到
`FlashTool/toolchain/openocd/`(`package_release.py` 打包时同样优先取这里)。
> **fw_pack.py**(v2 打包公共库)位于**仓库根目录**(与命令行工具共用,单一真相):
> 源码模式由 `app_gui.py`/`flash_gui.py` 自动把仓库根补进 `sys.path`;
> PyInstaller 打包由 `build_exe.py` 的 `--paths 仓库根 --hiddenimport fw_pack`
> 收进运行库,不要把副本复制进 `src/`。
## 6. 构建与发布
@@ -159,6 +173,16 @@ python publish_gitea.py DAPLinkFlash-v0.1.0-win64.zip --insecure
处理见 §2 的三种绕过方式;要彻底消除需购买代码签名证书对 exe 签名;
- **"找不到 OpenOCD"**:点"自动定位";发布包用户确认解压后 `toolchain/openocd/`
目录和 exe 在一起;
- **`Error connecting DP: cannot read IDR`(SWD 完全连不上)**:目标板对 SWD
无应答,与工具/命令无关,按序排查:
① 目标板是否已上电(DAPLink 指示灯亮 ≠ 板子有电,两者供电独立);
② DAPLink 的 **RST 引脚必须接到 GD32 的 NRST**——本类工程固件运行后会把
SWD 引脚复用为 LED,必须"按住复位连接"(内置 `cfgs/openocd_gdlink_gd32e23x.cfg`
已配置 `connect_assert_srst`),RST 线没接/松脱时就是这个错;
③ 拔插 DAPLink 的 USB、目标板彻底断电再上电(深睡/锁死状态恢复);
④ 速度降到 1000 kHz;
⑤ 确认运行的是 `DAPLinkFlash` 文件夹里的 exe(日志第一屏工具链应显示"内置",
OpenOCD 版本 0.11.0-3、target 为 `cfgs/...` 才是新版);
- **烧录失败排查**:①"检测设备"先验链路;② 探针序列号留空可排除序列号问题;
③ 速度过高可降到 1000 kHz;④ DAPLink 固件过旧换 GD-Link 固件试试;
- **exe 放只读目录**:`flash_gui_settings.json` 写在 exe 旁边,只读目录存不了配置;
+115 -4
View File
@@ -28,12 +28,20 @@ import openocd_runner
from fw_image import (CHIP_PROFILES, FLAG_VALUE_DEFAULT, FLASH_BASE_DEFAULT,
merge_firmware, pad_bin, parse_int)
# fw_pack (v2 双固件打包公共库) 位于仓库根目录: 源码模式补 sys.path,
# 打包模式(PyInstaller --paths+hiddenimport)由运行库提供
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
if os.path.isfile(os.path.join(_REPO_ROOT, 'fw_pack.py')) \
and _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
import fw_pack
MONO = ('Consolas', 12)
BOLD = ('Microsoft YaHei UI', 13, 'bold')
TITLE_FONT = ('Microsoft YaHei UI', 18, 'bold')
APP_NAME = 'DAPLink Flash'
APP_VERSION = '2.5.2'
APP_VERSION = '2.6.0'
REPO_URL = 'https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool'
OK_BLUE = '#3B8ED0'
@@ -110,6 +118,7 @@ class App(ctk.CTk):
self._op_buttons = []
self.v_pad_result = tk.StringVar(value='')
self.v_merge_result = tk.StringVar(value='')
self.v_pack_result = tk.StringVar(value='')
self._build()
self._log(f'{APP_NAME} v{APP_VERSION} — OpenOCD 烧录 · 固件补齐 · 镜像拼接')
@@ -159,8 +168,11 @@ class App(ctk.CTk):
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))
ctk.CTkButton(top, text='打包 (v2)', width=96, height=26,
command=self._open_pack_dialog).pack(side='left', padx=(16, 6),
pady=(4, 0))
ctk.CTkButton(top, text='补齐 (Pad)', width=88, height=26,
command=self._open_pad_dialog).pack(side='left', padx=(16, 6),
command=self._open_pad_dialog).pack(side='left', padx=(6, 6),
pady=(4, 0))
ctk.CTkButton(top, text='拼接 (Merge)', width=100, height=26,
command=self._open_merge_dialog).pack(side='left', pady=(4, 0))
@@ -288,7 +300,7 @@ class App(ctk.CTk):
self.btn_stop.grid(row=1, column=2, sticky='ew', padx=3, pady=3)
# ============================================================
# 固件工具弹窗: 补齐 / 拼接
# 固件工具弹窗: 打包(v2) / 补齐 / 拼接
# ============================================================
def _center_dialog(self, dlg, w, h):
"""弹窗叠在主窗口中上部"""
@@ -297,6 +309,41 @@ class App(ctk.CTk):
y = self.winfo_rooty() + max(0, (self.winfo_height() - h) // 4)
dlg.geometry(f'{w}x{h}+{x}+{y}')
def _open_pack_dialog(self):
dlg = ctk.CTkToplevel(self)
dlg.title('固件工具 — 打包 (v2 双固件)')
dlg.resizable(False, False)
dlg.transient(self)
box = ctk.CTkFrame(dlg, fg_color='transparent')
box.pack(fill='both', expand=True, padx=18, pady=(14, 12))
ctk.CTkLabel(box, text='打包 (v2 双固件)', font=BOLD, anchor='w').pack(fill='x')
ctk.CTkLabel(box, text='按 profile 布局自动识别 A/B 槽, 产出 UPDATE / B / FULL + MD5 清单',
text_color='gray60', anchor='w').pack(fill='x', pady=(0, 6))
row = ctk.CTkFrame(box, fg_color='transparent')
row.pack(fill='x', pady=(4, 0))
ctk.CTkLabel(row, text='Profile:', width=150, anchor='w').pack(side='left')
ctk.CTkComboBox(row, values=list(fw_pack.BUILTIN_PROFILES),
variable=self.v['pack_profile']).pack(
side='left', fill='x', expand=True)
ctk.CTkButton(row, text='浏览 JSON', width=84,
command=self._browse_pack_profile).pack(side='left', padx=(6, 0))
self._field(box, 'Bootloader (BL)', self.v['pack_bl'], self._browse_pack_bl)
self._field(box, 'A 包固件 (运行槽)', self.v['pack_a'], self._browse_pack_a)
self._field(box, 'B 包固件 (保底槽)', self.v['pack_b'], self._browse_pack_b)
self._field(box, '输出目录', self.v['pack_out'], self._browse_pack_out)
opts = ctk.CTkFrame(box, fg_color='transparent')
opts.pack(fill='x', pady=(6, 0))
ctk.CTkCheckBox(opts, text='生成 FULL 整片镜像', variable=self.v['pack_full'],
checkbox_width=18, checkbox_height=18).pack(side='left')
ctk.CTkButton(opts, text='执行打包', width=96,
command=self._on_pack).pack(side='right')
ctk.CTkLabel(box, textvariable=self.v_pack_result, text_color=OK_BLUE,
justify='left', anchor='w').pack(fill='x', pady=(8, 0))
ctk.CTkLabel(box, text='A 包→*_UPDATE.bin(OA/Ymodem) B 包→*_B.bin(满槽+记录) '
'FULL→整片镜像(@0x08000000)+*.md5.txt',
text_color='gray50', anchor='w').pack(fill='x', pady=(2, 0))
self._center_dialog(dlg, 720, 400)
def _open_pad_dialog(self):
dlg = ctk.CTkToplevel(self)
dlg.title('固件工具 — 补齐 (Pad)')
@@ -501,6 +548,38 @@ class App(ctk.CTk):
if path:
self.v['merge_out'].set(path)
def _browse_pack_profile(self):
path = filedialog.askopenfilename(
title='选择 profile JSON',
filetypes=[('Profile JSON', '*.json'), ('所有文件', '*.*')])
if path:
self.v['pack_profile'].set(path)
def _browse_pack_bl(self):
path = filedialog.askopenfilename(
title='选择 Bootloader BIN', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
if path:
self.v['pack_bl'].set(path)
def _browse_pack_a(self):
path = filedialog.askopenfilename(
title='选择 A 包固件 BIN (运行槽)', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
if path:
self.v['pack_a'].set(path)
if not self.v['pack_out'].get().strip():
self.v['pack_out'].set(os.path.dirname(path))
def _browse_pack_b(self):
path = filedialog.askopenfilename(
title='选择 B 包固件 BIN (保底槽)', filetypes=[('BIN 文件', '*.bin'), ('所有文件', '*.*')])
if path:
self.v['pack_b'].set(path)
def _browse_pack_out(self):
path = filedialog.askdirectory(title='选择产物输出目录')
if path:
self.v['pack_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()))
@@ -634,8 +713,39 @@ class App(ctk.CTk):
self._set_status('就绪')
# ============================================================
# 固件工具: 补齐 / 拼接
# 固件工具: 打包(v2) / 补齐 / 拼接
# ============================================================
def _on_pack(self):
"""v2 双固件打包: 与命令行共用 fw_pack.pack_all, 结果与校验流进主日志"""
profile = self.v['pack_profile'].get().strip()
a = self.v['pack_a'].get().strip()
b = self.v['pack_b'].get().strip()
bl = self.v['pack_bl'].get().strip()
out_dir = self.v['pack_out'].get().strip() or None
if not a and not b:
messagebox.showerror('缺少文件', '请至少选择 A 包或 B 包固件 BIN。')
return
for p, tag in ((a, 'A 包'), (b, 'B 包'), (bl, 'BL')):
if p and not os.path.isfile(p):
messagebox.showerror('缺少文件', f'{tag}文件不存在:\n{p}')
return
self._log('== 打包 (v2) ==')
try:
outcome = fw_pack.pack_all(
profile, a or None, b_path=b or None, bl_path=bl or None,
out_dir=out_dir, make_full=self.v['pack_full'].get(),
progress=lambda line: self._log_q.put(('log', ' ' + line)))
except fw_pack.FwPackError as exc:
messagebox.showerror('打包失败', str(exc))
self._log('打包失败: %s' % exc)
return
names = {'update': 'UPDATE', 'b': 'B 包', 'full': 'FULL', 'md5': 'MD5 清单'}
self.v_pack_result.set('完成: ' + ', '.join(
f"{names[k]} {os.path.basename(p)}" for k, p in outcome.paths.items()))
self._log('打包完成: %s' % ', '.join(outcome.paths.values()))
self._set_status('打包完成')
self._save()
def _on_pad(self):
src = self.v['pad_in'].get().strip()
dst = self.v['pad_out'].get().strip()
@@ -737,6 +847,7 @@ class App(ctk.CTk):
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)',
'打包 v2: 双固件 profile 布局 → UPDATE / B / FULL 镜像 + MD5 清单',
'补齐 Pad: BIN 尾部补 0xFF 对齐页边界, 适配 Bootloader Y-Modem',
'拼接 Merge: Bootloader + APP + 有效标志 → 整片 Flash 镜像',
'发布包内置 OpenOCD 工具链, 免安装解压即用',
+12 -3
View File
@@ -28,6 +28,9 @@ 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')
# 仓库根: fw_pack.py(v2 打包公共库)的规范位置, PyInstaller 需要显式加入
# 搜索路径并声明 hiddenimport(源码里是带 sys.path 回退的动态导入)
REPO_ROOT = os.path.dirname(BASE)
def ensure_pyinstaller():
@@ -41,13 +44,19 @@ def ensure_pyinstaller():
def extra_collect_args():
"""CustomTkinter 的主题/字体等数据文件必须显式收集, 否则打包后 GUI 启动报错"""
"""CustomTkinter 的主题/字体等数据文件必须显式收集, 否则打包后 GUI 启动报错;
fw_pack 是仓库根的公共库, 需进 module 搜索路径 + hiddenimport"""
args = []
try:
import customtkinter # noqa: F401
return ['--collect-all', 'customtkinter']
args += ['--collect-all', 'customtkinter']
except ImportError:
print('警告: 未安装 customtkinter, 打包产物将无法启动 GUI!')
return []
if os.path.isfile(os.path.join(REPO_ROOT, 'fw_pack.py')):
args += ['--paths', REPO_ROOT, '--hiddenimport', 'fw_pack']
else:
print('警告: 仓库根缺少 fw_pack.py, 打包产物将无法使用"打包 v2"功能!')
return args
def remove_stale(out_dir, name):
+7
View File
@@ -56,6 +56,13 @@ DEFAULTS = {
"flag_addr": "0x0800FFFC",
"merge_flash": "0x10000",
"flag_value": "0xEEEEEEEE",
# 打包 (v2 双固件, fw_pack)
"pack_profile": "ceramixnano_mb_v51",
"pack_bl": "",
"pack_a": "",
"pack_b": "",
"pack_out": "",
"pack_full": True,
}
+53 -1
View File
@@ -26,6 +26,8 @@ DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 — 入口与命
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 --pack a.bin b.bin --pack-bl bl.bin
v2 双固件打包(UPDATE/B/FULL+MD5)
python flash_gui.py --version 打印 OpenOCD 版本
"""
@@ -40,6 +42,14 @@ 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)
# fw_pack (v2 双固件打包公共库) 位于仓库根目录: 源码模式补 sys.path,
# 打包模式(PyInstaller --paths+hiddenimport)由运行库提供
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
if os.path.isfile(os.path.join(_REPO_ROOT, 'fw_pack.py')) \
and _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
import fw_pack
def _toolchain(args):
"""命令行工具链解析: 参数 > 配置文件 > 自动定位(内置/本机/PATH)"""
@@ -121,6 +131,37 @@ def run_merge(args):
return 0
def run_pack(args):
"""v2 双固件打包(GUI '打包 v2' 的命令行形态, 共用 fw_pack.pack_all)"""
try:
fw_pack.self_test()
profile = fw_pack.load_profile(args.pack_profile)
except fw_pack.FwPackError as exc:
sys.exit(f'{exc}')
found = {}
for path in args.pack:
if not os.path.isfile(path):
sys.exit(f'找不到文件: {path}')
try:
slot = fw_pack.detect_slot(open(path, 'rb').read(), profile)
except fw_pack.FwPackError as exc:
sys.exit(f'{path}: {exc}')
if slot in found:
sys.exit(f'{path} 与 {found[slot]} 都被识别为 {slot.upper()} 包, '
f'一个槽只能有一个输入')
found[slot] = path
a, b = found.get('a'), found.get('b')
if not a and not b:
sys.exit('未提供可识别的固件输入 (A 包或 B 包)')
try:
fw_pack.pack_all(args.pack_profile, a, b_path=b,
bl_path=args.pack_bl or None, out_dir=args.output,
make_full=not args.pack_no_full)
except fw_pack.FwPackError as exc:
sys.exit(f'打包失败: {exc}')
return 0
def _ensure_parent(path):
parent = os.path.dirname(os.path.abspath(path))
if parent:
@@ -145,9 +186,18 @@ def main():
op.add_argument('--pad', metavar='FILE', dest='pad', help='补齐 BIN 到页边界')
op.add_argument('--merge', nargs=2, metavar=('BL', 'APP'),
help='拼接 Bootloader + APP → 整片镜像')
op.add_argument('--pack', nargs='+', metavar='BIN',
help='v2 双固件打包: A/B 包自动识别 → UPDATE/B/FULL+MD5')
ap.add_argument('--pack-profile', default='ceramixnano_mb_v51',
help='--pack 布局 profile(内置名或 JSON 路径)')
ap.add_argument('--pack-bl', default=None, metavar='BIN',
help='--pack 使用的 Bootloader bin')
ap.add_argument('--pack-no-full', action='store_true',
help='--pack 只出 UPDATE/B 包, 不拼 FULL')
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('-o', '--output',
help='输出路径: --pad/--merge 为文件, --pack 为目录(默认自动命名)')
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,
@@ -174,6 +224,8 @@ def main():
ap.add_argument('--scripts', help='OpenOCD scripts 目录(默认自动定位)')
args = ap.parse_args()
if args.pack:
sys.exit(run_pack(args))
if args.pad:
sys.exit(run_pad(args))
if args.merge: