from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass from .formatting import h16 from .model import Instruction MANUAL_REFERENCES = [ "Manual/0900766b802125d0.md:12185 FRT FRC/OCRA/OCRB/ICR use TEMP for 16-bit CPU access", "Manual/0900766b802125d0.md:12193 FRT byte access order is upper byte then lower byte", "Manual/0900766b802125d0.md:12212 OCRA/OCRB reads are direct; writes still use TEMP", "Manual/0900766b802125d0.md:17546 A/D ADDRA-ADDRD lower byte is accessed through TEMP", "Manual/0900766b802125d0.md:17556 A/D full-result byte reads must be upper byte then lower byte", ] @dataclass(frozen=True) class TempRegisterPair: name: str high: int low: int module: str read_only: bool = False high_only_read_ok: bool = False direct_read_without_temp: bool = False TEMP_REGISTER_PAIRS: tuple[TempRegisterPair, ...] = ( TempRegisterPair("FRT1_FRC", 0xFE92, 0xFE93, "FRT TEMP"), TempRegisterPair("FRT1_OCRA", 0xFE94, 0xFE95, "FRT TEMP", direct_read_without_temp=True), TempRegisterPair("FRT1_OCRB", 0xFE96, 0xFE97, "FRT TEMP", direct_read_without_temp=True), TempRegisterPair("FRT1_ICR", 0xFE98, 0xFE99, "FRT TEMP", read_only=True), TempRegisterPair("FRT2_FRC", 0xFEA2, 0xFEA3, "FRT TEMP"), TempRegisterPair("FRT2_OCRA", 0xFEA4, 0xFEA5, "FRT TEMP", direct_read_without_temp=True), TempRegisterPair("FRT2_OCRB", 0xFEA6, 0xFEA7, "FRT TEMP", direct_read_without_temp=True), TempRegisterPair("FRT2_ICR", 0xFEA8, 0xFEA9, "FRT TEMP", read_only=True), TempRegisterPair("FRT3_FRC", 0xFEB2, 0xFEB3, "FRT TEMP"), TempRegisterPair("FRT3_OCRA", 0xFEB4, 0xFEB5, "FRT TEMP", direct_read_without_temp=True), TempRegisterPair("FRT3_OCRB", 0xFEB6, 0xFEB7, "FRT TEMP", direct_read_without_temp=True), TempRegisterPair("FRT3_ICR", 0xFEB8, 0xFEB9, "FRT TEMP", read_only=True), TempRegisterPair("ADDRA", 0xFEE0, 0xFEE1, "A/D TEMP", read_only=True, high_only_read_ok=True), TempRegisterPair("ADDRB", 0xFEE2, 0xFEE3, "A/D TEMP", read_only=True, high_only_read_ok=True), TempRegisterPair("ADDRC", 0xFEE4, 0xFEE5, "A/D TEMP", read_only=True, high_only_read_ok=True), TempRegisterPair("ADDRD", 0xFEE6, 0xFEE7, "A/D TEMP", read_only=True, high_only_read_ok=True), ) TEMP_PAIR_BY_ADDRESS = { address: (pair, byte_name) for pair in TEMP_REGISTER_PAIRS for address, byte_name in ((pair.high, "high"), (pair.low, "low")) } READ_ONLY_ROOTS = {"BTST", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"} WRITE_ROOTS = {"BCLR", "BNOT", "BSET", "CLR", "MOV:G", "MOV:S", "MOVTPE", "NEG", "NOT"} def analyze_peripheral_access(instructions: Mapping[int, Instruction]) -> dict[str, object]: annotations: dict[int, list[str]] = {} warnings: list[dict[str, object]] = [] instruction_metadata: dict[int, list[dict[str, object]]] = {} pending: dict[tuple[str, str], dict[str, object]] = {} for address in sorted(instructions): ins = instructions[address] accesses = _instruction_accesses(ins) if not accesses: continue instruction_metadata[address] = [_public_access(access) for access in accesses] for access in accesses: note, warning = _analyze_access(access, pending) if note: annotations.setdefault(address, []).append(note) if warning: warnings.append(warning) for item in pending.values(): pair = item["pair"] assert isinstance(pair, TempRegisterPair) if _pending_high_byte_is_suspicious(pair, str(item["direction"])): warnings.append( { "address": item["address"], "register": pair.name, "severity": "warning", "message": ( f"{pair.name} high byte access was not followed by low byte; " "manual recommends word access or high-byte then low-byte" ), "manual": MANUAL_REFERENCES, }, ) return { "manual_references": MANUAL_REFERENCES, "warnings": warnings, "annotations": { address: "; ".join(parts) for address, parts in sorted(annotations.items()) }, "instructions": instruction_metadata, } def peripheral_comment_for_instruction(analysis: Mapping[str, object] | None, address: int) -> str: if not analysis: return "" annotations = analysis.get("annotations") if not isinstance(annotations, Mapping): return "" comment = annotations.get(address) return str(comment) if comment else "" def peripheral_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 metadata if isinstance(metadata, list) else [] def peripheral_json_payload(analysis: Mapping[str, object] | None) -> dict[str, object]: if not analysis: return {"manual_references": MANUAL_REFERENCES, "warnings": []} return { "manual_references": analysis.get("manual_references", MANUAL_REFERENCES), "warnings": analysis.get("warnings", []), } def _analyze_access( access: dict[str, object], pending: dict[tuple[str, str], dict[str, object]], ) -> tuple[str, dict[str, object] | None]: pair = access["pair"] assert isinstance(pair, TempRegisterPair) direction = str(access["direction"]) byte = str(access["byte"]) size = str(access["size"]) key = (pair.name, direction) if size == "W" and byte == "high": pending.pop(key, None) return f"{pair.name} word {direction}; TEMP byte-order hazard avoided", None if byte == "high": pending[key] = access if direction == "read" and pair.high_only_read_ok: return f"{pair.name} high-byte read; valid 8-bit result, read low byte next for full 10-bit value", None if direction == "read" and pair.direct_read_without_temp: return f"{pair.name} high-byte read; OCRA/OCRB reads do not use TEMP", None return f"{pair.name} high-byte {direction}; next low-byte access completes TEMP transfer", None previous = pending.pop(key, None) if previous is not None: return f"{pair.name} low-byte {direction}; completes high->low TEMP transfer", None warning = { "address": access["address"], "register": pair.name, "severity": "warning", "message": ( f"{pair.name} low byte {direction} before matching high byte; " "TEMP may contain stale or unrelated data" ), "manual": MANUAL_REFERENCES, } return f"{pair.name} low-byte {direction} before high byte; check TEMP ordering", warning def _pending_high_byte_is_suspicious(pair: TempRegisterPair, direction: str) -> bool: if direction == "read" and pair.high_only_read_ok: return False if direction == "read" and pair.direct_read_without_temp: return False return True def _instruction_accesses(ins: Instruction) -> list[dict[str, object]]: size = _mnemonic_size(ins.mnemonic) direction = _access_direction(ins) if direction is None: return [] accesses: list[dict[str, object]] = [] for ref in ins.references: pair_info = TEMP_PAIR_BY_ADDRESS.get(ref) if pair_info is None: continue pair, byte = pair_info if size == "W" and ref == pair.low: byte = "low_unaligned" accesses.append( { "address": ins.address, "instruction": ins.text, "register": pair.name, "pair": pair, "high_address": pair.high, "low_address": pair.low, "referenced_address": ref, "referenced_address_hex": h16(ref), "byte": byte, "size": size, "direction": direction, }, ) return accesses def _public_access(access: dict[str, object]) -> dict[str, object]: return { key: value for key, value in access.items() if key != "pair" } def _access_direction(ins: Instruction) -> str | None: root = _mnemonic_root(ins.mnemonic) if root in READ_ONLY_ROOTS: return "read" if root in {"BCLR", "BNOT", "BSET", "CLR", "NEG", "NOT"}: return "write" if root in {"ADD:Q", "ADD:G", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}: return "write" if root in {"MOV:G", "MOV:S", "MOVTPE"}: return "write" if _destination_operand(ins.operands).startswith("@") else "read" if root in {"MOV:L", "MOV:F", "MOVFPE"}: return "read" if root == "STC": return "write" if root == "LDC": return "read" return None def _destination_operand(operands: str) -> str: if "," not in operands: return operands.strip() return operands.rsplit(",", 1)[1].strip() def _mnemonic_root(mnemonic: str) -> str: return mnemonic.rsplit(".", 1)[0] def _mnemonic_size(mnemonic: str) -> str: return "W" if mnemonic.endswith(".W") else "B"