1
0

EMualtor im

This commit is contained in:
Aiden
2026-05-25 18:43:36 +10:00
parent 81f5d7a150
commit 05e1237acc
18 changed files with 993 additions and 20 deletions

View File

@@ -39,6 +39,8 @@ To start the current emulator harness:
```powershell ```powershell
.\.venv\Scripts\python.exe h8536_emulator.py --max-steps 1000000 --stop-on-heartbeat --interval-steps 512 .\.venv\Scripts\python.exe h8536_emulator.py --max-steps 1000000 --stop-on-heartbeat --interval-steps 512
.\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 4000000 --stop-on-tx
.\.venv\Scripts\python.exe h8536_emulator_probe.py --max-steps 1000000 --stop-on-tx --p9-fast-path
``` ```
## What It Does ## What It Does
@@ -76,7 +78,8 @@ To start the current emulator harness:
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`. - Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes. - Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns. - Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, `SCB/F`, stack/call/return support, and scaffolded SCI1 TXI/interval interrupt scheduling. - Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, scaffolded SCI1 TXI/interval/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path.
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, and captured P9 byte candidates while running the real ROM.
Current serial observations: Current serial observations:
@@ -178,6 +181,7 @@ For the emulator harness:
```powershell ```powershell
python h8536_emulator.py --help python h8536_emulator.py --help
python h8536_emulator_probe.py --help
``` ```
- `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`. - `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`.
@@ -185,7 +189,8 @@ python h8536_emulator.py --help
- `--trace`: print executed instructions. - `--trace`: print executed instructions.
- `--stop-on-heartbeat`: stop only if `00 00 00 00 80 DA` is emitted through SCI1 TDR. - `--stop-on-heartbeat`: stop only if `00 00 00 00 80 DA` is emitted through SCI1 TDR.
- `--interval-steps N`: tune the scaffolded interval timer cadence. - `--interval-steps N`: tune the scaffolded interval timer cadence.
- Current status: boots from `H'1000`, initializes SCI1, supports the first stack/call/interrupt pieces, but does not yet reach the heartbeat. The next emulator work is a better external P9 bit-banged device/handshake model around the `BFE0/BFFE/C08B/C0DB/C121` routines. - `--p9-fast-path`: shortcut known P9 transfer routines for exploration.
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT2 OCIA, but does not yet reach the SCI1 heartbeat. The next emulator work is validating the P9 fast-path semantics against real captures/board behavior and tightening SCI RX/TX interrupt cadence.
## Code Layout ## Code Layout
@@ -215,7 +220,7 @@ python h8536_emulator.py --help
- `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction. - `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction.
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer. - `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
- `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation. - `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation.
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, runner, CLI, and peripheral scaffolding. - `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, P9 bus model, runner, probe, CLI, and peripheral scaffolding.
- `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path. - `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path.
- `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis. - `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis.
- `h8536/pseudocode.py`: JSON-to-C-like pseudocode generation. - `h8536/pseudocode.py`: JSON-to-C-like pseudocode generation.
@@ -225,4 +230,4 @@ python h8536_emulator.py --help
- `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper. - `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper.
- `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers. - `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers.
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`: sidecar analysis CLI wrappers. - `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`: sidecar analysis CLI wrappers.
- `h8536_emulator.py`: emulator CLI wrapper. - `h8536_emulator.py`, `h8536_emulator_probe.py`: emulator CLI wrappers.

View File

@@ -3,7 +3,12 @@ from __future__ import annotations
from .cli import build_arg_parser, discover_rom_path, load_rom, main from .cli import build_arg_parser, discover_rom_path, load_rom, main
from .constants import ( from .constants import (
HEARTBEAT_FRAME, HEARTBEAT_FRAME,
FRT_TCR_OCIEA,
FRT_TCSR_OCFA,
FRT2_TCR,
FRT2_TCSR,
IPRA, IPRA,
IPRC,
IPRE, IPRE,
ON_CHIP_RAM_END, ON_CHIP_RAM_END,
ON_CHIP_RAM_START, ON_CHIP_RAM_START,
@@ -28,11 +33,13 @@ from .constants import (
SCI1_SSR, SCI1_SSR,
SCI1_TDR, SCI1_TDR,
VECTOR_INTERVAL_TIMER, VECTOR_INTERVAL_TIMER,
VECTOR_FRT2_OCIA,
VECTOR_SCI1_TXI, VECTOR_SCI1_TXI,
WDT_TCSR_R, WDT_TCSR_R,
) )
from .cpu import CPUState from .cpu import CPUState
from .errors import EmulatorError, UnsupportedInstruction from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent
from .memory import MemoryAccess, MemoryMap, describe_regions from .memory import MemoryAccess, MemoryMap, describe_regions
from .runner import H8536Emulator, RunReport from .runner import H8536Emulator, RunReport
from .sci import SCI1, SciTxEvent from .sci import SCI1, SciTxEvent
@@ -40,9 +47,14 @@ from .sci import SCI1, SciTxEvent
__all__ = [ __all__ = [
"CPUState", "CPUState",
"EmulatorError", "EmulatorError",
"FRT2_TCR",
"FRT2_TCSR",
"FRT_TCR_OCIEA",
"FRT_TCSR_OCFA",
"HEARTBEAT_FRAME", "HEARTBEAT_FRAME",
"H8536Emulator", "H8536Emulator",
"IPRA", "IPRA",
"IPRC",
"IPRE", "IPRE",
"MemoryAccess", "MemoryAccess",
"MemoryMap", "MemoryMap",
@@ -50,6 +62,9 @@ __all__ = [
"ON_CHIP_RAM_START", "ON_CHIP_RAM_START",
"P9DDR", "P9DDR",
"P9DR", "P9DR",
"P9FastPath",
"P9FastPathConfig",
"P9FastPathEvent",
"RAMCR", "RAMCR",
"REGISTER_FIELD_END", "REGISTER_FIELD_END",
"REGISTER_FIELD_START", "REGISTER_FIELD_START",
@@ -73,6 +88,7 @@ __all__ = [
"SciTxEvent", "SciTxEvent",
"UnsupportedInstruction", "UnsupportedInstruction",
"VECTOR_INTERVAL_TIMER", "VECTOR_INTERVAL_TIMER",
"VECTOR_FRT2_OCIA",
"VECTOR_SCI1_TXI", "VECTOR_SCI1_TXI",
"WDT_TCSR_R", "WDT_TCSR_R",
"build_arg_parser", "build_arg_parser",

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import argparse import argparse
from pathlib import Path from pathlib import Path
from ..formatting import h16 from ..formatting import h16, parse_int
from .memory import describe_regions from .memory import describe_regions
from .runner import H8536Emulator from .runner import H8536Emulator
@@ -39,6 +39,9 @@ def build_arg_parser() -> argparse.ArgumentParser:
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("--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("--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") parser.add_argument("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt")
parser.add_argument("--frt2-ocia-steps", type=int, default=1024, help="rough step period for the scaffolded FRT2 OCIA interrupt")
parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by the P9 fast-path read routine")
return parser return parser
@@ -50,7 +53,13 @@ def main(argv: list[str] | None = None) -> int:
print(str(exc)) print(str(exc))
return 2 return 2
emulator = H8536Emulator(rom_bytes, interval_steps=args.interval_steps) emulator = H8536Emulator(
rom_bytes,
interval_steps=args.interval_steps,
frt2_ocia_steps=args.frt2_ocia_steps,
p9_fast_path_enabled=args.p9_fast_path,
p9_fast_default_input_byte=args.p9_fast_input,
)
print(f"rom={rom_path}") print(f"rom={rom_path}")
print(f"reset_vector={h16(emulator.reset_vector())}") print(f"reset_vector={h16(emulator.reset_vector())}")
if args.memory_map: if args.memory_map:

View File

@@ -12,9 +12,13 @@ P9DDR = 0xFEFE
P9DR = 0xFEFF P9DR = 0xFEFF
IPRA = 0xFF00 IPRA = 0xFF00
IPRC = 0xFF02
IPRE = 0xFF04 IPRE = 0xFF04
WDT_TCSR_R = 0xFEEC WDT_TCSR_R = 0xFEEC
FRT2_TCR = 0xFEA0
FRT2_TCSR = 0xFEA1
SCI_SCR_TIE = 0x80 SCI_SCR_TIE = 0x80
SCI_SCR_RIE = 0x40 SCI_SCR_RIE = 0x40
SCI_SCR_TE = 0x20 SCI_SCR_TE = 0x20
@@ -24,6 +28,8 @@ SCI_SSR_RDRF = 0x40
SCI_SSR_ORER = 0x20 SCI_SSR_ORER = 0x20
SCI_SSR_FER = 0x10 SCI_SSR_FER = 0x10
SCI_SSR_PER = 0x08 SCI_SSR_PER = 0x08
FRT_TCR_OCIEA = 0x20
FRT_TCSR_OCFA = 0x20
ON_CHIP_RAM_START = 0xF680 ON_CHIP_RAM_START = 0xF680
ON_CHIP_RAM_END = 0xFE7F ON_CHIP_RAM_END = 0xFE7F
@@ -33,4 +39,5 @@ RAMCR = 0xFF11
HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA]) HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA])
VECTOR_INTERVAL_TIMER = 0x0042 VECTOR_INTERVAL_TIMER = 0x0042
VECTOR_FRT2_OCIA = 0x006A
VECTOR_SCI1_TXI = 0x0084 VECTOR_SCI1_TXI = 0x0084

View File

@@ -0,0 +1,124 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from .cpu import mask, sign_bit
LOC_BFE0_TRANSFER_WRAPPER = 0xBFE0
LOC_BFFE_TRANSFER_WRAPPER = 0xBFFE
LOC_C08B_P9_WRITE_BYTE = 0xC08B
LOC_C0DB_P9_READ_BYTE = 0xC0DB
LOC_C10C_P9_MARKER = 0xC10C
LOC_C121_P9_MARKER = 0xC121
LOC_C142_P9_MARKER = 0xC142
@dataclass(frozen=True)
class P9FastPathConfig:
"""Configuration for optional ROM P9 transfer shortcuts.
The helper assumes the CPU PC is exactly at a known routine entry. It
models the routine as if it completed successfully and returned via RTS.
Integration should keep this disabled unless the runner intentionally opts
into skipping these ROM routines.
"""
enabled: bool = False
write_byte_pc: int = LOC_C08B_P9_WRITE_BYTE
read_byte_pc: int = LOC_C0DB_P9_READ_BYTE
marker_pcs: frozenset[int] = frozenset(
{
LOC_C10C_P9_MARKER,
LOC_C121_P9_MARKER,
LOC_C142_P9_MARKER,
}
)
wrapper_pcs: frozenset[int] = frozenset(
{
LOC_BFE0_TRANSFER_WRAPPER,
LOC_BFFE_TRANSFER_WRAPPER,
}
)
default_input_byte: int = 0xFF
account_step: bool = True
cycles_per_hit: int = 0
@dataclass(frozen=True)
class P9FastPathEvent:
kind: str
pc: int
value: int | None = None
@dataclass
class P9FastPath:
"""Optional fast-path scaffold for ROM P9 bit-transfer routines."""
config: P9FastPathConfig = field(default_factory=P9FastPathConfig)
input_bytes: list[int] = field(default_factory=list)
output_bytes: list[int] = field(default_factory=list)
events: list[P9FastPathEvent] = field(default_factory=list)
def queue_input(self, *values: int) -> None:
self.input_bytes.extend(value & 0xFF for value in values)
def try_handle(self, emulator: Any) -> bool:
if not self.config.enabled:
return False
pc = emulator.cpu.pc & 0xFFFF
if pc == (self.config.write_byte_pc & 0xFFFF):
self._handle_write_byte(emulator)
elif pc == (self.config.read_byte_pc & 0xFFFF):
self._handle_read_byte(emulator)
elif pc in self.config.marker_pcs:
self.events.append(P9FastPathEvent("marker", pc))
self._return_from_subroutine(emulator)
elif pc in self.config.wrapper_pcs:
self.events.append(P9FastPathEvent("wrapper_success", pc))
emulator.cpu.regs[0] = 1
self._set_logic_flags(emulator.cpu, 1, 1)
self._return_from_subroutine(emulator)
else:
return False
if self.config.account_step:
emulator.cpu.steps += 1
emulator.cpu.cycles += self.config.cycles_per_hit
return True
def _handle_write_byte(self, emulator: Any) -> None:
pc = emulator.cpu.pc & 0xFFFF
value = emulator.cpu.regs[0] & 0xFF
self.output_bytes.append(value)
self.events.append(P9FastPathEvent("write_byte", pc, value))
emulator.cpu.regs[0] = 1
self._set_logic_flags(emulator.cpu, 1, 1)
self._return_from_subroutine(emulator)
def _handle_read_byte(self, emulator: Any) -> None:
pc = emulator.cpu.pc & 0xFFFF
value = self.input_bytes.pop(0) if self.input_bytes else self.config.default_input_byte
value &= 0xFF
self.events.append(P9FastPathEvent("read_byte", pc, value))
# The ROM-side read routine yields a byte in R5. Model that as a byte
# register write so the existing high byte is not accidentally clobbered.
emulator.cpu.regs[5] = (emulator.cpu.regs[5] & 0xFF00) | value
self._set_logic_flags(emulator.cpu, value, 1)
self._return_from_subroutine(emulator)
def _return_from_subroutine(self, emulator: Any) -> None:
sp = emulator.cpu.regs[7] & 0xFFFF
emulator.cpu.pc = emulator.memory.read16(sp) & 0xFFFF
emulator.cpu.regs[7] = (sp + 2) & 0xFFFF
def _set_logic_flags(self, cpu: Any, value: int, size: int) -> None:
value &= mask(size)
cpu.z = value == 0
cpu.n = bool(value & sign_bit(size))
cpu.v = False

View File

@@ -21,7 +21,7 @@ from .constants import (
SCI1_TDR, SCI1_TDR,
) )
from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
from .peripherals.p9_bus import P9_ACK_BIT from .peripherals.p9_bus import P9Bus
from .sci import SCI1 from .sci import SCI1
@@ -38,6 +38,7 @@ class MemoryMap:
def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None: def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None:
self.rom = Rom(rom_bytes, base=0) self.rom = Rom(rom_bytes, base=0)
self.sci1 = sci1 if sci1 is not None else SCI1() self.sci1 = sci1 if sci1 is not None else SCI1()
self.p9_bus = P9Bus()
self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1) self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1)
self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1) self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1)
self.external: dict[int, int] = {} self.external: dict[int, int] = {}
@@ -58,22 +59,23 @@ class MemoryMap:
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR): if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
value = self.sci1.read(address) value = self.sci1.read(address)
self._set_register(address, value) self._set_register(address, value)
elif address in self.external: elif address == LCD_E_CLOCK_STATUS:
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 # LCD E-clock/status space. Default to ready/zero so boot can pass
# busy-flag polling until a fuller external bus model exists. # busy-flag polling until a fuller external bus model exists.
value = 0x00 value = 0x00
elif address == LCD_E_CLOCK_DATA:
value = self.external.get(address, 0x00)
elif address in self.external:
value = self.external[address]
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
value = self.ram[address - ON_CHIP_RAM_START] value = self.ram[address - ON_CHIP_RAM_START]
elif address == P9DDR:
value = self.p9_bus.read_ddr()
self._set_register(address, value)
elif address == P9DR:
value = self.p9_bus.read_dr()
elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
value = self.registers[address - REGISTER_FIELD_START] 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): elif self.rom.contains(address):
value = self.rom.u8(address) value = self.rom.u8(address)
else: else:
@@ -94,6 +96,10 @@ class MemoryMap:
self._set_register(address, self.sci1.read(address)) self._set_register(address, self.sci1.read(address))
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END: elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
self.ram[address - ON_CHIP_RAM_START] = value self.ram[address - ON_CHIP_RAM_START] = value
elif address == P9DDR:
self._set_register(address, self.p9_bus.write_ddr(value))
elif address == P9DR:
self._set_register(address, self.p9_bus.write_dr(value))
elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END: elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
self._set_register(address, value) self._set_register(address, value)
elif self.rom.contains(address): elif self.rom.contains(address):

View File

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

View File

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

View File

@@ -1,4 +1,80 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
P9_ACK_BIT = 0x80 P9_ACK_BIT = 0x80
P9_STROBE_BIT = 0x02
@dataclass(frozen=True)
class P9StrobeEvent:
edge: str
ddr: int
dr: int
data_bit: int
bit7_output: bool
class P9Bus:
"""Small model for the ROM's P9 bit-banged serial handshake."""
def __init__(self, ddr: int = 0x00, dr: int = 0x00, input_bits: Iterable[int] = ()) -> None:
self.ddr = ddr & 0xFF
self.dr_latch = dr & 0xFF
self.input_bits: list[int] = [1 if bit else 0 for bit in input_bits]
self.default_input_bit = 0
self.strobe_edges: list[P9StrobeEvent] = []
self.transmitted_bits: list[int] = []
self.byte_candidates: list[int] = []
def write_ddr(self, value: int) -> int:
self.ddr = value & 0xFF
return self.ddr
def write_dr(self, value: int) -> int:
previous = self.dr_latch
self.dr_latch = value & 0xFF
previous_strobe = bool(previous & P9_STROBE_BIT)
current_strobe = bool(self.dr_latch & P9_STROBE_BIT)
if previous_strobe != current_strobe:
edge = "rising" if current_strobe else "falling"
data_bit = 1 if self.dr_latch & P9_ACK_BIT else 0
bit7_output = bool(self.ddr & P9_ACK_BIT)
self.strobe_edges.append(P9StrobeEvent(edge, self.ddr, self.dr_latch, data_bit, bit7_output))
if edge == "rising" and bit7_output:
self._record_transmitted_bit(data_bit)
return self.dr_latch
def read_ddr(self) -> int:
return self.ddr
def read_dr(self) -> int:
value = self.dr_latch
if not (self.ddr & P9_ACK_BIT):
if self.input_bits:
input_bit = self.input_bits.pop(0)
else:
input_bit = self.default_input_bit
if input_bit:
value |= P9_ACK_BIT
else:
value &= ~P9_ACK_BIT
return value & 0xFF
def queue_input_bits(self, bits: Iterable[int]) -> None:
self.input_bits.extend(1 if bit else 0 for bit in bits)
def set_default_input_bit(self, bit: int) -> None:
self.default_input_bit = 1 if bit else 0
def _record_transmitted_bit(self, bit: int) -> None:
self.transmitted_bits.append(bit)
if len(self.transmitted_bits) % 8 == 0:
byte = 0
for data_bit in self.transmitted_bits[-8:]:
byte = (byte << 1) | data_bit
self.byte_candidates.append(byte)

260
h8536/emulator/probe.py Normal file
View File

@@ -0,0 +1,260 @@
from __future__ import annotations
import argparse
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from ..formatting import h16, parse_int
from .cli import load_rom
from .constants import P9DDR, P9DR, SCI1_TDR
from .errors import UnsupportedInstruction
from .runner import H8536Emulator
DEFAULT_WATCH_PCS = (0xC08B, 0xC0DB, 0xC121, 0xBFE0, 0xBFFE, 0xC059)
@dataclass(frozen=True)
class WatchSnapshot:
pc: int
step: int
regs: tuple[int, ...]
sp: int
stack_words: tuple[tuple[int, int], ...]
callers: tuple[tuple[int, int | None], ...]
def line(self) -> str:
regs = " ".join(f"R{idx}={h16(value)}" for idx, value in enumerate(self.regs))
stack = " ".join(f"{h16(address)}:{h16(value)}" for address, value in self.stack_words)
if self.callers:
callers = " ".join(
f"{h16(return_address)}<-{h16(call_site)}" if call_site is not None else h16(return_address)
for return_address, call_site in self.callers
)
else:
callers = "-"
return f"step={self.step} pc={h16(self.pc)} sp={h16(self.sp)} {regs} stack=[{stack}] callers=[{callers}]"
@dataclass
class ProbeReport:
steps: int
pc: int
stopped_reason: str
hot_pcs: Counter[int] = field(default_factory=Counter)
tx_bytes: bytes = b""
p9_bytes: list[int] = field(default_factory=list)
p9_fast_bytes: list[int] = field(default_factory=list)
p9_fast_events: int = 0
p9_accesses: list[str] = field(default_factory=list)
sci_accesses: list[str] = field(default_factory=list)
watch_snapshots: list[WatchSnapshot] = field(default_factory=list)
unsupported: str | None = None
def lines(self, hot_limit: int = 12) -> list[str]:
lines = [
f"steps={self.steps}",
f"pc={h16(self.pc)}",
f"stopped={self.stopped_reason}",
"tx_bytes=" + self.tx_bytes.hex(" ").upper(),
"p9_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_bytes[-32:]),
"p9_fast_bytes=" + " ".join(f"{byte:02X}" for byte in self.p9_fast_bytes[-32:]),
f"p9_fast_events={self.p9_fast_events}",
"hot_pcs=" + ", ".join(f"{h16(pc)}:{count}" for pc, count in self.hot_pcs.most_common(hot_limit)),
]
if self.unsupported:
lines.append(f"unsupported={self.unsupported}")
if self.p9_accesses:
lines.append("recent_p9:")
lines.extend(" " + line for line in self.p9_accesses[-24:])
if self.sci_accesses:
lines.append("recent_sci:")
lines.extend(" " + line for line in self.sci_accesses[-16:])
if self.watch_snapshots:
lines.append("recent_watch_snapshots:")
lines.extend(" " + snapshot.line() for snapshot in self.watch_snapshots)
return lines
def parse_watch_pc(text: str) -> int:
try:
value = parse_int(text)
except ValueError:
value = int(text, 16)
return value & 0xFFFF
def _likely_call_site(emulator: H8536Emulator, return_address: int) -> int | None:
rom = emulator.memory.rom
for size in (2, 3):
candidate = (return_address - size) & 0xFFFF
if not rom.contains(candidate, size):
continue
opcode = rom.u8(candidate)
if size == 2 and opcode == 0x0E:
return candidate
if size == 3 and opcode in (0x1E, 0x18):
return candidate
return None
def _watch_snapshot(emulator: H8536Emulator, *, stack_words: int = 6) -> WatchSnapshot:
sp = emulator.cpu.regs[7] & 0xFFFF
words: list[tuple[int, int]] = []
callers: list[tuple[int, int | None]] = []
seen_callers: set[int] = set()
for offset in range(0, stack_words * 2, 2):
address = (sp + offset) & 0xFFFF
try:
value = emulator.memory.read16(address)
except Exception:
continue
words.append((address, value))
if value in seen_callers or not emulator.memory.rom.contains(value):
continue
seen_callers.add(value)
callers.append((value, _likely_call_site(emulator, value)))
return WatchSnapshot(
pc=emulator.cpu.pc & 0xFFFF,
step=emulator.cpu.steps,
regs=tuple(register & 0xFFFF for register in emulator.cpu.regs),
sp=sp,
stack_words=tuple(words),
callers=tuple(callers),
)
def run_probe(
rom_bytes: bytes,
*,
max_steps: int,
interval_steps: int,
stop_on_tx: bool,
p9_log_limit: int,
frt2_ocia_steps: int = 1024,
p9_fast_path: bool = False,
p9_fast_input: int = 0xFF,
watch_pcs: list[int] | tuple[int, ...] | None = None,
watch_snapshot_limit: int = 32,
watch_pc_limit: int = 8,
watch_min_interval: int = 1024,
) -> ProbeReport:
emulator = H8536Emulator(
rom_bytes,
interval_steps=interval_steps,
frt2_ocia_steps=frt2_ocia_steps,
p9_fast_path_enabled=p9_fast_path,
p9_fast_default_input_byte=p9_fast_input,
)
hot_pcs: Counter[int] = Counter()
p9_accesses: list[str] = []
sci_accesses: list[str] = []
snapshots: list[WatchSnapshot] = []
watch_set = set(DEFAULT_WATCH_PCS if watch_pcs is None else watch_pcs)
watch_counts: Counter[int] = Counter()
watch_last_step: dict[int, int] = {}
stopped_reason = "max_steps"
unsupported: str | None = None
last_access_index = 0
for _ in range(max_steps):
pc = emulator.cpu.pc
hot_pcs[pc] += 1
if pc in watch_set and watch_counts[pc] < watch_pc_limit:
last_step = watch_last_step.get(pc)
if last_step is None or emulator.cpu.steps - last_step >= watch_min_interval:
snapshots.append(_watch_snapshot(emulator))
if len(snapshots) > watch_snapshot_limit:
del snapshots[: len(snapshots) - watch_snapshot_limit]
watch_counts[pc] += 1
watch_last_step[pc] = emulator.cpu.steps
last_access_index = len(emulator.memory.access_log)
try:
emulator.step()
except UnsupportedInstruction as exc:
stopped_reason = "unsupported_instruction"
unsupported = str(exc)
break
for access in emulator.memory.access_log[last_access_index:]:
if access.address in (P9DDR, P9DR):
p9_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}")
if len(p9_accesses) > p9_log_limit:
del p9_accesses[: len(p9_accesses) - p9_log_limit]
elif access.address == SCI1_TDR:
sci_accesses.append(f"{h16(pc)} {access.kind} {h16(access.address)}={access.value:02X}")
last_access_index = len(emulator.memory.access_log)
if stop_on_tx and emulator.sci1.tx_bytes:
stopped_reason = "tx"
break
if emulator.sci1.saw_heartbeat():
stopped_reason = "heartbeat"
break
return ProbeReport(
steps=emulator.cpu.steps,
pc=emulator.cpu.pc,
stopped_reason=stopped_reason,
hot_pcs=hot_pcs,
tx_bytes=bytes(emulator.sci1.tx_bytes),
p9_bytes=list(emulator.memory.p9_bus.byte_candidates),
p9_fast_bytes=list(emulator.p9_fast_path.output_bytes),
p9_fast_events=len(emulator.p9_fast_path.events),
p9_accesses=p9_accesses,
sci_accesses=sci_accesses,
watch_snapshots=snapshots,
unsupported=unsupported,
)
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Probe H8/536 emulator progress and likely hold-ups")
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to the repo ROM image")
parser.add_argument("--max-steps", type=int, default=250_000)
parser.add_argument("--interval-steps", type=int, default=512)
parser.add_argument("--frt2-ocia-steps", type=int, default=1024)
parser.add_argument("--stop-on-tx", action="store_true", help="stop when SCI1 TDR emits the first byte")
parser.add_argument("--p9-fast-path", action="store_true", help="shortcut known P9 bit-banged transfer routines for exploration")
parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF)
parser.add_argument("--p9-log-limit", type=int, default=80)
parser.add_argument("--hot-limit", type=int, default=12)
parser.add_argument(
"--watch-pc",
action="append",
type=parse_watch_pc,
default=[],
help="additional PC to snapshot when hit, e.g. C08B, 0xC08B, or H'C08B",
)
parser.add_argument("--watch-snapshot-limit", type=int, default=32)
parser.add_argument("--watch-pc-limit", type=int, default=8)
parser.add_argument("--watch-min-interval", type=int, default=1024)
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
print(f"rom={rom_path}")
report = run_probe(
rom_bytes,
max_steps=args.max_steps,
interval_steps=args.interval_steps,
frt2_ocia_steps=args.frt2_ocia_steps,
stop_on_tx=args.stop_on_tx,
p9_log_limit=args.p9_log_limit,
p9_fast_path=args.p9_fast_path,
p9_fast_input=args.p9_fast_input,
watch_pcs=tuple(dict.fromkeys((*DEFAULT_WATCH_PCS, *args.watch_pc))),
watch_snapshot_limit=args.watch_snapshot_limit,
watch_pc_limit=args.watch_pc_limit,
watch_min_interval=args.watch_min_interval,
)
for line in report.lines(hot_limit=args.hot_limit):
print(line)
return 0

View File

@@ -7,15 +7,22 @@ from ..formatting import h16
from ..rom import DecodeError from ..rom import DecodeError
from ..vectors import read_vectors_min from ..vectors import read_vectors_min
from .constants import ( from .constants import (
FRT_TCR_OCIEA,
FRT_TCSR_OCFA,
FRT2_TCR,
FRT2_TCSR,
IPRA, IPRA,
IPRC,
IPRE, IPRE,
SCI_SCR_TIE, SCI_SCR_TIE,
SCI_SSR_TDRE, SCI_SSR_TDRE,
VECTOR_FRT2_OCIA,
VECTOR_INTERVAL_TIMER, VECTOR_INTERVAL_TIMER,
VECTOR_SCI1_TXI, VECTOR_SCI1_TXI,
) )
from .cpu import CPUState, mask, s8, s16, sign_bit from .cpu import CPUState, mask, s8, s16, sign_bit
from .errors import EmulatorError, UnsupportedInstruction from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig
from .memory import MemoryMap from .memory import MemoryMap
from .sci import SCI1 from .sci import SCI1
@@ -51,15 +58,29 @@ class RunReport:
class H8536Emulator: class H8536Emulator:
def __init__(self, rom_bytes: bytes, *, interval_steps: int = 2048) -> None: def __init__(
self,
rom_bytes: bytes,
*,
interval_steps: int = 2048,
frt2_ocia_steps: int = 1024,
p9_fast_path: P9FastPath | None = None,
p9_fast_path_enabled: bool = False,
p9_fast_default_input_byte: int = 0xFF,
) -> None:
if not rom_bytes: if not rom_bytes:
raise ValueError("ROM image is empty") raise ValueError("ROM image is empty")
self.sci1 = SCI1() self.sci1 = SCI1()
self.memory = MemoryMap(rom_bytes, self.sci1) self.memory = MemoryMap(rom_bytes, self.sci1)
self.p9_fast_path = p9_fast_path or P9FastPath(
P9FastPathConfig(enabled=p9_fast_path_enabled, default_input_byte=p9_fast_default_input_byte)
)
self.cpu = CPUState() self.cpu = CPUState()
self.vectors = read_vectors_min(self.memory.rom) self.vectors = read_vectors_min(self.memory.rom)
self.interval_steps = max(1, interval_steps) self.interval_steps = max(1, interval_steps)
self.frt2_ocia_steps = max(1, frt2_ocia_steps)
self._interval_counter = 0 self._interval_counter = 0
self._frt2_ocia_counter = 0
self.reset() self.reset()
def reset(self) -> None: def reset(self) -> None:
@@ -72,6 +93,10 @@ class H8536Emulator:
def step(self) -> str: def step(self) -> str:
pc = self.cpu.pc pc = self.cpu.pc
if self.p9_fast_path.try_handle(self):
self._tick_peripherals()
return f"{h16(pc)}: {'<p9-fast-path>':<17} P9 fast-path"
decoder = H8536Decoder(self.memory.rom, br=self.cpu.br) decoder = H8536Decoder(self.memory.rom, br=self.cpu.br)
ins = decoder.decode(pc) ins = decoder.decode(pc)
if not ins.valid: if not ins.valid:
@@ -99,6 +124,7 @@ class H8536Emulator:
self._cmp(self.cpu.regs[reg], int.from_bytes(raw[1:3], "big"), 2) self._cmp(self.cpu.regs[reg], int.from_bytes(raw[1:3], "big"), 2)
elif 0x50 <= raw[0] <= 0x57 and len(raw) == 2: elif 0x50 <= raw[0] <= 0x57 and len(raw) == 2:
self._reg_write(raw[0] & 0x07, raw[1], 1) self._reg_write(raw[0] & 0x07, raw[1], 1)
self._set_logic_flags(raw[1], 1)
elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3: elif 0x58 <= raw[0] <= 0x5F and len(raw) == 3:
self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big") self.cpu.regs[raw[0] & 0x07] = int.from_bytes(raw[1:3], "big")
self._set_logic_flags(self.cpu.regs[raw[0] & 0x07], 2) self._set_logic_flags(self.cpu.regs[raw[0] & 0x07], 2)
@@ -221,6 +247,13 @@ class H8536Emulator:
self._set_logic_flags(result, size) self._set_logic_flags(result, size)
elif base == 0x70: elif base == 0x70:
self._cmp(self._reg_read(rd, size), self._read_ea(ea, size), size) self._cmp(self._reg_read(rd, size), self._read_ea(ea, size), size)
elif base == 0xA8:
if size != 1:
raise UnsupportedInstruction(pc, raw, H8536Decoder(self.memory.rom, br=self.cpu.br).decode(pc).text)
result = (self._reg_read(rd, 1) * self._read_ea(ea, 1)) & 0xFFFF
self.cpu.regs[rd] = result
self._set_logic_flags(result, 2)
self.cpu.c = False
elif op in (0x08, 0x09, 0x0C, 0x0D): elif op in (0x08, 0x09, 0x0C, 0x0D):
delta = {0x08: 1, 0x09: 2, 0x0C: -1, 0x0D: -2}[op] delta = {0x08: 1, 0x09: 2, 0x0C: -1, 0x0D: -2}[op]
old = self._read_ea(ea, size) old = self._read_ea(ea, size)
@@ -233,6 +266,10 @@ class H8536Emulator:
elif op == 0x13: elif op == 0x13:
self._write_ea(ea, 0, size) self._write_ea(ea, 0, size)
self._set_logic_flags(0, size) self._set_logic_flags(0, size)
elif op == 0x15:
result = (~self._read_ea(ea, size)) & mask(size)
self._write_ea(ea, result, size)
self._set_logic_flags(result, size)
elif op == 0x16: elif op == 0x16:
self._set_logic_flags(self._read_ea(ea, size), size) self._set_logic_flags(self._read_ea(ea, size), size)
elif op == 0x10: elif op == 0x10:
@@ -335,6 +372,7 @@ class H8536Emulator:
def _tick_peripherals(self) -> None: def _tick_peripherals(self) -> None:
self.sci1.tick() self.sci1.tick()
self._interval_counter += 1 self._interval_counter += 1
self._frt2_ocia_counter += 1
self._service_pending_interrupt() self._service_pending_interrupt()
def _service_pending_interrupt(self) -> None: def _service_pending_interrupt(self) -> None:
@@ -349,6 +387,10 @@ class H8536Emulator:
target = self._vector_target(VECTOR_INTERVAL_TIMER) target = self._vector_target(VECTOR_INTERVAL_TIMER)
if target is not None: if target is not None:
candidates.append((self._interval_priority(), target, "interval_timer")) candidates.append((self._interval_priority(), target, "interval_timer"))
if self._frt2_ocia_pending():
target = self._vector_target(VECTOR_FRT2_OCIA)
if target is not None:
candidates.append((self._frt2_priority(), target, "frt2_ocia"))
if not candidates: if not candidates:
return return
@@ -357,6 +399,9 @@ class H8536Emulator:
return return
if source == "interval_timer": if source == "interval_timer":
self._interval_counter = 0 self._interval_counter = 0
elif source == "frt2_ocia":
self._frt2_ocia_counter = 0
self.memory.write8(FRT2_TCSR, self.memory.read8(FRT2_TCSR) | FRT_TCSR_OCFA)
self._enter_interrupt(target) self._enter_interrupt(target)
def _enter_interrupt(self, target: int) -> None: def _enter_interrupt(self, target: int) -> None:
@@ -385,6 +430,16 @@ class H8536Emulator:
def _interval_priority(self) -> int: def _interval_priority(self) -> int:
return (self.memory.read8(IPRA) >> 4) & 0x07 return (self.memory.read8(IPRA) >> 4) & 0x07
def _frt2_ocia_pending(self) -> bool:
if self._frt2_ocia_counter < self.frt2_ocia_steps:
return False
return bool(self.memory.read8(FRT2_TCR) & FRT_TCR_OCIEA)
def _frt2_priority(self) -> int:
# H8/536 IPRC assigns bits 6..4 to FRT1 and bits 2..0 to FRT2;
# the ROM's IPRC=H'66 therefore gives both timers priority 6.
return self.memory.read8(IPRC) & 0x07
def _push16(self, value: int) -> None: def _push16(self, value: int) -> None:
sp = (self.cpu.regs[7] - 2) & 0xFFFF sp = (self.cpu.regs[7] - 2) & 0xFFFF
self.cpu.regs[7] = sp self.cpu.regs[7] = sp

8
h8536_emulator_probe.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 emulator progress probe."""
from h8536.emulator.probe import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,87 @@
import unittest
from h8536.emulator.fast_paths import (
LOC_C08B_P9_WRITE_BYTE,
LOC_C0DB_P9_READ_BYTE,
P9FastPath,
P9FastPathConfig,
)
from h8536.emulator.runner import H8536Emulator
def rom_with_reset(*, reset: int = 0x1000, size: int = 0xD000) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class P9FastPathTest(unittest.TestCase):
def test_disabled_fast_path_does_not_handle_known_pc(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE
fast_path = P9FastPath()
self.assertFalse(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.pc, LOC_C08B_P9_WRITE_BYTE)
def test_c08b_write_byte_logs_r0_sets_success_and_returns_to_caller(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C08B_P9_WRITE_BYTE
emulator.cpu.regs[0] = 0x12A5
emulator.cpu.regs[7] = 0xFE7E
emulator.cpu.c = True
emulator.cpu.v = True
emulator.memory.write16(0xFE7E, 0x3456)
fast_path = P9FastPath(P9FastPathConfig(enabled=True))
self.assertTrue(fast_path.try_handle(emulator))
self.assertEqual(fast_path.output_bytes, [0xA5])
self.assertEqual(fast_path.events[-1].kind, "write_byte")
self.assertEqual(fast_path.events[-1].value, 0xA5)
self.assertEqual(emulator.cpu.regs[0], 1)
self.assertEqual(emulator.cpu.pc, 0x3456)
self.assertEqual(emulator.cpu.regs[7], 0xFE80)
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
self.assertEqual(emulator.cpu.steps, 1)
def test_c0db_read_byte_puts_queued_byte_in_r5_low_and_returns(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
emulator.cpu.regs[5] = 0xBE00
emulator.cpu.regs[7] = 0xFE7C
emulator.memory.write16(0xFE7C, 0x4567)
fast_path = P9FastPath(P9FastPathConfig(enabled=True), input_bytes=[0x3C])
self.assertTrue(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.regs[5], 0xBE3C)
self.assertEqual(emulator.cpu.pc, 0x4567)
self.assertEqual(emulator.cpu.regs[7], 0xFE7E)
self.assertEqual(fast_path.events[-1].kind, "read_byte")
self.assertEqual(fast_path.events[-1].value, 0x3C)
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
def test_c0db_read_byte_uses_default_when_queue_is_empty(self):
emulator = H8536Emulator(bytes(rom_with_reset()))
emulator.cpu.pc = LOC_C0DB_P9_READ_BYTE
emulator.cpu.regs[7] = 0xFE80
emulator.memory.write16(0xFE80, 0x5678)
fast_path = P9FastPath(P9FastPathConfig(enabled=True, default_input_byte=0x81))
self.assertTrue(fast_path.try_handle(emulator))
self.assertEqual(emulator.cpu.regs[5], 0x0081)
self.assertEqual(emulator.cpu.pc, 0x5678)
self.assertFalse(emulator.cpu.z)
self.assertTrue(emulator.cpu.n)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,116 @@
import unittest
from h8536.emulator import H8536Emulator
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorMovFlagTest(unittest.TestCase):
def test_mov_e_byte_zero_sets_z_for_beq_and_preserves_c(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\x50\x00" # MOV:E.B #H'00, R0
rom[0x1002:0x1004] = b"\x27\x02" # BEQ H'1006
rom[0x1004:0x1006] = b"\x51\x01" # MOV:E.B #H'01, R1
rom[0x1006:0x1008] = b"\x52\x02" # MOV:E.B #H'02, R2
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[1], 0)
self.assertEqual(emulator.cpu.regs[2], 2)
self.assertEqual(emulator.cpu.pc, 0x1008)
def test_mov_e_byte_one_clears_z_for_beq_and_preserves_c(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\x50\x01" # MOV:E.B #H'01, R0
rom[0x1002:0x1004] = b"\x27\x02" # BEQ H'1006
rom[0x1004:0x1006] = b"\x51\x01" # MOV:E.B #H'01, R1
rom[0x1006:0x1008] = b"\x52\x02" # MOV:E.B #H'02, R2
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[1], 1)
self.assertEqual(emulator.cpu.regs[2], 0)
self.assertEqual(emulator.cpu.pc, 0x1006)
def test_mov_i_word_immediate_sets_word_flags_and_preserves_c(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x58\x80\x00" # MOV:I.W #H'8000, R0
rom[0x1003:0x1006] = b"\x59\x00\x00" # MOV:I.W #H'0000, R1
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertEqual(emulator.cpu.regs[0], 0x8000)
self.assertFalse(emulator.cpu.z)
self.assertTrue(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
emulator.step()
self.assertEqual(emulator.cpu.regs[1], 0)
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
def test_mulxu_byte_immediate_writes_word_result_and_clears_carry(self):
rom = rom_with_reset()
rom[0x1000:0x1002] = b"\x53\x12" # MOV:E.B #H'12, R3
rom[0x1002:0x1005] = b"\x04\x10\xAB" # MULXU.B #H'10, R3
emulator = H8536Emulator(bytes(rom))
emulator.cpu.c = True
emulator.run(max_steps=2)
self.assertEqual(emulator.cpu.regs[3], 0x0120)
self.assertFalse(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertFalse(emulator.cpu.c)
def test_not_byte_memory_updates_logic_flags_and_preserves_carry(self):
rom = rom_with_reset()
rom[0x1000:0x1005] = b"\x15\xF6\x80\x15\x00" # NOT.B @H'F680, then NOP
emulator = H8536Emulator(bytes(rom))
emulator.memory.write8(0xF680, 0xFF)
emulator.cpu.c = True
emulator.cpu.v = True
emulator.step()
self.assertEqual(emulator.memory.read8(0xF680), 0x00)
self.assertTrue(emulator.cpu.z)
self.assertFalse(emulator.cpu.n)
self.assertFalse(emulator.cpu.v)
self.assertTrue(emulator.cpu.c)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,25 @@
import unittest
from h8536.emulator import MemoryMap
from h8536.emulator.peripherals import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
class EmulatorLcdBusTest(unittest.TestCase):
def test_status_read_reports_ready_even_after_command_write(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(LCD_E_CLOCK_STATUS, 0x80)
self.assertEqual(memory.read8(LCD_E_CLOCK_STATUS), 0x00)
def test_data_read_returns_data_latch_defaulting_to_zero(self):
memory = MemoryMap(b"\x00" * 4)
self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x00)
memory.write8(LCD_E_CLOCK_DATA, 0x41)
self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x41)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,56 @@
import unittest
from h8536.emulator.probe import DEFAULT_WATCH_PCS, parse_watch_pc, run_probe
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1100) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
class EmulatorProbeTest(unittest.TestCase):
def test_parse_watch_pc_accepts_h8_hex_forms(self):
self.assertEqual(parse_watch_pc("C08B"), 0xC08B)
self.assertEqual(parse_watch_pc("0xC08B"), 0xC08B)
self.assertEqual(parse_watch_pc("H'C08B"), 0xC08B)
def test_default_watch_pcs_include_bit_bang_transfer_path(self):
self.assertIn(0xC08B, DEFAULT_WATCH_PCS)
self.assertIn(0xC0DB, DEFAULT_WATCH_PCS)
self.assertIn(0xC121, DEFAULT_WATCH_PCS)
self.assertIn(0xBFE0, DEFAULT_WATCH_PCS)
self.assertIn(0xBFFE, DEFAULT_WATCH_PCS)
self.assertIn(0xC059, DEFAULT_WATCH_PCS)
def test_watch_snapshot_includes_bsr_return_address_on_stack(self):
rom = rom_with_reset()
rom[0x1000:0x1003] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
rom[0x1003:0x1005] = b"\x0E\x03" # BSR H'1008, return H'1005
rom[0x1005:0x1008] = b"\x59\x12\x34" # MOV:I.W #H'1234, R1
rom[0x1008] = 0x19 # RTS
report = run_probe(
bytes(rom),
max_steps=4,
interval_steps=512,
stop_on_tx=False,
p9_log_limit=8,
watch_pcs=(0x1008,),
watch_snapshot_limit=4,
watch_pc_limit=2,
watch_min_interval=0,
)
self.assertEqual(len(report.watch_snapshots), 1)
snapshot = report.watch_snapshots[0]
self.assertEqual(snapshot.pc, 0x1008)
self.assertEqual(snapshot.sp, 0xFE7E)
self.assertIn((0xFE7E, 0x1005), snapshot.stack_words)
self.assertIn((0x1005, 0x1003), snapshot.callers)
self.assertIn("H'1005<-H'1003", snapshot.line())
self.assertTrue(any("recent_watch_snapshots:" == line for line in report.lines()))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,78 @@
import unittest
from h8536.emulator import H8536Emulator, ON_CHIP_RAM_START
from h8536.emulator.constants import (
FRT_TCR_OCIEA,
FRT_TCSR_OCFA,
FRT2_TCR,
FRT2_TCSR,
IPRC,
VECTOR_FRT2_OCIA,
)
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1040) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
return rom
def write_mov_b_abs_imm(rom: bytearray, address: int, target: int, value: int) -> int:
rom[address : address + 5] = bytes([0x15, (target >> 8) & 0xFF, target & 0xFF, 0x06, value & 0xFF])
return address + 5
class Frt2OciaTimerTest(unittest.TestCase):
def test_frt2_ocia_vector_can_fire_and_decrement_ram(self):
rom = rom_with_reset()
rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
# IPRC bits 6..4 are FRT1 and bits 2..0 are FRT2, so H'06 makes
# only the FRT2 priority field high enough to pass interrupt mask 0.
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x06)
pc = write_mov_b_abs_imm(rom, pc, FRT2_TCR, FRT_TCR_OCIEA)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
isr = 0x1020
rom[isr : isr + 4] = bytes([0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C])
isr += 4
rom[isr : isr + 4] = bytes([0x15, (FRT2_TCSR >> 8) & 0xFF, FRT2_TCSR & 0xFF, 0xD5])
rom[isr + 4] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt2_ocia_steps=2)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=5)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 2)
self.assertFalse(emulator.memory.read8(FRT2_TCSR) & FRT_TCSR_OCFA)
self.assertEqual(emulator.cpu.pc, isr + 4)
def test_frt2_ocia_does_not_fire_when_ociea_disabled(self):
rom = rom_with_reset()
rom[VECTOR_FRT2_OCIA : VECTOR_FRT2_OCIA + 2] = (0x1020).to_bytes(2, "big")
pc = 0x1000
rom[pc : pc + 3] = b"\x5F\xFE\x80" # MOV:I.W #H'FE80, R7
pc += 3
pc = write_mov_b_abs_imm(rom, pc, IPRC, 0x06)
rom[pc : pc + 2] = b"\x20\xFE" # BRA self
rom[0x1020 : 0x1024] = bytes(
[0x15, (ON_CHIP_RAM_START >> 8) & 0xFF, ON_CHIP_RAM_START & 0xFF, 0x0C]
)
rom[0x1024] = 0x0A # RTE
emulator = H8536Emulator(bytes(rom), frt2_ocia_steps=1)
emulator.memory.write8(ON_CHIP_RAM_START, 3)
emulator.run(max_steps=8)
self.assertEqual(emulator.memory.read8(ON_CHIP_RAM_START), 3)
self.assertEqual(emulator.memory.read8(FRT2_TCSR) & FRT_TCSR_OCFA, 0)
self.assertEqual(emulator.cpu.pc, pc)
if __name__ == "__main__":
unittest.main()

42
tests/test_p9_bus.py Normal file
View File

@@ -0,0 +1,42 @@
import unittest
from h8536.emulator import MemoryMap, P9DDR, P9DR
from h8536.emulator.peripherals import P9Bus
class P9BusTest(unittest.TestCase):
def test_bit7_input_uses_queued_then_default_low_response(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(P9DDR, 0x13)
memory.write8(P9DR, 0x80)
memory.p9_bus.queue_input_bits([1])
self.assertEqual(memory.read8(P9DDR), 0x13)
self.assertEqual(memory.read8(P9DR) & 0x80, 0x80)
self.assertEqual(memory.read8(P9DR) & 0x80, 0x00)
self.assertEqual(memory.registers[P9DR - 0xFE80], 0x80)
def test_bit7_output_reads_latch(self):
memory = MemoryMap(b"\x00" * 4)
memory.write8(P9DDR, 0x93)
memory.write8(P9DR, 0x80)
self.assertEqual(memory.read8(P9DDR), 0x93)
self.assertEqual(memory.read8(P9DR) & 0x80, 0x80)
self.assertEqual(memory.registers[P9DR - 0xFE80], 0x80)
def test_strobe_rising_edges_capture_output_bits_and_byte_candidates(self):
bus = P9Bus()
bus.write_ddr(0x93)
for bit in (1, 0, 1, 0, 0, 1, 0, 1):
bus.write_dr(0x80 if bit else 0x00)
bus.write_dr((0x80 if bit else 0x00) | 0x02)
bus.write_dr(0x80 if bit else 0x00)
self.assertEqual(bus.transmitted_bits, [1, 0, 1, 0, 0, 1, 0, 1])
self.assertEqual(bus.byte_candidates, [0xA5])
self.assertEqual([event.edge for event in bus.strobe_edges[:2]], ["rising", "falling"])
if __name__ == "__main__":
unittest.main()