+++ /dev/null
-""" Stage API
-
- Associated development bugs:
- * http://bugs.libre-riscv.org/show_bug.cgi?id=148
- * http://bugs.libre-riscv.org/show_bug.cgi?id=64
- * http://bugs.libre-riscv.org/show_bug.cgi?id=57
-
- Stage API:
- ---------
-
- stage requires compliance with a strict API that may be
- implemented in several means, including as a static class.
-
- Stages do not HOLD data, and they definitely do not contain
- signalling (ready/valid). They do however specify the FORMAT
- of the incoming and outgoing data, and they provide a means to
- PROCESS that data (from incoming format to outgoing format).
-
- Stage Blocks really should be combinatorial blocks (Moore FSMs).
- It would be ok to have input come in from sync'd sources
- (clock-driven, Mealy FSMs) however by doing so they would no longer
- be deterministic, and chaining such blocks with such side-effects
- together could result in unexpected, unpredictable, unreproduceable
- behaviour.
-
- So generally to be avoided, then unless you know what you are doing.
- https://en.wikipedia.org/wiki/Moore_machine
- https://en.wikipedia.org/wiki/Mealy_machine
-
- the methods of a stage instance must be as follows:
-
- * ispec() - Input data format specification. Takes a bit of explaining.
- The requirements are: something that eventually derives from
- nmigen Value must be returned *OR* an iterator or iterable
- or sequence (list, tuple etc.) or generator must *yield*
- thing(s) that (eventually) derive from the nmigen Value class.
-
- Complex to state, very simple in practice:
- see test_buf_pipe.py for over 25 worked examples.
-
- * ospec() - Output data format specification.
- format requirements identical to ispec.
-
- * process(m, i) - Optional function for processing ispec-formatted data.
- returns a combinatorial block of a result that
- may be assigned to the output, by way of the "nmoperator.eq"
- function. Note that what is returned here can be
- extremely flexible. Even a dictionary can be returned
- as long as it has fields that match precisely with the
- Record into which its values is intended to be assigned.
- Again: see example unit tests for details.
-
- * setup(m, i) - Optional function for setting up submodules.
- may be used for more complex stages, to link
- the input (i) to submodules. must take responsibility
- for adding those submodules to the module (m).
- the submodules must be combinatorial blocks and
- must have their inputs and output linked combinatorially.
-
- Both StageCls (for use with non-static classes) and Stage (for use
- by static classes) are abstract classes from which, for convenience
- and as a courtesy to other developers, anything conforming to the
- Stage API may *choose* to derive. See Liskov Substitution Principle:
- https://en.wikipedia.org/wiki/Liskov_substitution_principle
-
- StageChain:
- ----------
-
- A useful combinatorial wrapper around stages that chains them together
- and then presents a Stage-API-conformant interface. By presenting
- the same API as the stages it wraps, it can clearly be used recursively.
-
- StageHelper:
- ----------
-
- A convenience wrapper around a Stage-API-compliant "thing" which
- complies with the Stage API and provides mandatory versions of
- all the optional bits.
-"""
-
-from nmigen import Elaboratable
-from abc import ABCMeta, abstractmethod
-import inspect
-
-from nmutil import nmoperator
-
-
-def _spec(fn, name=None):
- """ useful function that determines if "fn" has an argument "name".
- if so, fn(name) is called otherwise fn() is called.
-
- means that ispec and ospec can be declared with *or without*
- a name argument. normally it would be necessary to have
- "ispec(name=None)" to achieve the same effect.
- """
- if name is None:
- return fn()
- varnames = dict(inspect.getmembers(fn.__code__))['co_varnames']
- if 'name' in varnames:
- return fn(name=name)
- return fn()
-
-
-class StageCls(metaclass=ABCMeta):
- """ Class-based "Stage" API. requires instantiation (after derivation)
-
- see "Stage API" above.. Note: python does *not* require derivation
- from this class. All that is required is that the pipelines *have*
- the functions listed in this class. Derivation from this class
- is therefore merely a "courtesy" to maintainers.
- """
- @abstractmethod
- def ispec(self): pass # REQUIRED
- @abstractmethod
- def ospec(self): pass # REQUIRED
- #@abstractmethod
- #def setup(self, m, i): pass # OPTIONAL
- #@abstractmethod
- #def process(self, i): pass # OPTIONAL
-
-
-class Stage(metaclass=ABCMeta):
- """ Static "Stage" API. does not require instantiation (after derivation)
-
- see "Stage API" above. Note: python does *not* require derivation
- from this class. All that is required is that the pipelines *have*
- the functions listed in this class. Derivation from this class
- is therefore merely a "courtesy" to maintainers.
- """
- @staticmethod
- @abstractmethod
- def ispec(): pass
-
- @staticmethod
- @abstractmethod
- def ospec(): pass
-
- #@staticmethod
- #@abstractmethod
- #def setup(m, i): pass
-
- #@staticmethod
- #@abstractmethod
- #def process(i): pass
-
-
-class StageHelper(Stage):
- """ a convenience wrapper around something that is Stage-API-compliant.
- (that "something" may be a static class, for example).
-
- StageHelper happens to also be compliant with the Stage API,
- it differs from the stage that it wraps in that all the "optional"
- functions are provided (hence the designation "convenience wrapper")
- """
- def __init__(self, stage):
- self.stage = stage
- self._ispecfn = None
- self._ospecfn = None
- if stage is not None:
- self.set_specs(self, self)
-
- def ospec(self, name=None):
- assert self._ospecfn is not None
- return _spec(self._ospecfn, name)
-
- def ispec(self, name=None):
- assert self._ispecfn is not None
- return _spec(self._ispecfn, name)
-
- def set_specs(self, p, n):
- """ sets up the ispecfn and ospecfn for getting input and output data
- """
- if hasattr(p, "stage"):
- p = p.stage
- if hasattr(n, "stage"):
- n = n.stage
- self._ispecfn = p.ispec
- self._ospecfn = n.ospec
-
- def new_specs(self, name):
- """ allocates new ispec and ospec pair
- """
- return (_spec(self.ispec, "%s_i" % name),
- _spec(self.ospec, "%s_o" % name))
-
- def process(self, i):
- if self.stage and hasattr(self.stage, "process"):
- return self.stage.process(i)
- return i
-
- def setup(self, m, i):
- if self.stage is not None and hasattr(self.stage, "setup"):
- self.stage.setup(m, i)
-
- def _postprocess(self, i): # XXX DISABLED
- return i # RETURNS INPUT
- if hasattr(self.stage, "postprocess"):
- return self.stage.postprocess(i)
- return i
-
-
-class StageChain(StageHelper):
- """ pass in a list of stages (combinatorial blocks), and they will
- automatically be chained together via their input and output specs
- into a combinatorial chain, to create one giant combinatorial
- block.
-
- the end result conforms to the exact same Stage API.
-
- * input to this class will be the input of the first stage
- * output of first stage goes into input of second
- * output of second goes into input into third
- * ... (etc. etc.)
- * the output of this class will be the output of the last stage
-
- NOTE: whilst this is very similar to ControlBase.connect(), it is
- *really* important to appreciate that StageChain is pure
- combinatorial and bypasses (does not involve, at all, ready/valid
- signalling OF ANY KIND).
-
- ControlBase.connect on the other hand respects, connects, and uses
- ready/valid signalling.
-
- Arguments:
-
- * :chain: a chain of combinatorial blocks conforming to the Stage API
- NOTE: StageChain.ispec and ospect have to have something
- to return (beginning and end specs of the chain),
- therefore the chain argument must be non-zero length
-
- * :specallocate: if set, new input and output data will be allocated
- and connected (eq'd) to each chained Stage.
- in some cases if this is not done, the nmigen warning
- "driving from two sources, module is being flattened"
- will be issued.
-
- NOTE: DO NOT use StageChain with combinatorial blocks that have
- side-effects (state-based / clock-based input) or conditional
- (inter-chain) dependencies, unless you really know what you are doing.
- """
- def __init__(self, chain, specallocate=False):
- assert len(chain) > 0, "stage chain must be non-zero length"
- self.chain = chain
- StageHelper.__init__(self, None)
- if specallocate:
- self.setup = self._sa_setup
- else:
- self.setup = self._na_setup
- self.set_specs(self.chain[0], self.chain[-1])
-
- def _sa_setup(self, m, i):
- for (idx, c) in enumerate(self.chain):
- if hasattr(c, "setup"):
- c.setup(m, i) # stage may have some module stuff
- ofn = self.chain[idx].ospec # last assignment survives
- cname = 'chainin%d' % idx
- o = _spec(ofn, cname)
- if isinstance(o, Elaboratable):
- setattr(m.submodules, cname, o)
- m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
- if idx == len(self.chain)-1:
- break
- ifn = self.chain[idx+1].ispec # new input on next loop
- i = _spec(ifn, 'chainin%d' % (idx+1))
- m.d.comb += nmoperator.eq(i, o) # assign to next input
- self.o = o
- return self.o # last loop is the output
-
- def _na_setup(self, m, i):
- for (idx, c) in enumerate(self.chain):
- if hasattr(c, "setup"):
- c.setup(m, i) # stage may have some module stuff
- i = o = c.process(i) # store input into "o"
- self.o = o
- return self.o # last loop is the output
-
- def process(self, i):
- return self.o # conform to Stage API: return last-loop output
-
-