260 lines
11 KiB
Python
260 lines
11 KiB
Python
#!/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()
|