further digging and basic emulator
This commit is contained in:
480
h8536/emulator.py
Normal file
480
h8536/emulator.py
Normal file
@@ -0,0 +1,480 @@
|
||||
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
|
||||
Reference in New Issue
Block a user