traces
This commit is contained in:
306
h8536/panel_button_trace.py
Normal file
306
h8536/panel_button_trace.py
Normal file
@@ -0,0 +1,306 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .decoder import H8536Decoder
|
||||
from .formatting import h16
|
||||
from .rom import Rom
|
||||
|
||||
|
||||
DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN")
|
||||
DEFAULT_TEXT_OUTPUT = Path("build/panel_button_trace.md")
|
||||
DEFAULT_JSON_OUTPUT = Path("build/panel_button_trace.json")
|
||||
|
||||
BUTTON_TABLE_BASE = 0x2706
|
||||
NOOP_HANDLER = 0x1C25
|
||||
QUEUE_FUNCTION = 0x3E54
|
||||
|
||||
INPUT_BYTES = [
|
||||
{
|
||||
"shadow": "F6D7",
|
||||
"source": "F102",
|
||||
"dirty": "F6F2.7",
|
||||
"dispatcher": "1B2D",
|
||||
"initial_r5": 0x7E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D6",
|
||||
"source": "F103",
|
||||
"dirty": "F6F2.6",
|
||||
"dispatcher": "1B44",
|
||||
"initial_r5": 0x6E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D5",
|
||||
"source": "F104",
|
||||
"dirty": "F6F2.5",
|
||||
"dispatcher": "1B5B",
|
||||
"initial_r5": 0x5E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D4",
|
||||
"source": "F105",
|
||||
"dirty": "F6F2.4",
|
||||
"dispatcher": "1BA0",
|
||||
"initial_r5": 0x4E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D3",
|
||||
"source": "F106",
|
||||
"dirty": "F6F2.3",
|
||||
"dispatcher": "1BB6",
|
||||
"initial_r5": 0x3E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D2",
|
||||
"source": "F107",
|
||||
"dirty": "F6F2.2",
|
||||
"dispatcher": "1BCC",
|
||||
"initial_r5": 0x2E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D1",
|
||||
"source": "F108",
|
||||
"dirty": "F6F2.1",
|
||||
"dispatcher": "1B72",
|
||||
"initial_r5": 0x1E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6D0",
|
||||
"source": "F109",
|
||||
"dirty": "F6F2.0",
|
||||
"dispatcher": "1B89",
|
||||
"initial_r5": 0x0E,
|
||||
"bank": "IRQ4 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6DC",
|
||||
"source": "F005",
|
||||
"dirty": "F6F3.4",
|
||||
"dispatcher": "1BE2",
|
||||
"initial_r5": 0xCE,
|
||||
"bank": "IRQ3 A8 panel byte path",
|
||||
},
|
||||
{
|
||||
"shadow": "F6DB",
|
||||
"source": "F006",
|
||||
"dirty": "F6F3.3",
|
||||
"dispatcher": "1BF8",
|
||||
"initial_r5": 0xBE,
|
||||
"bank": "IRQ3 A8 panel byte path",
|
||||
},
|
||||
]
|
||||
|
||||
KNOWN_REPORTS = {
|
||||
0x0007: "CAM POWER",
|
||||
0x0015: "CALL",
|
||||
}
|
||||
|
||||
|
||||
def analyze_panel_buttons(rom_bytes: bytes) -> dict[str, Any]:
|
||||
rom = Rom(rom_bytes)
|
||||
entries = _table_entries(rom)
|
||||
handler_summaries = {
|
||||
target: _summarize_handler(rom, target)
|
||||
for target in sorted({entry["handler"] for entry in entries if entry["handler"] != NOOP_HANDLER})
|
||||
}
|
||||
for entry in entries:
|
||||
entry["handler_summary"] = handler_summaries.get(entry["handler"], {})
|
||||
selectors = entry["handler_summary"].get("report_selectors", [])
|
||||
entry["known_report"] = ", ".join(
|
||||
KNOWN_REPORTS.get(int(selector), f"0x{int(selector):04X}") for selector in selectors
|
||||
)
|
||||
return {
|
||||
"kind": "panel_button_trace",
|
||||
"button_table_base": BUTTON_TABLE_BASE,
|
||||
"button_table_base_hex": h16(BUTTON_TABLE_BASE),
|
||||
"noop_handler": NOOP_HANDLER,
|
||||
"noop_handler_hex": h16(NOOP_HANDLER),
|
||||
"entries": entries,
|
||||
"known_paths": _known_paths(entries),
|
||||
"handler_summaries": [
|
||||
{"handler": handler, "handler_hex": h16(handler), **summary}
|
||||
for handler, summary in sorted(handler_summaries.items())
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def format_markdown(analysis: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
"# PT2 Known Button ROM Trace",
|
||||
"",
|
||||
"This report follows the panel button edge path from the serial-visible reports back into the ROM input scanner.",
|
||||
"The key table is the indirect handler table at `H'2706`, used by `loc_1C0E` after byte-level panel input changes are detected.",
|
||||
"",
|
||||
"## Known Anchors",
|
||||
"",
|
||||
]
|
||||
for path in analysis["known_paths"]:
|
||||
lines.extend(
|
||||
[
|
||||
f"### {path['name']}",
|
||||
"",
|
||||
f"- Emitted selector: `0x{path['selector']:04X}`",
|
||||
f"- Handler: `H'{path['handler']:04X}`",
|
||||
f"- Edge source: `{path['source']} -> {path['shadow']}` via `{path['dirty']}`",
|
||||
f"- Trigger bit: `{path['shadow']}.{path['bit']}`",
|
||||
f"- Table slot: `H'{path['table_address']:04X}` -> `H'{path['handler']:04X}`",
|
||||
f"- Current-level tests: {', '.join(path['handler_summary'].get('level_tests', [])) or 'none found'}",
|
||||
f"- State writes: {', '.join(path['handler_summary'].get('state_writes', [])) or 'none found'}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"## Button Matrix Entries With Serial Reports",
|
||||
"",
|
||||
"| Source | Shadow bit | Dirty | Handler | Selector(s) | State writes |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for entry in analysis["entries"]:
|
||||
selectors = entry["handler_summary"].get("report_selectors", [])
|
||||
if not selectors:
|
||||
continue
|
||||
selector_text = ", ".join(f"`0x{int(selector):04X}`" for selector in selectors)
|
||||
writes = ", ".join(f"`{write}`" for write in entry["handler_summary"].get("state_writes", [])[:4])
|
||||
lines.append(
|
||||
f"| `{entry['source']}` | `{entry['shadow']}.{entry['bit']}` | `{entry['dirty']}` | "
|
||||
f"`H'{entry['handler']:04X}` | {selector_text} | {writes} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Practical Read",
|
||||
"",
|
||||
"- CALL and CAM POWER do share the general panel edge path with many other buttons.",
|
||||
"- The shared path is: panel byte snapshot -> shadow byte -> dirty bit -> `loc_1C0E` jump table -> handler -> `loc_3E54` report.",
|
||||
"- Other buttons diverge in their handlers: many require `F731/F730/F791` session/menu gates, mutate page state, or emit different selectors.",
|
||||
"- Some table entries are `H'1C25`, an immediate `RTS`, so those physical matrix positions are intentionally ignored in this firmware context.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Trace panel button matrix handlers and known CALL/CAM report sources.")
|
||||
parser.add_argument("rom", nargs="?", type=Path, default=DEFAULT_ROM)
|
||||
parser.add_argument("--out", type=Path, default=DEFAULT_TEXT_OUTPUT)
|
||||
parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON_OUTPUT)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
analysis = analyze_panel_buttons(args.rom.read_bytes())
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(format_markdown(analysis), encoding="utf-8")
|
||||
args.json_out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.json_out.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(f"wrote {args.out}")
|
||||
print(f"wrote {args.json_out}")
|
||||
return 0
|
||||
|
||||
|
||||
def _table_entries(rom: Rom) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for lane in INPUT_BYTES:
|
||||
initial_r5 = int(lane["initial_r5"])
|
||||
for bit in range(7, -1, -1):
|
||||
table_offset = initial_r5 - (2 * (7 - bit))
|
||||
table_address = BUTTON_TABLE_BASE + table_offset
|
||||
handler = rom.u16(table_address)
|
||||
rows.append(
|
||||
{
|
||||
"source": lane["source"],
|
||||
"shadow": lane["shadow"],
|
||||
"dirty": lane["dirty"],
|
||||
"dispatcher": lane["dispatcher"],
|
||||
"bank": lane["bank"],
|
||||
"bit": bit,
|
||||
"table_offset": table_offset,
|
||||
"table_address": table_address,
|
||||
"table_address_hex": h16(table_address),
|
||||
"handler": handler,
|
||||
"handler_hex": h16(handler),
|
||||
"is_noop": handler == NOOP_HANDLER,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _known_paths(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
paths: list[dict[str, Any]] = []
|
||||
for entry in entries:
|
||||
selectors = entry["handler_summary"].get("report_selectors", [])
|
||||
for selector in selectors:
|
||||
name = KNOWN_REPORTS.get(int(selector))
|
||||
if not name:
|
||||
continue
|
||||
paths.append({"name": name, "selector": int(selector), **entry})
|
||||
return paths
|
||||
|
||||
|
||||
def _summarize_handler(rom: Rom, address: int) -> dict[str, Any]:
|
||||
instructions = _decode_linear_handler(rom, address)
|
||||
report_selectors: list[int] = []
|
||||
last_r3: int | None = None
|
||||
level_tests: list[str] = []
|
||||
state_writes: list[str] = []
|
||||
for ins in instructions:
|
||||
text = ins.text
|
||||
if match := re.search(r"MOV:[^#]+#H'([0-9A-F]{4}), R3", text):
|
||||
last_r3 = int(match.group(1), 16)
|
||||
if QUEUE_FUNCTION in ins.targets and last_r3 is not None:
|
||||
if last_r3 not in report_selectors:
|
||||
report_selectors.append(last_r3)
|
||||
if match := re.search(r"BTST\.B #([0-7]), @H'(F6[0-9A-F]{2})", text):
|
||||
test = f"{match.group(2)}.{match.group(1)}"
|
||||
if test not in level_tests:
|
||||
level_tests.append(test)
|
||||
if match := re.search(r"@H'(E[89][0-9A-F]{2})", text):
|
||||
write = match.group(1)
|
||||
if _looks_like_write(text) and write not in state_writes:
|
||||
state_writes.append(write)
|
||||
return {
|
||||
"report_selectors": report_selectors,
|
||||
"report_selectors_hex": [f"0x{selector:04X}" for selector in report_selectors],
|
||||
"level_tests": level_tests,
|
||||
"state_writes": state_writes,
|
||||
"instruction_count_scanned": len(instructions),
|
||||
}
|
||||
|
||||
|
||||
def _decode_linear_handler(rom: Rom, address: int, *, max_bytes: int = 0x90) -> list[Any]:
|
||||
decoder = H8536Decoder(rom, labels={QUEUE_FUNCTION: "loc_3E54"})
|
||||
instructions: list[Any] = []
|
||||
pc = address
|
||||
end = min(len(rom.data), address + max_bytes)
|
||||
while pc < end:
|
||||
ins = decoder.decode(pc)
|
||||
instructions.append(ins)
|
||||
pc += max(1, ins.size)
|
||||
if ins.mnemonic == "RTS":
|
||||
break
|
||||
return instructions
|
||||
|
||||
|
||||
def _looks_like_write(text: str) -> bool:
|
||||
return text.startswith("MOV") or text.startswith("BSET") or text.startswith("BCLR") or text.startswith("CLR")
|
||||
|
||||
|
||||
__all__ = ["analyze_panel_buttons", "format_markdown", "main"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user