1
0

emulator improvements

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

View File

@@ -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.

View File

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

View File

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

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

@@ -0,0 +1,66 @@
from __future__ import annotations
import argparse
from pathlib import Path
from ..formatting import h16
from .memory import describe_regions
from .runner import H8536Emulator
def discover_rom_path(root: Path) -> Path | None:
candidates = [
root / "ROM" / "M27C512@DIP28_1.BIN",
root / "rom.bin",
]
candidates.extend(sorted((root / "ROM").glob("*.BIN")) if (root / "ROM").exists() else [])
candidates.extend(sorted((root / "ROM").glob("*.bin")) if (root / "ROM").exists() else [])
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def load_rom(path: Path | None = None, root: Path | None = None) -> tuple[bytes, Path]:
root = root if root is not None else Path.cwd()
rom_path = path if path is not None else discover_rom_path(root)
if rom_path is None:
raise FileNotFoundError(
"could not discover ROM bytes; pass --rom PATH, expected ROM/M27C512@DIP28_1.BIN or another ROM/*.BIN"
)
return rom_path.read_bytes(), rom_path
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Minimal H8/536 emulation harness scaffold")
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present")
parser.add_argument("--max-steps", type=int, default=64, help="maximum CPU steps to execute")
parser.add_argument("--trace", action="store_true", help="print decoded/executed instruction trace")
parser.add_argument("--stop-on-heartbeat", action="store_true", help="stop only when 00 00 00 00 80 DA is emitted through SCI1 TDR")
parser.add_argument("--memory-map", action="store_true", help="print the scaffolded memory map before running")
parser.add_argument("--interval-steps", type=int, default=2048, help="rough step period for the scaffolded timer interrupt")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_arg_parser().parse_args(argv)
try:
rom_bytes, rom_path = load_rom(args.rom)
except FileNotFoundError as exc:
print(str(exc))
return 2
emulator = H8536Emulator(rom_bytes, interval_steps=args.interval_steps)
print(f"rom={rom_path}")
print(f"reset_vector={h16(emulator.reset_vector())}")
if args.memory_map:
print(describe_regions())
report = emulator.run(args.max_steps, trace=args.trace, stop_on_heartbeat=args.stop_on_heartbeat)
if args.trace:
for line in report.trace:
print(line)
for line in report.summary_lines():
print(line)
if not report.heartbeat_seen:
print("heartbeat_status=not reached; no heartbeat is reported unless bytes are emitted via SCI1_TDR")
return 0

View File

@@ -0,0 +1,36 @@
from __future__ import annotations
SCI1_SMR = 0xFED8
SCI1_BRR = 0xFED9
SCI1_SCR = 0xFEDA
SCI1_TDR = 0xFEDB
SCI1_SSR = 0xFEDC
SCI1_RDR = 0xFEDD
P9DDR = 0xFEFE
P9DR = 0xFEFF
IPRA = 0xFF00
IPRE = 0xFF04
WDT_TCSR_R = 0xFEEC
SCI_SCR_TIE = 0x80
SCI_SCR_RIE = 0x40
SCI_SCR_TE = 0x20
SCI_SCR_RE = 0x10
SCI_SSR_TDRE = 0x80
SCI_SSR_RDRF = 0x40
SCI_SSR_ORER = 0x20
SCI_SSR_FER = 0x10
SCI_SSR_PER = 0x08
ON_CHIP_RAM_START = 0xF680
ON_CHIP_RAM_END = 0xFE7F
REGISTER_FIELD_START = 0xFE80
REGISTER_FIELD_END = 0xFFFF
RAMCR = 0xFF11
HEARTBEAT_FRAME = bytes([0x00, 0x00, 0x00, 0x00, 0x80, 0xDA])
VECTOR_INTERVAL_TIMER = 0x0042
VECTOR_SCI1_TXI = 0x0084

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

@@ -0,0 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class CPUState:
pc: int = 0
sr: int = 0
br: int = 0
regs: list[int] = field(default_factory=lambda: [0] * 8)
cycles: int = 0
steps: int = 0
z: bool = False
c: bool = False
n: bool = False
v: bool = False
interrupt_depth: int = 0
def s8(value: int) -> int:
return value - 0x100 if value & 0x80 else value
def s16(value: int) -> int:
return value - 0x10000 if value & 0x8000 else value
def mask(size: int) -> int:
return 0xFFFF if size == 2 else 0xFF
def sign_bit(size: int) -> int:
return 0x8000 if size == 2 else 0x80

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

@@ -0,0 +1,16 @@
from __future__ import annotations
from ..formatting import h16
class EmulatorError(Exception):
pass
class UnsupportedInstruction(EmulatorError):
def __init__(self, pc: int, raw: bytes, text: str) -> None:
raw_text = " ".join(f"{byte:02X}" for byte in raw)
super().__init__(f"unsupported instruction at {h16(pc)}: {raw_text} {text}".rstrip())
self.pc = pc
self.raw = raw
self.text = text

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

@@ -0,0 +1,120 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from ..formatting import h16
from ..memory import MEMORY_REGIONS, MemoryRegion, region_for
from ..rom import Rom
from .constants import (
ON_CHIP_RAM_END,
ON_CHIP_RAM_START,
P9DDR,
P9DR,
REGISTER_FIELD_END,
REGISTER_FIELD_START,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
)
from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
from .peripherals.p9_bus import P9_ACK_BIT
from .sci import SCI1
@dataclass
class MemoryAccess:
address: int
size: int
value: int
kind: str
region: str
class MemoryMap:
def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None:
self.rom = Rom(rom_bytes, base=0)
self.sci1 = sci1 if sci1 is not None else SCI1()
self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1)
self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1)
self.external: dict[int, int] = {}
self.access_log: list[MemoryAccess] = []
self._set_register(SCI1_SMR, self.sci1.smr)
self._set_register(SCI1_BRR, self.sci1.brr)
self._set_register(SCI1_SCR, self.sci1.scr)
self._set_register(SCI1_TDR, self.sci1.tdr)
self._set_register(SCI1_SSR, self.sci1.ssr)
self._set_register(SCI1_RDR, self.sci1.rdr)
def region(self, address: int) -> MemoryRegion:
return region_for(address & 0xFFFF)
def read8(self, address: int) -> int:
address &= 0xFFFF
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
value = self.sci1.read(address)
self._set_register(address, value)
elif address in self.external:
value = self.external[address]
elif address in (LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS):
# LCD E-clock/status space. Default to ready/zero so boot can pass
# busy-flag polling until a fuller external bus model exists.
value = 0x00
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
value = self.ram[address - ON_CHIP_RAM_START]
elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
value = self.registers[address - REGISTER_FIELD_START]
if address == P9DR and not (self.registers[P9DDR - REGISTER_FIELD_START] & 0x80):
# P97 is used as an input during the serial panel/camera-side
# bit-bang handshake. With no external device modeled, hold the
# input low so the firmware sees an idle/acknowledged bus rather
# than reading back its previous output latch forever.
value &= ~P9_ACK_BIT
elif self.rom.contains(address):
value = self.rom.u8(address)
else:
value = self.external.get(address, 0xFF)
self._log("read", address, 1, value)
return value
def read16(self, address: int) -> int:
high = self.read8(address)
low = self.read8((address + 1) & 0xFFFF)
return (high << 8) | low
def write8(self, address: int, value: int) -> None:
address &= 0xFFFF
value &= 0xFF
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
self.sci1.write(address, value)
self._set_register(address, self.sci1.read(address))
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
self.ram[address - ON_CHIP_RAM_START] = value
elif REGISTER_FIELD_START <= address <= REGISTER_FIELD_END:
self._set_register(address, value)
elif self.rom.contains(address):
# The ROM image spans the whole address space, but the H8/536 map
# can place external RAM/peripherals in these ranges. Keep writes
# as external overrides while leaving instruction fetch immutable.
self.external[address] = value
else:
self.external[address] = value
self._log("write", address, 1, value)
def write16(self, address: int, value: int) -> None:
self.write8(address, (value >> 8) & 0xFF)
self.write8((address + 1) & 0xFFFF, value & 0xFF)
def _set_register(self, address: int, value: int) -> None:
self.registers[address - REGISTER_FIELD_START] = value & 0xFF
def _log(self, kind: str, address: int, size: int, value: int) -> None:
self.access_log.append(MemoryAccess(address, size, value, kind, self.region(address).name))
def describe_regions(regions: Iterable[MemoryRegion] = MEMORY_REGIONS) -> str:
return "\n".join(f"{h16(region.start)}-{h16(region.end)} {region.name} {region.kind}" for region in regions)

View File

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

View File

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

View File

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

View File

@@ -1,260 +1,23 @@
from __future__ import annotations 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
View File

@@ -0,0 +1,110 @@
from __future__ import annotations
from dataclasses import dataclass, field
from .constants import (
HEARTBEAT_FRAME,
SCI1_BRR,
SCI1_RDR,
SCI1_SCR,
SCI1_SMR,
SCI1_SSR,
SCI1_TDR,
SCI_SCR_TE,
SCI_SSR_RDRF,
SCI_SSR_TDRE,
)
@dataclass
class SciTxEvent:
address: int
value: int
scr: int
ssr: int
emitted: bool
@dataclass
class SCI1:
"""Small SCI1 model for the H8/536 serial path.
Manual anchors:
- RDR/TDR/SCR/SSR live at H'FEDD/H'FEDB/H'FEDA/H'FEDC for SCI1.
- SCR bit 7 TIE, bit 6 RIE, bit 5 TE, bit 4 RE.
- SSR bit 7 TDRE, bit 6 RDRF, bits 5..3 ORER/FER/PER.
- Software normally writes TDR after TDRE=1, then clears SSR.TDRE.
"""
smr: int = 0x00
brr: int = 0xFF
scr: int = 0x0C
tdr: int = 0xFF
ssr: int = 0x87
rdr: int = 0x00
tx_bytes: list[int] = field(default_factory=list)
tx_events: list[SciTxEvent] = field(default_factory=list)
tx_frames: list[bytes] = field(default_factory=list)
_frame_buffer: bytearray = field(default_factory=bytearray)
tx_ready_delay: int = 0
def read(self, address: int) -> int:
if address == SCI1_SMR:
return self.smr
if address == SCI1_BRR:
return self.brr
if address == SCI1_SCR:
return self.scr
if address == SCI1_TDR:
return self.tdr
if address == SCI1_SSR:
return self.ssr
if address == SCI1_RDR:
return self.rdr
raise KeyError(address)
def write(self, address: int, value: int) -> None:
value &= 0xFF
if address == SCI1_SMR:
self.smr = value
elif address == SCI1_BRR:
self.brr = value
elif address == SCI1_SCR:
self.scr = value
elif address == SCI1_TDR:
self.tdr = value
self._write_tdr(value)
elif address == SCI1_SSR:
# The real SSR is R/(W)*: writable zeroes clear latched flags after
# the required read sequence. This scaffold applies zero bits
# directly so ROM BCLR/BSET style accesses can be modeled.
self.ssr = value
elif address == SCI1_RDR:
self.rdr = value
else:
raise KeyError(address)
def _write_tdr(self, value: int) -> None:
emitted = bool(self.scr & SCI_SCR_TE)
if emitted:
self.tx_bytes.append(value)
self._frame_buffer.append(value)
if len(self._frame_buffer) == len(HEARTBEAT_FRAME):
self.tx_frames.append(bytes(self._frame_buffer))
self._frame_buffer.clear()
self.ssr |= SCI_SSR_TDRE
self.tx_ready_delay = 2
self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted))
def inject_rx(self, value: int) -> None:
self.rdr = value & 0xFF
self.ssr |= SCI_SSR_RDRF
def saw_heartbeat(self) -> bool:
return HEARTBEAT_FRAME in self.tx_frames
def tick(self) -> None:
if self.tx_ready_delay:
self.tx_ready_delay -= 1
if self.tx_ready_delay == 0:
self.ssr |= SCI_SSR_TDRE