1
0

emulator improvements

This commit is contained in:
Aiden
2026-05-25 18:07:55 +10:00
parent 9d93d88840
commit 81f5d7a150
13 changed files with 629 additions and 366 deletions

View File

@@ -0,0 +1,83 @@
from __future__ import annotations
from .cli import build_arg_parser, discover_rom_path, load_rom, main
from .constants import (
HEARTBEAT_FRAME,
IPRA,
IPRE,
ON_CHIP_RAM_END,
ON_CHIP_RAM_START,
P9DDR,
P9DR,
RAMCR,
REGISTER_FIELD_END,
REGISTER_FIELD_START,
SCI_SCR_RE,
SCI_SCR_RIE,
SCI_SCR_TE,
SCI_SCR_TIE,
SCI_SSR_FER,
SCI_SSR_ORER,
SCI_SSR_PER,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
VECTOR_INTERVAL_TIMER,
VECTOR_SCI1_TXI,
WDT_TCSR_R,
)
from .cpu import CPUState
from .errors import EmulatorError, UnsupportedInstruction
from .memory import MemoryAccess, MemoryMap, describe_regions
from .runner import H8536Emulator, RunReport
from .sci import SCI1, SciTxEvent
__all__ = [
"CPUState",
"EmulatorError",
"HEARTBEAT_FRAME",
"H8536Emulator",
"IPRA",
"IPRE",
"MemoryAccess",
"MemoryMap",
"ON_CHIP_RAM_END",
"ON_CHIP_RAM_START",
"P9DDR",
"P9DR",
"RAMCR",
"REGISTER_FIELD_END",
"REGISTER_FIELD_START",
"RunReport",
"SCI1",
"SCI1_BRR",
"SCI1_RDR",
"SCI1_SCR",
"SCI1_SMR",
"SCI1_SSR",
"SCI1_TDR",
"SCI_SCR_RE",
"SCI_SCR_RIE",
"SCI_SCR_TE",
"SCI_SCR_TIE",
"SCI_SSR_FER",
"SCI_SSR_ORER",
"SCI_SSR_PER",
"SCI_SSR_RDRF",
"SCI_SSR_TDRE",
"SciTxEvent",
"UnsupportedInstruction",
"VECTOR_INTERVAL_TIMER",
"VECTOR_SCI1_TXI",
"WDT_TCSR_R",
"build_arg_parser",
"describe_regions",
"discover_rom_path",
"load_rom",
"main",
]

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
from .cli import main
if __name__ == "__main__":
raise SystemExit(main())

66
h8536/emulator/cli.py Normal file
View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import argparse
from pathlib import Path
from ..formatting import h16
from .memory import describe_regions
from .runner import H8536Emulator
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 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")
parser.add_argument("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt")
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, interval_steps=args.interval_steps)
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

View File

@@ -0,0 +1,36 @@
from __future__ import annotations
SCI1_SMR = 0xFED8
SCI1_BRR = 0xFED9
SCI1_SCR = 0xFEDA
SCI1_TDR = 0xFEDB
SCI1_SSR = 0xFEDC
SCI1_RDR = 0xFEDD
P9DDR = 0xFEFE
P9DR = 0xFEFF
IPRA = 0xFF00
IPRE = 0xFF04
WDT_TCSR_R = 0xFEEC
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])
VECTOR_INTERVAL_TIMER = 0x0042
VECTOR_SCI1_TXI = 0x0084

34
h8536/emulator/cpu.py Normal file
View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass, field
@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
c: bool = False
n: bool = False
v: bool = False
interrupt_depth: int = 0
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 mask(size: int) -> int:
return 0xFFFF if size == 2 else 0xFF
def sign_bit(size: int) -> int:
return 0x8000 if size == 2 else 0x80

16
h8536/emulator/errors.py Normal file
View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from ..formatting import h16
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

120
h8536/emulator/memory.py Normal file
View File

@@ -0,0 +1,120 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from ..formatting import h16
from ..memory import MEMORY_REGIONS, MemoryRegion, region_for
from ..rom import Rom
from .constants import (
ON_CHIP_RAM_END,
ON_CHIP_RAM_START,
P9DDR,
P9DR,
REGISTER_FIELD_END,
REGISTER_FIELD_START,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
)
from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
from .peripherals.p9_bus import P9_ACK_BIT
from .sci import SCI1
@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 address in self.external:
value = self.external[address]
elif address in (LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS):
# LCD E-clock/status space. Default to ready/zero so boot can pass
# busy-flag polling until a fuller external bus model exists.
value = 0x00
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]
if address == P9DR and not (self.registers[P9DDR - REGISTER_FIELD_START] & 0x80):
# P97 is used as an input during the serial panel/camera-side
# bit-bang handshake. With no external device modeled, hold the
# input low so the firmware sees an idle/acknowledged bus rather
# than reading back its previous output latch forever.
value &= ~P9_ACK_BIT
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):
# The ROM image spans the whole address space, but the H8/536 map
# can place external RAM/peripherals in these ranges. Keep writes
# as external overrides while leaving instruction fetch immutable.
self.external[address] = value
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))
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)

View File

@@ -0,0 +1,10 @@
from __future__ import annotations
from .lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
from .p9_bus import P9_ACK_BIT
__all__ = [
"LCD_E_CLOCK_DATA",
"LCD_E_CLOCK_STATUS",
"P9_ACK_BIT",
]

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
LCD_E_CLOCK_DATA = 0xF200
LCD_E_CLOCK_STATUS = 0xF201

View File

@@ -0,0 +1,4 @@
from __future__ import annotations
P9_ACK_BIT = 0x80

View File

@@ -1,260 +1,23 @@
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
P9DDR = 0xFEFE
P9DR = 0xFEFF
IPRA = 0xFF00
IPRE = 0xFF04
WDT_TCSR_R = 0xFEEC
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])
VECTOR_INTERVAL_TIMER = 0x0042
VECTOR_SCI1_TXI = 0x0084
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)
tx_ready_delay: int = 0
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_ready_delay = 2
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
def tick(self) -> None:
if self.tx_ready_delay:
self.tx_ready_delay -= 1
if self.tx_ready_delay == 0:
self.ssr |= SCI_SSR_TDRE
@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 address in self.external:
value = self.external[address]
elif address in (0xF200, 0xF201):
# LCD E-clock/status space. Default to ready/zero so boot can pass
# busy-flag polling until a fuller external bus model exists.
value = 0x00
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]
if address == P9DR and not (self.registers[P9DDR - REGISTER_FIELD_START] & 0x80):
# P97 is used as an input during the serial panel/camera-side
# bit-bang handshake. With no external device modeled, hold the
# input low so the firmware sees an idle/acknowledged bus rather
# than reading back its previous output latch forever.
value &= 0x7F
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):
# The ROM image spans the whole address space, but the H8/536 map
# can place external RAM/peripherals in these ranges. Keep writes
# as external overrides while leaving instruction fetch immutable.
self.external[address] = value
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
c: bool = False
n: bool = False
v: bool = False
interrupt_depth: int = 0
from ..decoder import H8536Decoder
from ..formatting import h16
from ..rom import DecodeError
from ..vectors import read_vectors_min
from .constants import (
IPRA,
IPRE,
SCI_SCR_TIE,
SCI_SSR_TDRE,
VECTOR_INTERVAL_TIMER,
VECTOR_SCI1_TXI,
)
from .cpu import CPUState, mask, s8, s16, sign_bit
from .errors import EmulatorError, UnsupportedInstruction
from .memory import MemoryMap
from .sci import SCI1
@dataclass
@@ -281,7 +44,9 @@ class RunReport:
]
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")
lines.append(
"next_todo=implement the stopped opcode, then add interrupt scheduling for SCI1 TXI and interval/watchdog timer overflow"
)
return lines
@@ -390,7 +155,7 @@ class H8536Emulator:
def _execute_general(self, pc: int, next_pc: int) -> int:
ea = self._decode_ea(pc)
op = self.memory.rom.u8(pc + ea["length"])
op = self.memory.rom.u8(pc + int(ea["length"]))
size = int(ea["size"])
raw = self.memory.rom.slice(pc, self._general_length(pc, ea, op))
@@ -481,8 +246,8 @@ class H8536Emulator:
self._set_logic_flags(value, size)
elif op == 0x1A:
value = self._read_ea(ea, size)
result = (value << 1) & _mask(size)
self.cpu.c = bool(value & _sign_bit(size))
result = (value << 1) & mask(size)
self.cpu.c = bool(value & sign_bit(size))
self._write_ea(ea, result, size)
self._set_logic_flags(result, size)
elif op == 0x1B:
@@ -522,7 +287,7 @@ class H8536Emulator:
def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int:
cond = raw[0] & 0x0F
disp = _s8(raw[1])
disp = s8(raw[1])
target = (pc + 2 + disp) & 0xFFFF
if self._branch_condition(cond):
return target
@@ -530,7 +295,7 @@ class H8536Emulator:
def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int:
cond = raw[0] & 0x0F
disp = _s16(int.from_bytes(raw[1:3], "big"))
disp = s16(int.from_bytes(raw[1:3], "big"))
target = (pc + 3 + disp) & 0xFFFF
if self._branch_condition(cond):
return target
@@ -550,9 +315,9 @@ class H8536Emulator:
def _direct_call(self, raw: bytes, next_pc: int) -> int:
self._push16(next_pc)
if raw[0] == 0x0E:
return (next_pc + _s8(raw[1])) & 0xFFFF
return (next_pc + s8(raw[1])) & 0xFFFF
if raw[0] == 0x1E:
return (next_pc + _s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF
return (next_pc + s16(int.from_bytes(raw[1:3], "big"))) & 0xFFFF
return int.from_bytes(raw[1:3], "big")
def _scb(self, raw: bytes, pc: int, next_pc: int) -> int:
@@ -564,7 +329,7 @@ class H8536Emulator:
self.cpu.regs[reg] = value
self.cpu.z = value == 0
if value != 0:
return (pc + 3 + _s8(raw[2])) & 0xFFFF
return (pc + 3 + s8(raw[2])) & 0xFFFF
return next_pc
def _tick_peripherals(self) -> None:
@@ -631,14 +396,14 @@ class H8536Emulator:
self.cpu.regs[7] = (sp + 2) & 0xFFFF
return value
def _push_register_mask(self, mask: int) -> None:
def _push_register_mask(self, mask_value: int) -> None:
for reg in range(8):
if mask & (1 << reg):
if mask_value & (1 << reg):
self._push16(self.cpu.regs[reg])
def _pop_register_mask(self, mask: int) -> None:
def _pop_register_mask(self, mask_value: int) -> None:
for reg in reversed(range(8)):
if mask & (1 << reg):
if mask_value & (1 << reg):
self.cpu.regs[reg] = self._pop16()
def _decode_ea(self, pc: int) -> dict[str, int | str | None]:
@@ -652,9 +417,9 @@ class H8536Emulator:
if 0xD0 <= first <= 0xDF:
return {"mode": "indirect", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
if 0xE0 <= first <= 0xEF:
return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 2, "value": _s8(self.memory.rom.u8(pc + 1))}
return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 2, "value": s8(self.memory.rom.u8(pc + 1))}
if 0xF0 <= first <= 0xFF:
return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 3, "value": _s16(self.memory.rom.u16(pc + 1))}
return {"mode": "disp", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 3, "value": s16(self.memory.rom.u16(pc + 1))}
if first in (0x04, 0x0C):
size = 2 if first == 0x0C else 1
value = self.memory.rom.u16(pc + 1) if size == 2 else self.memory.rom.u8(pc + 1)
@@ -697,7 +462,7 @@ class H8536Emulator:
def _read_ea(self, ea: dict[str, int | str | None], size: int) -> int:
mode = ea["mode"]
if mode == "imm":
return int(ea["value"]) & _mask(size)
return int(ea["value"]) & mask(size)
if mode == "reg":
return self._reg_read(int(ea["reg"]), size)
address = self._ea_address(ea, size)
@@ -727,28 +492,28 @@ class H8536Emulator:
self._set_sub_flags(lhs, rhs, lhs - rhs, size)
def _set_logic_flags(self, value: int, size: int) -> None:
value &= _mask(size)
value &= mask(size)
self.cpu.z = value == 0
self.cpu.n = bool(value & _sign_bit(size))
self.cpu.n = bool(value & sign_bit(size))
self.cpu.v = False
def _set_add_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
mask = _mask(size)
sign = _sign_bit(size)
result &= mask
lhs &= mask
rhs &= mask
max_value = mask(size)
sign = sign_bit(size)
result &= max_value
lhs &= max_value
rhs &= max_value
self.cpu.z = result == 0
self.cpu.n = bool(result & sign)
self.cpu.c = lhs + rhs > mask
self.cpu.c = lhs + rhs > max_value
self.cpu.v = bool((~(lhs ^ rhs) & (lhs ^ result) & sign) != 0)
def _set_sub_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
mask = _mask(size)
sign = _sign_bit(size)
result &= mask
lhs &= mask
rhs &= mask
max_value = mask(size)
sign = sign_bit(size)
result &= max_value
lhs &= max_value
rhs &= max_value
self.cpu.z = result == 0
self.cpu.n = bool(result & sign)
self.cpu.c = lhs < rhs
@@ -756,15 +521,15 @@ class H8536Emulator:
def _bit_operation(self, ea: dict[str, int | str | None], size: int, op_base: int, bit: int) -> None:
value = self._read_ea(ea, size)
bit &= (15 if size == 2 else 7)
mask = 1 << bit
self.cpu.z = not bool(value & mask)
bit &= 15 if size == 2 else 7
bit_mask = 1 << bit
self.cpu.z = not bool(value & bit_mask)
if op_base == 0xC0:
self._write_ea(ea, value | mask, size)
self._write_ea(ea, value | bit_mask, size)
elif op_base == 0xD0:
self._write_ea(ea, value & ~mask, size)
self._write_ea(ea, value & ~bit_mask, size)
elif op_base == 0xE0:
self._write_ea(ea, value ^ mask, size)
self._write_ea(ea, value ^ bit_mask, size)
def _branch_condition(self, cond: int) -> bool:
if cond == 0x0:
@@ -798,81 +563,3 @@ class H8536Emulator:
if cond == 0xE:
return not self.cpu.z and self.cpu.n == self.cpu.v
return self.cpu.z or self.cpu.n != self.cpu.v
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 _mask(size: int) -> int:
return 0xFFFF if size == 2 else 0xFF
def _sign_bit(size: int) -> int:
return 0x8000 if size == 2 else 0x80
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")
parser.add_argument("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt")
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, interval_steps=args.interval_steps)
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

110
h8536/emulator/sci.py Normal file
View File

@@ -0,0 +1,110 @@
from __future__ import annotations
from dataclasses import dataclass, field
from .constants import (
HEARTBEAT_FRAME,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
SCI_SCR_TE,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
)
@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)
tx_ready_delay: int = 0
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_ready_delay = 2
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
def tick(self) -> None:
if self.tx_ready_delay:
self.tx_ready_delay -= 1
if self.tx_ready_delay == 0:
self.ssr |= SCI_SSR_TDRE