Pin BIN3 to fixed 0xA71AC and reject packs where 2.bin drift shifts it.

Variable 2.bin length previously moved 3.bin; firmware still read 0xA71AC and saw zeros. Pack now pads to the fixed slot and publish verifies DAB headers before shipping ALL.res.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-09 11:47:15 +08:00
parent cd21e4e45e
commit 99f7ed3e5a
2 changed files with 99 additions and 29 deletions

View File

@ -2,12 +2,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Pack external W25Q128 tone/logo image for K1 (0903). """Pack external W25Q128 tone/logo image for K1 (0903).
Layout (absolute W25Q128 offsets): Layout (absolute W25Q128 offsets BIN2/BIN3 are FIXED so FW addresses stay stable):
0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo) 0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo)
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen) 0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen)
0x0001B8F0 1.bin 普通/专业 31 rhythms 0x0001B8F0 1.bin 普通/专业 31 rhythms
0x0009D07D 2.bin 海阔天空 0x0009D07D 2.bin 本地曲目变长尾部 0xFF 填到 BIN3
0x000A71AC 3.bin 万能模式紧随 2.bin具体偏移以打包结果为准 0x000A71AC 3.bin 万能模式固定勿随 2.bin 长度漂移
Outputs: Outputs:
tools/out/extflash_tone_0903.bin tools/out/extflash_tone_0903.bin
@ -16,7 +16,7 @@ Outputs:
""" """
from __future__ import annotations from __future__ import annotations
import os import struct
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@ -30,6 +30,15 @@ OUT_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
TONE_DIR = REPO / "Doc" / "音色文件" / "0903" TONE_DIR = REPO / "Doc" / "音色文件" / "0903"
# Fixed map — must match project/inc/ExtFlash_Tone_Addr.h consumed by firmware.
OFF_LOGO = 0x00000000
OFF_CHARGING = 0x0000CB70
OFF_BIN1 = 0x0001B8F0
OFF_BIN2 = 0x0009D07D
OFF_BIN3 = 0x000A71AC # FIXED: never place 3.bin by concatenating after variable 2.bin
UI0902_RES_BASE = 0x00100000
DAB_MAGIC = b"\xABDAB"
def find_ziliao() -> Path: def find_ziliao() -> Path:
for p in REPO.iterdir(): for p in REPO.iterdir():
@ -38,29 +47,51 @@ def find_ziliao() -> Path:
raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo") raise FileNotFoundError("资料/logo.bin + Charg.bin not found under repo")
def dab_ok(blob: bytes, off: int, expect_cnt: int | None = None) -> None:
if off + 16 > len(blob):
raise SystemExit(f"DAB check @0x{off:X}: past end of pack ({len(blob)})")
magic = blob[off : off + 4]
cnt = struct.unpack_from("<I", blob, off + 12)[0]
if magic != DAB_MAGIC:
raise SystemExit(
f"DAB check @0x{off:X}: bad magic {magic.hex()} (want {DAB_MAGIC.hex()})"
)
if expect_cnt is not None and cnt != expect_cnt:
raise SystemExit(f"DAB check @0x{off:X}: cnt={cnt} want {expect_cnt}")
def main() -> None: def main() -> None:
ziliao = find_ziliao() ziliao = find_ziliao()
parts = [ parts = [
("LOGO", ziliao / "logo.bin", 0x00000000, "legacy pad; boot uses UI0902_BOOT_LOGO"), ("LOGO", ziliao / "logo.bin", OFF_LOGO, "legacy pad; boot uses UI0902_BOOT_LOGO"),
("CHARGING", ziliao / "Charg.bin", 0x0000CB70, "legacy pad; UI uses UI0902_CHARGE_SCREEN"), ("CHARGING", ziliao / "Charg.bin", OFF_CHARGING, "legacy pad; UI uses UI0902_CHARGE_SCREEN"),
("BIN1_RHYTHM", TONE_DIR / "1.bin", 0x0001B8F0, "普通/专业 31 条节奏"), ("BIN1_RHYTHM", TONE_DIR / "1.bin", OFF_BIN1, "普通/专业 31 条节奏"),
("BIN2_SONG_HAITIAN", TONE_DIR / "2.bin", None, "本地曲目 海阔天空"), ("BIN2_SONG_HAITIAN", TONE_DIR / "2.bin", OFF_BIN2, "本地曲目"),
("BIN3_UNIVERSAL", TONE_DIR / "3.bin", None, "万能模式"), ("BIN3_UNIVERSAL", TONE_DIR / "3.bin", OFF_BIN3, "万能模式(固定偏移)"),
] ]
blobs = [] blobs: list[bytes] = []
cursor = 0 cursor = 0
rows = [] rows: list[tuple[str, int, int, str]] = []
for name, path, force_off, note in parts: for name, path, force_off, note in parts:
data = path.read_bytes() data = path.read_bytes()
if force_off is not None:
if cursor > force_off: if cursor > force_off:
raise SystemExit(f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X}") raise SystemExit(
f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X} "
f"(previous blob too large; enlarge next fixed gap or shrink prior file)"
)
if cursor < force_off: if cursor < force_off:
pad = force_off - cursor pad = force_off - cursor
blobs.append(b"\xFF" * pad) blobs.append(b"\xFF" * pad)
rows.append(("(pad)", cursor, pad, "gap fill 0xFF"))
cursor = force_off cursor = force_off
rows.append((f"(pad)", force_off - pad, pad, "gap fill 0xFF")) # BIN2 must not overflow into fixed BIN3 slot
if name == "BIN2_SONG_HAITIAN" and force_off + len(data) > OFF_BIN3:
raise SystemExit(
f"BIN2 too large ({len(data)} bytes): "
f"0x{force_off:X}+{len(data)} overflows fixed BIN3 @0x{OFF_BIN3:X} "
f"(max {OFF_BIN3 - force_off} bytes)"
)
off = cursor off = cursor
blobs.append(data) blobs.append(data)
cursor += len(data) cursor += len(data)
@ -70,9 +101,21 @@ def main() -> None:
packed = b"".join(blobs) packed = b"".join(blobs)
OUT_BIN.write_bytes(packed) OUT_BIN.write_bytes(packed)
# named lookup
by_name = {r[0]: r for r in rows if not r[0].startswith("(")} by_name = {r[0]: r for r in rows if not r[0].startswith("(")}
# Hard integrity gates — catch 2.bin drift / missing 3.bin before publish.
dab_ok(packed, by_name["BIN1_RHYTHM"][1], expect_cnt=31)
dab_ok(packed, by_name["BIN2_SONG_HAITIAN"][1], expect_cnt=1)
dab_ok(packed, OFF_BIN3, expect_cnt=3)
if by_name["BIN3_UNIVERSAL"][1] != OFF_BIN3:
raise SystemExit(
f"BIN3 placed @0x{by_name['BIN3_UNIVERSAL'][1]:X} but FW requires 0x{OFF_BIN3:X}"
)
if packed[OFF_BIN3 : OFF_BIN3 + len(Path(TONE_DIR / "3.bin").read_bytes())] != (
TONE_DIR / "3.bin"
).read_bytes():
raise SystemExit("BIN3 region does not match Doc/.../0903/3.bin")
hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H
#define __EXTFLASH_TONE_ADDR_H #define __EXTFLASH_TONE_ADDR_H
@ -98,7 +141,7 @@ def main() -> None:
#define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{by_name['BIN2_SONG_HAITIAN'][1]:08X}UL #define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{by_name['BIN2_SONG_HAITIAN'][1]:08X}UL
#define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL #define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL
#define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{by_name['BIN3_UNIVERSAL'][1]:08X}UL #define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{OFF_BIN3:08X}UL
#define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL #define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL
/* Convenience aliases used by UI ADDRESS */ /* Convenience aliases used by UI ADDRESS */
@ -123,6 +166,9 @@ def main() -> None:
f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)", f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)",
f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={0x100000 - cursor} bytes", f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={0x100000 - cursor} bytes",
"", "",
"NOTE: BIN3 is FIXED at 0x000A71AC. Shrinking 2.bin pads 0xFF up to BIN3;",
" growing 2.bin beyond (0xA71AC-0x9D07D)=41263 bytes fails the pack.",
"",
f"{'Name':<22} {'Offset':>10} {'Size':>10} Note", f"{'Name':<22} {'Offset':>10} {'Size':>10} Note",
"-" * 72, "-" * 72,
] ]
@ -132,17 +178,17 @@ def main() -> None:
"", "",
"Firmware ADDRESS mapping:", "Firmware ADDRESS mapping:",
" 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin)", " 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin)",
" 海阔天空 -> FLASH_ADDR_SONG_HAITIAN (2.bin)", " 本地曲目 -> FLASH_ADDR_SONG_HAITIAN (2.bin @ 0x9D07D)",
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin)", " 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0xA71AC FIXED)",
" AutoBand -> FLASH_ADDR_AUTOBAND_LEGACY 0x9EB5F (unchanged; overlaps 2.bin region — do not enable until remapped)", " AutoBand -> FLASH_ADDR_AUTOBAND_LEGACY 0x9EB5F (unchanged; overlaps 2.bin region — do not enable until remapped)",
" Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70", " Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70",
" Charging -> EXTFLASH_CHARGING_ADDR", " Charging -> EXTFLASH_CHARGING_ADDR",
"", "",
"Add a local song:", "Add a local song:",
" 1. Pack the new preset into 2.bin", " 1. Pack the new preset into 2.bin (must stay <= 41263 bytes unless BIN3 offset is raised in pack script + FW)",
" 2. Append a row to local_songs.csv (index,code,name)", " 2. Append a row to local_songs.csv (index,code,name)",
" 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)", " 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)",
" 4. Rebuild firmware and flash MCU + ExtFlash", " 4. Rebuild firmware and flash MCU + ExtFlash ALL.res",
] ]
OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n") OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n")
@ -151,11 +197,11 @@ def main() -> None:
print(f"Wrote {OUT_MAP}") print(f"Wrote {OUT_MAP}")
for name, off, size, note in rows: for name, off, size, note in rows:
print(f" 0x{off:08X} {size:8d} {name} {note}") print(f" 0x{off:08X} {size:8d} {name} {note}")
if cursor > 0x00100000: if cursor > UI0902_RES_BASE:
raise SystemExit("ERROR: pack overflows into UI0902_RES_BASE") raise SystemExit("ERROR: pack overflows into UI0902_RES_BASE")
print(f"OK: {0x00100000 - cursor} bytes free before UI0902 @ 0x00100000") print(f"OK: {UI0902_RES_BASE - cursor} bytes free before UI0902 @ 0x{UI0902_RES_BASE:X}")
print(f"OK: BIN3 fixed @0x{OFF_BIN3:X} magic=ABDAB cnt=3 matches 3.bin")
# Keep LocalSongNames.h in sync with Doc/.../local_songs.csv when packing tones.
gen = ROOT / "tools" / "gen_local_song_names.py" gen = ROOT / "tools" / "gen_local_song_names.py"
r = subprocess.run([sys.executable, str(gen)], check=False) r = subprocess.run([sys.executable, str(gen)], check=False)
if r.returncode != 0: if r.returncode != 0:

View File

@ -69,6 +69,28 @@ def build_combined_extflash(tone: bytes, ui: bytes) -> bytes:
return tone + (b"\xFF" * pad) + ui return tone + (b"\xFF" * pad) + ui
def verify_tone_layout(tone: bytes) -> None:
"""Refuse to ship ALL.res if BIN3 is missing/shifted (2.bin length drift)."""
dab = b"\xABDAB"
checks = [
(0x0001B8F0, 31, "BIN1"),
(0x0009D07D, 1, "BIN2"),
(0x000A71AC, 3, "BIN3"),
]
for off, want_cnt, name in checks:
if off + 16 > len(tone):
raise SystemExit(f"verify {name}: tone pack too short for 0x{off:X}")
magic = tone[off : off + 4]
cnt = int.from_bytes(tone[off + 12 : off + 16], "little")
if magic != dab or cnt != want_cnt:
raise SystemExit(
f"verify {name} @0x{off:X} failed: magic={magic.hex()} cnt={cnt} "
f"(want ABDAB/{want_cnt}). Re-run pack_extflash_tone_0903.py; "
f"do not publish a pack where 2.bin length shifted 3.bin."
)
print("verify tone layout: BIN1/BIN2/BIN3 DAB OK at fixed FW addresses")
def main() -> None: def main() -> None:
if not BOOT_BIN.is_file(): if not BOOT_BIN.is_file():
raise SystemExit(f"missing Boot bin: {BOOT_BIN}") raise SystemExit(f"missing Boot bin: {BOOT_BIN}")
@ -97,8 +119,10 @@ def main() -> None:
pkg.mkdir(parents=True) pkg.mkdir(parents=True)
tone_data = TONE_BIN.read_bytes() tone_data = TONE_BIN.read_bytes()
verify_tone_layout(tone_data)
ui_data = UI0902_BIN.read_bytes() ui_data = UI0902_BIN.read_bytes()
combined = build_combined_extflash(tone_data, ui_data) combined = build_combined_extflash(tone_data, ui_data)
verify_tone_layout(combined) # same offsets in ALL.res
boot_name = f"AT32F403ARCT7_BOOT_v{FW_VER}_{stamp}.bin" boot_name = f"AT32F403ARCT7_BOOT_v{FW_VER}_{stamp}.bin"
mcu_name = f"YNGJ-GT1-M_MCU_v{FW_VER}_{stamp}.bin" mcu_name = f"YNGJ-GT1-M_MCU_v{FW_VER}_{stamp}.bin"
@ -147,7 +171,7 @@ def main() -> None:
"", "",
"外部 Flash 分区:", "外部 Flash 分区:",
" 0x00000000 音色/充电图 (toneRes)", " 0x00000000 音色/充电图 (toneRes)",
" logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin(HKTK/2s) + 3.bin", " logo pad + Charg@0xCB70 + 1.bin@0x1B8F0 + 2.bin@0x9D07D + 3.bin@0xA71AC(FIXED)",
" 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)", " 0x00100000 UI0902 图片 (模式选择、开机 Logo、充电、烧录模式、设置/调音台等)",
" 0x001D2000 UI0902_FLASH_MODE 烧录模式全屏图Boot 读取)", " 0x001D2000 UI0902_FLASH_MODE 烧录模式全屏图Boot 读取)",
"", "",