Skip to content

Commit

Permalink
Move
Browse files Browse the repository at this point in the history
  • Loading branch information
Lekcyjna committed Mar 12, 2024
1 parent 0487193 commit d2fb089
Show file tree
Hide file tree
Showing 9 changed files with 1,571 additions and 1,489 deletions.
1,489 changes: 0 additions & 1,489 deletions transactron/core.py

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions transactron/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .tmodule import *
from .schedulers import *
from .transaction_base import *
from .method import *
from .transaction import*
from .manager import *
from .sugar import *
435 changes: 435 additions & 0 deletions transactron/core/manager.py

Large diffs are not rendered by default.

298 changes: 298 additions & 0 deletions transactron/core/method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
from transactron.utils import *
from amaranth import *
from amaranth import tracer
from typing import Optional, Callable, Iterator, TYPE_CHECKING
from .transaction_base import *
from .sugar import def_method
from contextlib import contextmanager
from transactron.utils.assign import AssignArg

if TYPE_CHECKING:
from .tmodule import TModule

__all__ = ["Method"]

class Method(TransactionBase):
"""Transactional method.
A `Method` serves to interface a module with external `Transaction`\\s
or `Method`\\s. It can be called by at most once in a given clock cycle.
When a given `Method` is required by multiple `Transaction`\\s
(either directly, or indirectly via another `Method`) simultenaously,
at most one of them is granted by the `TransactionManager`, and the rest
of them must wait. (Non-exclusive methods are an exception to this
behavior.) Calling a `Method` always takes a single clock cycle.
Data is combinationally transferred between to and from `Method`\\s
using Amaranth structures (`View` with a `StructLayout`). The transfer
can take place in both directions at the same time: from the called
`Method` to the caller (`data_out`) and from the caller to the called
`Method` (`data_in`).
A module which defines a `Method` should use `body` or `def_method`
to describe the method's effect on the module state.
Attributes
----------
name: str
Name of this `Method`.
ready: Signal, in
Signals that the method is ready to run in the current cycle.
Typically defined by calling `body`.
run: Signal, out
Signals that the method is called in the current cycle by some
`Transaction`. Defined by the `TransactionManager`.
data_in: MethodStruct, out
Contains the data passed to the `Method` by the caller
(a `Transaction` or another `Method`).
data_out: MethodStruct, in
Contains the data passed from the `Method` to the caller
(a `Transaction` or another `Method`). Typically defined by
calling `body`.
"""

def __init__(
self,
*,
name: Optional[str] = None,
i: MethodLayout = (),
o: MethodLayout = (),
nonexclusive: bool = False,
single_caller: bool = False,
src_loc: int | SrcLoc = 0,
):
"""
Parameters
----------
name: str or None
Name hint for this `Method`. If `None` (default) the name is
inferred from the variable name this `Method` is assigned to.
i: method layout
The format of `data_in`.
o: method layout
The format of `data_out`.
nonexclusive: bool
If true, the method is non-exclusive: it can be called by multiple
transactions in the same clock cycle. If such a situation happens,
the method still is executed only once, and each of the callers
receive its output. Nonexclusive methods cannot have inputs.
single_caller: bool
If true, this method is intended to be called from a single
transaction. An error will be thrown if called from multiple
transactions.
src_loc: int | SrcLoc
How many stack frames deep the source location is taken from.
Alternatively, the source location to use instead of the default.
"""
super().__init__(src_loc=get_src_loc(src_loc))
self.owner, owner_name = get_caller_class_name(default="$method")
self.name = name or tracer.get_var_name(depth=2, default=owner_name)
self.ready = Signal(name=self.owned_name + "_ready")
self.run = Signal(name=self.owned_name + "_run")
self.data_in: MethodStruct = Signal(from_method_layout(i))
self.data_out: MethodStruct = Signal(from_method_layout(o))
self.nonexclusive = nonexclusive
self.single_caller = single_caller
self.validate_arguments: Optional[Callable[..., ValueLike]] = None
if nonexclusive:
assert len(self.data_in.as_value()) == 0

@property
def layout_in(self):
return self.data_in.shape()

@property
def layout_out(self):
return self.data_out.shape()

@staticmethod
def like(other: "Method", *, name: Optional[str] = None, src_loc: int | SrcLoc = 0) -> "Method":
"""Constructs a new `Method` based on another.
The returned `Method` has the same input/output data layouts as the
`other` `Method`.
Parameters
----------
other : Method
The `Method` which serves as a blueprint for the new `Method`.
name : str, optional
Name of the new `Method`.
src_loc: int | SrcLoc
How many stack frames deep the source location is taken from.
Alternatively, the source location to use instead of the default.
Returns
-------
Method
The freshly constructed `Method`.
"""
return Method(name=name, i=other.layout_in, o=other.layout_out, src_loc=get_src_loc(src_loc))

def proxy(self, m: 'TModule', method: "Method"):
"""Define as a proxy for another method.
The calls to this method will be forwarded to `method`.
Parameters
----------
m : TModule
Module in which operations on signals should be executed,
`proxy` uses the combinational domain only.
method : Method
Method for which this method is a proxy for.
"""

@def_method(m, self)
def _(arg):
return method(m, arg)

@contextmanager
def body(
self,
m: 'TModule',
*,
ready: ValueLike = C(1),
out: ValueLike = C(0, 0),
validate_arguments: Optional[Callable[..., ValueLike]] = None,
) -> Iterator[MethodStruct]:
"""Define method body
The `body` context manager can be used to define the actions
performed by a `Method` when it's run. Each assignment added to
a domain under `body` is guarded by the `run` signal.
Combinational assignments which do not need to be guarded by `run`
can be added to `m.d.av_comb` or `m.d.top_comb` instead of `m.d.comb`.
`Method` calls can be performed under `body`.
Parameters
----------
m : TModule
Module in which operations on signals should be executed,
`body` uses the combinational domain only.
ready : Signal, in
Signal to indicate if the method is ready to be run. By
default it is `Const(1)`, so the method is always ready.
Assigned combinationially to the `ready` attribute.
out : Value, in
Data generated by the `Method`, which will be passed to
the caller (a `Transaction` or another `Method`). Assigned
combinationally to the `data_out` attribute.
validate_arguments: Optional[Callable[..., ValueLike]]
Function that takes input arguments used to call the method
and checks whether the method can be called with those arguments.
It instantiates a combinational circuit for each
method caller. By default, there is no function, so all arguments
are accepted.
Returns
-------
data_in : Record, out
Data passed from the caller (a `Transaction` or another
`Method`) to this `Method`.
Examples
--------
.. highlight:: python
.. code-block:: python
m = Module()
my_sum_method = Method(i = Layout([("arg1",8),("arg2",8)]))
sum = Signal(16)
with my_sum_method.body(m, out = sum) as data_in:
m.d.comb += sum.eq(data_in.arg1 + data_in.arg2)
"""
if self.defined:
raise RuntimeError(f"Method '{self.name}' already defined")
self.def_order = next(TransactionBase.def_counter)
self.validate_arguments = validate_arguments

m.d.av_comb += self.ready.eq(ready)
m.d.top_comb += self.data_out.eq(out)
with self.context(m):
with m.AvoidedIf(self.run):
yield self.data_in

def _validate_arguments(self, arg_rec: MethodStruct) -> ValueLike:
if self.validate_arguments is not None:
return self.ready & method_def_helper(self, self.validate_arguments, arg_rec)
return self.ready

def __call__(
self, m: 'TModule', arg: Optional[AssignArg] = None, enable: ValueLike = C(1), /, **kwargs: AssignArg
) -> MethodStruct:
"""Call a method.
Methods can only be called from transaction and method bodies.
Calling a `Method` marks, for the purpose of transaction scheduling,
the dependency between the calling context and the called `Method`.
It also connects the method's inputs to the parameters and the
method's outputs to the return value.
Parameters
----------
m : TModule
Module in which operations on signals should be executed,
arg : Value or dict of Values
Call argument. Can be passed as a `View` of the method's
input layout or as a dictionary. Alternative syntax uses
keyword arguments.
enable : Value
Configures the call as enabled in the current clock cycle.
Disabled calls still lock the called method in transaction
scheduling. Calls are by default enabled.
**kwargs : Value or dict of Values
Allows to pass method arguments using keyword argument
syntax. Equivalent to passing a dict as the argument.
Returns
-------
data_out : MethodStruct
The result of the method call.
Examples
--------
.. highlight:: python
.. code-block:: python
m = Module()
with Transaction().body(m):
ret = my_sum_method(m, arg1=2, arg2=3)
Alternative syntax:
.. highlight:: python
.. code-block:: python
with Transaction().body(m):
ret = my_sum_method(m, {"arg1": 2, "arg2": 3})
"""
arg_rec = Signal.like(self.data_in)

if arg is not None and kwargs:
raise ValueError(f"Method '{self.name}' call with both keyword arguments and legacy record argument")

if arg is None:
arg = kwargs

enable_sig = Signal(name=self.owned_name + "_enable")
m.d.av_comb += enable_sig.eq(enable)
m.d.top_comb += assign(arg_rec, arg, fields=AssignType.ALL)

caller = TransactionBase.get()
if not all(ctrl_path.exclusive_with(m.ctrl_path) for ctrl_path, _, _ in caller.method_calls[self]):
raise RuntimeError(f"Method '{self.name}' can't be called twice from the same caller '{caller.name}'")
caller.method_calls[self].append((m.ctrl_path, arg_rec, enable_sig))

if self not in caller.method_uses:
arg_rec_use = Signal(self.layout_in)
arg_rec_enable_sig = Signal()
caller.method_uses[self] = (arg_rec_use, arg_rec_enable_sig)

return self.data_out

def __repr__(self) -> str:
return "(method {})".format(self.name)

def debug_signals(self) -> SignalBundle:
return [self.ready, self.run, self.data_in, self.data_out]
76 changes: 76 additions & 0 deletions transactron/core/schedulers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from amaranth import *
from typing import TYPE_CHECKING
from transactron.utils import *

if TYPE_CHECKING:
from .manager import MethodMap, TransactionGraph, TransactionGraphCC, PriorityOrder

__all__ = ["eager_deterministic_cc_scheduler", "trivial_roundrobin_cc_scheduler"]

def eager_deterministic_cc_scheduler(
method_map: 'MethodMap', gr: 'TransactionGraph', cc: 'TransactionGraphCC', porder: 'PriorityOrder'
) -> Module:
"""eager_deterministic_cc_scheduler
This function generates an eager scheduler for the transaction
subsystem. It isn't fair, because it starts transactions using
transaction index in `cc` as a priority. Transaction with the lowest
index has the highest priority.
If there are two different transactions which have no conflicts then
they will be started concurrently.
Parameters
----------
manager : TransactionManager
TransactionManager which uses this instance of scheduler for
arbitrating which agent should get a grant signal.
gr : TransactionGraph
Graph of conflicts between transactions, where vertices are transactions and edges are conflicts.
cc : Set[Transaction]
Connected components of the graph `gr` for which scheduler
should be generated.
porder : PriorityOrder
Linear ordering of transactions which is consistent with priority constraints.
"""
m = Module()
ccl = list(cc)
ccl.sort(key=lambda transaction: porder[transaction])
for k, transaction in enumerate(ccl):
conflicts = [ccl[j].grant for j in range(k) if ccl[j] in gr[transaction]]
noconflict = ~Cat(conflicts).any()
m.d.comb += transaction.grant.eq(transaction.request & transaction.runnable & noconflict)
return m


def trivial_roundrobin_cc_scheduler(
method_map: 'MethodMap', gr: 'TransactionGraph', cc: 'TransactionGraphCC', porder: 'PriorityOrder'
) -> Module:
"""trivial_roundrobin_cc_scheduler
This function generates a simple round-robin scheduler for the transaction
subsystem. In a one cycle there will be at most one transaction granted
(in a given connected component of the conflict graph), even if there is
another ready, non-conflicting, transaction. It is mainly for testing
purposes.
Parameters
----------
manager : TransactionManager
TransactionManager which uses this instance of scheduler for
arbitrating which agent should get grant signal.
gr : TransactionGraph
Graph of conflicts between transactions, where vertices are transactions and edges are conflicts.
cc : Set[Transaction]
Connected components of the graph `gr` for which scheduler
should be generated.
porder : PriorityOrder
Linear ordering of transactions which is consistent with priority constraints.
"""
m = Module()
sched = Scheduler(len(cc))
m.submodules.scheduler = sched
for k, transaction in enumerate(cc):
m.d.comb += sched.requests[k].eq(transaction.request & transaction.runnable)
m.d.comb += transaction.grant.eq(sched.grant[k] & sched.valid)
return m
Loading

0 comments on commit d2fb089

Please sign in to comment.