Source code for peakrdl_pybind11.masters.sim
"""Behavioural simulator master (sketch §13.7).
:class:`SimMaster` extends :class:`~peakrdl_pybind11.masters.mock.MockMaster`
with a side-effect engine that honours the five RDL behaviours the runtime
currently models:
* ``onread = rclr`` -- pre-read value returned, storage clears.
* ``onread = rset`` -- pre-read value returned, storage sets.
* ``onwrite = woclr`` / ``wclr`` -- per-bit write-1-to-clear.
* ``onwrite = wzc`` -- per-bit write-0-to-clear (inverse of ``woclr``).
* ``onwrite = woset`` / ``wset`` -- per-bit write-1-to-set.
* ``onwrite = wzs`` -- per-bit write-0-to-set.
* ``singlepulse`` -- write through then self-clear so the next read
observes the field low.
Out of scope for this round (tracked as future work): ``sticky``,
``stickybit``, ``hwclr``, ``hwset``, ``ruser``, ``wuser``,
``onread = ruser``. These tokens are accepted without crashing but the
simulator treats them as pass-through.
Per the project's convention, ``wclr`` and ``wset`` are handled as
per-bit transformations to match the helpers in
:mod:`~peakrdl_pybind11.runtime.side_effects` and the published tests --
strict SystemRDL spells ``wclr`` as "any write clears all bits", which is
intentionally *not* what this simulator emulates. See
:mod:`_side_effect_model` for the bit-level rules.
Construct a :class:`SimMaster` standalone (it behaves identically to
:class:`MockMaster`) or pass ``soc=`` to attach the side-effect models
immediately. ``attach_soc`` can be called later to switch into the
side-effect-aware mode.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
from ._side_effect_model import (
RegisterSideEffectModel,
build_models_for_soc,
)
from .base import AccessOp
from .mock import MockMaster
__all__ = ["SimMaster"]
[docs]
class SimMaster(MockMaster):
"""In-memory master with optional RDL side-effect simulation.
Args:
state: Mapping of ``address -> value`` to seed the in-memory
store. Copied so the caller can safely mutate the original.
``None`` (the default) starts with empty state, which makes
:class:`SimMaster` a drop-in replacement for
:class:`MockMaster`.
soc: When provided, walk the SoC tree at construction time and
build the per-register side-effect models. Equivalent to
constructing without ``soc=`` and calling :meth:`attach_soc`
immediately afterwards.
Without an attached SoC, every read/write goes straight through to
the underlying dict (i.e. ``MockMaster`` semantics). With an SoC
attached, register accesses honour the field-level RDL side effects
described in the module docstring.
"""
[docs]
def __init__(
self,
state: Mapping[int, int] | None = None,
*,
soc: Any = None,
) -> None:
super().__init__()
if state:
self.memory.update(state)
self._models: dict[int, RegisterSideEffectModel] = {}
if soc is not None:
self.attach_soc(soc)
# ------------------------------------------------------------------
# Attaching the side-effect map
# ------------------------------------------------------------------
[docs]
def attach_soc(self, soc: Any) -> None:
"""Build (or rebuild) the side-effect map from ``soc``.
Existing memory contents are preserved. A subsequent call
replaces the models -- handy for tests that swap in different
SoC shapes against a single master instance.
"""
self._models = build_models_for_soc(soc)
# ------------------------------------------------------------------
# Per-access overrides
# ------------------------------------------------------------------
[docs]
def read(self, address: int, width: int) -> int:
# Fast path: no model for this address -> behave like MockMaster.
model = self._models.get(address)
if model is None or not model.has_read_effects:
return super().read(address, width)
storage = super().read(address, width)
returned, new_storage = model.apply_read(storage)
if new_storage != storage:
super().write(address, new_storage, width)
return returned
[docs]
def write(self, address: int, value: int, width: int) -> None:
model = self._models.get(address)
if model is None or not model.has_write_effects:
super().write(address, value, width)
return
storage = super().read(address, width)
new_storage = model.apply_write(storage, value)
super().write(address, new_storage, width)
# ------------------------------------------------------------------
# Batched variants -- defer to single-op so the transformations run
# per access. The C++ MockMaster takes the dict-shortcut path here;
# we explicitly *don't* because every op might tickle a different
# side-effect model and we want bit-for-bit semantic parity with the
# single-op path.
# ------------------------------------------------------------------
[docs]
def read_many(self, ops: Sequence[AccessOp]) -> list[int]:
return [self.read(op.address, op.width) for op in ops]
[docs]
def write_many(self, ops: Sequence[AccessOp]) -> None:
for op in ops:
self.write(op.address, op.value, op.width)