Files
gd32e230f8_firmware_merge_tool/gd32e230_firmware_tool.py
T

376 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
GD32E230 固件打包工具 v2 — 布局 profile 化双固件打包 (A/B 槽 + 记录/CRC)
================================================================
v2 模式(默认): 输入编译好的 APP bin, 自动识别 A/B 槽, 产出
*_UPDATE.bin A 槽 OTA/Ymodem 包 (整 N 页 + 末 4B trailer {页数16|CRC16})
*_FULL.bin 整片镜像 (BL + A 包 + 槽尾锚点 + B 满槽 + 0xEEEE 标志)
*_FACTORY.bin 工厂烧录镜像 (BL + B 满槽 + flag, 无 A 内容; 上电跳 B)
*.md5.txt 产物 MD5 清单 + 布局摘要 (随主镜像生成)
打包规则与 BL (dev_fact v5.1) 逐字一致, 写盘后回读复核。计算与校验在
fw_pack.py 公共库 (FlashTool GUI 共用)。
legacy 模式(过渡期兼容, -f/-m 二选一时启用): 旧单槽方案
-f/--fill 纯 0xFF 补页, 无 trailer (旧 BL 的 Ymodem 升级包)
-m/--merge BL + APP + 0xEEEE 标志 拼整片镜像 (不补齐)
v2 Flash 布局(默认 profile CeramixNano_MB_Dual_FW, GD32E230F8/64K):
0x08000000 Bootloader 8K 页 0~7
0x08002000 FW_A 运行槽 36K 页 8~43 OTA 唯一接收区, 槽尾锚点@0x0800AFFC
0x0800B000 FW_B 保底槽 19K 页 44~62 固定满槽, B 记录@0x0800FBF8
0x0800FC00 标志页 1K 页 63 0xEEEE@0x0800FFFC
其他工程换 profile 即可(保底槽 8K/28K/无保底槽等), 见 --profile。
"""
import argparse
import os
import struct
import sys
import fw_pack
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)
TOOL_VERSION = '2.1.0'
# 默认布局 profile (与 fw_pack.BUILTIN_PROFILES 键一致)
DEFAULT_PROFILE = 'CeramixNano_MB_Dual_FW'
# ═══════════════════════════════════════════════════════════
# v2 模式: 双固件 profile 打包
# ═══════════════════════════════════════════════════════════
def _collect_inputs(args, profile):
"""汇合 位置参数(自动识别) 与 --a/--b(显式指定), 冲突报错。
返回 (a_path|None, b_path|None)。"""
if (args.a or args.b) and args.bins:
raise fw_pack.FwPackError(
'位置输入(自动识别)与显式 --a/--b 不能混用, 二选一')
if args.bins:
found = {}
for path in args.bins:
if not os.path.isfile(path):
raise fw_pack.FwPackError(f'找不到输入文件: {path}')
data = open(path, 'rb').read()
slot = fw_pack.detect_slot(data, profile)
if slot in found:
raise fw_pack.FwPackError(
f'{path!r} 与 {found[slot]!r} 都被识别为 {slot.upper()} 包, '
f'一个槽只能有一个输入')
found[slot] = path
return found.get('a'), found.get('b')
if args.a and args.b and os.path.abspath(args.a) == os.path.abspath(args.b):
raise fw_pack.FwPackError('--a 与 --b 指向同一个文件')
for opt, path, slot in (('--a', args.a, 'a'), ('--b', args.b, 'b')):
if not path:
continue
if not os.path.isfile(path):
raise fw_pack.FwPackError(f'找不到 {opt} 输入文件: {path}')
data = open(path, 'rb').read()
which = fw_pack.detect_slot(data, profile)
if which != slot: # 显式指定与向量表判定冲突 → 报错(规格 4.1)
raise fw_pack.FwPackError(
f'{opt} 显式指定为 {slot.upper()} 槽, 但其复位向量落在 '
f'{which.upper()} 槽 — 请改用 --{which} 或调整输入')
return args.a, args.b
def run_v2(args):
fw_pack.self_test()
try:
profile = fw_pack.load_profile(args.profile)
except fw_pack.FwPackError as exc:
print(f'错误:{exc}', file=sys.stderr)
return 2
try:
a_path, b_path = _collect_inputs(args, profile)
if not a_path and not b_path:
print('错误:v2 模式至少需要一个输入 bin(A 包或 B 包)。\n'
'示例: python gd32e230_firmware_tool.py app_a.bin app_b.bin -b bl.bin',
file=sys.stderr)
return 2
out_dir = args.output
if out_dir and os.path.splitext(args.output)[1].lower() == '.bin':
print('错误:v2 模式 -o 是输出目录(产物自动命名);'
'legacy 模式 -o 才是输出文件。', file=sys.stderr)
return 2
fw_pack.pack_all(
args.profile, a_path, b_path=b_path, bl_path=args.bootloader,
out_dir=out_dir, make_full=not args.no_full,
make_factory=not args.no_factory)
except fw_pack.FwPackError as exc:
print(f'\n错误:{exc}', file=sys.stderr)
return 1
return 0
# ═══════════════════════════════════════════════════════════
# legacy 模式: 旧单槽方案 (纯 0xFF 补页 / 拼接, 无 trailer)
# ═══════════════════════════════════════════════════════════
FLASH_BASE = 0x08000000
BOOTLOADER_SIZE = 8 * 1024 # 8KB
CAL_PAGE_SIZE = 1 * 1024 # 1KB 校准数据预留
FLAG_PAGE_SIZE = 1 * 1024 # 1KB Flag 页
PAGE_SIZE = 1024 # Flash 页大小 (1KB)
FILL_BYTE = 0xFF # 补齐字节
FLAG_VALUE = 0xEEEE # 与 bootloader 统一
CHIP_CONFIG = {
"F4": {"size": 16 * 1024, "label": "GD32E230F4 (16KB)"},
"F6": {"size": 32 * 1024, "label": "GD32E230F6 (32KB)"},
"F8": {"size": 64 * 1024, "label": "GD32E230F8 (64KB)"},
}
DEFAULT_CHIP = "F8"
DEFAULT_BOOTLOADER = os.path.join(SCRIPT_DIR, "gd32e230f8_bootloader_hulk.bin")
def calc_layout(flash_size, app_addr=None):
if app_addr is None:
app_addr = FLASH_BASE + BOOTLOADER_SIZE # 0x08002000
cal_addr = FLASH_BASE + flash_size - CAL_PAGE_SIZE - FLAG_PAGE_SIZE
app_max = cal_addr - app_addr
cal_reserved = True
else:
# 自定义落位(如 FW_B 槽 0x0800B000):APP 可用到标志页前,校准页让渡给 APP
cal_addr = None
app_max = FLASH_BASE + flash_size - FLAG_PAGE_SIZE - app_addr
cal_reserved = False
flag_addr = FLASH_BASE + flash_size - 4 # 最后一页末尾 - 4B
flag_page = FLASH_BASE + flash_size - FLAG_PAGE_SIZE
return {
"flash_size": flash_size,
"flash_end": FLASH_BASE + flash_size,
"app_addr": app_addr,
"app_max": app_max,
"cal_addr": cal_addr,
"cal_reserved": cal_reserved,
"flag_page": flag_page,
"flag_addr": flag_addr,
}
def pad_to_page(data, page_size=PAGE_SIZE, fill_byte=FILL_BYTE):
remainder = len(data) % page_size
if remainder == 0:
return data, 0
pad_len = page_size - remainder
return data + bytes([fill_byte]) * pad_len, pad_len
def merge(bootloader_path, app_path, output_path, layout):
with open(bootloader_path, "rb") as f:
bl = f.read()
with open(app_path, "rb") as f:
app = f.read()
app_offset = layout["app_addr"] - FLASH_BASE
if len(bl) > app_offset:
print(f"错误:Bootloader ({len(bl)}B) 超过 APP 偏移 ({app_offset}B)", file=sys.stderr)
return False
if len(app) > layout["app_max"]:
limit_desc = (
f"{layout['flash_size']//1024}KB - 8KB(BL) - 1KB(校准) - 1KB(Flag) "
f"= {layout['app_max']//1024}KB"
if layout["cal_reserved"] else
f"APP 0x{layout['app_addr']:08X} ~ 0x{layout['flag_page']:08X}(Flag 页前)"
f"= {layout['app_max']//1024}KB"
)
print(
f"错误:APP ({len(app)}B / {len(app)/1024:.1f}KB) 超过最大容量 "
f"({layout['app_max']}B / {layout['app_max']//1024:.0f}KB)\n"
f" 芯片 {layout['flash_size']//1024}KB: {limit_desc}",
file=sys.stderr,
)
return False
image = bytearray(b'\xFF' * layout["flash_size"])
image[0:len(bl)] = bl
image[app_offset:app_offset + len(app)] = app
flag_offset = layout["flag_addr"] - FLASH_BASE
struct.pack_into("<I", image, flag_offset, FLAG_VALUE)
with open(output_path, "wb") as f:
f.write(image)
print(f"\n合成完成: {output_path}")
print(f" 芯片 : {layout['flash_size']//1024}KB")
print(f" Bootloader : {len(bl):>6}B @ 0x{FLASH_BASE:08X}")
print(f" Application: {len(app):>6}B @ 0x{layout['app_addr']:08X}")
print(f" Flag : 0x{FLAG_VALUE:04X} @ 0x{layout['flag_addr']:08X}")
if layout["cal_reserved"]:
print(f" 校准预留 : 1KB @ 0x{layout['cal_addr']:08X}")
print(f" APP 剩余 : {layout['app_max'] - len(app):>6}B")
print(f" 总大小 : {layout['flash_size']:>6}B ({layout['flash_size']//1024}KB)")
return True
def fill_only(input_path, output_path, layout):
with open(input_path, "rb") as f:
data = f.read()
padded, pad_len = pad_to_page(data)
if len(padded) > layout["app_max"]:
limit_desc = (
f"{layout['flash_size']//1024}KB - 8KB(BL) - 1KB(校准) - 1KB(Flag) "
f"= {layout['app_max']//1024}KB"
if layout["cal_reserved"] else
f"APP 0x{layout['app_addr']:08X} ~ 0x{layout['flag_page']:08X}(Flag 页前)"
f"= {layout['app_max']//1024}KB"
)
print(
f"错误:补齐后 ({len(padded)}B / {len(padded)/1024:.1f}KB) 超过 APP 最大容量 "
f"({layout['app_max']}B / {layout['app_max']//1024:.0f}KB)\n"
f" 芯片 {layout['flash_size']//1024}KB: {limit_desc}",
file=sys.stderr,
)
sys.exit(1)
with open(output_path, "wb") as f:
f.write(padded)
print(f"补齐完成: {output_path}")
print(f" 原始大小: {len(data)}B")
print(f" 页大小 : {PAGE_SIZE}B (1KB)")
print(f" 补齐字节: {pad_len}B")
print(f" 输出大小: {len(padded)}B")
print(f" APP 上限: {layout['app_max']}B ({layout['app_max']//1024}KB) → "
f"剩余 {layout['app_max'] - len(padded)}B")
if pad_len == 0:
print(f" 说明 : 已是整页对齐,原样输出")
def run_legacy(args):
chip = CHIP_CONFIG[args.flash_size]
app_addr = None
if args.app_addr is not None:
try:
app_addr = int(args.app_addr, 0)
except ValueError:
print(f"错误:-a/--app-addr 无法解析:{args.app_addr}", file=sys.stderr)
return 2
if app_addr % PAGE_SIZE != 0:
print(f"错误:APP 落位地址 0x{app_addr:08X} 未按 1KB 页对齐", file=sys.stderr)
return 2
addr_min = FLASH_BASE + BOOTLOADER_SIZE
addr_max = FLASH_BASE + chip["size"] - FLAG_PAGE_SIZE
if not (addr_min <= app_addr < addr_max):
print(
f"错误:APP 落位地址 0x{app_addr:08X} 超出允许范围 "
f"[0x{addr_min:08X}, 0x{addr_max:08X})",
file=sys.stderr,
)
return 2
layout = calc_layout(chip["size"], app_addr)
print(f"[legacy] 芯片: {chip['label']}")
app = args.bins[0] if args.bins else None
if not app or not os.path.isfile(app):
print(f"错误:找不到 APP 文件 {app}", file=sys.stderr)
return 2
if args.output is None:
base, ext = os.path.splitext(os.path.basename(app))
suffix = "_BL" if args.merge else "_UPDATE"
args.output = os.path.join(os.path.dirname(os.path.abspath(app)),
f"{base}{suffix}{ext}")
if args.merge:
bl = args.bootloader or DEFAULT_BOOTLOADER
if not os.path.isfile(bl):
print(f"错误:找不到 Bootloader 文件 {bl}", file=sys.stderr)
return 2
if not merge(bl, app, args.output, layout):
return 1
else:
fill_only(app, args.output, layout)
return 0
# ═══════════════════════════════════════════════════════════
# 入口
# ═══════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(
prog='gd32e230_firmware_tool',
description=f"GD32E230 固件打包工具 v{TOOL_VERSION} — 双固件 profile 打包"
"(默认)/ 旧单槽补齐·拼接(-f/-m)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"""
v2 双固件模式(默认, 规格: UPGRADE_PLAN_v2.md):
python %(prog)s app_a.bin # 识别 A 包 → *_UPDATE.bin (+FULL)
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.bin --profile my_layout.json # 自定义布局 profile JSON
legacy 模式(旧单槽方案, -f/-m 触发):
python %(prog)s -f app.bin # 纯 0xFF 补齐升级包(无 trailer)
python %(prog)s -m app.bin # BL+APP+flag 拼整片
python %(prog)s -m app.bin -s F6 # F6 芯片
python %(prog)s -m app.bin -a 0x0800B000 -b bl.bin # 自定义落位
""",
)
parser.add_argument(
"bins", nargs="*",
help="v2: 输入 bin(A/B 包自动识别, 可多个);legacy: APP 路径")
mode = parser.add_mutually_exclusive_group()
mode.add_argument("-m", "--merge", action="store_true",
help="legacy: 合成完整 Flash 镜像 (bootloader + APP + FLAG)")
mode.add_argument("-f", "--fill", action="store_true",
help="legacy: 仅补齐 APP 到整页边界 (纯 0xFF, 无 trailer)")
parser.add_argument("--legacy", action="store_true",
help="显式声明 legacy 模式(需配合 -f/-m)")
parser.add_argument("--profile", default=DEFAULT_PROFILE,
help="v2 布局 profile: 内置名(%s)或 JSON 文件路径"
% "/".join(fw_pack.BUILTIN_PROFILES))
parser.add_argument("--a", metavar="BIN", default=None,
help="v2: 显式指定 A 包(运行槽固件)")
parser.add_argument("--b", metavar="BIN", default=None,
help="v2: 显式指定 B 包(出厂保底固件)")
parser.add_argument("-b", "--bootloader", default=None,
help="BL bin (v2: 参与 FULL 拼接; legacy -m: 默认内置 BL)")
parser.add_argument("-o", "--output", default=None,
help="输出路径: v2=输出目录(自动命名); legacy=输出文件")
parser.add_argument("--no-full", action="store_true",
help="v2: 不拼 FULL 整片镜像")
parser.add_argument("--no-factory", action="store_true",
help="v2: 不出 FACTORY 工厂镜像")
parser.add_argument("-s", "--flash-size", default=DEFAULT_CHIP,
choices=list(CHIP_CONFIG.keys()),
help="legacy 芯片型号: F4(16K) F6(32K) F8(64K)")
parser.add_argument("-a", "--app-addr", default=None,
help="legacy: APP 落位地址, 如 0x0800B000 (默认 0x08002000)")
args = parser.parse_args()
print(f"gd32e230_firmware_tool v{TOOL_VERSION} (fw_pack {fw_pack.TOOL_VERSION})")
legacy = args.fill or args.merge
if args.legacy and not legacy:
parser.error("--legacy 需配合 -f/--fill 或 -m/--merge 使用")
if legacy and (args.profile != DEFAULT_PROFILE or args.a or args.b
or args.no_full or args.no_factory or args.app_addr):
parser.error("--profile/--a/--b/--no-full/--no-factory/-a 是 v2/legacy "
"专属参数, 不能与 -f/-m 同用")
if legacy and len(args.bins) != 1:
parser.error("legacy 模式需要一个 APP 位置参数, 例: python %(prog)s -f app.bin"
% {'prog': parser.prog})
rc = run_legacy(args) if legacy else run_v2(args)
sys.exit(rc)
if __name__ == "__main__":
main()