from __future__ import annotations import re from collections.abc import Iterable, Mapping from .formatting import h16 from .model import Instruction LCD_STATUS_CONTROL = 0xF200 LCD_DATA = 0xF201 LCD_ADDRESSES = { LCD_STATUS_CONTROL: "lcd_status_control", LCD_DATA: "lcd_data", } ADDRESS_RE = re.compile(r"H'(?P[0-9A-Fa-f]{4})|0x(?P[0-9A-Fa-f]{4})") REGISTER_RE = re.compile(r"\b(?PR[0-7])\b") def analyze_lcd_driver(instructions: Mapping[int, Instruction] | Iterable[Instruction]) -> dict[str, object]: ordered = _instruction_sequence(instructions) by_address = {ins.address: ins for ins in ordered} accesses = [_access_for_instruction(ins) for ins in ordered] accesses = [access for access in accesses if access] instruction_metadata: dict[int, list[dict[str, object]]] = {} for access in accesses: instruction_metadata.setdefault(int(access["address"]), []).append(access) polling_loops = _find_polling_loops(ordered, by_address) for loop in polling_loops: for address, role in ( (int(loop["read_address"]), "lcd_busy_status_read"), (int(loop["test_address"]), "lcd_busy_flag_test"), (int(loop["branch_address"]), "lcd_busy_wait_branch"), ): instruction_metadata.setdefault(address, []).append( { "address": address, "kind": role, "summary": loop["summary"], "loop_start": loop["read_address"], }, ) routines = _routine_candidates(ordered, accesses, polling_loops) return { "addresses": [ {"address": address, "name": name, "role": _address_role(address)} for address, name in LCD_ADDRESSES.items() ], "accesses": accesses, "polling_loops": polling_loops, "routines": routines, "instructions": instruction_metadata, } def lcd_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str: metadata = lcd_metadata_for_instruction(analysis, address) if not metadata: return "" summaries = [] for item in metadata: if isinstance(item, Mapping) and item.get("summary"): summary = str(item["summary"]) if summary not in summaries: summaries.append(summary) return "; ".join(summaries) def lcd_metadata_for_instruction( analysis: Mapping[str, object] | None, address: int, ) -> list[dict[str, object]]: if not analysis: return [] instructions = analysis.get("instructions") if not isinstance(instructions, Mapping): return [] metadata = instructions.get(address) return list(metadata) if isinstance(metadata, list) else [] def _instruction_sequence(instructions: Mapping[int, Instruction] | Iterable[Instruction]) -> list[Instruction]: values = instructions.values() if isinstance(instructions, Mapping) else instructions return sorted(values, key=lambda ins: ins.address) def _access_for_instruction(ins: Instruction) -> dict[str, object] | None: address = _lcd_address_for_instruction(ins) if address is None: return None direction = _direction_for_instruction(ins, address) role = _access_role(ins, address, direction) register = _last_register(ins.operands) return { "address": ins.address, "instruction": ins.text, "lcd_address": address, "lcd_name": LCD_ADDRESSES[address], "direction": direction, "role": role, "register": register, "summary": _access_summary(address, direction, role), } def _lcd_address_for_instruction(ins: Instruction) -> int | None: for address in ins.references: if address in LCD_ADDRESSES: return address for match in ADDRESS_RE.finditer(ins.operands): value = int(match.group("hex") or match.group("c_hex"), 16) if value in LCD_ADDRESSES: return value return None def _direction_for_instruction(ins: Instruction, lcd_address: int) -> str: mnemonic = ins.mnemonic.upper() operands = [part.strip() for part in ins.operands.split(",")] if mnemonic.startswith("MOVFPE"): return "read" if mnemonic.startswith("MOVTPE"): return "write" if len(operands) >= 2: source_has_lcd = _operand_has_address(operands[0], lcd_address) dest_has_lcd = _operand_has_address(operands[-1], lcd_address) if source_has_lcd and not dest_has_lcd: return "read" if dest_has_lcd and not source_has_lcd: return "write" return "unknown" def _operand_has_address(operand: str, address: int) -> bool: return f"H'{address:04X}" in operand.upper() or f"0X{address:04X}" in operand.upper() def _access_role(ins: Instruction, address: int, direction: str) -> str: if address == LCD_STATUS_CONTROL and direction == "read": return "lcd_status_read" if address == LCD_STATUS_CONTROL and direction == "write": return "lcd_command_or_address_write" if address == LCD_DATA and direction == "read": return "lcd_data_read" if address == LCD_DATA and direction == "write": return "lcd_data_write" return "lcd_access" def _access_summary(address: int, direction: str, role: str) -> str: if role == "lcd_status_read": return f"LCD status read from E-clock {h16(address)}" if role == "lcd_command_or_address_write": return f"LCD command/address write to E-clock {h16(address)}" if role == "lcd_data_read": return f"LCD data read from E-clock {h16(address)}" if role == "lcd_data_write": return f"LCD data write to E-clock {h16(address)}" return f"LCD {direction} at E-clock {h16(address)}" def _find_polling_loops( ordered: list[Instruction], by_address: Mapping[int, Instruction], ) -> list[dict[str, object]]: loops: list[dict[str, object]] = [] for index, ins in enumerate(ordered): access = _access_for_instruction(ins) if not access or access["role"] != "lcd_status_read": continue register = access.get("register") if not isinstance(register, str): continue test = _next_register_bit_test(ordered, index, register) if test is None: continue branch = _next_back_branch_to(ordered, by_address, ordered.index(test), ins.address, test.address) if branch is None: continue loops.append( { "read_address": ins.address, "test_address": test.address, "branch_address": branch.address, "register": register, "bit": 7, "summary": f"LCD busy-flag poll: read {h16(LCD_STATUS_CONTROL)}, test bit 7, branch until clear", }, ) return loops def _next_register_bit_test(ordered: list[Instruction], index: int, register: str) -> Instruction | None: for candidate in ordered[index + 1 : index + 5]: if not candidate.mnemonic.upper().startswith("BTST"): continue if "#7" not in candidate.operands: continue if re.search(rf"\b{re.escape(register)}\b", candidate.operands): return candidate return None def _next_back_branch_to( ordered: list[Instruction], by_address: Mapping[int, Instruction], index: int, read_address: int, test_address: int, ) -> Instruction | None: _ = by_address for candidate in ordered[index + 1 : index + 5]: if candidate.kind != "branch": continue targets = [int(target) for target in candidate.targets] if read_address in targets or test_address in targets: return candidate return None def _routine_candidates( ordered: list[Instruction], accesses: list[dict[str, object]], polling_loops: list[dict[str, object]], ) -> list[dict[str, object]]: if not accesses: return [] address_to_access = {int(access["address"]): access for access in accesses} loop_addresses = {int(loop["read_address"]) for loop in polling_loops} routines: dict[tuple[int, int], dict[str, object]] = {} for address in sorted(address_to_access): start, end = _routine_span(ordered, address) key = (start, end) routine = routines.setdefault( key, { "start": start, "end": end, "accesses": [], "roles": [], "role_hint": "lcd_access", }, ) routine["accesses"].append(address_to_access[address]) for routine in routines.values(): roles = sorted({str(access["role"]) for access in routine["accesses"]}) routine["roles"] = roles routine["role_hint"] = _routine_role_hint(routine, loop_addresses) return sorted(routines.values(), key=lambda item: int(item["start"])) def _routine_span(ordered: list[Instruction], address: int) -> tuple[int, int]: addresses = [ins.address for ins in ordered] index = addresses.index(address) start = ordered[index].address cursor = index - 1 while cursor >= 0: previous = ordered[cursor] if previous.kind in {"return", "rte"}: break if previous.address + max(previous.size, 1) != start: break start = previous.address cursor -= 1 end = ordered[index].address cursor = index while cursor < len(ordered): current = ordered[cursor] end = current.address if current.kind in {"return", "rte"}: break next_index = cursor + 1 if next_index >= len(ordered): break next_address = ordered[next_index].address if current.address + max(current.size, 1) != next_address: break cursor = next_index return start, end def _routine_role_hint(routine: Mapping[str, object], loop_addresses: set[int]) -> str: access_addresses = {int(access["address"]) for access in routine.get("accesses", []) if isinstance(access, Mapping)} roles = {str(role) for role in routine.get("roles", [])} if access_addresses & loop_addresses and "lcd_data_write" in roles and "lcd_command_or_address_write" in roles: return "lcd_wait_and_transfer" if access_addresses & loop_addresses: return "lcd_wait_ready" if "lcd_data_write" in roles: return "lcd_write_data" if "lcd_command_or_address_write" in roles: return "lcd_write_command_or_address" if "lcd_data_read" in roles: return "lcd_read_data" return "lcd_access" def _last_register(operands: str) -> str | None: matches = list(REGISTER_RE.finditer(operands)) return matches[-1].group("reg") if matches else None def _address_role(address: int) -> str: if address == LCD_STATUS_CONTROL: return "status/control register inferred from busy polling and command writes" if address == LCD_DATA: return "data register inferred from paired data reads/writes" return "unknown"