K1Guitar/tools/pack_extflash_tone_0903.py

260 lines
11 KiB
Python
Raw Permalink 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
# -*- coding: utf-8 -*-
"""Pack external W25Q128 tone/logo image for K1 (0903).
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)
0x0000CB70 Charg.bin (legacy pad; charge UI uses UI0902_CHARGE_SCREEN full-screen)
0x0001B8F0 1.bin 普通/专业 31 rhythms
0x0009D07D 2.bin 本地曲目(变长,尾部 0xFF 填到 BIN3<=41KB
0x000A71AC 3.bin 万能模式(固定;勿随 2.bin 长度漂移)
Outputs:
tools/out/extflash_tone_0903.bin
tools/out/extflash_tone_0903.res (same bytes, .res alias)
tools/out/extflash_ALL_tone0903_ui0902.res (tone + pad + ui0902, if ui pack present)
project/inc/ExtFlash_Tone_Addr.h
Doc/音色文件/0903/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"
OUT_BIN = OUT_DIR / "extflash_tone_0903.bin"
OUT_RES = OUT_DIR / "extflash_tone_0903.res"
OUT_ALL_RES = OUT_DIR / "extflash_ALL_tone0903_ui0902.res"
OUT_HDR = ROOT / "project" / "inc" / "ExtFlash_Tone_Addr.h"
OUT_MAP = REPO / "Doc" / "音色文件" / "0903" / "FLASH_MAP.txt"
UI0902_BIN = OUT_DIR / "ui0902_res.bin"
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"
MAX_SONG_BIN_BYTES = 41 * 1024 # 曲目文件(2.bin)硬上限 41KB
BIN2_SLOT_BYTES = OFF_BIN3 - OFF_BIN2 # 41263; must keep BIN3 fixed
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("<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 require_region_equals(blob: bytes, off: int, src: bytes, label: str) -> 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:
ziliao = find_ziliao()
bin1_path = TONE_DIR / "1.bin"
bin2_path = TONE_DIR / "2.bin"
bin3_path = TONE_DIR / "3.bin"
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, "万能模式(固定偏移)"),
]
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)"
)
if len(bin2_data) > BIN2_SLOT_BYTES:
raise SystemExit(
f"2.bin too large for fixed BIN3 slot: {len(bin2_data)} > {BIN2_SLOT_BYTES} "
f"(would shift 3.bin past 0x{OFF_BIN3:X})"
)
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; enlarge next fixed gap or shrink prior file)"
)
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) # 总音色 .bin 同步为 .res
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()
# Hard integrity gates
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_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")
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 "0903"
#define EXTFLASH_TONE_RES_VER_MAJOR 0
#define EXTFLASH_TONE_RES_VER_MINOR 9
#define EXTFLASH_TONE_RES_VER_PATCH 3
#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 in 0903; left unchanged in firmware */
#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 = [
"K1 external Flash map — tone pack 0903",
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={0x100000 - cursor} bytes",
"",
"NOTE: BIN3 is FIXED at 0x000A71AC. 2.bin must be <= 41KB and <= slot 41263 bytes.",
"",
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)",
" 万能 -> FLASH_ADDR_MODE_UNIVERSAL (3.bin @ 0xA71AC FIXED)",
" 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",
" Charging -> EXTFLASH_CHARGING_ADDR",
"",
"Add a local song:",
" 1. Pack the new preset into 2.bin (must stay <= 41KB / 41263 slot)",
" 2. Append a row to local_songs.csv (index,code,name)",
" 3. python tools/pack_extflash_tone_0903.py (also regenerates LocalSongNames.h)",
" 4. Rebuild firmware and flash MCU + ExtFlash ALL.res",
]
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}")
if cursor > UI0902_RES_BASE:
raise SystemExit("ERROR: pack overflows into UI0902_RES_BASE")
print(f"OK: {UI0902_RES_BASE - cursor} bytes free before UI0902 @ 0x{UI0902_RES_BASE:X}")
print("OK: BIN1/BIN2/BIN3 present at fixed addresses and match source files")
print(f"OK: 2.bin size {len(bin2_data)} <= 41KB ({MAX_SONG_BIN_BYTES})")
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)
OUT_ALL_RES.write_bytes(all_res)
# Mirror under repo tools/out for publish/consumers
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_RES.name).write_bytes(packed)
print(f"Wrote {OUT_ALL_RES} ({len(all_res)} bytes) — verified 1/2/3.bin")
else:
print(f"WARN: {UI0902_BIN} missing; skipped ALL.res")
gen = ROOT / "tools" / "gen_local_song_names.py"
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()