feat(pack): FACTORY 工厂镜像 + 内置 profile 改名 CeramixNano_MB_Dual_FW
This commit is contained in:
@@ -13,8 +13,8 @@ GD32E230 系列固件处理与烧录工具仓库。本文件为项目标杆文
|
|||||||
- **项目结构**:
|
- **项目结构**:
|
||||||
- 根目录:`gd32e230_firmware_tool.py` 命令行固件工具 v2(默认双固件 profile
|
- 根目录:`gd32e230_firmware_tool.py` 命令行固件工具 v2(默认双固件 profile
|
||||||
打包;`-f`/`-m` 触发 legacy 单槽模式),`fw_pack.py` 打包公共库(CLI 与 GUI
|
打包;`-f`/`-m` 触发 legacy 单槽模式),`fw_pack.py` 打包公共库(CLI 与 GUI
|
||||||
共用的单一真相:profile 解析/校验/CRC/A 包/B 包/FULL/回读复核/MD5 清单),
|
共用的单一真相:profile 解析/校验/CRC/A 包/B 包/FULL/FACTORY/回读复核/
|
||||||
`test_fw_pack.py` 回归测试套件(49 项,含 CeramixNano 实测基准);
|
MD5 清单),`test_fw_pack.py` 回归测试套件(55 项,含 CeramixNano 实测基准);
|
||||||
`merge_bin.py`/`fill_bin.py` 旧版脚本保留兼容,
|
`merge_bin.py`/`fill_bin.py` 旧版脚本保留兼容,
|
||||||
`gd32e230f8_bootloader_hulk.bin` 出厂 bootloader;
|
`gd32e230f8_bootloader_hulk.bin` 出厂 bootloader;
|
||||||
- `FlashTool/`:DAPLink Flash GUI v2.6.0(烧录 + 打包 v2 + 补齐 Pad + 拼接
|
- `FlashTool/`:DAPLink Flash GUI v2.6.0(烧录 + 打包 v2 + 补齐 Pad + 拼接
|
||||||
@@ -30,7 +30,11 @@ GD32E230 系列固件处理与烧录工具仓库。本文件为项目标杆文
|
|||||||
CRC-16/XMODEM(自检向量 `"123456789"→0x31C3`);A 包补 0xFF 至整 N 页 +
|
CRC-16/XMODEM(自检向量 `"123456789"→0x31C3`);A 包补 0xFF 至整 N 页 +
|
||||||
末 4B trailer{页数16|CRC16},CRC 范围=去末 4B,恰整页或余量<4B 扩 1 页;
|
末 4B trailer{页数16|CRC16},CRC 范围=去末 4B,恰整页或余量<4B 扩 1 页;
|
||||||
B 包固定满槽 + 槽末 8B 记录{magic=0xC0DE|页数|CRC|2B FF},CRC 范围=去末 8B,
|
B 包固定满槽 + 槽末 8B 记录{magic=0xC0DE|页数|CRC|2B FF},CRC 范围=去末 8B,
|
||||||
固件>槽-8B 报错;FULL=BL+A 包+槽尾锚点(0x0800AFFC)+B 满槽+0xEEEE@0x0800FFFC。
|
固件>槽-8B 报错;FULL=BL+A 包+槽尾锚点(0x0800AFFC)+B 满槽+0xEEEE@0x0800FFFC;
|
||||||
|
FACTORY=BL+B 满槽+flag **无 A**(A 槽整体擦除态,上电判定链落 B 跳出厂固件),
|
||||||
|
12 项字节校验;不再单独落盘 B 包文件(B 内容已含于 FULL/FACTORY)。
|
||||||
|
默认 profile 名为 `CeramixNano_MB_Dual_FW`(旧名 `ceramixnano_mb_v51` 已
|
||||||
|
废弃不留别名,GUI 旧配置自动迁移)。
|
||||||
回归基准(CeramixNano 实测):1.0.17→20 页 trailer=0xD30F0014;1.0.18→
|
回归基准(CeramixNano 实测):1.0.17→20 页 trailer=0xD30F0014;1.0.18→
|
||||||
0x6F380014;V1.0.12 B→记录 `DE C0 13 00 6F 18 FF FF`;FULL 镜像 MD5
|
0x6F380014;V1.0.12 B→记录 `DE C0 13 00 6F 18 FF FF`;FULL 镜像 MD5
|
||||||
`f704b837...` 逐字节复现。**改 `fw_pack.py` 后必须
|
`f704b837...` 逐字节复现。**改 `fw_pack.py` 后必须
|
||||||
@@ -56,14 +60,15 @@ GD32E230 系列固件处理与烧录工具仓库。本文件为项目标杆文
|
|||||||
cfgs/ 自定义 cfg 机制、全操作 SRST 连接序列、整包 0.11 工具链、
|
cfgs/ 自定义 cfg 机制、全操作 SRST 连接序列、整包 0.11 工具链、
|
||||||
Gitea Release 发布流水线;
|
Gitea Release 发布流水线;
|
||||||
- 2026-10-22 **v2 彻底升级落地**(`UPGRADE_PLAN_v2.md` 全部实现并自验收):
|
- 2026-10-22 **v2 彻底升级落地**(`UPGRADE_PLAN_v2.md` 全部实现并自验收):
|
||||||
- `fw_pack.py` 公共库:布局 profile(内置 `ceramixnano_mb_v51` /
|
- `fw_pack.py` 公共库:布局 profile(内置 `CeramixNano_MB_Dual_FW` /
|
||||||
`f8_single_slot` + JSON 自定义,完整性校验)、向量表自动识别 A/B 包
|
`f8_single_slot` + JSON 自定义,完整性校验)、向量表自动识别 A/B 包
|
||||||
(显式指定冲突报错)、A 包动态补页+trailer+整页扩展、B 包固定满槽+记录、
|
(显式指定冲突报错)、A 包动态补页+trailer+整页扩展、B 包固定满槽+记录、
|
||||||
FULL 整片镜像+槽尾锚点+flag、CRC-16/XMODEM 启动自检、写盘回读复核、
|
FULL 整片镜像+槽尾锚点+flag、CRC-16/XMODEM 启动自检、写盘回读复核、
|
||||||
19 项 FULL 字节校验、MD5 清单+布局摘要、FULL 自动命名(项目/双版本/日期);
|
19 项 FULL 字节校验、MD5 清单+布局摘要、FULL 自动命名(项目/双版本/日期);
|
||||||
- `gd32e230_firmware_tool.py` v2.0.0 重构:默认 v2 模式(位置参数自动识别、
|
- `gd32e230_firmware_tool.py` v2.0.0 重构:默认 v2 模式(位置参数自动识别、
|
||||||
`--a/--b/--bl/--profile/-o 目录/--no-full`),`-f/-m` 保留 legacy 行为不变;
|
`--a/--b/--bl/--profile/-o 目录/--no-full/--no-factory`),`-f/-m` 保留
|
||||||
- `test_fw_pack.py`:49 项回归全绿——规格第五节基准逐项复现(1.0.17 UPDATE
|
legacy 行为不变;
|
||||||
|
- `test_fw_pack.py` 回归全绿(55 项)——规格第五节基准逐项复现(1.0.17 UPDATE
|
||||||
与验收包逐字节一致、FULL MD5 `f704b837...` 复现、恰整页/余量<4 扩页、
|
与验收包逐字节一致、FULL MD5 `f704b837...` 复现、恰整页/余量<4 扩页、
|
||||||
LDC1612 legacy update.bin 复现)、变体 profile(B 8K/28K/无)、错误路径、
|
LDC1612 legacy update.bin 复现)、变体 profile(B 8K/28K/无)、错误路径、
|
||||||
CLI 冒烟;
|
CLI 冒烟;
|
||||||
@@ -72,6 +77,17 @@ GD32E230 系列固件处理与烧录工具仓库。本文件为项目标杆文
|
|||||||
`build_exe.py` 以 `--paths+hiddenimport` 收录根目录 `fw_pack.py`;
|
`build_exe.py` 以 `--paths+hiddenimport` 收录根目录 `fw_pack.py`;
|
||||||
- `make_v5_update.py` 已删除(能力并入 `fw_pack.py`);README(根 + FlashTool)
|
- `make_v5_update.py` 已删除(能力并入 `fw_pack.py`);README(根 + FlashTool)
|
||||||
已重写;尚未发布 GUI 新版 zip(待下次 `build_exe.py`→`package_release.py`)。
|
已重写;尚未发布 GUI 新版 zip(待下次 `build_exe.py`→`package_release.py`)。
|
||||||
|
- 2026-10-22 **FACTORY 工厂镜像改造 + profile 改名**(上个会话收尾、本会话续完):
|
||||||
|
- `fw_pack.py`:新增 `build_factory`/`verify_factory_blob`/`suggest_factory_name`
|
||||||
|
(BL+B 满槽+flag、无 A、12 项字节校验),`pack_all` 产物改为 UPDATE/FULL/
|
||||||
|
FACTORY/MD5,**不再单独落盘 B 包文件**;仅 B 输入时只出 FACTORY(不拼 FULL);
|
||||||
|
- CLI:根工具与 `flash_gui --pack` 均加 `--no-factory`(flash_gui 侧为
|
||||||
|
`--pack-no-factory`),GUI 弹窗文案/结果映射同步;
|
||||||
|
- 内置 profile 改名 `ceramixnano_mb_v51` → `CeramixNano_MB_Dual_FW`(旧名
|
||||||
|
废弃不留别名;CLI/GUI 默认值、config 默认+旧配置迁移、测试、文档全量同步);
|
||||||
|
- 参数拼接审计:`flash_gui --pack -o` 加目录护栏(拒绝 .bin 路径,对齐根工具),
|
||||||
|
全链路冒烟(GUI 弹窗/CLI/命名拼接)通过;`bootloader_disasm*.txt` 为分析
|
||||||
|
副产物留在工作区未入库。
|
||||||
|
|
||||||
### 计划 / TODO
|
### 计划 / TODO
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -1,8 +1,8 @@
|
|||||||
# DAPLink Flash — GD32 固件烧录 · 双固件打包 · 补齐 · 拼接
|
# DAPLink Flash — GD32 固件烧录 · 双固件打包 · 补齐 · 拼接
|
||||||
|
|
||||||
> GD32E230 系列的桌面烧录/固件处理工具:**OpenOCD 烧录**(DAPLink / GD-Link /
|
> GD32E230 系列的桌面烧录/固件处理工具:**OpenOCD 烧录**(DAPLink / GD-Link /
|
||||||
> ST-Link / J-Link)+ **打包 v2**(双固件 profile 布局 → UPDATE / B / FULL 镜像
|
> ST-Link / J-Link)+ **打包 v2**(双固件 profile 布局 → UPDATE / FULL / FACTORY
|
||||||
> + MD5 清单,与命令行工具共用 `fw_pack.py` 公共库)+ **补齐 Pad**(Y-Modem
|
> 镜像 + MD5 清单,与命令行工具共用 `fw_pack.py` 公共库)+ **补齐 Pad**(Y-Modem
|
||||||
> 页对齐)+ **拼接 Merge**(Bootloader + APP + 有效标志 → 整片镜像)。
|
> 页对齐)+ **拼接 Merge**(Bootloader + APP + 有效标志 → 整片镜像)。
|
||||||
>
|
>
|
||||||
> - **只想用工具**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases)
|
> - **只想用工具**:到 [Gitea Releases](https://gitea.hulk.wang/hulk/gd32e230f8_firmware_merge_tool/releases)
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
| 固件格式 | BIN / HEX / ELF——HEX/ELF 自动用文件内地址,仅 BIN 需填基地址(完整镜像 `0x08000000`,单独 APP `0x08002000`) |
|
| 固件格式 | BIN / HEX / ELF——HEX/ELF 自动用文件内地址,仅 BIN 需填基地址(完整镜像 `0x08000000`,单独 APP `0x08002000`) |
|
||||||
| 芯片型号 | GD32E230F4 / F6 / F8 / C8,自动带出 Flash 大小与 target 配置 |
|
| 芯片型号 | GD32E230F4 / F6 / F8 / C8,自动带出 Flash 大小与 target 配置 |
|
||||||
| 烧录器 | DAPLink / CMSIS-DAP、GD-Link、ST-Link、J-Link、CMSIS-DAP(TCP)、Custom;支持适配器速度与探针序列号 |
|
| 烧录器 | 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 清单、写盘回读复核 |
|
| 打包 v2 | 双固件 profile 布局(默认 `CeramixNano_MB_Dual_FW`):A/B 包向量表自动识别、补页+trailer/满槽+记录、CRC-16/XMODEM、FULL 整片镜像、FACTORY 工厂镜像(BL+B+flag,上电跳 B)、MD5 清单、写盘回读复核 |
|
||||||
| 补齐 Pad | BIN 尾部补 `0xFF` 到 1K/2K/4K 页边界;bootloader 的 Y-Modem 只按 1K 整包写 Flash,最后一包不满必须先补齐 |
|
| 补齐 Pad | BIN 尾部补 `0xFF` 到 1K/2K/4K 页边界;bootloader 的 Y-Modem 只按 1K 整包写 Flash,最后一包不满必须先补齐 |
|
||||||
| 拼接 Merge | Bootloader + APP + 有效标志(`0xEEEEEEEE` @`0x0800FFFC`) → 整片 Flash 镜像,逻辑与 `merge_bin.py` 一致 |
|
| 拼接 Merge | Bootloader + APP + 有效标志(`0xEEEEEEEE` @`0x0800FFFC`) → 整片 Flash 镜像,逻辑与 `merge_bin.py` 一致 |
|
||||||
| 内置工具链 | 发布包自带 OpenOCD(`toolchain/openocd/`),GUI 自动定位,免安装免配置 |
|
| 内置工具链 | 发布包自带 OpenOCD(`toolchain/openocd/`),GUI 自动定位,免安装免配置 |
|
||||||
@@ -79,13 +79,14 @@ DAPLinkFlash/
|
|||||||
4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区;
|
4. **点"检测设备"**确认链路通 → **点"烧录固件"**;输出实时显示在右下日志区;
|
||||||
5. **固件工具**(顶栏按钮弹出的独立窗口,非模态):
|
5. **固件工具**(顶栏按钮弹出的独立窗口,非模态):
|
||||||
- 打包 v2:选 profile(内置下拉或浏览 JSON)+ BL/A 包/B 包 bin(至少一个)+
|
- 打包 v2:选 profile(内置下拉或浏览 JSON)+ BL/A 包/B 包 bin(至少一个)+
|
||||||
输出目录,点"执行打包"→ `*_UPDATE.bin` / `*_B.bin` / `*_FULL.bin` + MD5 清单;
|
输出目录,点"执行打包"→ `*_UPDATE.bin`(A 包)/ `*_FACTORY.bin`(工厂镜像
|
||||||
|
BL+B+flag,给了 B 包才出)/ `*_FULL.bin`(整片镜像,需 A 包)+ MD5 清单;
|
||||||
A/B 槽按向量表自动校验,逐项字节校验与回读复核结果流进主日志;
|
A/B 槽按向量表自动校验,逐项字节校验与回读复核结果流进主日志;
|
||||||
- 补齐:选输入 BIN 后输出名自动带出(`*_update.bin`,Y-Modem 升级包),对齐默认 1K,点"执行补齐";
|
- 补齐:选输入 BIN 后输出名自动带出(`*_update.bin`,Y-Modem 升级包),对齐默认 1K,点"执行补齐";
|
||||||
- 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`,整片镜像),地址按典型布局
|
- 拼接:选 Bootloader 与 APP 后输出名自动带出(`*_BL.bin`,整片镜像),地址按典型布局
|
||||||
预填,点"执行拼接"。
|
预填,点"执行拼接"。
|
||||||
|
|
||||||
v2 双固件布局(默认 profile `ceramixnano_mb_v51`)与打包规则详见仓库根
|
v2 双固件布局(默认 profile `CeramixNano_MB_Dual_FW`)与打包规则详见仓库根
|
||||||
[README.md](../README.md);legacy 单槽布局:`0x08000000` Bootloader 8KB |
|
[README.md](../README.md);legacy 单槽布局:`0x08000000` Bootloader 8KB |
|
||||||
`0x08002000` APP | `0x0800FFFC` 有效标志 `0xEEEEEEEE`。
|
`0x08002000` APP | `0x0800FFFC` 有效标志 `0xEEEEEEEE`。
|
||||||
|
|
||||||
@@ -109,10 +110,10 @@ python src/flash_gui.py --reset 复位运行
|
|||||||
python src/flash_gui.py --pad app.bin 补齐到 1K → app_pad.bin
|
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
|
python src/flash_gui.py --merge bl.bin app.bin 拼接 → app_BL.bin
|
||||||
python src/flash_gui.py --pack a.bin b.bin --pack-bl bl.bin -o outdir
|
python src/flash_gui.py --pack a.bin b.bin --pack-bl bl.bin -o outdir
|
||||||
v2 双固件打包 → UPDATE/B/FULL+MD5
|
v2 双固件打包 → UPDATE/FULL/FACTORY+MD5
|
||||||
--align N / --pad-byte 0xFF / -o OUT pad 参数(-o 对 pack 为目录)
|
--align N / --pad-byte 0xFF / -o OUT pad 参数(-o 对 pack 为目录)
|
||||||
--app-addr / --flag-addr / --flash-size / --flag-value merge 参数
|
--app-addr / --flag-addr / --flash-size / --flag-value merge 参数
|
||||||
--pack-profile ceramixnano_mb_v51|JSON / --pack-no-full pack 参数
|
--pack-profile 名字|JSON / --pack-no-full / --pack-no-factory pack 参数
|
||||||
--speed 4000 / --serial SN / --interface cfg / --target cfg
|
--speed 4000 / --serial SN / --interface cfg / --target cfg
|
||||||
--openocd PATH / --scripts DIR 手动指定工具链(默认自动定位)
|
--openocd PATH / --scripts DIR 手动指定工具链(默认自动定位)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ class App(ctk.CTk):
|
|||||||
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
box = ctk.CTkFrame(dlg, fg_color='transparent')
|
||||||
box.pack(fill='both', expand=True, padx=18, pady=(14, 12))
|
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='打包 (v2 双固件)', font=BOLD, anchor='w').pack(fill='x')
|
||||||
ctk.CTkLabel(box, text='按 profile 布局自动识别 A/B 槽, 产出 UPDATE / B / FULL + MD5 清单',
|
ctk.CTkLabel(box, text='按 profile 布局自动识别 A/B 槽, 产出 UPDATE / FULL / FACTORY + MD5 清单',
|
||||||
text_color='gray60', anchor='w').pack(fill='x', pady=(0, 6))
|
text_color='gray60', anchor='w').pack(fill='x', pady=(0, 6))
|
||||||
row = ctk.CTkFrame(box, fg_color='transparent')
|
row = ctk.CTkFrame(box, fg_color='transparent')
|
||||||
row.pack(fill='x', pady=(4, 0))
|
row.pack(fill='x', pady=(4, 0))
|
||||||
@@ -339,8 +339,8 @@ class App(ctk.CTk):
|
|||||||
command=self._on_pack).pack(side='right')
|
command=self._on_pack).pack(side='right')
|
||||||
ctk.CTkLabel(box, textvariable=self.v_pack_result, text_color=OK_BLUE,
|
ctk.CTkLabel(box, textvariable=self.v_pack_result, text_color=OK_BLUE,
|
||||||
justify='left', anchor='w').pack(fill='x', pady=(8, 0))
|
justify='left', anchor='w').pack(fill='x', pady=(8, 0))
|
||||||
ctk.CTkLabel(box, text='A 包→*_UPDATE.bin(OA/Ymodem) B 包→*_B.bin(满槽+记录) '
|
ctk.CTkLabel(box, text='A 包→*_UPDATE.bin(OTA/Ymodem) B 包→*_FACTORY.bin(工厂'
|
||||||
'FULL→整片镜像(@0x08000000)+*.md5.txt',
|
' BL+B+flag, 上电跳 B) FULL→整片镜像(@0x08000000)',
|
||||||
text_color='gray50', anchor='w').pack(fill='x', pady=(2, 0))
|
text_color='gray50', anchor='w').pack(fill='x', pady=(2, 0))
|
||||||
self._center_dialog(dlg, 720, 400)
|
self._center_dialog(dlg, 720, 400)
|
||||||
|
|
||||||
@@ -739,7 +739,8 @@ class App(ctk.CTk):
|
|||||||
messagebox.showerror('打包失败', str(exc))
|
messagebox.showerror('打包失败', str(exc))
|
||||||
self._log('打包失败: %s' % exc)
|
self._log('打包失败: %s' % exc)
|
||||||
return
|
return
|
||||||
names = {'update': 'UPDATE', 'b': 'B 包', 'full': 'FULL', 'md5': 'MD5 清单'}
|
names = {'update': 'UPDATE', 'full': 'FULL', 'factory': 'FACTORY',
|
||||||
|
'md5': 'MD5 清单'}
|
||||||
self.v_pack_result.set('完成: ' + ', '.join(
|
self.v_pack_result.set('完成: ' + ', '.join(
|
||||||
f"{names[k]} {os.path.basename(p)}" for k, p in outcome.paths.items()))
|
f"{names[k]} {os.path.basename(p)}" for k, p in outcome.paths.items()))
|
||||||
self._log('打包完成: %s' % ', '.join(outcome.paths.values()))
|
self._log('打包完成: %s' % ', '.join(outcome.paths.values()))
|
||||||
@@ -847,7 +848,7 @@ class App(ctk.CTk):
|
|||||||
ctk.CTkLabel(box, text=f'v{APP_VERSION} — GD32E230 固件烧录与打包工具',
|
ctk.CTkLabel(box, text=f'v{APP_VERSION} — GD32E230 固件烧录与打包工具',
|
||||||
text_color='gray60', anchor='w').pack(anchor='w', pady=(0, 12))
|
text_color='gray60', anchor='w').pack(anchor='w', pady=(0, 12))
|
||||||
for line in ('OpenOCD 烧录: 检测 / 烧录 / 擦除 / 读取 / 复位 (DAPLink·GD-Link·ST-Link·J-Link)',
|
for line in ('OpenOCD 烧录: 检测 / 烧录 / 擦除 / 读取 / 复位 (DAPLink·GD-Link·ST-Link·J-Link)',
|
||||||
'打包 v2: 双固件 profile 布局 → UPDATE / B / FULL 镜像 + MD5 清单',
|
'打包 v2: 双固件 profile 布局 → UPDATE / FACTORY / FULL 镜像 + MD5 清单',
|
||||||
'补齐 Pad: BIN 尾部补 0xFF 对齐页边界, 适配 Bootloader Y-Modem',
|
'补齐 Pad: BIN 尾部补 0xFF 对齐页边界, 适配 Bootloader Y-Modem',
|
||||||
'拼接 Merge: Bootloader + APP + 有效标志 → 整片 Flash 镜像',
|
'拼接 Merge: Bootloader + APP + 有效标志 → 整片 Flash 镜像',
|
||||||
'发布包内置 OpenOCD 工具链, 免安装解压即用',
|
'发布包内置 OpenOCD 工具链, 免安装解压即用',
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ DEFAULTS = {
|
|||||||
"merge_flash": "0x10000",
|
"merge_flash": "0x10000",
|
||||||
"flag_value": "0xEEEEEEEE",
|
"flag_value": "0xEEEEEEEE",
|
||||||
# 打包 (v2 双固件, fw_pack)
|
# 打包 (v2 双固件, fw_pack)
|
||||||
"pack_profile": "ceramixnano_mb_v51",
|
"pack_profile": "CeramixNano_MB_Dual_FW",
|
||||||
"pack_bl": "",
|
"pack_bl": "",
|
||||||
"pack_a": "",
|
"pack_a": "",
|
||||||
"pack_b": "",
|
"pack_b": "",
|
||||||
@@ -80,6 +80,10 @@ def _legacy_settings():
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# 旧内置 profile 名 → 现名 (改名前保存的配置一次性迁移, 旧名不再受支持)
|
||||||
|
_RENAMED_PROFILE = {"ceramixnano_mb_v51": "CeramixNano_MB_Dual_FW"}
|
||||||
|
|
||||||
|
|
||||||
def load_settings():
|
def load_settings():
|
||||||
"""读配置, 带默认值兜底; 新配置不存在时尝试从旧版 per-host 配置迁移"""
|
"""读配置, 带默认值兜底; 新配置不存在时尝试从旧版 per-host 配置迁移"""
|
||||||
data = dict(DEFAULTS)
|
data = dict(DEFAULTS)
|
||||||
@@ -97,6 +101,8 @@ def load_settings():
|
|||||||
for key in DEFAULTS:
|
for key in DEFAULTS:
|
||||||
if key in stored:
|
if key in stored:
|
||||||
data[key] = stored[key]
|
data[key] = stored[key]
|
||||||
|
if data.get("pack_profile") in _RENAMED_PROFILE:
|
||||||
|
data["pack_profile"] = _RENAMED_PROFILE[data["pack_profile"]]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ DAPLink Flash — GD32 固件烧录 · 补齐 · 镜像拼接 — 入口与命
|
|||||||
python flash_gui.py --pad app.bin 补齐到 1K 边界(app_pad.bin)
|
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 --merge bl.bin app.bin 拼接整片镜像(*_BL.bin)
|
||||||
python flash_gui.py --pack a.bin b.bin --pack-bl bl.bin
|
python flash_gui.py --pack a.bin b.bin --pack-bl bl.bin
|
||||||
v2 双固件打包(UPDATE/B/FULL+MD5)
|
v2 双固件打包(UPDATE/FULL/FACTORY+MD5)
|
||||||
python flash_gui.py --version 打印 OpenOCD 版本
|
python flash_gui.py --version 打印 OpenOCD 版本
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -153,10 +153,13 @@ def run_pack(args):
|
|||||||
a, b = found.get('a'), found.get('b')
|
a, b = found.get('a'), found.get('b')
|
||||||
if not a and not b:
|
if not a and not b:
|
||||||
sys.exit('未提供可识别的固件输入 (A 包或 B 包)')
|
sys.exit('未提供可识别的固件输入 (A 包或 B 包)')
|
||||||
|
if args.output and os.path.splitext(args.output)[1].lower() == '.bin':
|
||||||
|
sys.exit('--pack 的 -o 是输出目录(产物自动命名), 不能是 .bin 文件路径')
|
||||||
try:
|
try:
|
||||||
fw_pack.pack_all(args.pack_profile, a, b_path=b,
|
fw_pack.pack_all(args.pack_profile, a, b_path=b,
|
||||||
bl_path=args.pack_bl or None, out_dir=args.output,
|
bl_path=args.pack_bl or None, out_dir=args.output,
|
||||||
make_full=not args.pack_no_full)
|
make_full=not args.pack_no_full,
|
||||||
|
make_factory=not args.pack_no_factory)
|
||||||
except fw_pack.FwPackError as exc:
|
except fw_pack.FwPackError as exc:
|
||||||
sys.exit(f'打包失败: {exc}')
|
sys.exit(f'打包失败: {exc}')
|
||||||
return 0
|
return 0
|
||||||
@@ -187,13 +190,15 @@ def main():
|
|||||||
op.add_argument('--merge', nargs=2, metavar=('BL', 'APP'),
|
op.add_argument('--merge', nargs=2, metavar=('BL', 'APP'),
|
||||||
help='拼接 Bootloader + APP → 整片镜像')
|
help='拼接 Bootloader + APP → 整片镜像')
|
||||||
op.add_argument('--pack', nargs='+', metavar='BIN',
|
op.add_argument('--pack', nargs='+', metavar='BIN',
|
||||||
help='v2 双固件打包: A/B 包自动识别 → UPDATE/B/FULL+MD5')
|
help='v2 双固件打包: A/B 包自动识别 → UPDATE/FULL/FACTORY+MD5')
|
||||||
ap.add_argument('--pack-profile', default='ceramixnano_mb_v51',
|
ap.add_argument('--pack-profile', default='CeramixNano_MB_Dual_FW',
|
||||||
help='--pack 布局 profile(内置名或 JSON 路径)')
|
help='--pack 布局 profile(内置名或 JSON 路径)')
|
||||||
ap.add_argument('--pack-bl', default=None, metavar='BIN',
|
ap.add_argument('--pack-bl', default=None, metavar='BIN',
|
||||||
help='--pack 使用的 Bootloader bin')
|
help='--pack 使用的 Bootloader bin')
|
||||||
ap.add_argument('--pack-no-full', action='store_true',
|
ap.add_argument('--pack-no-full', action='store_true',
|
||||||
help='--pack 只出 UPDATE/B 包, 不拼 FULL')
|
help='--pack 不拼 FULL 整片镜像')
|
||||||
|
ap.add_argument('--pack-no-factory', action='store_true',
|
||||||
|
help='--pack 不出 FACTORY 工厂镜像')
|
||||||
ap.add_argument('--align', default='1024', help='--pad 对齐字节(默认 1024)')
|
ap.add_argument('--align', default='1024', help='--pad 对齐字节(默认 1024)')
|
||||||
ap.add_argument('--pad-byte', default='0xFF', help='--pad 填充字节(默认 0xFF)')
|
ap.add_argument('--pad-byte', default='0xFF', help='--pad 填充字节(默认 0xFF)')
|
||||||
ap.add_argument('-o', '--output',
|
ap.add_argument('-o', '--output',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
## v2 双固件打包
|
## v2 双固件打包
|
||||||
|
|
||||||
### 布局(默认 profile `ceramixnano_mb_v51`,GD32E230F8/64K)
|
### 布局(默认 profile `CeramixNano_MB_Dual_FW`,GD32E230F8/64K)
|
||||||
|
|
||||||
```text
|
```text
|
||||||
0x08000000 ┌──────────────────┐
|
0x08000000 ┌──────────────────┐
|
||||||
@@ -42,40 +42,43 @@
|
|||||||
### 快速开始
|
### 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 全产物: A 包 → *_UPDATE.bin, B 包 → *_B.bin, 整片 → *_FULL.bin + *.md5.txt
|
# 全产物: A 包 → *_UPDATE.bin, 整片 → *_FULL.bin, 工厂镜像 → *_FACTORY.bin (+*.md5.txt)
|
||||||
python gd32e230_firmware_tool.py app_a.bin app_b.bin -b bootloader.bin
|
python gd32e230_firmware_tool.py app_a.bin app_b.bin -b bootloader.bin
|
||||||
|
|
||||||
# 只打 A 包(OTA 升级包)
|
# 只打 A 包(OTA 升级包)
|
||||||
python gd32e230_firmware_tool.py app_a.bin
|
python gd32e230_firmware_tool.py app_a.bin --no-full --no-factory
|
||||||
|
|
||||||
# 仅 B 底包(产线备用,不拼 FULL)
|
# 只打工厂烧录镜像(BL + B 满槽 + flag, 上电跳 B, 后续 OTA 写 A)
|
||||||
python gd32e230_firmware_tool.py app_b.bin
|
python gd32e230_firmware_tool.py app_b.bin -b bootloader.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
输入 bin 从**向量表自动识别** A/B 包(初始 SP 须在 SRAM `[0x20000000,0x20002000]`、
|
输入 bin 从**向量表自动识别** A/B 包(初始 SP 须在 SRAM `[0x20000000,0x20002000]`、
|
||||||
复位向量 Thumb 且落在对应槽区间);也可 `--a`/`--b` 显式指定,与向量表判定冲突时报错。
|
复位向量 Thumb 且落在对应槽区间);也可 `--a`/`--b` 显式指定,与向量表判定冲突时报错。
|
||||||
写盘后回读复核(重算 trailer/记录/锚点一致才报成功),并输出布局摘要 + 19 项字节校验。
|
写盘后回读复核(重算 trailer/记录/锚点一致才报成功),并输出布局摘要与逐项字节校验
|
||||||
|
(FULL 19 项 / FACTORY 12 项)。
|
||||||
|
|
||||||
### 产物
|
### 产物
|
||||||
|
|
||||||
| 产物 | 用途 |
|
| 产物 | 用途 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `*_UPDATE.bin` | A 包(补页+trailer),Ymodem/OTA 推送 |
|
| `*_UPDATE.bin` | A 包(补页+trailer),Ymodem/OTA 推送 |
|
||||||
| `*_B.bin` | B 底包(满槽+记录),产线单独烧 B 槽备用 |
|
| `*_FULL.bin` | 整片镜像(BL + A + B + flag),`@0x08000000` 烧录后直接运行 A(开发/验收底包) |
|
||||||
| `*_FULL.bin` | 整片镜像,`@0x08000000` 整片烧录(底包) |
|
| `*_FACTORY.bin` | **工厂烧录镜像**(BL + B 满槽 + flag,**无 A 内容、锚点擦除态**),上电判定链落 B 跳出厂固件,之后 OTA 写 A |
|
||||||
| `*.md5.txt` | 全部产物 MD5 清单 + 布局摘要 |
|
| `*.md5.txt` | 全部产物 MD5 清单 + 布局摘要(挂在 FULL,无 FULL 挂 FACTORY) |
|
||||||
|
|
||||||
FULL 镜像自动命名 `<项目>_FULL_A-V<版本>_B-V<版本>[_日期].bin`(从输入文件名推导)。
|
命名从输入文件名推导:FULL 为 `<项目>_FULL_A-V<版本>_B-V<版本>[_日期].bin`,
|
||||||
|
FACTORY 为 `<项目>_FACTORY_B-V<版本>[_日期].bin`。不再单独输出 B 包文件
|
||||||
|
(B 内容已包含在 FULL/FACTORY 中)。
|
||||||
|
|
||||||
### 布局 profile(可复用到任意工程)
|
### 布局 profile(可复用到任意工程)
|
||||||
|
|
||||||
内置 `ceramixnano_mb_v51`(默认)与 `f8_single_slot`(无保底槽)。其他工程写一份
|
内置 `CeramixNano_MB_Dual_FW`(默认)与 `f8_single_slot`(无保底槽)。其他工程写一份
|
||||||
JSON 即可(保底槽 8K/28K、无保底槽、BL 更大等都支持),工具做完整性校验
|
JSON 即可(保底槽 8K/28K、无保底槽、BL 更大等都支持),工具做完整性校验
|
||||||
(分区不重叠、页对齐、总和=flash_size):
|
(分区不重叠、页对齐、总和=flash_size):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"profile": "ceramixnano_mb_v51",
|
"profile": "CeramixNano_MB_Dual_FW",
|
||||||
"chip": "GD32E230F8",
|
"chip": "GD32E230F8",
|
||||||
"flash_base": "0x08000000",
|
"flash_base": "0x08000000",
|
||||||
"flash_size": "64K",
|
"flash_size": "64K",
|
||||||
@@ -103,14 +106,15 @@ python gd32e230_firmware_tool.py app.bin --profile f8_single_slot # 无保底
|
|||||||
| `bins...` | (可选) | 输入 bin,A/B 包按向量表自动识别 |
|
| `bins...` | (可选) | 输入 bin,A/B 包按向量表自动识别 |
|
||||||
| `--a BIN` / `--b BIN` | — | 显式指定 A/B 包(与自动识别冲突时报错) |
|
| `--a BIN` / `--b BIN` | — | 显式指定 A/B 包(与自动识别冲突时报错) |
|
||||||
| `-b, --bootloader` | — | BL bin(参与 FULL 拼接;缺省仅警告) |
|
| `-b, --bootloader` | — | BL bin(参与 FULL 拼接;缺省仅警告) |
|
||||||
| `--profile` | `ceramixnano_mb_v51` | 内置名或 profile JSON 路径 |
|
| `--profile` | `CeramixNano_MB_Dual_FW` | 内置名或 profile JSON 路径 |
|
||||||
| `-o, --output` | 输入所在目录 | **输出目录**(产物自动命名) |
|
| `-o, --output` | 输入所在目录 | **输出目录**(产物自动命名) |
|
||||||
| `--no-full` | — | 只出 UPDATE/B 包,不拼 FULL |
|
| `--no-full` | — | 不拼 FULL 整片镜像 |
|
||||||
|
| `--no-factory` | — | 不出 FACTORY 工厂镜像 |
|
||||||
|
|
||||||
### 回归测试
|
### 回归测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python test_fw_pack.py # 49 项: 规格基准 + 边界 + 篡改检测 + CLI 冒烟
|
python test_fw_pack.py # 55 项: 规格基准 + 边界 + 篡改检测 + CLI 冒烟
|
||||||
```
|
```
|
||||||
|
|
||||||
覆盖 UPGRADE_PLAN_v2.md 第五节全部基准(CeramixNano 1.0.17/1.0.18/V1.0.12
|
覆盖 UPGRADE_PLAN_v2.md 第五节全部基准(CeramixNano 1.0.17/1.0.18/V1.0.12
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
> 本文可直接作为 AI 辅助重构的提示词使用;也是本次彻底升级的需求基线。
|
> 本文可直接作为 AI 辅助重构的提示词使用;也是本次彻底升级的需求基线。
|
||||||
> 背景仓库:本仓库(main 分支);配套 skill:hulk-mcu-firmware(SKILL.md + tool/ 缓存)。
|
> 背景仓库:本仓库(main 分支);配套 skill:hulk-mcu-firmware(SKILL.md + tool/ 缓存)。
|
||||||
> 首个目标工程:CeramixNano-MB(GD32E230F8/64K,双固件 v5.1 布局,BL 侧 dev_fact@512bf7a)。
|
> 首个目标工程:CeramixNano-MB(GD32E230F8/64K,双固件 v5.1 布局,BL 侧 dev_fact@512bf7a)。
|
||||||
|
>
|
||||||
|
> **落地后注记(2026-10-22)**:内置 profile 已定名 `CeramixNano_MB_Dual_FW`
|
||||||
|
> (本文示例中的 `ceramixnano_mb_v51` 为规划期旧名,已废弃);打包产物在
|
||||||
|
> UPDATE/FULL 之外新增 FACTORY 工厂镜像(BL+B 满槽+flag、无 A),不再单独
|
||||||
|
> 落盘 B 包文件。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+150
-38
@@ -4,10 +4,11 @@
|
|||||||
fw_pack — GD32E230 双固件打包公共库 (CLI 与 FlashTool GUI 共用)
|
fw_pack — GD32E230 双固件打包公共库 (CLI 与 FlashTool GUI 共用)
|
||||||
================================================================
|
================================================================
|
||||||
输入编译好的 APP bin, 按 layout profile 补页 / 算 CRC / 写记录, 产出:
|
输入编译好的 APP bin, 按 layout profile 补页 / 算 CRC / 写记录, 产出:
|
||||||
*_UPDATE.bin A 槽 OTA/Ymodem 包 (整 N 页 + 末 4B trailer {pages16|crc16})
|
*_UPDATE.bin A 槽 OTA/Ymodem 包 (整 N 页 + 末 4B trailer {pages16|crc16})
|
||||||
*_B.bin B 槽底包 (固定满槽 + 槽末 8B 记录 {magic16|pages16|crc16|2B FF})
|
*_FULL.bin 整片镜像 (BL + A 包 + 槽尾锚点 + B 满槽 + 0xEEEE 标志)
|
||||||
*_FULL.bin 整片镜像 (BL + A 包 + 槽尾锚点 + B 满槽 + 0xEEEE 标志)
|
*_FACTORY.bin 工厂烧录镜像 (BL + B 满槽 + flag, 无 A 内容; 上电判定链
|
||||||
*.md5.txt 产物 MD5 清单 + 布局摘要
|
锚点擦除态 → A 向量非法 → 落 B 记录校验 → 跳出厂固件)
|
||||||
|
*.md5.txt 产物 MD5 清单 + 布局摘要
|
||||||
|
|
||||||
规则与 BL dev_fact 侧 (v5.1) 逐字一致:
|
规则与 BL dev_fact 侧 (v5.1) 逐字一致:
|
||||||
- CRC-16/XMODEM (poly 0x1021, init 0x0000, 不反转, 无 xorout),
|
- CRC-16/XMODEM (poly 0x1021, init 0x0000, 不反转, 无 xorout),
|
||||||
@@ -32,12 +33,12 @@ __all__ = [
|
|||||||
'FwPackError', 'crc16_xmodem', 'self_test', 'parse_num',
|
'FwPackError', 'crc16_xmodem', 'self_test', 'parse_num',
|
||||||
'BUILTIN_PROFILES', 'load_profile', 'Profile',
|
'BUILTIN_PROFILES', 'load_profile', 'Profile',
|
||||||
'detect_slot', 'read_vector', 'validate_app_bin',
|
'detect_slot', 'read_vector', 'validate_app_bin',
|
||||||
'build_a_pack', 'build_b_pack', 'build_full',
|
'build_a_pack', 'build_b_pack', 'build_full', 'build_factory',
|
||||||
'verify_a_blob', 'verify_b_blob', 'verify_full_blob',
|
'verify_a_blob', 'verify_b_blob', 'verify_full_blob', 'verify_factory_blob',
|
||||||
'pack_all', 'suggest_full_name',
|
'pack_all', 'suggest_full_name', 'suggest_factory_name',
|
||||||
]
|
]
|
||||||
|
|
||||||
TOOL_VERSION = '2.0'
|
TOOL_VERSION = '2.1'
|
||||||
|
|
||||||
# GD32E230 家族 SRAM: F8 最大 8K, SP 必须落在其中 (F4/F6 的 4K 也在范围内)
|
# GD32E230 家族 SRAM: F8 最大 8K, SP 必须落在其中 (F4/F6 的 4K 也在范围内)
|
||||||
SRAM_LO, SRAM_HI = 0x20000000, 0x20002000
|
SRAM_LO, SRAM_HI = 0x20000000, 0x20002000
|
||||||
@@ -196,9 +197,9 @@ class SlotSpec:
|
|||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
BUILTIN_PROFILES = {
|
BUILTIN_PROFILES = {
|
||||||
# CeramixNano-MB v5.1 双固件布局 (BL dev_fact@512bf7a)
|
# CeramixNano-MB v5.1 双固件 BL 的布局 (BL dev_fact@512bf7a)
|
||||||
'ceramixnano_mb_v51': {
|
'CeramixNano_MB_Dual_FW': {
|
||||||
'profile': 'ceramixnano_mb_v51',
|
'profile': 'CeramixNano_MB_Dual_FW',
|
||||||
'chip': 'GD32E230F8',
|
'chip': 'GD32E230F8',
|
||||||
'flash_base': '0x08000000',
|
'flash_base': '0x08000000',
|
||||||
'flash_size': '64K',
|
'flash_size': '64K',
|
||||||
@@ -378,9 +379,9 @@ class Profile:
|
|||||||
|
|
||||||
|
|
||||||
def load_profile(spec):
|
def load_profile(spec):
|
||||||
"""内置名(ceramixnano_mb_v51 / f8_single_slot) 或 profile JSON 文件路径"""
|
"""内置名(CeramixNano_MB_Dual_FW / f8_single_slot) 或 profile JSON 文件路径"""
|
||||||
if not spec:
|
if not spec:
|
||||||
spec = 'ceramixnano_mb_v51'
|
spec = 'CeramixNano_MB_Dual_FW'
|
||||||
if spec in BUILTIN_PROFILES:
|
if spec in BUILTIN_PROFILES:
|
||||||
return Profile(dict(BUILTIN_PROFILES[spec]))
|
return Profile(dict(BUILTIN_PROFILES[spec]))
|
||||||
if os.path.isfile(spec):
|
if os.path.isfile(spec):
|
||||||
@@ -551,6 +552,28 @@ def build_full(profile, bl_data, a_pack, b_pack=None):
|
|||||||
return bytes(image)
|
return bytes(image)
|
||||||
|
|
||||||
|
|
||||||
|
def build_factory(profile, bl_data, b_pack):
|
||||||
|
"""工厂烧录镜像: BL + B 满槽(含记录) + flag, **无 A 包、无锚点**。
|
||||||
|
|
||||||
|
上电判定链(v5.1 BL): A 锚点为擦除态(0xFFFF...) → legacy flag + A 向量校验
|
||||||
|
(A 槽全 0xFF → 向量非法) → 落 B 记录 {magic|pages|crc} 校验 → 跳出厂固件;
|
||||||
|
后续 OTA 写入 A 槽后即切运行固件。
|
||||||
|
"""
|
||||||
|
if b_pack is None:
|
||||||
|
raise FwPackError('FACTORY 镜像需要 B 底包')
|
||||||
|
image = bytearray(b'\xFF' * profile.flash_size)
|
||||||
|
if bl_data:
|
||||||
|
if len(bl_data) > profile.bl_size:
|
||||||
|
raise FwPackError(f'BL {len(bl_data)}B 超过 BL 区 {profile.bl_size}B')
|
||||||
|
image[0:len(bl_data)] = bl_data
|
||||||
|
sb = profile.slot_b
|
||||||
|
off = sb.addr - profile.flash_base
|
||||||
|
image[off:off + sb.size] = b_pack.data
|
||||||
|
struct.pack_into('<I', image, profile.flag_addr - profile.flash_base,
|
||||||
|
profile.flag_value)
|
||||||
|
return bytes(image)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# 回读复核 (从产物字节独立重算, 与构建结果交叉验证)
|
# 回读复核 (从产物字节独立重算, 与构建结果交叉验证)
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
@@ -677,6 +700,60 @@ def verify_full_blob(blob, profile, bl_data, a_body, b_body=None):
|
|||||||
return checks
|
return checks
|
||||||
|
|
||||||
|
|
||||||
|
def verify_factory_blob(blob, profile, bl_data, b_body):
|
||||||
|
"""FACTORY 工厂镜像逐项字节校验: BL/B 记录/flag 在位, A 槽与锚点保持擦除态。
|
||||||
|
返回 [(名称, 通过?, 说明)] 列表。"""
|
||||||
|
base = profile.flash_base
|
||||||
|
sa, sb = profile.slot_a, profile.slot_b
|
||||||
|
pg = profile.page_size
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
def chk(name, ok, note=''):
|
||||||
|
checks.append((name, bool(ok), note))
|
||||||
|
|
||||||
|
chk('总长度=flash_size', len(blob) == profile.flash_size,
|
||||||
|
f'{len(blob)}/{profile.flash_size}')
|
||||||
|
if bl_data:
|
||||||
|
chk(f'BL 内容==输入({len(bl_data)}B)', blob[:len(bl_data)] == bl_data)
|
||||||
|
chk(f'BL 余量 0xFF({profile.bl_size - len(bl_data)}B)',
|
||||||
|
all(b == 0xFF for b in blob[len(bl_data):profile.bl_size]))
|
||||||
|
else:
|
||||||
|
chk('BL 区全 0xFF(未提供 BL)', all(b == 0xFF for b in blob[:profile.bl_size]))
|
||||||
|
# A 槽必须整体擦除态: 无 A 包、无锚点 (判定链靠"锚点擦除+A 向量非法"落 B)
|
||||||
|
chk(f'A 槽全 0xFF·锚点擦除态({sa.size}B)',
|
||||||
|
all(b == 0xFF for b in blob[sa.addr - base:sa.end - base]))
|
||||||
|
# B 槽
|
||||||
|
b_off = sb.addr - base
|
||||||
|
rb = sb.record_area_len(pg)
|
||||||
|
body_n = min(len(b_body), sb.size - rb)
|
||||||
|
chk(f'B 体前缀==输入固件({body_n}B)',
|
||||||
|
blob[b_off:b_off + body_n] == b_body[:body_n])
|
||||||
|
chk(f'B 补页 0xFF({sb.size - rb - body_n}B)',
|
||||||
|
all(b == 0xFF for b in blob[b_off + body_n:sb.end - rb - base]))
|
||||||
|
rec_off = sb.end - rb - base
|
||||||
|
_bo = {f.name: f.offset for f in sb.record.fields}
|
||||||
|
m_magic = struct.unpack_from('<H', blob, rec_off + _bo['magic'])[0]
|
||||||
|
m_pages = struct.unpack_from('<H', blob, rec_off + _bo['pages'])[0]
|
||||||
|
m_crc = struct.unpack_from('<H', blob, rec_off + _bo['crc'])[0]
|
||||||
|
chk(f'B 记录 magic==0x{m_magic:04X} 存在', m_magic != 0xFFFF)
|
||||||
|
chk(f'B 记录 页数=={sb.size // pg}', m_pages == sb.size // pg,
|
||||||
|
f'实为 {m_pages}')
|
||||||
|
bcrc = crc16_xmodem(blob[b_off:sb.end - rb - base])
|
||||||
|
chk(f'B 记录 CRC==镜像重算(0x{bcrc:04X})', m_crc == bcrc, f'记录 0x{m_crc:04X}')
|
||||||
|
chk(f'B 记录区尾部保留 0xFF({rb - sb.record.length}B)',
|
||||||
|
blob[rec_off + sb.record.length:rec_off + rb]
|
||||||
|
== b'\xFF' * (rb - sb.record.length))
|
||||||
|
# flag
|
||||||
|
chk(f'flag 0x{profile.flag_value:04X} @0x{profile.flag_addr:08X}',
|
||||||
|
struct.unpack_from('<I', blob, profile.flag_addr - base)[0] == profile.flag_value)
|
||||||
|
fp = profile.flag_page - base
|
||||||
|
chk('标志页其余字节 0xFF',
|
||||||
|
blob[fp:profile.flag_addr - base] == b'\xFF' * (profile.flag_addr - base - fp)
|
||||||
|
and blob[profile.flag_addr - base + 4:] == b'\xFF' * (base + profile.flash_size
|
||||||
|
- profile.flag_addr - 4))
|
||||||
|
return checks
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# 产物命名 / 一站式打包 (CLI 与 GUI 共用入口)
|
# 产物命名 / 一站式打包 (CLI 与 GUI 共用入口)
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
@@ -698,24 +775,37 @@ def suggest_full_name(a_path, b_path=None):
|
|||||||
return a_base + '_FULL' + '.bin'
|
return a_base + '_FULL' + '.bin'
|
||||||
|
|
||||||
|
|
||||||
|
def suggest_factory_name(b_path):
|
||||||
|
"""从 B 包名推导 FACTORY 镜像名: 项目前缀 + B 版本号 (+日期后缀)。"""
|
||||||
|
b_base = os.path.splitext(os.path.basename(b_path))[0]
|
||||||
|
date = _DATE_RE.search(b_base)
|
||||||
|
m = _VER_RE.match(b_base)
|
||||||
|
if m:
|
||||||
|
return (f'{m.group("proj")}_FACTORY_B-V{m.group("ver")}'
|
||||||
|
+ (f'_{date.group(1)}' if date else '') + '.bin')
|
||||||
|
return b_base + '_FACTORY.bin'
|
||||||
|
|
||||||
|
|
||||||
class PackOutcome:
|
class PackOutcome:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.paths = {} # update/b/full/md5 -> 文件路径
|
self.paths = {} # update/full/factory/md5 -> 文件路径
|
||||||
self.summary = [] # 展示用文本行
|
self.summary = [] # 展示用文本行
|
||||||
self.checks = [] # FULL 逐项校验 (名称, ok, note)
|
self.checks = [] # FULL 逐项校验 (名称, ok, note)
|
||||||
|
self.factory_checks = [] # FACTORY 逐项校验
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ok(self):
|
def ok(self):
|
||||||
return all(ok for _n, ok, _t in self.checks) if self.checks else True
|
pools = self.checks + self.factory_checks
|
||||||
|
return all(ok for _n, ok, _t in pools) if pools else True
|
||||||
|
|
||||||
|
|
||||||
def pack_all(profile_spec, a_path, b_path=None, bl_path=None, out_dir=None,
|
def pack_all(profile_spec, a_path, b_path=None, bl_path=None, out_dir=None,
|
||||||
make_full=True, progress=print):
|
make_full=True, make_factory=True, progress=print):
|
||||||
"""一站式打包: 自动识别/校验 → A包/B包/FULL → 写盘 → 回读复核 → MD5 清单。
|
"""一站式打包: 自动识别/校验 → A包/FULL/FACTORY → 写盘 → 回读复核 → MD5 清单。
|
||||||
|
|
||||||
a_path 可为 None (仅 B 底包 → 只出 *_B.bin, 不拼 FULL —— 没有 A 运行槽的
|
a_path 可为 None (仅 B 底包 → 只出 *_FACTORY.bin 工厂镜像, 不拼 FULL ——
|
||||||
整片镜像无法启动, 不允许生成)。progress: 每行输出的回调(CLI 用 print,
|
没有 A 运行槽的整片镜像无法启动, 不允许生成)。progress: 每行输出的回调
|
||||||
GUI 转日志)。返回 PackOutcome; 任何硬错误抛 FwPackError。
|
(CLI 用 print, GUI 转日志)。返回 PackOutcome; 任何硬错误抛 FwPackError。
|
||||||
"""
|
"""
|
||||||
self_test()
|
self_test()
|
||||||
profile = load_profile(profile_spec)
|
profile = load_profile(profile_spec)
|
||||||
@@ -764,7 +854,6 @@ def pack_all(profile_spec, a_path, b_path=None, bl_path=None, out_dir=None,
|
|||||||
out_dir = out_dir or os.path.dirname(os.path.abspath(a_path or b_path))
|
out_dir = out_dir or os.path.dirname(os.path.abspath(a_path or b_path))
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
a_base = os.path.splitext(os.path.basename(a_path))[0] if a_path else None
|
a_base = os.path.splitext(os.path.basename(a_path))[0] if a_path else None
|
||||||
b_base = os.path.splitext(os.path.basename(b_path))[0] if b_path else None
|
|
||||||
|
|
||||||
# -- A 包 --
|
# -- A 包 --
|
||||||
ap = None
|
ap = None
|
||||||
@@ -781,21 +870,16 @@ def pack_all(profile_spec, a_path, b_path=None, bl_path=None, out_dir=None,
|
|||||||
f'trailer=0x{ap.trailer_value:08X} (CRC 0x{ap.crc:04X}) — 回读复核通过')
|
f'trailer=0x{ap.trailer_value:08X} (CRC 0x{ap.crc:04X}) — 回读复核通过')
|
||||||
say(f' {upd_path}')
|
say(f' {upd_path}')
|
||||||
|
|
||||||
# -- B 包 --
|
# -- B 包 (仅作为镜像组成, 不再单独落盘) --
|
||||||
bp = None
|
bp = None
|
||||||
if b_body is not None:
|
if b_body is not None:
|
||||||
bp = build_b_pack(b_body, profile)
|
bp = build_b_pack(b_body, profile)
|
||||||
b_out = os.path.join(out_dir, b_base + '_B.bin')
|
verify_b_blob(bp.data, profile)
|
||||||
with open(b_out, 'wb') as f:
|
|
||||||
f.write(bp.data)
|
|
||||||
out.paths['b'] = b_out
|
|
||||||
verify_b_blob(open(b_out, 'rb').read(), profile)
|
|
||||||
area = profile.slot_b.record_area_len(profile.page_size)
|
area = profile.slot_b.record_area_len(profile.page_size)
|
||||||
rec_disp = (bp.record_bytes + b'\xFF' * (area - len(bp.record_bytes))) \
|
rec_disp = (bp.record_bytes + b'\xFF' * (area - len(bp.record_bytes))) \
|
||||||
.hex(' ').upper()
|
.hex(' ').upper()
|
||||||
say(f'B 包: 满槽 {bp.pages} 页({len(bp.data)}B) 记录 {rec_disp} '
|
say(f'B 包: 满槽 {bp.pages} 页({len(bp.data)}B) 记录 {rec_disp} '
|
||||||
f'(CRC 0x{bp.crc:04X}) — 回读复核通过')
|
f'(CRC 0x{bp.crc:04X}) — 复核通过')
|
||||||
say(f' {b_out}')
|
|
||||||
elif profile.slot_b and make_full and a_body is not None:
|
elif profile.slot_b and make_full and a_body is not None:
|
||||||
say('提示: 未提供 B 底包, FULL 镜像中 B 槽保持 0xFF (擦除态)')
|
say('提示: 未提供 B 底包, FULL 镜像中 B 槽保持 0xFF (擦除态)')
|
||||||
|
|
||||||
@@ -816,27 +900,55 @@ def pack_all(profile_spec, a_path, b_path=None, bl_path=None, out_dir=None,
|
|||||||
bad = [n for n, ok, _t in out.checks if not ok]
|
bad = [n for n, ok, _t in out.checks if not ok]
|
||||||
if bad:
|
if bad:
|
||||||
raise FwPackError('FULL 镜像复核未通过: ' + '; '.join(bad))
|
raise FwPackError('FULL 镜像复核未通过: ' + '; '.join(bad))
|
||||||
n_ok = len(out.checks)
|
say(f'FULL: {len(image)}B — {len(out.checks)} 项字节校验全部通过')
|
||||||
say(f'FULL: {len(image)}B — {n_ok} 项字节校验全部通过')
|
|
||||||
for n, ok, note in out.checks:
|
for n, ok, note in out.checks:
|
||||||
say(f' [{"PASS" if ok else "FAIL"}] {n}' + (f' ({note})' if note else ''))
|
say(f' [{"PASS" if ok else "FAIL"}] {n}' + (f' ({note})' if note else ''))
|
||||||
say(f' {full_path}')
|
say(f' {full_path}')
|
||||||
|
|
||||||
# MD5 清单
|
# -- FACTORY (工厂烧录镜像: BL + B 满槽 + flag, 无 A; 需 B 底包) --
|
||||||
md5_path = full_path + '.md5.txt'
|
if make_factory and b_body is not None:
|
||||||
products = [(k, p) for k, p in out.paths.items()]
|
if bl_data is None:
|
||||||
|
say('警告: 未提供 BL bin, FACTORY 镜像 BL 区为 0xFF — 工厂烧录镜像'
|
||||||
|
' 必须带 BL, 请用 -b 提供!')
|
||||||
|
fac_name = suggest_factory_name(b_path)
|
||||||
|
fac_path = os.path.join(out_dir, fac_name)
|
||||||
|
fac_image = build_factory(profile, bl_data, bp)
|
||||||
|
with open(fac_path, 'wb') as f:
|
||||||
|
f.write(fac_image)
|
||||||
|
out.paths['factory'] = fac_path
|
||||||
|
fac_blob = open(fac_path, 'rb').read()
|
||||||
|
out.factory_checks = verify_factory_blob(fac_blob, profile, bl_data, b_body)
|
||||||
|
bad = [n for n, ok, _t in out.factory_checks if not ok]
|
||||||
|
if bad:
|
||||||
|
raise FwPackError('FACTORY 镜像复核未通过: ' + '; '.join(bad))
|
||||||
|
say(f'FACTORY: {len(fac_image)}B — {len(out.factory_checks)} 项字节校验'
|
||||||
|
f'全部通过 (A 槽保持擦除态, 上电跳 B)')
|
||||||
|
for n, ok, note in out.factory_checks:
|
||||||
|
say(f' [{"PASS" if ok else "FAIL"}] {n}' + (f' ({note})' if note else ''))
|
||||||
|
say(f' {fac_path}')
|
||||||
|
|
||||||
|
# -- MD5 清单 (挂在 FULL, 无 FULL 则挂在 FACTORY) --
|
||||||
|
anchor_path = out.paths.get('full') or out.paths.get('factory')
|
||||||
|
if anchor_path:
|
||||||
|
md5_path = anchor_path + '.md5.txt'
|
||||||
with open(md5_path, 'w', encoding='utf-8', newline='\n') as f:
|
with open(md5_path, 'w', encoding='utf-8', newline='\n') as f:
|
||||||
f.write(f'# 布局摘要 — gd32e230_firmware_merge_tool v{TOOL_VERSION}\n')
|
f.write(f'# 布局摘要 — gd32e230_firmware_merge_tool v{TOOL_VERSION}\n')
|
||||||
f.write('# ' + profile.describe().replace('\n', '\n# ') + '\n')
|
f.write('# ' + profile.describe().replace('\n', '\n# ') + '\n')
|
||||||
f.write(f'# 生成: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}\n')
|
f.write(f'# 生成: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}\n')
|
||||||
f.write('# A 包: %s -> %d 页, trailer=0x%08X (CRC 0x%04X)\n'
|
if ap:
|
||||||
% (os.path.basename(a_path), ap.pages, ap.trailer_value, ap.crc))
|
f.write('# A 包: %s -> %d 页, trailer=0x%08X (CRC 0x%04X)\n'
|
||||||
|
% (os.path.basename(a_path), ap.pages,
|
||||||
|
ap.trailer_value, ap.crc))
|
||||||
if bp:
|
if bp:
|
||||||
f.write('# B 包: %s -> 满槽 %d 页, 记录 %s (CRC 0x%04X)\n'
|
f.write('# B 包: %s -> 满槽 %d 页, 记录 %s (CRC 0x%04X)\n'
|
||||||
% (os.path.basename(b_path), bp.pages,
|
% (os.path.basename(b_path), bp.pages,
|
||||||
bp.record_bytes.hex(' ').upper(), bp.crc))
|
(bp.record_bytes + b'\xFF'
|
||||||
|
* (profile.slot_b.record_area_len(profile.page_size)
|
||||||
|
- len(bp.record_bytes))).hex(' ').upper(), bp.crc))
|
||||||
f.write('#\n')
|
f.write('#\n')
|
||||||
for _k, p in products:
|
for _k, p in out.paths.items():
|
||||||
|
if _k == 'md5':
|
||||||
|
continue
|
||||||
data = open(p, 'rb').read()
|
data = open(p, 'rb').read()
|
||||||
f.write('%s %8dB %s\n' % (hashlib.md5(data).hexdigest(),
|
f.write('%s %8dB %s\n' % (hashlib.md5(data).hexdigest(),
|
||||||
len(data), os.path.basename(p)))
|
len(data), os.path.basename(p)))
|
||||||
|
|||||||
+23
-17
@@ -4,10 +4,10 @@ GD32E230 固件打包工具 v2 — 布局 profile 化双固件打包 (A/B 槽 +
|
|||||||
================================================================
|
================================================================
|
||||||
|
|
||||||
v2 模式(默认): 输入编译好的 APP bin, 自动识别 A/B 槽, 产出
|
v2 模式(默认): 输入编译好的 APP bin, 自动识别 A/B 槽, 产出
|
||||||
*_UPDATE.bin A 槽 OTA/Ymodem 包 (整 N 页 + 末 4B trailer {页数16|CRC16})
|
*_UPDATE.bin A 槽 OTA/Ymodem 包 (整 N 页 + 末 4B trailer {页数16|CRC16})
|
||||||
*_B.bin B 槽底包 (固定满槽 + 槽末 8B 记录 {magic|页数|CRC|2B FF})
|
*_FULL.bin 整片镜像 (BL + A 包 + 槽尾锚点 + B 满槽 + 0xEEEE 标志)
|
||||||
*_FULL.bin 整片镜像 (BL + A 包 + 槽尾锚点 + B 满槽 + 0xEEEE 标志)
|
*_FACTORY.bin 工厂烧录镜像 (BL + B 满槽 + flag, 无 A 内容; 上电跳 B)
|
||||||
*.md5.txt 产物 MD5 清单 + 布局摘要 (随 FULL 生成)
|
*.md5.txt 产物 MD5 清单 + 布局摘要 (随主镜像生成)
|
||||||
打包规则与 BL (dev_fact v5.1) 逐字一致, 写盘后回读复核。计算与校验在
|
打包规则与 BL (dev_fact v5.1) 逐字一致, 写盘后回读复核。计算与校验在
|
||||||
fw_pack.py 公共库 (FlashTool GUI 共用)。
|
fw_pack.py 公共库 (FlashTool GUI 共用)。
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ legacy 模式(过渡期兼容, -f/-m 二选一时启用): 旧单槽方案
|
|||||||
-f/--fill 纯 0xFF 补页, 无 trailer (旧 BL 的 Ymodem 升级包)
|
-f/--fill 纯 0xFF 补页, 无 trailer (旧 BL 的 Ymodem 升级包)
|
||||||
-m/--merge BL + APP + 0xEEEE 标志 拼整片镜像 (不补齐)
|
-m/--merge BL + APP + 0xEEEE 标志 拼整片镜像 (不补齐)
|
||||||
|
|
||||||
v2 Flash 布局(默认 profile ceramixnano_mb_v51, GD32E230F8/64K):
|
v2 Flash 布局(默认 profile CeramixNano_MB_Dual_FW, GD32E230F8/64K):
|
||||||
0x08000000 Bootloader 8K 页 0~7
|
0x08000000 Bootloader 8K 页 0~7
|
||||||
0x08002000 FW_A 运行槽 36K 页 8~43 OTA 唯一接收区, 槽尾锚点@0x0800AFFC
|
0x08002000 FW_A 运行槽 36K 页 8~43 OTA 唯一接收区, 槽尾锚点@0x0800AFFC
|
||||||
0x0800B000 FW_B 保底槽 19K 页 44~62 固定满槽, B 记录@0x0800FBF8
|
0x0800B000 FW_B 保底槽 19K 页 44~62 固定满槽, B 记录@0x0800FBF8
|
||||||
@@ -33,7 +33,10 @@ import fw_pack
|
|||||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, SCRIPT_DIR)
|
sys.path.insert(0, SCRIPT_DIR)
|
||||||
|
|
||||||
TOOL_VERSION = '2.0.0'
|
TOOL_VERSION = '2.1.0'
|
||||||
|
|
||||||
|
# 默认布局 profile (与 fw_pack.BUILTIN_PROFILES 键一致)
|
||||||
|
DEFAULT_PROFILE = 'CeramixNano_MB_Dual_FW'
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
@@ -96,7 +99,8 @@ def run_v2(args):
|
|||||||
return 2
|
return 2
|
||||||
fw_pack.pack_all(
|
fw_pack.pack_all(
|
||||||
args.profile, a_path, b_path=b_path, bl_path=args.bootloader,
|
args.profile, a_path, b_path=b_path, bl_path=args.bootloader,
|
||||||
out_dir=out_dir, make_full=not args.no_full)
|
out_dir=out_dir, make_full=not args.no_full,
|
||||||
|
make_factory=not args.no_factory)
|
||||||
except fw_pack.FwPackError as exc:
|
except fw_pack.FwPackError as exc:
|
||||||
print(f'\n错误:{exc}', file=sys.stderr)
|
print(f'\n错误:{exc}', file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -305,11 +309,11 @@ def main():
|
|||||||
epilog=f"""
|
epilog=f"""
|
||||||
v2 双固件模式(默认, 规格: UPGRADE_PLAN_v2.md):
|
v2 双固件模式(默认, 规格: UPGRADE_PLAN_v2.md):
|
||||||
python %(prog)s app_a.bin # 识别 A 包 → *_UPDATE.bin (+FULL)
|
python %(prog)s app_a.bin # 识别 A 包 → *_UPDATE.bin (+FULL)
|
||||||
python %(prog)s app_a.bin app_b.bin -b bl.bin # 全产物: UPDATE/B/FULL/md5
|
python %(prog)s app_a.bin app_b.bin -b bl.bin # 全产物: UPDATE/FULL/FACTORY/md5
|
||||||
|
python %(prog)s app_a.bin --no-full # 仅升级包
|
||||||
|
python %(prog)s app_b.bin -b bl.bin # 仅工厂镜像 → *_FACTORY.bin
|
||||||
python %(prog)s --a a.bin --b b.bin # 显式指定槽位(与向量表冲突报错)
|
python %(prog)s --a a.bin --b b.bin # 显式指定槽位(与向量表冲突报错)
|
||||||
python %(prog)s a.bin --profile f8_single_slot # 换布局(无保底槽)
|
python %(prog)s a.bin --profile my_layout.json # 自定义布局 profile JSON
|
||||||
python %(prog)s a.bin --profile my_layout.json # 自定义 profile JSON
|
|
||||||
python %(prog)s b.bin # 仅 B 底包 → *_B.bin
|
|
||||||
|
|
||||||
legacy 模式(旧单槽方案, -f/-m 触发):
|
legacy 模式(旧单槽方案, -f/-m 触发):
|
||||||
python %(prog)s -f app.bin # 纯 0xFF 补齐升级包(无 trailer)
|
python %(prog)s -f app.bin # 纯 0xFF 补齐升级包(无 trailer)
|
||||||
@@ -328,7 +332,7 @@ legacy 模式(旧单槽方案, -f/-m 触发):
|
|||||||
help="legacy: 仅补齐 APP 到整页边界 (纯 0xFF, 无 trailer)")
|
help="legacy: 仅补齐 APP 到整页边界 (纯 0xFF, 无 trailer)")
|
||||||
parser.add_argument("--legacy", action="store_true",
|
parser.add_argument("--legacy", action="store_true",
|
||||||
help="显式声明 legacy 模式(需配合 -f/-m)")
|
help="显式声明 legacy 模式(需配合 -f/-m)")
|
||||||
parser.add_argument("--profile", default="ceramixnano_mb_v51",
|
parser.add_argument("--profile", default=DEFAULT_PROFILE,
|
||||||
help="v2 布局 profile: 内置名(%s)或 JSON 文件路径"
|
help="v2 布局 profile: 内置名(%s)或 JSON 文件路径"
|
||||||
% "/".join(fw_pack.BUILTIN_PROFILES))
|
% "/".join(fw_pack.BUILTIN_PROFILES))
|
||||||
parser.add_argument("--a", metavar="BIN", default=None,
|
parser.add_argument("--a", metavar="BIN", default=None,
|
||||||
@@ -340,7 +344,9 @@ legacy 模式(旧单槽方案, -f/-m 触发):
|
|||||||
parser.add_argument("-o", "--output", default=None,
|
parser.add_argument("-o", "--output", default=None,
|
||||||
help="输出路径: v2=输出目录(自动命名); legacy=输出文件")
|
help="输出路径: v2=输出目录(自动命名); legacy=输出文件")
|
||||||
parser.add_argument("--no-full", action="store_true",
|
parser.add_argument("--no-full", action="store_true",
|
||||||
help="v2: 只出 UPDATE/B 包, 不拼 FULL")
|
help="v2: 不拼 FULL 整片镜像")
|
||||||
|
parser.add_argument("--no-factory", action="store_true",
|
||||||
|
help="v2: 不出 FACTORY 工厂镜像")
|
||||||
parser.add_argument("-s", "--flash-size", default=DEFAULT_CHIP,
|
parser.add_argument("-s", "--flash-size", default=DEFAULT_CHIP,
|
||||||
choices=list(CHIP_CONFIG.keys()),
|
choices=list(CHIP_CONFIG.keys()),
|
||||||
help="legacy 芯片型号: F4(16K) F6(32K) F8(64K)")
|
help="legacy 芯片型号: F4(16K) F6(32K) F8(64K)")
|
||||||
@@ -353,10 +359,10 @@ legacy 模式(旧单槽方案, -f/-m 触发):
|
|||||||
legacy = args.fill or args.merge
|
legacy = args.fill or args.merge
|
||||||
if args.legacy and not legacy:
|
if args.legacy and not legacy:
|
||||||
parser.error("--legacy 需配合 -f/--fill 或 -m/--merge 使用")
|
parser.error("--legacy 需配合 -f/--fill 或 -m/--merge 使用")
|
||||||
if legacy and (args.profile != "ceramixnano_mb_v51" or args.a or args.b
|
if legacy and (args.profile != DEFAULT_PROFILE or args.a or args.b
|
||||||
or args.no_full or args.app_addr):
|
or args.no_full or args.no_factory or args.app_addr):
|
||||||
parser.error("--profile/--a/--b/--no-full/-a 是 v2/legacy 专属参数, "
|
parser.error("--profile/--a/--b/--no-full/--no-factory/-a 是 v2/legacy "
|
||||||
"不能与 -f/-m 同用")
|
"专属参数, 不能与 -f/-m 同用")
|
||||||
if legacy and len(args.bins) != 1:
|
if legacy and len(args.bins) != 1:
|
||||||
parser.error("legacy 模式需要一个 APP 位置参数, 例: python %(prog)s -f app.bin"
|
parser.error("legacy 模式需要一个 APP 位置参数, 例: python %(prog)s -f app.bin"
|
||||||
% {'prog': parser.prog})
|
% {'prog': parser.prog})
|
||||||
|
|||||||
+100
-20
@@ -10,9 +10,10 @@ fw_pack / gd32e230_firmware_tool v2 回归测试套件
|
|||||||
4. A 包整页扩展规则(恰整页 / 余量<4 / 余量==4 / 超槽);
|
4. A 包整页扩展规则(恰整页 / 余量<4 / 余量==4 / 超槽);
|
||||||
5. B 包固定满槽 + 记录(满槽输入 / 小输入 / 超槽-8);
|
5. B 包固定满槽 + 记录(满槽输入 / 小输入 / 超槽-8);
|
||||||
6. FULL 拼装 + 逐项字节校验 + 回读复核(含篡改检测);
|
6. FULL 拼装 + 逐项字节校验 + 回读复核(含篡改检测);
|
||||||
7. 真机固件回归(CeramixNano 1.0.17/1.0.18/V1.0.12, 字节级复现
|
7. FACTORY 工厂镜像(BL+B+flag 无 A, 锚点擦除态) + 篡改检测;
|
||||||
|
8. 真机固件回归(CeramixNano 1.0.17/1.0.18/V1.0.12, 字节级复现
|
||||||
2026-10-22 验收包, 文件缺失时自动跳过);
|
2026-10-22 验收包, 文件缺失时自动跳过);
|
||||||
8. CLI 冒烟(v2 / legacy / 错误路径)。
|
9. CLI 冒烟(v2 / legacy / 错误路径)。
|
||||||
|
|
||||||
运行: python test_fw_pack.py [-v]
|
运行: python test_fw_pack.py [-v]
|
||||||
全部通过 = v2 升级验收通过。
|
全部通过 = v2 升级验收通过。
|
||||||
@@ -89,11 +90,11 @@ def synth_bin(base, size, rst_off=0x101):
|
|||||||
|
|
||||||
|
|
||||||
def ceramix_profile():
|
def ceramix_profile():
|
||||||
return fw_pack.load_profile('ceramixnano_mb_v51')
|
return fw_pack.load_profile('CeramixNano_MB_Dual_FW')
|
||||||
|
|
||||||
|
|
||||||
def variant_profile(slot_a_k, slot_b_k, with_b=True):
|
def variant_profile(slot_a_k, slot_b_k, with_b=True):
|
||||||
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['ceramixnano_mb_v51']))
|
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['CeramixNano_MB_Dual_FW']))
|
||||||
raw['slot_a']['size'] = f'{slot_a_k}K'
|
raw['slot_a']['size'] = f'{slot_a_k}K'
|
||||||
if with_b:
|
if with_b:
|
||||||
raw['slot_b']['size'] = f'{slot_b_k}K'
|
raw['slot_b']['size'] = f'{slot_b_k}K'
|
||||||
@@ -183,7 +184,7 @@ class TestProfile(unittest.TestCase):
|
|||||||
fw_pack.parse_num('')
|
fw_pack.parse_num('')
|
||||||
|
|
||||||
def _invalid(self, mutate, needle):
|
def _invalid(self, mutate, needle):
|
||||||
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['ceramixnano_mb_v51']))
|
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['CeramixNano_MB_Dual_FW']))
|
||||||
mutate(raw)
|
mutate(raw)
|
||||||
with self.assertRaises(fw_pack.FwPackError) as cm:
|
with self.assertRaises(fw_pack.FwPackError) as cm:
|
||||||
fw_pack.Profile(raw)
|
fw_pack.Profile(raw)
|
||||||
@@ -208,9 +209,9 @@ class TestProfile(unittest.TestCase):
|
|||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
path = os.path.join(td, 'p.json')
|
path = os.path.join(td, 'p.json')
|
||||||
with open(path, 'w', encoding='utf-8') as f:
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(fw_pack.BUILTIN_PROFILES['ceramixnano_mb_v51'], f)
|
json.dump(fw_pack.BUILTIN_PROFILES['CeramixNano_MB_Dual_FW'], f)
|
||||||
p = fw_pack.load_profile(path)
|
p = fw_pack.load_profile(path)
|
||||||
self.assertEqual(p.name, 'ceramixnano_mb_v51')
|
self.assertEqual(p.name, 'CeramixNano_MB_Dual_FW')
|
||||||
bad = os.path.join(td, 'bad.json')
|
bad = os.path.join(td, 'bad.json')
|
||||||
with open(bad, 'w', encoding='utf-8') as f:
|
with open(bad, 'w', encoding='utf-8') as f:
|
||||||
f.write('{ not json')
|
f.write('{ not json')
|
||||||
@@ -441,7 +442,60 @@ class TestFull(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
# 7. 真机回归: CeramixNano v5.1 (字节级复现验收包)
|
# 7. FACTORY 工厂镜像 (BL + B 满槽 + flag, 无 A)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class TestFactory(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.p = ceramix_profile()
|
||||||
|
self.bl = synth_bin(BL_BASE, 4044, rst_off=0xD00)
|
||||||
|
self.b_body = synth_bin(B_BASE, SLOT_B_SIZE - 8)
|
||||||
|
self.bp = fw_pack.build_b_pack(self.b_body, self.p)
|
||||||
|
|
||||||
|
def test_factory_layout_bytes(self):
|
||||||
|
"""BL + B 满槽 + flag; A 槽(含锚点)整体擦除态 → 上电判定链落 B"""
|
||||||
|
img = fw_pack.build_factory(self.p, self.bl, self.bp)
|
||||||
|
self.assertEqual(len(img), 64 * 1024)
|
||||||
|
self.assertEqual(img[:4044], self.bl)
|
||||||
|
self.assertTrue(all(b == 0xFF for b in img[4044:0xB000])) # BL 余量 + 整个 A 槽
|
||||||
|
self.assertEqual(img[0xAFFC:0xB000], b'\xff' * 4) # 锚点擦除态
|
||||||
|
self.assertEqual(img[0xB000:0xFC00], self.bp.data)
|
||||||
|
self.assertEqual(img[0xFFFC:0x10000], b'\xee\xee\x00\x00')
|
||||||
|
|
||||||
|
def test_verify_factory_all_pass(self):
|
||||||
|
img = fw_pack.build_factory(self.p, self.bl, self.bp)
|
||||||
|
checks = fw_pack.verify_factory_blob(img, self.p, self.bl, self.b_body)
|
||||||
|
bad = [(n, t) for n, ok, t in checks if not ok]
|
||||||
|
self.assertEqual(bad, [], checks)
|
||||||
|
self.assertGreaterEqual(len(checks), 10)
|
||||||
|
|
||||||
|
def test_verify_factory_detects_corruption(self):
|
||||||
|
img = bytearray(fw_pack.build_factory(self.p, self.bl, self.bp))
|
||||||
|
img[0xB000 + 100] ^= 0xFF # 破坏 B 体
|
||||||
|
checks = fw_pack.verify_factory_blob(bytes(img), self.p, self.bl,
|
||||||
|
self.b_body)
|
||||||
|
self.assertFalse(all(ok for _n, ok, _t in checks))
|
||||||
|
img2 = bytearray(fw_pack.build_factory(self.p, self.bl, self.bp))
|
||||||
|
img2[0xAFFC] = 0x14 # A 槽出现锚点痕迹
|
||||||
|
checks = fw_pack.verify_factory_blob(bytes(img2), self.p, self.bl,
|
||||||
|
self.b_body)
|
||||||
|
self.assertFalse(all(ok for _n, ok, _t in checks))
|
||||||
|
|
||||||
|
def test_requires_b_pack(self):
|
||||||
|
with self.assertRaises(fw_pack.FwPackError):
|
||||||
|
fw_pack.build_factory(self.p, self.bl, None)
|
||||||
|
|
||||||
|
def test_suggest_factory_name(self):
|
||||||
|
self.assertEqual(
|
||||||
|
fw_pack.suggest_factory_name(
|
||||||
|
r'x\CeramixNano-MB_V1.0.12_APP_2026-10-20.bin'),
|
||||||
|
'CeramixNano-MB_FACTORY_B-V1.0.12_2026-10-20.bin')
|
||||||
|
self.assertEqual(fw_pack.suggest_factory_name(r'x\app_b.bin'),
|
||||||
|
'app_b_FACTORY.bin')
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 8. 真机回归: CeramixNano v5.1 (字节级复现验收包)
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
@unittest.skipUnless(HAVE_REAL, '需要 CeramixNano 固件(见 test_fw_pack.py 头部路径)')
|
@unittest.skipUnless(HAVE_REAL, '需要 CeramixNano 固件(见 test_fw_pack.py 头部路径)')
|
||||||
@@ -499,25 +553,31 @@ class TestRegressionCeramixNano(unittest.TestCase):
|
|||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
bl_path = os.path.join(td, 'bl.bin')
|
bl_path = os.path.join(td, 'bl.bin')
|
||||||
open(bl_path, 'wb').write(self.bl)
|
open(bl_path, 'wb').write(self.bl)
|
||||||
out = fw_pack.pack_all('ceramixnano_mb_v51', A17, b_path=B12,
|
out = fw_pack.pack_all('CeramixNano_MB_Dual_FW', A17, b_path=B12,
|
||||||
bl_path=bl_path, out_dir=td, progress=quiet())
|
bl_path=bl_path, out_dir=td, progress=quiet())
|
||||||
upd = open(out.paths['update'], 'rb').read()
|
upd = open(out.paths['update'], 'rb').read()
|
||||||
bpk = open(out.paths['b'], 'rb').read()
|
|
||||||
full = open(out.paths['full'], 'rb').read()
|
full = open(out.paths['full'], 'rb').read()
|
||||||
|
fac = open(out.paths['factory'], 'rb').read()
|
||||||
self.assertEqual(upd, open(U17_REF, 'rb').read())
|
self.assertEqual(upd, open(U17_REF, 'rb').read())
|
||||||
self.assertEqual(bpk[-8:], bytes.fromhex('de c0 13 00 6f 18 ff ff'))
|
|
||||||
self.assertEqual(full, self.ref_full)
|
self.assertEqual(full, self.ref_full)
|
||||||
|
# FACTORY = 参考 FULL 把 A 槽区(含锚点)整体置 0xFF
|
||||||
|
expect_fac = bytearray(self.ref_full)
|
||||||
|
expect_fac[0x2000:0xB000] = b'\xFF' * (0xB000 - 0x2000)
|
||||||
|
self.assertEqual(fac, bytes(expect_fac))
|
||||||
self.assertTrue(out.ok)
|
self.assertTrue(out.ok)
|
||||||
# FULL 自动命名与验收包一致
|
# 自动命名: FULL 与验收包一致, FACTORY 带 B 版本号
|
||||||
self.assertEqual(os.path.basename(out.paths['full']),
|
self.assertEqual(os.path.basename(out.paths['full']),
|
||||||
'CeramixNano-MB_FULL_A-V1.0.17_B-V1.0.12_2026-10-21.bin')
|
'CeramixNano-MB_FULL_A-V1.0.17_B-V1.0.12_2026-10-21.bin')
|
||||||
# MD5 清单包含全部产物与正确摘要
|
self.assertEqual(os.path.basename(out.paths['factory']),
|
||||||
|
'CeramixNano-MB_FACTORY_B-V1.0.12_2026-10-20.bin')
|
||||||
|
# MD5 清单挂在 FULL, 包含全部产物与正确摘要
|
||||||
mani = open(out.paths['md5'], encoding='utf-8').read()
|
mani = open(out.paths['md5'], encoding='utf-8').read()
|
||||||
for key in ('update', 'b', 'full'):
|
for key in ('update', 'full', 'factory'):
|
||||||
blob = open(out.paths[key], 'rb').read()
|
blob = open(out.paths[key], 'rb').read()
|
||||||
self.assertIn(hashlib.md5(blob).hexdigest(), mani)
|
self.assertIn(hashlib.md5(blob).hexdigest(), mani)
|
||||||
self.assertIn('ceramixnano_mb_v51', mani)
|
self.assertIn('CeramixNano_MB_Dual_FW', mani)
|
||||||
self.assertIn('0xD30F0014', mani)
|
self.assertIn('0xD30F0014', mani)
|
||||||
|
self.assertIn('DE C0 13 00 6F 18 FF FF', mani)
|
||||||
|
|
||||||
def test_suggest_full_name(self):
|
def test_suggest_full_name(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -539,8 +599,10 @@ class TestCLI(unittest.TestCase):
|
|||||||
self.dir = self.td.name
|
self.dir = self.td.name
|
||||||
self.a = os.path.join(self.dir, 'app_a.bin')
|
self.a = os.path.join(self.dir, 'app_a.bin')
|
||||||
self.b = os.path.join(self.dir, 'app_b.bin')
|
self.b = os.path.join(self.dir, 'app_b.bin')
|
||||||
|
self.bl = os.path.join(self.dir, 'bl.bin')
|
||||||
open(self.a, 'wb').write(synth_bin(A_BASE, 19580))
|
open(self.a, 'wb').write(synth_bin(A_BASE, 19580))
|
||||||
open(self.b, 'wb').write(synth_bin(B_BASE, SLOT_B_SIZE))
|
open(self.b, 'wb').write(synth_bin(B_BASE, SLOT_B_SIZE))
|
||||||
|
open(self.bl, 'wb').write(synth_bin(BL_BASE, 4044, rst_off=0xD00))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.td.cleanup()
|
self.td.cleanup()
|
||||||
@@ -548,21 +610,31 @@ class TestCLI(unittest.TestCase):
|
|||||||
def test_v2_full_pipeline(self):
|
def test_v2_full_pipeline(self):
|
||||||
rc, out, err = run_cli(self.a, self.b, '-o', self.dir)
|
rc, out, err = run_cli(self.a, self.b, '-o', self.dir)
|
||||||
self.assertEqual(rc, 0, err or out)
|
self.assertEqual(rc, 0, err or out)
|
||||||
for name in ('app_a_UPDATE.bin', 'app_b_B.bin', 'app_a_FULL.bin'):
|
for name in ('app_a_UPDATE.bin', 'app_a_FULL.bin', 'app_b_FACTORY.bin'):
|
||||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, name)), name)
|
self.assertTrue(os.path.isfile(os.path.join(self.dir, name)), name)
|
||||||
self.assertTrue(os.path.isfile(
|
self.assertTrue(os.path.isfile(
|
||||||
os.path.join(self.dir, 'app_a_FULL.bin.md5.txt')))
|
os.path.join(self.dir, 'app_a_FULL.bin.md5.txt')))
|
||||||
full = open(os.path.join(self.dir, 'app_a_FULL.bin'), 'rb').read()
|
full = open(os.path.join(self.dir, 'app_a_FULL.bin'), 'rb').read()
|
||||||
self.assertEqual(len(full), 65536)
|
self.assertEqual(len(full), 65536)
|
||||||
|
fac = open(os.path.join(self.dir, 'app_b_FACTORY.bin'), 'rb').read()
|
||||||
|
self.assertEqual(fac[0x2000:0xB000], b'\xFF' * 0x9000) # A 槽擦除态
|
||||||
|
self.assertFalse([f for f in os.listdir(self.dir)
|
||||||
|
if f.endswith('_B.bin')]) # 不再单独出 B 包
|
||||||
self.assertIn('回读复核通过', out)
|
self.assertIn('回读复核通过', out)
|
||||||
|
|
||||||
def test_v2_b_only_no_full(self):
|
def test_v2_b_only_factory(self):
|
||||||
"""仅 B 输入 → 只出 *_B.bin, 不拼 FULL (无 A 运行槽的整片镜像不可启动)"""
|
"""仅 B 输入 → 只出 *_FACTORY.bin(+MD5), 不拼 FULL"""
|
||||||
rc, out, err = run_cli(self.b, '-o', self.dir)
|
rc, out, err = run_cli(self.b, '-b', self.bl, '-o', self.dir)
|
||||||
self.assertEqual(rc, 0, err or out)
|
self.assertEqual(rc, 0, err or out)
|
||||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'app_b_B.bin')))
|
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'app_b_FACTORY.bin')))
|
||||||
|
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'app_b_FACTORY.bin.md5.txt')))
|
||||||
self.assertFalse([f for f in os.listdir(self.dir) if 'FULL' in f])
|
self.assertFalse([f for f in os.listdir(self.dir) if 'FULL' in f])
|
||||||
|
|
||||||
|
def test_v2_no_factory_flag(self):
|
||||||
|
rc, out, err = run_cli(self.a, self.b, '-o', self.dir, '--no-factory')
|
||||||
|
self.assertEqual(rc, 0, err or out)
|
||||||
|
self.assertFalse([f for f in os.listdir(self.dir) if 'FACTORY' in f])
|
||||||
|
|
||||||
def test_v2_explicit_conflict(self):
|
def test_v2_explicit_conflict(self):
|
||||||
rc, out, err = run_cli('--b', self.a) # A 固件显式塞给 B 槽
|
rc, out, err = run_cli('--b', self.a) # A 固件显式塞给 B 槽
|
||||||
self.assertNotEqual(rc, 0)
|
self.assertNotEqual(rc, 0)
|
||||||
@@ -603,8 +675,16 @@ class TestCLI(unittest.TestCase):
|
|||||||
td, 'CeramixNano-MB_FULL_A-V1.0.17_B-V1.0.12_2026-10-21.bin'),
|
td, 'CeramixNano-MB_FULL_A-V1.0.17_B-V1.0.12_2026-10-21.bin'),
|
||||||
'rb').read()
|
'rb').read()
|
||||||
self.assertEqual(full, open(FULL_REF, 'rb').read())
|
self.assertEqual(full, open(FULL_REF, 'rb').read())
|
||||||
|
expect_fac = bytearray(open(FULL_REF, 'rb').read())
|
||||||
|
expect_fac[0x2000:0xB000] = b'\xFF' * (0xB000 - 0x2000)
|
||||||
|
fac = open(os.path.join(td,
|
||||||
|
'CeramixNano-MB_FACTORY_B-V1.0.12_2026-10-20.bin'),
|
||||||
|
'rb').read()
|
||||||
|
self.assertEqual(fac, bytes(expect_fac))
|
||||||
self.assertIn('0xD30F0014', out)
|
self.assertIn('0xD30F0014', out)
|
||||||
self.assertIn('DE C0 13 00 6F 18 FF FF', out)
|
self.assertIn('DE C0 13 00 6F 18 FF FF', out)
|
||||||
|
self.assertIn('FACTORY', out)
|
||||||
|
self.assertFalse([f for f in os.listdir(td) if f.endswith('_B.bin')])
|
||||||
|
|
||||||
def test_legacy_fill(self):
|
def test_legacy_fill(self):
|
||||||
out = os.path.join(self.dir, 'legacy_update.bin')
|
out = os.path.join(self.dir, 'legacy_update.bin')
|
||||||
|
|||||||
Reference in New Issue
Block a user