from __future__ import annotations import argparse from dataclasses import dataclass, field from pathlib import Path from typing import Iterable from .decoder import H8536Decoder from .formatting import h8, h16 from .memory import MEMORY_REGIONS, MemoryRegion, region_for from .rom import DecodeError, Rom from .vectors import read_vectors_min SCI1_SMR = 0xFED8 SCI1_BRR = 0xFED9 SCI1_SCR = 0xFEDA SCI1_TDR = 0xFEDB SCI1_SSR = 0xFEDC SCI1_RDR = 0xFEDD SCI_SCR_TIE = 0x80 SCI_SCR_RIE = 0x40 SCI_SCR_TE = 0x20 SCI_SCR_RE = 0x10 SCI_SSR_TDRE = 0x80 SCI_SSR_RDRF = 0x40 SCI_SSR_ORER = 0x20 SCI_SSR_FER = 0x10 SCI_SSR_PER = 0x08 ON_CHIP_RAM_START = 0xF680 ON_CHIP_RAM_END = 0xFE7F REGISTER_FIELD_START = 0xFE80 REGISTER_FIELD_END = 0xFFFF RAMCR = 0xFF11 HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) class EmulatorError(Exception): pass class UnsupportedInstruction(EmulatorError): def __init__(self, pc: int, raw: bytes, text: str) -> None: raw_text = " ".join(f"{byte:02X}" for byte in raw) super().__init__(f"unsupported instruction at {h16(pc)}: {raw_text} {text}".rstrip()) self.pc = pc self.raw = raw self.text = text @dataclass class SciTxEvent: address: int value: int scr: int ssr: int emitted: bool @dataclass class SCI1: """Small SCI1 model for the H8/536 serial path. Manual anchors: - RDR/TDR/SCR/SSR live at H'FEDD/H'FEDB/H'FEDA/H'FEDC for SCI1. - SCR bit 7 TIE, bit 6 RIE, bit 5 TE, bit 4 RE. - SSR bit 7 TDRE, bit 6 RDRF, bits 5..3 ORER/FER/PER. - Software normally writes TDR after TDRE=1, then clears SSR.TDRE. """ smr: int = 0x00 brr: int = 0xFF scr: int = 0x0C tdr: int = 0xFF ssr: int = 0x87 rdr: int = 0x00 tx_bytes: list[int] = field(default_factory=list) tx_events: list[SciTxEvent] = field(default_factory=list) tx_frames: list[bytes] = field(default_factory=list) _frame_buffer: bytearray = field(default_factory=bytearray) def read(self, address: int) -> int: if address == SCI1_SMR: return self.smr if address == SCI1_BRR: return self.brr if address == SCI1_SCR: return self.scr if address == SCI1_TDR: return self.tdr if address == SCI1_SSR: return self.ssr if address == SCI1_RDR: return self.rdr raise KeyError(address) def write(self, address: int, value: int) -> None: value &= 0xFF if address == SCI1_SMR: self.smr = value elif address == SCI1_BRR: self.brr = value elif address == SCI1_SCR: self.scr = value elif address == SCI1_TDR: self.tdr = value self._write_tdr(value) elif address == SCI1_SSR: # The real SSR is R/(W)*: writable zeroes clear latched flags after # the required read sequence. This scaffold applies zero bits # directly so ROM BCLR/BSET style accesses can be modeled. self.ssr = value elif address == SCI1_RDR: self.rdr = value else: raise KeyError(address) def _write_tdr(self, value: int) -> None: emitted = bool(self.scr & SCI_SCR_TE) if emitted: self.tx_bytes.append(value) self._frame_buffer.append(value) if len(self._frame_buffer) == len(HEARTBEAT_FRAME): self.tx_frames.append(bytes(self._frame_buffer)) self._frame_buffer.clear() self.ssr |= SCI_SSR_TDRE self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) def inject_rx(self, value: int) -> None: self.rdr = value & 0xFF self.ssr |= SCI_SSR_RDRF def saw_heartbeat(self) -> bool: return HEARTBEAT_FRAME in self.tx_frames @dataclass class MemoryAccess: address: int size: int value: int kind: str region: str class MemoryMap: def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None: self.rom = Rom(rom_bytes, base=0) self.sci1 = sci1 if sci1 is not None else SCI1() self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1) self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1) self.external: dict[int, int] = {} self.access_log: list[MemoryAccess] = [] self._set_register(SCI1_SMR, self.sci1.smr) self._set_register(SCI1_BRR, self.sci1.brr) self._set_register(SCI1_SCR, self.sci1.scr) self._set_register(SCI1_TDR, self.sci1.tdr) self._set_register(SCI1_SSR, self.sci1.ssr) self._set_register(SCI1_RDR, self.sci1.rdr) def region(self, address: int) -> MemoryRegion: return region_for(address & 0xFFFF) def read8(self, address: int) -> int: address &= 0xFFFF if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): value = self.sci1.read(address) self._set_register(address, value) elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: value = self.ram[address - ON_CHIP_RAM_START] elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: value = self.registers[address - REGISTER_FIELD_START] elif self.rom.contains(address): value = self.rom.u8(address) else: value = self.external.get(address, 0xFF) self._log("read", address, 1, value) return value def read16(self, address: int) -> int: high = self.read8(address) low = self.read8((address + 1) & 0xFFFF) return (high << 8) | low def write8(self, address: int, value: int) -> None: address &= 0xFFFF value &= 0xFF if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): self.sci1.write(address, value) self._set_register(address, self.sci1.read(address)) elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: self.ram[address - ON_CHIP_RAM_START] = value elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: self._set_register(address, value) elif self.rom.contains(address): # ROM writes are logged but ignored. This keeps the scaffold useful # for accidental writes without mutating the loaded image. pass else: self.external[address] = value self._log("write", address, 1, value) def write16(self, address: int, value: int) -> None: self.write8(address, (value >> 8) & 0xFF) self.write8((address + 1) & 0xFFFF, value & 0xFF) def _set_register(self, address: int, value: int) -> None: self.registers[address - REGISTER_FIELD_START] = value & 0xFF def _log(self, kind: str, address: int, size: int, value: int) -> None: self.access_log.append(MemoryAccess(address, size, value, kind, self.region(address).name)) @dataclass class CPUState: pc: int = 0 sr: int = 0 br: int = 0 regs: list[int] = field(default_factory=lambda: [0] * 8) cycles: int = 0 steps: int = 0 z: bool = False @dataclass class RunReport: steps: int cycles: int pc: int stopped_reason: str tx_bytes: bytes tx_frames: list[bytes] heartbeat_seen: bool unsupported: str | None = None trace: list[str] = field(default_factory=list) def summary_lines(self) -> list[str]: lines = [ f"steps={self.steps}", f"cycles={self.cycles}", f"pc={h16(self.pc)}", f"stopped={self.stopped_reason}", "tx_bytes=" + self.tx_bytes.hex(" ").upper(), "tx_frames=" + ", ".join(frame.hex(" ").upper() for frame in self.tx_frames), f"heartbeat_seen={self.heartbeat_seen}", ] if self.unsupported: lines.append(f"unsupported={self.unsupported}") lines.append("next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow") return lines class H8536Emulator: def __init__(self, rom_bytes: bytes) -> None: if not rom_bytes: raise ValueError("ROM image is empty") self.sci1 = SCI1() self.memory = MemoryMap(rom_bytes, self.sci1) self.cpu = CPUState() self.vectors = read_vectors_min(self.memory.rom) self.reset() def reset(self) -> None: self.cpu = CPUState(pc=self.reset_vector()) def reset_vector(self) -> int: if self.memory.rom.contains(0, 2): return self.memory.rom.u16(0) raise DecodeError("ROM does not contain a reset vector at H'0000") def step(self) -> str: pc = self.cpu.pc decoder = H8536Decoder(self.memory.rom, br=self.cpu.br) ins = decoder.decode(pc) if not ins.valid: raise UnsupportedInstruction(pc, ins.raw, ins.text) next_pc = (pc + ins.size) & 0xFFFF raw = ins.raw text = ins.text if raw[0] == 0x00: pass elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3: self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big") elif raw[:2] == bytes([0x0C, 0x07]) and len(raw) == 4: self.cpu.sr = int.from_bytes(raw[2:4], "big") elif raw[0] in (0x15, 0x1D) and len(raw) >= 4: next_pc = self._execute_general_abs(raw, pc, next_pc) elif raw[0] in range(0x20, 0x30) and len(raw) == 2: next_pc = self._branch8(raw, pc, next_pc) elif raw[0] in range(0x30, 0x40) and len(raw) == 3: next_pc = self._branch16(raw, pc, next_pc) else: raise UnsupportedInstruction(pc, raw, text) self.cpu.pc = next_pc self.cpu.steps += 1 self.cpu.cycles += self._rough_cycles(raw) return f"{h16(pc)}: {' '.join(f'{byte:02X}' for byte in raw):<17} {text}" def run(self, max_steps: int, trace: bool = False, stop_on_heartbeat: bool = False) -> RunReport: trace_lines: list[str] = [] stopped_reason = "max_steps" unsupported: str | None = None for _ in range(max_steps): try: line = self.step() except UnsupportedInstruction as exc: stopped_reason = "unsupported_instruction" unsupported = str(exc) break if trace: trace_lines.append(line) if stop_on_heartbeat and self.sci1.saw_heartbeat(): stopped_reason = "heartbeat" break return RunReport( steps=self.cpu.steps, cycles=self.cpu.cycles, pc=self.cpu.pc, stopped_reason=stopped_reason, tx_bytes=bytes(self.sci1.tx_bytes), tx_frames=list(self.sci1.tx_frames), heartbeat_seen=self.sci1.saw_heartbeat(), unsupported=unsupported, trace=trace_lines, ) def _execute_general_abs(self, raw: bytes, pc: int, next_pc: int) -> int: size = "W" if raw[0] == 0x1D else "B" address = int.from_bytes(raw[1:3], "big") op = raw[3] if op == 0x06: value = raw[4] self.memory.write8(address, value) elif op == 0x07: value = int.from_bytes(raw[4:6], "big") self.memory.write16(address, value) elif 0x90 <= op <= 0x97: reg = self.cpu.regs[op & 0x07] if size == "W": self.memory.write16(address, reg) else: self.memory.write8(address, reg & 0xFF) elif 0x80 <= op <= 0x87: reg = op & 0x07 self.cpu.regs[reg] = self.memory.read16(address) if size == "W" else self.memory.read8(address) elif 0xC0 <= op <= 0xCF: bit = op & 0x0F self.memory.write8(address, self.memory.read8(address) | (1 << bit)) elif 0xD0 <= op <= 0xDF: bit = op & 0x0F self.memory.write8(address, self.memory.read8(address) & ~(1 << bit)) elif 0xF0 <= op <= 0xFF: bit = op & 0x0F self.cpu.z = not bool(self.memory.read8(address) & (1 << bit)) elif op in (0x04, 0x05): if op == 0x04: value = raw[4] actual = self.memory.read8(address) else: value = int.from_bytes(raw[4:6], "big") actual = self.memory.read16(address) self.cpu.z = actual == value elif op == 0x08: value = self.memory.read16(address) if size == "W" else self.memory.read8(address) value = (value + 1) & (0xFFFF if size == "W" else 0xFF) if size == "W": self.memory.write16(address, value) else: self.memory.write8(address, value) self.cpu.z = value == 0 else: raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text) return next_pc def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int: cond = raw[0] & 0x0F disp = _s8(raw[1]) target = (pc + 2 + disp) & 0xFFFF if cond == 0x0: return target if cond == 0x7 and self.cpu.z: return target if cond == 0x6 and not self.cpu.z: return target return next_pc def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int: cond = raw[0] & 0x0F disp = _s16(int.from_bytes(raw[1:3], "big")) target = (pc + 3 + disp) & 0xFFFF if cond == 0x0: return target if cond == 0x7 and self.cpu.z: return target if cond == 0x6 and not self.cpu.z: return target return next_pc def _rough_cycles(self, raw: bytes) -> int: if raw[0] in (0x15, 0x1D): return 9 if raw[0] in range(0x20, 0x40): return 8 if (raw[0] & 0x0F) == 0 else 4 return 3 def _s8(value: int) -> int: return value - 0x100 if value & 0x80 else value def _s16(value: int) -> int: return value - 0x10000 if value & 0x8000 else value def discover_rom_path(root: Path) -> Path | None: candidates = [ root / "ROM" / "M27C512@DIP28_1.BIN", root / "rom.bin", ] candidates.extend(sorted((root / "ROM").glob("*.BIN")) if (root / "ROM").exists() else []) candidates.extend(sorted((root / "ROM").glob("*.bin")) if (root / "ROM").exists() else []) for candidate in candidates: if candidate.is_file(): return candidate return None def load_rom(path: Path | None = None, root: Path | None = None) -> tuple[bytes, Path]: root = root if root is not None else Path.cwd() rom_path = path if path is not None else discover_rom_path(root) if rom_path is None: raise FileNotFoundError( "could not discover ROM bytes; pass --rom PATH, expected ROM/M27C512@DIP28_1.BIN or another ROM/*.BIN" ) return rom_path.read_bytes(), rom_path def describe_regions(regions: Iterable[MemoryRegion] = MEMORY_REGIONS) -> str: return "\n".join(f"{h16(region.start)}-{h16(region.end)} {region.name} {region.kind}" for region in regions) def build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Minimal H8/536 emulation harness scaffold") parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present") parser.add_argument("--max-steps", type=int, default=64, help="maximum CPU steps to execute") parser.add_argument("--trace", action="store_true", help="print decoded/executed instruction trace") parser.add_argument("--stop-on-heartbeat", action="store_true", help="stop only when 00 00 00 00 80 DA is emitted through SCI1 TDR") parser.add_argument("--memory-map", action="store_true", help="print the scaffolded memory map before running") return parser def main(argv: list[str] | None = None) -> int: args = build_arg_parser().parse_args(argv) try: rom_bytes, rom_path = load_rom(args.rom) except FileNotFoundError as exc: print(str(exc)) return 2 emulator = H8536Emulator(rom_bytes) print(f"rom={rom_path}") print(f"reset_vector={h16(emulator.reset_vector())}") if args.memory_map: print(describe_regions()) report = emulator.run(args.max_steps, trace=args.trace, stop_on_heartbeat=args.stop_on_heartbeat) if args.trace: for line in report.trace: print(line) for line in report.summary_lines(): print(line) if not report.heartbeat_seen: print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR") return 0