feat(pack): FACTORY 工厂镜像 + 内置 profile 改名 CeramixNano_MB_Dual_FW
This commit is contained in:
+150
-38
@@ -4,10 +4,11 @@
|
||||
fw_pack — GD32E230 双固件打包公共库 (CLI 与 FlashTool GUI 共用)
|
||||
================================================================
|
||||
输入编译好的 APP bin, 按 layout profile 补页 / 算 CRC / 写记录, 产出:
|
||||
*_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 标志)
|
||||
*.md5.txt 产物 MD5 清单 + 布局摘要
|
||||
*_UPDATE.bin A 槽 OTA/Ymodem 包 (整 N 页 + 末 4B trailer {pages16|crc16})
|
||||
*_FULL.bin 整片镜像 (BL + A 包 + 槽尾锚点 + B 满槽 + 0xEEEE 标志)
|
||||
*_FACTORY.bin 工厂烧录镜像 (BL + B 满槽 + flag, 无 A 内容; 上电判定链
|
||||
锚点擦除态 → A 向量非法 → 落 B 记录校验 → 跳出厂固件)
|
||||
*.md5.txt 产物 MD5 清单 + 布局摘要
|
||||
|
||||
规则与 BL dev_fact 侧 (v5.1) 逐字一致:
|
||||
- CRC-16/XMODEM (poly 0x1021, init 0x0000, 不反转, 无 xorout),
|
||||
@@ -32,12 +33,12 @@ __all__ = [
|
||||
'FwPackError', 'crc16_xmodem', 'self_test', 'parse_num',
|
||||
'BUILTIN_PROFILES', 'load_profile', 'Profile',
|
||||
'detect_slot', 'read_vector', 'validate_app_bin',
|
||||
'build_a_pack', 'build_b_pack', 'build_full',
|
||||
'verify_a_blob', 'verify_b_blob', 'verify_full_blob',
|
||||
'pack_all', 'suggest_full_name',
|
||||
'build_a_pack', 'build_b_pack', 'build_full', 'build_factory',
|
||||
'verify_a_blob', 'verify_b_blob', 'verify_full_blob', 'verify_factory_blob',
|
||||
'pack_all', 'suggest_full_name', 'suggest_factory_name',
|
||||
]
|
||||
|
||||
TOOL_VERSION = '2.0'
|
||||
TOOL_VERSION = '2.1'
|
||||
|
||||
# GD32E230 家族 SRAM: F8 最大 8K, SP 必须落在其中 (F4/F6 的 4K 也在范围内)
|
||||
SRAM_LO, SRAM_HI = 0x20000000, 0x20002000
|
||||
@@ -196,9 +197,9 @@ class SlotSpec:
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
BUILTIN_PROFILES = {
|
||||
# CeramixNano-MB v5.1 双固件布局 (BL dev_fact@512bf7a)
|
||||
'ceramixnano_mb_v51': {
|
||||
'profile': 'ceramixnano_mb_v51',
|
||||
# CeramixNano-MB v5.1 双固件 BL 的布局 (BL dev_fact@512bf7a)
|
||||
'CeramixNano_MB_Dual_FW': {
|
||||
'profile': 'CeramixNano_MB_Dual_FW',
|
||||
'chip': 'GD32E230F8',
|
||||
'flash_base': '0x08000000',
|
||||
'flash_size': '64K',
|
||||
@@ -378,9 +379,9 @@ class Profile:
|
||||
|
||||
|
||||
def load_profile(spec):
|
||||
"""内置名(ceramixnano_mb_v51 / f8_single_slot) 或 profile JSON 文件路径"""
|
||||
"""内置名(CeramixNano_MB_Dual_FW / f8_single_slot) 或 profile JSON 文件路径"""
|
||||
if not spec:
|
||||
spec = 'ceramixnano_mb_v51'
|
||||
spec = 'CeramixNano_MB_Dual_FW'
|
||||
if spec in BUILTIN_PROFILES:
|
||||
return Profile(dict(BUILTIN_PROFILES[spec]))
|
||||
if os.path.isfile(spec):
|
||||
@@ -551,6 +552,28 @@ def build_full(profile, bl_data, a_pack, b_pack=None):
|
||||
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
|
||||
|
||||
|
||||
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 共用入口)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@@ -698,24 +775,37 @@ def suggest_full_name(a_path, b_path=None):
|
||||
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:
|
||||
def __init__(self):
|
||||
self.paths = {} # update/b/full/md5 -> 文件路径
|
||||
self.paths = {} # update/full/factory/md5 -> 文件路径
|
||||
self.summary = [] # 展示用文本行
|
||||
self.checks = [] # FULL 逐项校验 (名称, ok, note)
|
||||
self.factory_checks = [] # FACTORY 逐项校验
|
||||
|
||||
@property
|
||||
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,
|
||||
make_full=True, progress=print):
|
||||
"""一站式打包: 自动识别/校验 → A包/B包/FULL → 写盘 → 回读复核 → MD5 清单。
|
||||
make_full=True, make_factory=True, progress=print):
|
||||
"""一站式打包: 自动识别/校验 → A包/FULL/FACTORY → 写盘 → 回读复核 → MD5 清单。
|
||||
|
||||
a_path 可为 None (仅 B 底包 → 只出 *_B.bin, 不拼 FULL —— 没有 A 运行槽的
|
||||
整片镜像无法启动, 不允许生成)。progress: 每行输出的回调(CLI 用 print,
|
||||
GUI 转日志)。返回 PackOutcome; 任何硬错误抛 FwPackError。
|
||||
a_path 可为 None (仅 B 底包 → 只出 *_FACTORY.bin 工厂镜像, 不拼 FULL ——
|
||||
没有 A 运行槽的整片镜像无法启动, 不允许生成)。progress: 每行输出的回调
|
||||
(CLI 用 print, GUI 转日志)。返回 PackOutcome; 任何硬错误抛 FwPackError。
|
||||
"""
|
||||
self_test()
|
||||
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))
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
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 包 --
|
||||
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}) — 回读复核通过')
|
||||
say(f' {upd_path}')
|
||||
|
||||
# -- B 包 --
|
||||
# -- B 包 (仅作为镜像组成, 不再单独落盘) --
|
||||
bp = None
|
||||
if b_body is not None:
|
||||
bp = build_b_pack(b_body, profile)
|
||||
b_out = os.path.join(out_dir, b_base + '_B.bin')
|
||||
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)
|
||||
verify_b_blob(bp.data, profile)
|
||||
area = profile.slot_b.record_area_len(profile.page_size)
|
||||
rec_disp = (bp.record_bytes + b'\xFF' * (area - len(bp.record_bytes))) \
|
||||
.hex(' ').upper()
|
||||
say(f'B 包: 满槽 {bp.pages} 页({len(bp.data)}B) 记录 {rec_disp} '
|
||||
f'(CRC 0x{bp.crc:04X}) — 回读复核通过')
|
||||
say(f' {b_out}')
|
||||
f'(CRC 0x{bp.crc:04X}) — 复核通过')
|
||||
elif profile.slot_b and make_full and a_body is not None:
|
||||
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]
|
||||
if bad:
|
||||
raise FwPackError('FULL 镜像复核未通过: ' + '; '.join(bad))
|
||||
n_ok = len(out.checks)
|
||||
say(f'FULL: {len(image)}B — {n_ok} 项字节校验全部通过')
|
||||
say(f'FULL: {len(image)}B — {len(out.checks)} 项字节校验全部通过')
|
||||
for n, ok, note in out.checks:
|
||||
say(f' [{"PASS" if ok else "FAIL"}] {n}' + (f' ({note})' if note else ''))
|
||||
say(f' {full_path}')
|
||||
|
||||
# MD5 清单
|
||||
md5_path = full_path + '.md5.txt'
|
||||
products = [(k, p) for k, p in out.paths.items()]
|
||||
# -- FACTORY (工厂烧录镜像: BL + B 满槽 + flag, 无 A; 需 B 底包) --
|
||||
if make_factory and b_body is not None:
|
||||
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:
|
||||
f.write(f'# 布局摘要 — gd32e230_firmware_merge_tool v{TOOL_VERSION}\n')
|
||||
f.write('# ' + profile.describe().replace('\n', '\n# ') + '\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'
|
||||
% (os.path.basename(a_path), ap.pages, ap.trailer_value, ap.crc))
|
||||
if ap:
|
||||
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:
|
||||
f.write('# B 包: %s -> 满槽 %d 页, 记录 %s (CRC 0x%04X)\n'
|
||||
% (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')
|
||||
for _k, p in products:
|
||||
for _k, p in out.paths.items():
|
||||
if _k == 'md5':
|
||||
continue
|
||||
data = open(p, 'rb').read()
|
||||
f.write('%s %8dB %s\n' % (hashlib.md5(data).hexdigest(),
|
||||
len(data), os.path.basename(p)))
|
||||
|
||||
Reference in New Issue
Block a user