emulator improvements
This commit is contained in:
85
README.md
85
README.md
@@ -26,6 +26,21 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction
|
|||||||
.\.venv\Scripts\python.exe h8536_serial_pseudocode.py build\rom_decompiled.json --out build\rom_serial_pseudocode.c
|
.\.venv\Scripts\python.exe h8536_serial_pseudocode.py build\rom_decompiled.json --out build\rom_serial_pseudocode.c
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To run the newer sidecar protocol and gate/queue analysis tools:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe h8536_serial_gate.py build\rom_decompiled.json --out build\rom_serial_gate.txt
|
||||||
|
.\.venv\Scripts\python.exe h8536_report_source_trace.py build\rom_decompiled.json --out build\rom_report_sources.txt
|
||||||
|
.\.venv\Scripts\python.exe h8536_table_xrefs.py --out build\rom_table_xrefs.txt
|
||||||
|
.\.venv\Scripts\python.exe h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
To start the current emulator harness:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe h8536_emulator.py --max-steps 1000000 --stop-on-heartbeat --interval-steps 512
|
||||||
|
```
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
- Decodes the H8/500 instruction set used by the H8/536.
|
- Decodes the H8/500 instruction set used by the H8/536.
|
||||||
@@ -43,6 +58,11 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction
|
|||||||
- Reconstructs evidence-supported SCI1 serial frame candidates, including the apparent six-byte TX/RX units and XOR checksum seeded by `0x5A`.
|
- Reconstructs evidence-supported SCI1 serial frame candidates, including the apparent six-byte TX/RX units and XOR checksum seeded by `0x5A`.
|
||||||
- Infers candidate serial protocol semantics from validated frames, including `RX[0] & 0x07` command dispatch, likely index/value byte roles, and response staging through `F850-F854`.
|
- Infers candidate serial protocol semantics from validated frames, including `RX[0] & 0x07` command dispatch, likely index/value byte roles, and response staging through `F850-F854`.
|
||||||
- Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates.
|
- Generates a focused RX/TX serial-path pseudocode view from those serial reconstruction and protocol-semantic candidates.
|
||||||
|
- Decodes observed serial byte captures into six-byte frames, validates checksums, labels capture-observed heartbeat/call/camera-power candidates, and summarizes heartbeat cadence.
|
||||||
|
- Accepts both analyzer-style lines such as `RX 006 bytes ...` and the idle reference `frame 006 ...` format in `ROM/rcp-txd-idle-only.txt`.
|
||||||
|
- Reconstructs the autonomous serial gate/queue state-machine around `loc_3FD3`, `loc_BAF2`, `F9B0/F9B5`, `FAA2/FAA3/FAA5`, and the resend path through `BE9E/BED5`.
|
||||||
|
- Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations.
|
||||||
|
- Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations.
|
||||||
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
|
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
|
||||||
- Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers.
|
- Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers.
|
||||||
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
|
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
|
||||||
@@ -56,6 +76,15 @@ To generate a focused RX/TX serial-path pseudocode view from the reconstruction
|
|||||||
- 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.
|
||||||
|
|
||||||
|
Current serial observations:
|
||||||
|
|
||||||
|
- Idle capture reference: `ROM/rcp-txd-idle-only.txt`.
|
||||||
|
- Idle frame: `00 00 00 00 80 DA`.
|
||||||
|
- Capture-side label: `heartbeat_alive_candidate`.
|
||||||
|
- Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms.
|
||||||
|
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
|
||||||
|
|
||||||
The generated listing is written to:
|
The generated listing is written to:
|
||||||
|
|
||||||
@@ -69,6 +98,17 @@ The optional JSON output is useful for scripts or later analysis:
|
|||||||
build/rom_decompiled.json
|
build/rom_decompiled.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Common derived outputs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
build/rom_pseudocode.c
|
||||||
|
build/rom_serial_pseudocode.c
|
||||||
|
build/rom_serial_gate.txt
|
||||||
|
build/rom_report_sources.txt
|
||||||
|
build/rom_table_xrefs.txt
|
||||||
|
build/callgraph.dot
|
||||||
|
```
|
||||||
|
|
||||||
## Useful Options
|
## Useful Options
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -111,6 +151,42 @@ python h8536_serial_pseudocode.py --help
|
|||||||
- `--no-board`: omit board/MAX202 comments.
|
- `--no-board`: omit board/MAX202 comments.
|
||||||
- `--no-semantics`: omit candidate command/field semantics.
|
- `--no-semantics`: omit candidate command/field semantics.
|
||||||
|
|
||||||
|
For protocol trace and capture logs:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python h8536_protocol_trace.py --help
|
||||||
|
python h8536_protocol_capture.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
- `h8536_protocol_trace.py --direction tx 00 00 15 80 00 CF`: decode raw bytes as protocol frames.
|
||||||
|
- `h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt`: parse timestamped captures, recombine split chunks, validate checksums, and summarize cadence/gate hints.
|
||||||
|
- `--json` on the capture tool emits machine-readable frame and cadence data.
|
||||||
|
|
||||||
|
For gate/queue and table reports:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python h8536_serial_gate.py --help
|
||||||
|
python h8536_report_source_trace.py --help
|
||||||
|
python h8536_table_xrefs.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
- `h8536_serial_gate.py`: reports the autonomous TX gate and report queue evidence.
|
||||||
|
- `h8536_report_source_trace.py`: traces direct `loc_3E54` report enqueue sources. Current finding: no direct static `R3 = 0x0007` enqueue in the JSON, so CAM power `0x0007` remains runtime/capture-observed unless a later indirect/table path proves it.
|
||||||
|
- `h8536_table_xrefs.py`: emits candidate table/index xrefs and LCD text correlation hints.
|
||||||
|
|
||||||
|
For the emulator harness:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python h8536_emulator.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--rom PATH`: use an explicit ROM path instead of auto-discovering `ROM\M27C512@DIP28_1.BIN`.
|
||||||
|
- `--max-steps N`: bound execution.
|
||||||
|
- `--trace`: print executed instructions.
|
||||||
|
- `--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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
## Code Layout
|
## Code Layout
|
||||||
|
|
||||||
- `h8536_decompiler.py`: compatibility wrapper for the CLI.
|
- `h8536_decompiler.py`: compatibility wrapper for the CLI.
|
||||||
@@ -134,6 +210,12 @@ python h8536_serial_pseudocode.py --help
|
|||||||
- `h8536/serial_reconstruction.py`: cautious higher-level SCI frame reconstruction from decompiled evidence.
|
- `h8536/serial_reconstruction.py`: cautious higher-level SCI frame reconstruction from decompiled evidence.
|
||||||
- `h8536/serial_semantics.py`: candidate command/field semantics inferred from serial frame use.
|
- `h8536/serial_semantics.py`: candidate command/field semantics inferred from serial frame use.
|
||||||
- `h8536/serial_pseudocode.py`: focused RX/TX protocol pseudocode generation from reconstruction metadata.
|
- `h8536/serial_pseudocode.py`: focused RX/TX protocol pseudocode generation from reconstruction metadata.
|
||||||
|
- `h8536/protocol_trace.py`: raw six-byte protocol frame decoder/checksum validator.
|
||||||
|
- `h8536/protocol_capture.py`: timestamped serial capture parser, frame recombiner, and cadence/gate-session analyzer.
|
||||||
|
- `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction.
|
||||||
|
- `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/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, runner, 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.
|
||||||
@@ -141,3 +223,6 @@ python h8536_serial_pseudocode.py --help
|
|||||||
- `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers.
|
- `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers.
|
||||||
- `h8536_pseudocode.py`: pseudocode CLI wrapper.
|
- `h8536_pseudocode.py`: pseudocode CLI wrapper.
|
||||||
- `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_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`: sidecar analysis CLI wrappers.
|
||||||
|
- `h8536_emulator.py`: emulator CLI wrapper.
|
||||||
|
|||||||
83
h8536/emulator/__init__.py
Normal file
83
h8536/emulator/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
7
h8536/emulator/__main__.py
Normal file
7
h8536/emulator/__main__.py
Normal 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
66
h8536/emulator/cli.py
Normal 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
|
||||||
36
h8536/emulator/constants.py
Normal file
36
h8536/emulator/constants.py
Normal 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
34
h8536/emulator/cpu.py
Normal 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
16
h8536/emulator/errors.py
Normal 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
120
h8536/emulator/memory.py
Normal 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)
|
||||||
10
h8536/emulator/peripherals/__init__.py
Normal file
10
h8536/emulator/peripherals/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
5
h8536/emulator/peripherals/lcd.py
Normal file
5
h8536/emulator/peripherals/lcd.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
LCD_E_CLOCK_DATA = 0xF200
|
||||||
|
LCD_E_CLOCK_STATUS = 0xF201
|
||||||
4
h8536/emulator/peripherals/p9_bus.py
Normal file
4
h8536/emulator/peripherals/p9_bus.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
P9_ACK_BIT = 0x80
|
||||||
@@ -1,260 +1,23 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
|
||||||
from typing import Iterable
|
|
||||||
|
|
||||||
from .decoder import H8536Decoder
|
from ..decoder import H8536Decoder
|
||||||
from .formatting import h8, h16
|
from ..formatting import h16
|
||||||
from .memory import MEMORY_REGIONS, MemoryRegion, region_for
|
from ..rom import DecodeError
|
||||||
from .rom import DecodeError, Rom
|
from ..vectors import read_vectors_min
|
||||||
from .vectors import read_vectors_min
|
from .constants import (
|
||||||
|
IPRA,
|
||||||
|
IPRE,
|
||||||
SCI1_SMR = 0xFED8
|
SCI_SCR_TIE,
|
||||||
SCI1_BRR = 0xFED9
|
SCI_SSR_TDRE,
|
||||||
SCI1_SCR = 0xFEDA
|
VECTOR_INTERVAL_TIMER,
|
||||||
SCI1_TDR = 0xFEDB
|
VECTOR_SCI1_TXI,
|
||||||
SCI1_SSR = 0xFEDC
|
)
|
||||||
SCI1_RDR = 0xFEDD
|
from .cpu import CPUState, mask, s8, s16, sign_bit
|
||||||
P9DDR = 0xFEFE
|
from .errors import EmulatorError, UnsupportedInstruction
|
||||||
P9DR = 0xFEFF
|
from .memory import MemoryMap
|
||||||
IPRA = 0xFF00
|
from .sci import SCI1
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -281,7 +44,9 @@ class RunReport:
|
|||||||
]
|
]
|
||||||
if self.unsupported:
|
if self.unsupported:
|
||||||
lines.append(f"unsupported={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
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@@ -390,7 +155,7 @@ class H8536Emulator:
|
|||||||
|
|
||||||
def _execute_general(self, pc: int, next_pc: int) -> int:
|
def _execute_general(self, pc: int, next_pc: int) -> int:
|
||||||
ea = self._decode_ea(pc)
|
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"])
|
size = int(ea["size"])
|
||||||
raw = self.memory.rom.slice(pc, self._general_length(pc, ea, op))
|
raw = self.memory.rom.slice(pc, self._general_length(pc, ea, op))
|
||||||
|
|
||||||
@@ -481,8 +246,8 @@ class H8536Emulator:
|
|||||||
self._set_logic_flags(value, size)
|
self._set_logic_flags(value, size)
|
||||||
elif op == 0x1A:
|
elif op == 0x1A:
|
||||||
value = self._read_ea(ea, size)
|
value = self._read_ea(ea, size)
|
||||||
result = (value << 1) & _mask(size)
|
result = (value << 1) & mask(size)
|
||||||
self.cpu.c = bool(value & _sign_bit(size))
|
self.cpu.c = bool(value & sign_bit(size))
|
||||||
self._write_ea(ea, result, size)
|
self._write_ea(ea, result, size)
|
||||||
self._set_logic_flags(result, size)
|
self._set_logic_flags(result, size)
|
||||||
elif op == 0x1B:
|
elif op == 0x1B:
|
||||||
@@ -522,7 +287,7 @@ class H8536Emulator:
|
|||||||
|
|
||||||
def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int:
|
def _branch8(self, raw: bytes, pc: int, next_pc: int) -> int:
|
||||||
cond = raw[0] & 0x0F
|
cond = raw[0] & 0x0F
|
||||||
disp = _s8(raw[1])
|
disp = s8(raw[1])
|
||||||
target = (pc + 2 + disp) & 0xFFFF
|
target = (pc + 2 + disp) & 0xFFFF
|
||||||
if self._branch_condition(cond):
|
if self._branch_condition(cond):
|
||||||
return target
|
return target
|
||||||
@@ -530,7 +295,7 @@ class H8536Emulator:
|
|||||||
|
|
||||||
def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int:
|
def _branch16(self, raw: bytes, pc: int, next_pc: int) -> int:
|
||||||
cond = raw[0] & 0x0F
|
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
|
target = (pc + 3 + disp) & 0xFFFF
|
||||||
if self._branch_condition(cond):
|
if self._branch_condition(cond):
|
||||||
return target
|
return target
|
||||||
@@ -550,9 +315,9 @@ class H8536Emulator:
|
|||||||
def _direct_call(self, raw: bytes, next_pc: int) -> int:
|
def _direct_call(self, raw: bytes, next_pc: int) -> int:
|
||||||
self._push16(next_pc)
|
self._push16(next_pc)
|
||||||
if raw[0] == 0x0E:
|
if raw[0] == 0x0E:
|
||||||
return (next_pc + _s8(raw[1])) & 0xFFFF
|
return (next_pc + s8(raw[1])) & 0xFFFF
|
||||||
if raw[0] == 0x1E:
|
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")
|
return int.from_bytes(raw[1:3], "big")
|
||||||
|
|
||||||
def _scb(self, raw: bytes, pc: int, next_pc: int) -> int:
|
def _scb(self, raw: bytes, pc: int, next_pc: int) -> int:
|
||||||
@@ -564,7 +329,7 @@ class H8536Emulator:
|
|||||||
self.cpu.regs[reg] = value
|
self.cpu.regs[reg] = value
|
||||||
self.cpu.z = value == 0
|
self.cpu.z = value == 0
|
||||||
if value != 0:
|
if value != 0:
|
||||||
return (pc + 3 + _s8(raw[2])) & 0xFFFF
|
return (pc + 3 + s8(raw[2])) & 0xFFFF
|
||||||
return next_pc
|
return next_pc
|
||||||
|
|
||||||
def _tick_peripherals(self) -> None:
|
def _tick_peripherals(self) -> None:
|
||||||
@@ -631,14 +396,14 @@ class H8536Emulator:
|
|||||||
self.cpu.regs[7] = (sp + 2) & 0xFFFF
|
self.cpu.regs[7] = (sp + 2) & 0xFFFF
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def _push_register_mask(self, mask: int) -> None:
|
def _push_register_mask(self, mask_value: int) -> None:
|
||||||
for reg in range(8):
|
for reg in range(8):
|
||||||
if mask & (1 << reg):
|
if mask_value & (1 << reg):
|
||||||
self._push16(self.cpu.regs[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)):
|
for reg in reversed(range(8)):
|
||||||
if mask & (1 << reg):
|
if mask_value & (1 << reg):
|
||||||
self.cpu.regs[reg] = self._pop16()
|
self.cpu.regs[reg] = self._pop16()
|
||||||
|
|
||||||
def _decode_ea(self, pc: int) -> dict[str, int | str | None]:
|
def _decode_ea(self, pc: int) -> dict[str, int | str | None]:
|
||||||
@@ -652,9 +417,9 @@ class H8536Emulator:
|
|||||||
if 0xD0 <= first <= 0xDF:
|
if 0xD0 <= first <= 0xDF:
|
||||||
return {"mode": "indirect", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
|
return {"mode": "indirect", "reg": first & 0x07, "size": 2 if first & 0x08 else 1, "length": 1, "value": None}
|
||||||
if 0xE0 <= first <= 0xEF:
|
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:
|
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):
|
if first in (0x04, 0x0C):
|
||||||
size = 2 if first == 0x0C else 1
|
size = 2 if first == 0x0C else 1
|
||||||
value = self.memory.rom.u16(pc + 1) if size == 2 else self.memory.rom.u8(pc + 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:
|
def _read_ea(self, ea: dict[str, int | str | None], size: int) -> int:
|
||||||
mode = ea["mode"]
|
mode = ea["mode"]
|
||||||
if mode == "imm":
|
if mode == "imm":
|
||||||
return int(ea["value"]) & _mask(size)
|
return int(ea["value"]) & mask(size)
|
||||||
if mode == "reg":
|
if mode == "reg":
|
||||||
return self._reg_read(int(ea["reg"]), size)
|
return self._reg_read(int(ea["reg"]), size)
|
||||||
address = self._ea_address(ea, size)
|
address = self._ea_address(ea, size)
|
||||||
@@ -727,28 +492,28 @@ class H8536Emulator:
|
|||||||
self._set_sub_flags(lhs, rhs, lhs - rhs, size)
|
self._set_sub_flags(lhs, rhs, lhs - rhs, size)
|
||||||
|
|
||||||
def _set_logic_flags(self, value: int, size: int) -> None:
|
def _set_logic_flags(self, value: int, size: int) -> None:
|
||||||
value &= _mask(size)
|
value &= mask(size)
|
||||||
self.cpu.z = value == 0
|
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
|
self.cpu.v = False
|
||||||
|
|
||||||
def _set_add_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
|
def _set_add_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
|
||||||
mask = _mask(size)
|
max_value = mask(size)
|
||||||
sign = _sign_bit(size)
|
sign = sign_bit(size)
|
||||||
result &= mask
|
result &= max_value
|
||||||
lhs &= mask
|
lhs &= max_value
|
||||||
rhs &= mask
|
rhs &= max_value
|
||||||
self.cpu.z = result == 0
|
self.cpu.z = result == 0
|
||||||
self.cpu.n = bool(result & sign)
|
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)
|
self.cpu.v = bool((~(lhs ^ rhs) & (lhs ^ result) & sign) != 0)
|
||||||
|
|
||||||
def _set_sub_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
|
def _set_sub_flags(self, lhs: int, rhs: int, result: int, size: int) -> None:
|
||||||
mask = _mask(size)
|
max_value = mask(size)
|
||||||
sign = _sign_bit(size)
|
sign = sign_bit(size)
|
||||||
result &= mask
|
result &= max_value
|
||||||
lhs &= mask
|
lhs &= max_value
|
||||||
rhs &= mask
|
rhs &= max_value
|
||||||
self.cpu.z = result == 0
|
self.cpu.z = result == 0
|
||||||
self.cpu.n = bool(result & sign)
|
self.cpu.n = bool(result & sign)
|
||||||
self.cpu.c = lhs < rhs
|
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:
|
def _bit_operation(self, ea: dict[str, int | str | None], size: int, op_base: int, bit: int) -> None:
|
||||||
value = self._read_ea(ea, size)
|
value = self._read_ea(ea, size)
|
||||||
bit &= (15 if size == 2 else 7)
|
bit &= 15 if size == 2 else 7
|
||||||
mask = 1 << bit
|
bit_mask = 1 << bit
|
||||||
self.cpu.z = not bool(value & mask)
|
self.cpu.z = not bool(value & bit_mask)
|
||||||
if op_base == 0xC0:
|
if op_base == 0xC0:
|
||||||
self._write_ea(ea, value | mask, size)
|
self._write_ea(ea, value | bit_mask, size)
|
||||||
elif op_base == 0xD0:
|
elif op_base == 0xD0:
|
||||||
self._write_ea(ea, value & ~mask, size)
|
self._write_ea(ea, value & ~bit_mask, size)
|
||||||
elif op_base == 0xE0:
|
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:
|
def _branch_condition(self, cond: int) -> bool:
|
||||||
if cond == 0x0:
|
if cond == 0x0:
|
||||||
@@ -798,81 +563,3 @@ class H8536Emulator:
|
|||||||
if cond == 0xE:
|
if cond == 0xE:
|
||||||
return not self.cpu.z and self.cpu.n == self.cpu.v
|
return not self.cpu.z and self.cpu.n == self.cpu.v
|
||||||
return self.cpu.z or 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
110
h8536/emulator/sci.py
Normal 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
|
||||||
Reference in New Issue
Block a user