K1Guitar/tools/pack_extflash_tone_0903.py

283 lines
12 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
# -*- 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("<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:
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()