1
0

UART simulation

This commit is contained in:
Aiden
2026-05-25 22:22:05 +10:00
parent 4b50d0e98f
commit c3eb09ddc8
8 changed files with 198 additions and 15 deletions

View File

@@ -88,9 +88,9 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\
- 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, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path. - Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval timer scheduling, manual-derived FRT1/FRT2 OCIA cycle scheduling, a P9 bit-banged bus model, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path.
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM. - Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects. - Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, can optionally schedule 38400 8N1 byte arrivals at real UART spacing, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects.
- Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs. - Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs.
- Includes a bench-log replay harness that feeds recorded host TX frames back into the ROM emulator and asserts parity against the real device's observed response/LCD state. - Includes a bench-log replay harness that feeds recorded host TX frames back into the ROM emulator with bench-style UART byte timing by default and asserts parity against the real device's observed response/LCD state.
Current serial observations: Current serial observations:
@@ -216,9 +216,10 @@ python h8536_emulator_rx_probe.py --help
- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history. - `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history.
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame. - `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses. - `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
- `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with 8N1 wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`.
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates. - `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
- `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text. - `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text.
- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. - `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode.
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`. - Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`.
## Code Layout ## Code Layout
@@ -250,7 +251,7 @@ python h8536_emulator_rx_probe.py --help
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer. - `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
- `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation. - `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation.
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks. - `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, P9 bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding. - `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, 38400 8N1 UART injection timing, P9 bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding.
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments. - `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
- `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.

View File

@@ -58,6 +58,7 @@ from .memory import MemoryAccess, MemoryMap, describe_regions
from .peripherals import LCD from .peripherals import LCD
from .runner import H8536Emulator, RunReport from .runner import H8536Emulator, RunReport
from .sci import SCI1, SciTxEvent from .sci import SCI1, SciTxEvent
from .uart import UartTiming
__all__ = [ __all__ = [
"CPUState", "CPUState",
@@ -114,6 +115,7 @@ __all__ = [
"SCI_SSR_TDRE", "SCI_SSR_TDRE",
"SciTxEvent", "SciTxEvent",
"UnsupportedInstruction", "UnsupportedInstruction",
"UartTiming",
"VECTOR_FRT1_OCIA", "VECTOR_FRT1_OCIA",
"VECTOR_INTERVAL_TIMER", "VECTOR_INTERVAL_TIMER",
"VECTOR_FRT2_OCIA", "VECTOR_FRT2_OCIA",

View File

@@ -14,6 +14,8 @@ from .errors import UnsupportedInstruction
from .runner import H8536Emulator from .runner import H8536Emulator
from .rx_probe import ( from .rx_probe import (
RunContext, RunContext,
UartTiming,
_inject_frame_uart_timed,
_interrupt_mask, _interrupt_mask,
_rx_byte_consumed, _rx_byte_consumed,
_rx_ready, _rx_ready,
@@ -88,6 +90,7 @@ class ReplayFrameResult:
host_frame: bytes host_frame: bytes
host_timestamp: str host_timestamp: str
host_delta_ms: int host_delta_ms: int
rx_injection: str
steps_before: int steps_before: int
steps_during_rx: int steps_during_rx: int
emulator_gap_frames_before: tuple[bytes, ...] emulator_gap_frames_before: tuple[bytes, ...]
@@ -120,6 +123,7 @@ class BenchReplayResult:
"host_timestamp": item.host_timestamp, "host_timestamp": item.host_timestamp,
"host_delta_ms": item.host_delta_ms, "host_delta_ms": item.host_delta_ms,
"host_frame": format_frame(item.host_frame), "host_frame": format_frame(item.host_frame),
"rx_injection": item.rx_injection,
"steps_before": item.steps_before, "steps_before": item.steps_before,
"steps_during_rx": item.steps_during_rx, "steps_during_rx": item.steps_during_rx,
"emulator_gap_frames_before": [format_frame(frame) for frame in item.emulator_gap_frames_before], "emulator_gap_frames_before": [format_frame(frame) for frame in item.emulator_gap_frames_before],
@@ -150,6 +154,7 @@ class BenchReplayResult:
lines.append( lines.append(
( (
f" [{index}] {item.host_timestamp} delta={item.host_delta_ms}ms " f" [{index}] {item.host_timestamp} delta={item.host_delta_ms}ms "
f"rx_injection={item.rx_injection} "
f"steps_before={item.steps_before} steps_rx={item.steps_during_rx} " f"steps_before={item.steps_before} steps_rx={item.steps_during_rx} "
f"host={format_frame(item.host_frame)} " f"host={format_frame(item.host_frame)} "
f"gap_emu={_format_frame_list(item.emulator_gap_frames_before)} " f"gap_emu={_format_frame_list(item.emulator_gap_frames_before)} "
@@ -171,6 +176,8 @@ class ReplayConfig:
frt1_ocia_steps: int | None = None frt1_ocia_steps: int | None = None
frt2_ocia_steps: int | None = None frt2_ocia_steps: int | None = None
clock_hz: int = 10_000_000 clock_hz: int = 10_000_000
uart_timing: bool = True
uart_baud: int = 38_400
p9_fast_path: bool = True p9_fast_path: bool = True
p9_fast_input: int = 0xFF p9_fast_input: int = 0xFF
@@ -256,12 +263,25 @@ def run_bench_replay(log_path: Path, *, rom_path: Path | None = None, config: Re
steps_before = _run_cycles_for_ms(emulator, delta_ms, config.clock_hz, context) steps_before = _run_cycles_for_ms(emulator, delta_ms, config.clock_hz, context)
gap_frames = tuple(emulator.sci1.tx_frames[tx_frame_start_before_delay:]) gap_frames = tuple(emulator.sci1.tx_frames[tx_frame_start_before_delay:])
tx_frame_start = len(emulator.sci1.tx_frames) tx_frame_start = len(emulator.sci1.tx_frames)
steps_during_rx = _inject_host_frame(emulator, host.frame, config.per_byte_steps, context) if config.uart_timing:
timing = UartTiming(baud=config.uart_baud)
steps_during_rx, inject_reason = _inject_frame_uart_timed(
emulator,
host.frame,
timing=timing,
max_steps_per_gap=config.per_byte_steps,
context=context,
)
rx_injection = f"{timing.summary(emulator.clock_hz)} reason={inject_reason}"
else:
steps_during_rx = _inject_host_frame(emulator, host.frame, config.per_byte_steps, context)
rx_injection = "polite_wait_for_rdrf_clear"
replay_results.append( replay_results.append(
ReplayFrameResult( ReplayFrameResult(
host_frame=host.frame, host_frame=host.frame,
host_timestamp=host.timestamp, host_timestamp=host.timestamp,
host_delta_ms=delta_ms, host_delta_ms=delta_ms,
rx_injection=rx_injection,
steps_before=steps_before, steps_before=steps_before,
steps_during_rx=steps_during_rx, steps_during_rx=steps_during_rx,
emulator_gap_frames_before=gap_frames, emulator_gap_frames_before=gap_frames,
@@ -336,10 +356,12 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("log", type=Path, help="bench log produced by scripts/bench_connect_lcd_sequence.py") parser.add_argument("log", type=Path, help="bench log produced by scripts/bench_connect_lcd_sequence.py")
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN") parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN")
parser.add_argument("--boot-steps", type=int, default=ReplayConfig.boot_steps) parser.add_argument("--boot-steps", type=int, default=ReplayConfig.boot_steps)
parser.add_argument("--per-byte-steps", type=int, default=ReplayConfig.per_byte_steps) parser.add_argument("--per-byte-steps", type=int, default=ReplayConfig.per_byte_steps, help="UART mode step limit between byte arrivals, or polite mode byte-consume limit")
parser.add_argument("--post-log-steps", type=int, default=ReplayConfig.post_log_steps) parser.add_argument("--post-log-steps", type=int, default=ReplayConfig.post_log_steps)
parser.add_argument("--interval-steps", type=int, default=ReplayConfig.interval_steps) parser.add_argument("--interval-steps", type=int, default=ReplayConfig.interval_steps)
parser.add_argument("--clock-hz", type=lambda text: int(text, 0), default=ReplayConfig.clock_hz) parser.add_argument("--clock-hz", type=lambda text: int(text, 0), default=ReplayConfig.clock_hz)
parser.add_argument("--uart-baud", type=lambda text: int(text, 0), default=ReplayConfig.uart_baud, help="baud rate for bench-style UART injection")
parser.add_argument("--polite-rx", action="store_true", help="wait for each RX byte to be consumed before injecting the next byte")
parser.add_argument("--frt1-ocia-steps", type=int, default=ReplayConfig.frt1_ocia_steps) parser.add_argument("--frt1-ocia-steps", type=int, default=ReplayConfig.frt1_ocia_steps)
parser.add_argument("--frt2-ocia-steps", type=int, default=ReplayConfig.frt2_ocia_steps) parser.add_argument("--frt2-ocia-steps", type=int, default=ReplayConfig.frt2_ocia_steps)
parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines") parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines")
@@ -362,6 +384,8 @@ def main(argv: list[str] | None = None) -> int:
frt1_ocia_steps=args.frt1_ocia_steps, frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps,
clock_hz=args.clock_hz, clock_hz=args.clock_hz,
uart_timing=not args.polite_rx,
uart_baud=args.uart_baud,
p9_fast_path=not args.no_p9_fast_path, p9_fast_path=not args.no_p9_fast_path,
p9_fast_input=args.p9_fast_input, p9_fast_input=args.p9_fast_input,
), ),

View File

@@ -18,6 +18,7 @@ from .constants import (
from .errors import UnsupportedInstruction from .errors import UnsupportedInstruction
from .memory import MemoryAccess from .memory import MemoryAccess
from .runner import H8536Emulator from .runner import H8536Emulator
from .uart import UartTiming
CHECKSUM_SEED = 0x5A CHECKSUM_SEED = 0x5A
@@ -120,6 +121,8 @@ class RunContext:
class FrameResult: class FrameResult:
input_frame: bytes input_frame: bytes
checksum_ok: bool checksum_ok: bool
rx_injection: str
uart_byte_cycles: int | None
steps: int steps: int
stopped_reason: str stopped_reason: str
new_tx_bytes: bytes new_tx_bytes: bytes
@@ -132,6 +135,7 @@ class FrameResult:
def lines(self, index: int) -> list[str]: def lines(self, index: int) -> list[str]:
lines = [ lines = [
f"host_frame[{index}]={format_frame(self.input_frame)} checksum_ok={int(self.checksum_ok)}", f"host_frame[{index}]={format_frame(self.input_frame)} checksum_ok={int(self.checksum_ok)}",
f" rx_injection={self.rx_injection}",
f" stopped={self.stopped_reason} steps={self.steps}", f" stopped={self.stopped_reason} steps={self.steps}",
f" new_tx_bytes={format_frame(self.new_tx_bytes) if self.new_tx_bytes else 'none'}", f" new_tx_bytes={format_frame(self.new_tx_bytes) if self.new_tx_bytes else 'none'}",
] ]
@@ -201,6 +205,8 @@ def run_rx_probe(
boot_steps: int = 250_000, boot_steps: int = 250_000,
per_byte_steps: int = 5_000, per_byte_steps: int = 5_000,
post_frame_steps: int = 80_000, post_frame_steps: int = 80_000,
uart_timing: bool = False,
uart_baud: int = 38_400,
interval_steps: int = 512, interval_steps: int = 512,
frt1_ocia_steps: int | None = None, frt1_ocia_steps: int | None = None,
frt2_ocia_steps: int | None = None, frt2_ocia_steps: int | None = None,
@@ -236,6 +242,8 @@ def run_rx_probe(
frame, frame,
per_byte_steps=per_byte_steps, per_byte_steps=per_byte_steps,
post_frame_steps=post_frame_steps, post_frame_steps=post_frame_steps,
uart_timing=uart_timing,
uart_baud=uart_baud,
stop_after_tx_frame=stop_after_tx_frame, stop_after_tx_frame=stop_after_tx_frame,
) )
for frame in frames for frame in frames
@@ -249,8 +257,10 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present") parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN when present")
parser.add_argument("--preset", choices=("connect-lcd",), help="append a built-in host-frame set") parser.add_argument("--preset", choices=("connect-lcd",), help="append a built-in host-frame set")
parser.add_argument("--boot-steps", type=int, default=250_000, help="maximum steps to boot until SCI1 RXI is serviceable") parser.add_argument("--boot-steps", type=int, default=250_000, help="maximum steps to boot until SCI1 RXI is serviceable")
parser.add_argument("--per-byte-steps", type=int, default=5_000, help="maximum steps after each injected RX byte") parser.add_argument("--per-byte-steps", type=int, default=5_000, help="polite mode byte-consume limit, or UART mode step limit between byte arrivals")
parser.add_argument("--post-frame-steps", type=int, default=80_000, help="maximum steps after a full injected frame") parser.add_argument("--post-frame-steps", type=int, default=80_000, help="maximum steps after a full injected frame")
parser.add_argument("--uart-timing", action="store_true", help="inject frame bytes at real 8N1 UART inter-byte timing instead of waiting for RDRF consumption")
parser.add_argument("--uart-baud", type=parse_int, default=38_400, help="baud rate for --uart-timing; 38400 gives about 260 us per 8N1 byte")
parser.add_argument("--keep-listening", action="store_true", help="use all post-frame steps instead of stopping at the first new TX frame") parser.add_argument("--keep-listening", action="store_true", help="use all post-frame steps instead of stopping at the first new TX frame")
parser.add_argument("--interval-steps", type=int, default=512, help="rough step period for the scaffolded interval timer interrupt") parser.add_argument("--interval-steps", type=int, default=512, help="rough step period for the scaffolded interval timer interrupt")
parser.add_argument("--clock-hz", type=parse_int, default=10_000_000, help="CPU/phi clock in Hz for calibrated FRT timing") parser.add_argument("--clock-hz", type=parse_int, default=10_000_000, help="CPU/phi clock in Hz for calibrated FRT timing")
@@ -275,6 +285,8 @@ def main(argv: list[str] | None = None) -> int:
boot_steps=args.boot_steps, boot_steps=args.boot_steps,
per_byte_steps=args.per_byte_steps, per_byte_steps=args.per_byte_steps,
post_frame_steps=args.post_frame_steps, post_frame_steps=args.post_frame_steps,
uart_timing=args.uart_timing,
uart_baud=args.uart_baud,
interval_steps=args.interval_steps, interval_steps=args.interval_steps,
frt1_ocia_steps=args.frt1_ocia_steps, frt1_ocia_steps=args.frt1_ocia_steps,
frt2_ocia_steps=args.frt2_ocia_steps, frt2_ocia_steps=args.frt2_ocia_steps,
@@ -300,6 +312,8 @@ def _run_frame(
*, *,
per_byte_steps: int, per_byte_steps: int,
post_frame_steps: int, post_frame_steps: int,
uart_timing: bool,
uart_baud: int,
stop_after_tx_frame: bool, stop_after_tx_frame: bool,
) -> FrameResult: ) -> FrameResult:
state_before = _state_snapshot(emulator) state_before = _state_snapshot(emulator)
@@ -309,15 +323,29 @@ def _run_frame(
context = RunContext() context = RunContext()
stopped_reason = "post_frame_steps" stopped_reason = "post_frame_steps"
steps_total = 0 steps_total = 0
timing = UartTiming(baud=uart_baud)
for offset, value in enumerate(frame): if uart_timing:
emulator.inject_sci1_rx_byte(value) steps_total, stopped_reason = _inject_frame_uart_timed(
steps, reason = _run_until(emulator, per_byte_steps, _rx_byte_consumed, context) emulator,
steps_total += steps frame,
if reason != "predicate": timing=timing,
stopped_reason = f"rx_byte_{offset}_{reason}" max_steps_per_gap=per_byte_steps,
break context=context,
)
injected_all_bytes = stopped_reason == "frame_injected_uart_timing"
else: else:
injected_all_bytes = True
for offset, value in enumerate(frame):
emulator.inject_sci1_rx_byte(value)
steps, reason = _run_until(emulator, per_byte_steps, _rx_byte_consumed, context)
steps_total += steps
if reason != "predicate":
stopped_reason = f"rx_byte_{offset}_{reason}"
injected_all_bytes = False
break
if injected_all_bytes:
target_frame_count = tx_frame_start + 1 target_frame_count = tx_frame_start + 1
def post_predicate(inner: H8536Emulator) -> bool: def post_predicate(inner: H8536Emulator) -> bool:
@@ -332,6 +360,8 @@ def _run_frame(
return FrameResult( return FrameResult(
input_frame=frame, input_frame=frame,
checksum_ok=frame_checksum_ok(frame), checksum_ok=frame_checksum_ok(frame),
rx_injection=timing.summary(emulator.clock_hz) if uart_timing else "polite_wait_for_rdrf_clear",
uart_byte_cycles=timing.cycles_per_character(emulator.clock_hz) if uart_timing else None,
steps=steps_total, steps=steps_total,
stopped_reason=stopped_reason, stopped_reason=stopped_reason,
new_tx_bytes=bytes(emulator.sci1.tx_bytes[tx_byte_start:]), new_tx_bytes=bytes(emulator.sci1.tx_bytes[tx_byte_start:]),
@@ -343,6 +373,28 @@ def _run_frame(
) )
def _inject_frame_uart_timed(
emulator: H8536Emulator,
frame: bytes,
*,
timing: UartTiming,
max_steps_per_gap: int,
context: RunContext,
) -> tuple[int, str]:
steps_total = 0
start_cycles = emulator.cpu.cycles
byte_cycles = timing.cycles_per_character(emulator.clock_hz)
for offset, value in enumerate(frame):
if offset:
target_cycles = start_cycles + (offset * byte_cycles)
steps, reason = _run_until_cycle(emulator, target_cycles, max_steps_per_gap, context)
steps_total += steps
if reason != "target_cycle":
return steps_total, f"rx_byte_{offset}_{reason}"
emulator.inject_sci1_rx_byte(value)
return steps_total, "frame_injected_uart_timing"
def _run_until( def _run_until(
emulator: H8536Emulator, emulator: H8536Emulator,
max_steps: int, max_steps: int,
@@ -362,6 +414,27 @@ def _run_until(
return max_steps, "max_steps" return max_steps, "max_steps"
def _run_until_cycle(
emulator: H8536Emulator,
target_cycles: int,
max_steps: int,
context: RunContext,
) -> tuple[int, str]:
for index in range(max(0, max_steps)):
if emulator.cpu.cycles >= target_cycles:
return index, "target_cycle"
pc = emulator.cpu.pc
context.record_pc(pc)
try:
emulator.step()
except UnsupportedInstruction as exc:
context.unsupported = str(exc)
return index, "unsupported_instruction"
if emulator.cpu.cycles >= target_cycles:
return max(0, max_steps), "target_cycle"
return max(0, max_steps), "max_steps"
def _rx_ready(emulator: H8536Emulator) -> bool: def _rx_ready(emulator: H8536Emulator) -> bool:
if not (emulator.sci1.scr & SCI_SCR_RIE and emulator.sci1.scr & SCI_SCR_RE): if not (emulator.sci1.scr & SCI_SCR_RIE and emulator.sci1.scr & SCI_SCR_RE):
return False return False
@@ -477,4 +550,5 @@ __all__ = [
"main", "main",
"parse_frame", "parse_frame",
"run_rx_probe", "run_rx_probe",
"UartTiming",
] ]

View File

@@ -105,6 +105,11 @@ class SCI1:
self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags) self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags)
def inject_rx(self, value: int) -> None: def inject_rx(self, value: int) -> None:
if self.ssr & SCI_SSR_ORER:
return
if self.ssr & SCI_SSR_RDRF:
self.ssr |= SCI_SSR_ORER
return
self.rdr = value & 0xFF self.rdr = value & 0xFF
self.ssr |= SCI_SSR_RDRF self.ssr |= SCI_SSR_RDRF

35
h8536/emulator/uart.py Normal file
View File

@@ -0,0 +1,35 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class UartTiming:
baud: int = 38_400
data_bits: int = 8
parity_bits: int = 0
stop_bits: int = 1
start_bits: int = 1
@property
def bits_per_character(self) -> int:
return self.start_bits + self.data_bits + self.parity_bits + self.stop_bits
def seconds_per_character(self) -> float:
return self.bits_per_character / max(1, self.baud)
def micros_per_character(self) -> float:
return 1_000_000.0 * self.seconds_per_character()
def cycles_per_character(self, clock_hz: int) -> int:
return max(1, round(max(1, clock_hz) * self.seconds_per_character()))
def summary(self, clock_hz: int) -> str:
return (
f"uart_{self.data_bits}{'N' if self.parity_bits == 0 else 'P'}{self.stop_bits} "
f"baud={self.baud} byte_us={self.micros_per_character():.3f} "
f"byte_cycles={self.cycles_per_character(clock_hz)}"
)
__all__ = ["UartTiming"]

View File

@@ -1,7 +1,15 @@
import argparse import argparse
import unittest import unittest
from h8536.emulator.rx_probe import frame_checksum, frame_checksum_ok, parse_frame from h8536.emulator import H8536Emulator, SCI1_RDR, SCI1_SSR, SCI_SSR_ORER, SCI_SSR_RDRF
from h8536.emulator.rx_probe import RunContext, UartTiming, _inject_frame_uart_timed, frame_checksum, frame_checksum_ok, parse_frame
def rom_with_reset(*, reset: int = 0x1000, size: int = 0x1020) -> bytearray:
rom = bytearray([0xFF] * size)
rom[0:2] = reset.to_bytes(2, "big")
rom[reset : reset + 2] = b"\x20\xFE" # BRA self
return rom
class EmulatorRxProbeTest(unittest.TestCase): class EmulatorRxProbeTest(unittest.TestCase):
@@ -22,6 +30,23 @@ class EmulatorRxProbeTest(unittest.TestCase):
with self.assertRaises(argparse.ArgumentTypeError): with self.assertRaises(argparse.ArgumentTypeError):
parse_frame("04 00 00 40") parse_frame("04 00 00 40")
def test_uart_timed_injection_does_not_wait_for_rdrf_consumption(self):
emulator = H8536Emulator(bytes(rom_with_reset()), clock_hz=10_000_000)
context = RunContext()
steps, reason = _inject_frame_uart_timed(
emulator,
b"\x11\x22",
timing=UartTiming(baud=38_400),
max_steps_per_gap=1000,
context=context,
)
self.assertEqual(reason, "frame_injected_uart_timing")
self.assertGreater(steps, 0)
self.assertEqual(emulator.memory.read8(SCI1_RDR), 0x11)
self.assertEqual(emulator.memory.read8(SCI1_SSR) & (SCI_SSR_RDRF | SCI_SSR_ORER), SCI_SSR_RDRF | SCI_SSR_ORER)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -12,6 +12,7 @@ from h8536.emulator import (
SCI_SSR_RDRF, SCI_SSR_RDRF,
SCI_SSR_TDRE, SCI_SSR_TDRE,
SCI1, SCI1,
UartTiming,
) )
@@ -66,6 +67,22 @@ class SciTimingTest(unittest.TestCase):
self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME]) self.assertEqual(sci.tx_frames, [HEARTBEAT_FRAME])
self.assertTrue(sci.saw_heartbeat()) self.assertTrue(sci.saw_heartbeat())
def test_inject_rx_sets_overrun_if_rdrf_is_still_full(self):
sci = SCI1()
sci.inject_rx(0x11)
sci.inject_rx(0x22)
self.assertEqual(sci.read(SCI1_SSR) & (SCI_SSR_RDRF | SCI_SSR_ORER), SCI_SSR_RDRF | SCI_SSR_ORER)
self.assertEqual(sci.rdr, 0x11)
def test_uart_8n1_38400_byte_timing(self):
timing = UartTiming(baud=38_400)
self.assertEqual(timing.bits_per_character, 10)
self.assertAlmostEqual(timing.micros_per_character(), 260.416666, places=3)
self.assertEqual(timing.cycles_per_character(10_000_000), 2604)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()