#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Pack external W25Q128 tone/logo image for K1 (0914). Layout (absolute W25Q128 offsets): 0x00000000 logo.bin (legacy pad; boot UI uses UI0902 full-screen logo) 0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN) 0x0001B8F0 1.bin 普通/专业 31 rhythms 0x0009D07D 2.bin 本地曲目(变长,<=41KB) after 2.bin 3.bin 万能模式(随 2.bin 长度后移;写入 ExtFlash_Tone_Addr.h) Outputs: tools/out/extflash_tone_0914.bin / .res tools/out/extflash_ALL_tone0914_ui0902.res project/inc/ExtFlash_Tone_Addr.h Doc/音色文件/0914/FLASH_MAP.txt """ from __future__ import annotations import struct import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] # firmware project root REPO = ROOT.parent.parent # 一诺国际吉他 OUT_DIR = ROOT / "tools" / "out" TONE_TAG = "0914" OUT_BIN = OUT_DIR / f"extflash_tone_{TONE_TAG}.bin" OUT_RES = OUT_DIR / f"extflash_tone_{TONE_TAG}.res" OUT_ALL_RES = OUT_DIR / f"extflash_ALL_tone{TONE_TAG}_ui0902.res" # Keep legacy alias names for older tools that still look for 0903 OUT_BIN_LEGACY = OUT_DIR / "extflash_tone_0903.bin" OUT_RES_LEGACY = OUT_DIR / "extflash_tone_0903.res" OUT_ALL_LEGACY = OUT_DIR / "extflash_ALL_tone0903_ui0902.res" OUT_HDR = ROOT / "project" / "inc" / "ExtFlash_Tone_Addr.h" OUT_MAP = REPO / "Doc" / "音色文件" / TONE_TAG / "FLASH_MAP.txt" UI0902_BIN = OUT_DIR / "ui0902_res.bin" TONE_DIR = REPO / "Doc" / "音色文件" / TONE_TAG OFF_LOGO = 0x00000000 OFF_CHARGING = 0x0000CB70 OFF_BIN1 = 0x0001B8F0 OFF_BIN2 = 0x0009D07D # BIN3 placed immediately after 2.bin (computed at pack time) UI0902_RES_BASE = 0x00100000 DAB_MAGIC = b"\xABDAB" MAX_SONG_BIN_BYTES = 41 * 1024 # 曲目文件(2.bin)硬上限 41KB def find_ziliao() -> Path: for p in REPO.iterdir(): if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file(): return p 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(" None: end = off + len(src) if end > len(blob): raise SystemExit(f"{label}: pack too short for region @0x{off:X}+{len(src)}") if blob[off:end] != src: raise SystemExit(f"{label}: bytes @0x{off:X} do not match source file") def main() -> None: if not TONE_DIR.is_dir(): raise SystemExit(f"missing tone dir: {TONE_DIR}") ziliao = find_ziliao() bin1_path = TONE_DIR / "1.bin" bin2_path = TONE_DIR / "2.bin" bin3_path = TONE_DIR / "3.bin" for p in (bin1_path, bin2_path, bin3_path): if not p.is_file(): raise SystemExit(f"missing {p}") bin2_data = bin2_path.read_bytes() if len(bin2_data) > MAX_SONG_BIN_BYTES: raise SystemExit( f"2.bin (曲目) too large: {len(bin2_data)} bytes > {MAX_SONG_BIN_BYTES} (41KB)" ) off_bin3 = OFF_BIN2 + len(bin2_data) parts = [ ("LOGO", ziliao / "logo.bin", OFF_LOGO, "legacy pad; boot uses UI0902_BOOT_LOGO"), ("CHARGING", ziliao / "Charg.bin", OFF_CHARGING, "legacy pad; UI uses UI0902_CHARGE_SCREEN"), ("BIN1_RHYTHM", bin1_path, OFF_BIN1, "普通/专业 31 条节奏"), ("BIN2_SONG_HAITIAN", bin2_path, OFF_BIN2, "本地曲目"), ("BIN3_UNIVERSAL", bin3_path, off_bin3, "万能模式(紧跟 2.bin 之后)"), ] blobs: list[bytes] = [] cursor = 0 rows: list[tuple[str, int, int, str]] = [] for name, path, force_off, note in parts: data = path.read_bytes() if cursor > force_off: raise SystemExit( f"{name}: cursor 0x{cursor:X} past forced 0x{force_off:X} " f"(previous blob too large)" ) if cursor < force_off: pad = force_off - cursor blobs.append(b"\xFF" * pad) rows.append(("(pad)", cursor, pad, "gap fill 0xFF")) cursor = force_off off = cursor blobs.append(data) cursor += len(data) rows.append((name, off, len(data), f"{note} <- {path.name}")) OUT_DIR.mkdir(parents=True, exist_ok=True) packed = b"".join(blobs) OUT_BIN.write_bytes(packed) OUT_RES.write_bytes(packed) OUT_BIN_LEGACY.write_bytes(packed) OUT_RES_LEGACY.write_bytes(packed) by_name = {r[0]: r for r in rows if not r[0].startswith("(")} bin1_data = bin1_path.read_bytes() bin3_data = bin3_path.read_bytes() dab_ok(packed, OFF_BIN1, expect_cnt=31) dab_ok(packed, OFF_BIN2, expect_cnt=1) dab_ok(packed, off_bin3, expect_cnt=3) if by_name["BIN1_RHYTHM"][1] != OFF_BIN1: raise SystemExit(f"BIN1 placed @0x{by_name['BIN1_RHYTHM'][1]:X} want 0x{OFF_BIN1:X}") if by_name["BIN2_SONG_HAITIAN"][1] != OFF_BIN2: raise SystemExit(f"BIN2 placed @0x{by_name['BIN2_SONG_HAITIAN'][1]:X} want 0x{OFF_BIN2:X}") if by_name["BIN3_UNIVERSAL"][1] != off_bin3: raise SystemExit(f"BIN3 placed @0x{by_name['BIN3_UNIVERSAL'][1]:X} want 0x{off_bin3:X}") require_region_equals(packed, OFF_LOGO, (ziliao / "logo.bin").read_bytes(), "LOGO") require_region_equals(packed, OFF_CHARGING, (ziliao / "Charg.bin").read_bytes(), "CHARGING") require_region_equals(packed, OFF_BIN1, bin1_data, "BIN1/1.bin") require_region_equals(packed, OFF_BIN2, bin2_data, "BIN2/2.bin") require_region_equals(packed, off_bin3, bin3_data, "BIN3/3.bin") if cursor > UI0902_RES_BASE: raise SystemExit( f"ERROR: tone pack end 0x{cursor:X} overflows UI0902 @ 0x{UI0902_RES_BASE:X}" ) hdr = f"""#ifndef __EXTFLASH_TONE_ADDR_H #define __EXTFLASH_TONE_ADDR_H /* Auto-generated by tools/pack_extflash_tone_0903.py — do not hand-edit. */ /* W25Q128 absolute offsets. Boot logo display uses UI0902_BOOT_LOGO_ADDR. */ /* Tone resource pack has no in-bin version field; release tag = EXTFLASH_TONE_RES_VER. */ #define EXTFLASH_TONE_RES_VER "{TONE_TAG}" #define EXTFLASH_TONE_RES_VER_MAJOR 0 #define EXTFLASH_TONE_RES_VER_MINOR 9 #define EXTFLASH_TONE_RES_VER_PATCH 14 #define EXTFLASH_LOGO_ADDR 0x{by_name['LOGO'][1]:08X}UL #define EXTFLASH_LOGO_SIZE {by_name['LOGO'][2]}UL #define EXTFLASH_CHARGING_ADDR 0x{by_name['CHARGING'][1]:08X}UL #define EXTFLASH_CHARGING_SIZE {by_name['CHARGING'][2]}UL #define EXTFLASH_CHARGING_W 160 #define EXTFLASH_CHARGING_H 190 #define EXTFLASH_BIN1_RHYTHM_ADDR 0x{OFF_BIN1:08X}UL #define EXTFLASH_BIN1_RHYTHM_SIZE {by_name['BIN1_RHYTHM'][2]}UL #define EXTFLASH_BIN2_SONG_HAITIAN_ADDR 0x{OFF_BIN2:08X}UL #define EXTFLASH_BIN2_SONG_HAITIAN_SIZE {by_name['BIN2_SONG_HAITIAN'][2]}UL #define EXTFLASH_BIN3_UNIVERSAL_ADDR 0x{off_bin3:08X}UL #define EXTFLASH_BIN3_UNIVERSAL_SIZE {by_name['BIN3_UNIVERSAL'][2]}UL /* Convenience aliases used by UI ADDRESS */ #define FLASH_ADDR_MODE_NORMAL EXTFLASH_BIN1_RHYTHM_ADDR /* 普通 */ #define FLASH_ADDR_MODE_EXPERT EXTFLASH_BIN1_RHYTHM_ADDR /* 专业 */ #define FLASH_ADDR_SONG_HAITIAN EXTFLASH_BIN2_SONG_HAITIAN_ADDR #define FLASH_ADDR_MODE_UNIVERSAL EXTFLASH_BIN3_UNIVERSAL_ADDR /* 万能 */ /* Legacy AutoBand bank — not repacked; may overlap 2.bin — do not enable until remapped */ #define FLASH_ADDR_AUTOBAND_LEGACY 0x0009EB5FUL #define EXTFLASH_TONE_PACK_END 0x{cursor:08X}UL #define EXTFLASH_TONE_PACK_SIZE {cursor}UL #define UI0902_RES_BASE_SAFE_GAP (0x00100000UL - EXTFLASH_TONE_PACK_END) #endif """ OUT_HDR.write_text(hdr, encoding="utf-8", newline="\n") map_lines = [ f"K1 external Flash map — tone pack {TONE_TAG}", f"Packed file: Code/.../tools/out/{OUT_BIN.name} ({len(packed)} bytes)", f"Also: {OUT_RES.name}; ALL.res when ui0902_res.bin present", f"Ends at 0x{cursor:X}; UI0902_RES_BASE=0x00100000; free gap={UI0902_RES_BASE - cursor} bytes", "", f"NOTE: BIN3 follows 2.bin @ 0x{off_bin3:X} (2.bin grew past old fixed 0xA71AC).", " 2.bin must be <= 41KB; LOGO/CHARG/BIN1/BIN2 offsets remain fixed.", "", f"{'Name':<22} {'Offset':>10} {'Size':>10} Note", "-" * 72, ] for name, off, size, note in rows: map_lines.append(f"{name:<22} 0x{off:08X} {size:10d} {note}") map_lines += [ "", "Firmware ADDRESS mapping:", " 普通/专业 -> FLASH_ADDR_MODE_NORMAL/EXPERT (1.bin @ 0x1B8F0)", " 本地曲目 -> FLASH_ADDR_SONG_HAITIAN (2.bin @ 0x9D07D, max 41KB)", f" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0x{off_bin3:X})", " Boot logo -> UI0902_BOOT_LOGO_ADDR (full-screen); packed logo.bin only pads 0x0..0xCB70", " Charging -> EXTFLASH_CHARGING_ADDR (legacy pad) / UI0902_CHARGE_SCREEN", "", "Rebuild after pack:", " 1. python tools/pack_extflash_tone_0903.py", " 2. Rebuild MCU (ExtFlash_Tone_Addr.h updated)", " 3. Flash MCU + ExtFlash ALL.res", ] OUT_MAP.parent.mkdir(parents=True, exist_ok=True) OUT_MAP.write_text("\n".join(map_lines) + "\n", encoding="utf-8", newline="\n") print(f"Wrote {OUT_BIN} ({len(packed)} bytes)") print(f"Wrote {OUT_RES} ({len(packed)} bytes)") print(f"Wrote {OUT_HDR}") print(f"Wrote {OUT_MAP}") for name, off, size, note in rows: print(f" 0x{off:08X} {size:8d} {name} {note}") print(f"OK: {UI0902_RES_BASE - cursor} bytes free before UI0902 @ 0x{UI0902_RES_BASE:X}") print("OK: LOGO/CHARG/BIN1/BIN2/BIN3 present and match source files") print(f"OK: 2.bin size {len(bin2_data)} <= 41KB; BIN3 @ 0x{off_bin3:X}") if UI0902_BIN.is_file(): ui = UI0902_BIN.read_bytes() all_res = packed + (b"\xFF" * (UI0902_RES_BASE - len(packed))) + ui require_region_equals(all_res, OFF_BIN1, bin1_data, "ALL.res BIN1") require_region_equals(all_res, OFF_BIN2, bin2_data, "ALL.res BIN2") require_region_equals(all_res, off_bin3, bin3_data, "ALL.res BIN3") dab_ok(all_res, OFF_BIN1, 31) dab_ok(all_res, OFF_BIN2, 1) dab_ok(all_res, off_bin3, 3) # UI0902 magic/presence: non-empty and starts within expected region if len(ui) < 1024: raise SystemExit(f"UI0902 pack suspiciously small: {len(ui)}") OUT_ALL_RES.write_bytes(all_res) OUT_ALL_LEGACY.write_bytes(all_res) repo_out = REPO / "tools" / "out" repo_out.mkdir(parents=True, exist_ok=True) (repo_out / OUT_ALL_RES.name).write_bytes(all_res) (repo_out / OUT_ALL_LEGACY.name).write_bytes(all_res) (repo_out / OUT_RES.name).write_bytes(packed) (repo_out / OUT_RES_LEGACY.name).write_bytes(packed) print(f"Wrote {OUT_ALL_RES} ({len(all_res)} bytes) — verified logo/charg/1/2/3 + UI0902") else: print(f"WARN: {UI0902_BIN} missing; skipped ALL.res") # Prefer 0914 CSV; fall back to 0903 csv_0914 = TONE_DIR / "local_songs.csv" gen = ROOT / "tools" / "gen_local_song_names.py" if not csv_0914.is_file(): src = REPO / "Doc" / "音色文件" / "0903" / "local_songs.csv" if src.is_file(): csv_0914.write_bytes(src.read_bytes()) print(f"Copied {src.name} -> {csv_0914}") r = subprocess.run([sys.executable, str(gen)], check=False) if r.returncode != 0: raise SystemExit(f"gen_local_song_names.py failed ({r.returncode})") if __name__ == "__main__": main()