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_FER, SCI_SSR_ORER, SCI_SSR_PER, SCI_SSR_RDRF, SCI_SSR_TDRE, ) from .uart import UartTiming @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. - SSR status flags are R/(W)*: writing 0 clears a latched flag, while writing 1 leaves the hardware-owned flag unchanged. """ 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 tx_ready_ticks: int = 2 clock_hz: int = 10_000_000 tx_timing: UartTiming | None = None _tx_ready_pending: bool = False 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: self._write_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.tx_ready_delay = self._tx_ready_delay() self._tx_ready_pending = True self.tx_events.append(SciTxEvent(SCI1_TDR, value, self.scr, self.ssr, emitted)) def _write_ssr(self, value: int) -> None: writable_zero_flags = SCI_SSR_TDRE | SCI_SSR_RDRF | SCI_SSR_ORER | SCI_SSR_FER | SCI_SSR_PER self.ssr = (self.ssr & (writable_zero_flags & value)) | (value & ~writable_zero_flags) 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.ssr |= SCI_SSR_RDRF def saw_heartbeat(self) -> bool: return HEARTBEAT_FRAME in self.tx_frames def configure_tx_timing(self, timing: UartTiming | None, *, clock_hz: int | None = None) -> None: self.tx_timing = timing if clock_hz is not None: self.clock_hz = max(1, clock_hz) def tx_busy(self) -> bool: return self._tx_ready_pending and self.tx_ready_delay > 0 def tick(self, cycles: int = 1) -> None: if self._tx_ready_pending and self.tx_ready_delay: self.tx_ready_delay = max(0, self.tx_ready_delay - max(1, cycles)) if self._tx_ready_pending and self.tx_ready_delay == 0 and not (self.ssr & SCI_SSR_TDRE): self.ssr |= SCI_SSR_TDRE self._tx_ready_pending = False def _tx_ready_delay(self) -> int: if self.tx_timing is None: return max(0, self.tx_ready_ticks) return self.tx_timing.cycles_per_character(self.clock_hz)