-""" Pipeline and BufferedHandshake implementation, conforming to the same API.
- For multi-input and multi-output variants, see multipipe.
-
- eq:
- --
-
- a strategically very important function that is identical in function
- to nmigen's Signal.eq function, except it may take objects, or a list
- of objects, or a tuple of objects, and where objects may also be
- Records.
-
- Stage API:
- ---------
-
- stage requires compliance with a strict API that may be
- implemented in several means, including as a static class.
- the methods of a stage instance must be as follows:
-
- * ispec() - Input data format specification
- returns an object or a list or tuple of objects, or
- a Record, each object having an "eq" function which
- takes responsibility for copying by assignment all
- sub-objects
- * ospec() - Output data format specification
- requirements as for ospec
- * process(m, i) - Processes an ispec-formatted object
- returns a combinatorial block of a result that
- may be assigned to the output, by way of the "eq"
- function
- * 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.
-
- 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.
+""" Pipeline API. For multi-input and multi-output variants, see multipipe.
+
+ Associated development bugs:
+ * http://bugs.libre-riscv.org/show_bug.cgi?id=64
+ * http://bugs.libre-riscv.org/show_bug.cgi?id=57
+
+ Important: see Stage API (stageapi.py) in combination with below
RecordBasedStage:
----------------
connect a chain of pipelines and present the exact same prev/next
ready/valid/data API.
+ Note: pipelines basically do not become pipelines as such until
+ handed to a derivative of ControlBase. ControlBase itself is *not*
+ strictly considered a pipeline class. Wishbone and AXI4 (master or
+ slave) could be derived from ControlBase, for example.
UnbufferedPipeline:
------------------
logic, if chained together, is *combinatorial*, resulting in
progressively larger gate delay.
+ PassThroughHandshake:
+ ------------------
+
+ A Control class that introduces a single clock delay, passing its
+ data through unaltered. Unlike RegisterPipeline (which relies
+ on UnbufferedPipeline and PassThroughStage) it handles ready/valid
+ itself.
+
RegisterPipeline:
----------------
where data will flow on *every* clock when the conditions are right.
input acceptance conditions are when:
- * incoming previous-stage strobe (p.i_valid) is HIGH
- * outgoing previous-stage ready (p.o_ready) is LOW
+ * incoming previous-stage strobe (p.valid_i) is HIGH
+ * outgoing previous-stage ready (p.ready_o) is LOW
output transmission conditions are when:
- * outgoing next-stage strobe (n.o_valid) is HIGH
- * outgoing next-stage ready (n.i_ready) is LOW
+ * outgoing next-stage strobe (n.valid_o) is HIGH
+ * outgoing next-stage ready (n.ready_i) is LOW
the tricky bit is when the input has valid data and the output is not
ready to accept it. if it wasn't for the clock synchronisation, it
https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v
"""
-from nmigen import Signal, Cat, Const, Mux, Module, Value
+from nmigen import Signal, Mux, Module, Elaboratable
from nmigen.cli import verilog, rtlil
-from nmigen.hdl.ast import ArrayProxy
-from nmigen.hdl.rec import Record, Layout
-
-from abc import ABCMeta, abstractmethod
-from collections.abc import Sequence
-
-
-class PrevControl:
- """ contains signals that come *from* the previous stage (both in and out)
- * i_valid: previous stage indicating all incoming data is valid.
- may be a multi-bit signal, where all bits are required
- to be asserted to indicate "valid".
- * o_ready: output to next stage indicating readiness to accept data
- * i_data : an input - added by the user of this class
- """
+from nmigen.hdl.rec import Record
- def __init__(self, i_width=1, stage_ctl=False):
- self.stage_ctl = stage_ctl
- self.i_valid = Signal(i_width, name="p_i_valid") # prev >>in self
- self._o_ready = Signal(name="p_o_ready") # prev <<out self
- self.i_data = None # XXX MUST BE ADDED BY USER
- if stage_ctl:
- self.s_o_ready = Signal(name="p_s_o_rdy") # prev <<out self
-
- @property
- def o_ready(self):
- """ public-facing API: indicates (externally) that stage is ready
- """
- if self.stage_ctl:
- return self.s_o_ready # set dynamically by stage
- return self._o_ready # return this when not under dynamic control
-
- def _connect_in(self, prev):
- """ internal helper function to connect stage to an input source.
- do not use to connect stage-to-stage!
- """
- return [self.i_valid.eq(prev.i_valid_test),
- prev.o_ready.eq(self.o_ready),
- eq(self.i_data, prev.i_data),
- ]
-
- @property
- def i_valid_test(self):
- vlen = len(self.i_valid)
- if vlen > 1:
- # multi-bit case: valid only when i_valid is all 1s
- all1s = Const(-1, (len(self.i_valid), False))
- i_valid = (self.i_valid == all1s)
- else:
- # single-bit i_valid case
- i_valid = self.i_valid
-
- # when stage indicates not ready, incoming data
- # must "appear" to be not ready too
- if self.stage_ctl:
- i_valid = i_valid & self.s_o_ready
-
- return i_valid
-
-
-class NextControl:
- """ contains the signals that go *to* the next stage (both in and out)
- * o_valid: output indicating to next stage that data is valid
- * i_ready: input from next stage indicating that it can accept data
- * o_data : an output - added by the user of this class
- """
- def __init__(self, stage_ctl=False):
- self.stage_ctl = stage_ctl
- self.o_valid = Signal(name="n_o_valid") # self out>> next
- self.i_ready = Signal(name="n_i_ready") # self <<in next
- self.o_data = None # XXX MUST BE ADDED BY USER
- #if self.stage_ctl:
- self.d_valid = Signal(reset=1) # INTERNAL (data valid)
-
- @property
- def i_ready_test(self):
- if self.stage_ctl:
- return self.i_ready & self.d_valid
- return self.i_ready
-
- def connect_to_next(self, nxt):
- """ helper function to connect to the next stage data/valid/ready.
- data/valid is passed *TO* nxt, and ready comes *IN* from nxt.
- use this when connecting stage-to-stage
- """
- return [nxt.i_valid.eq(self.o_valid),
- self.i_ready.eq(nxt.o_ready),
- eq(nxt.i_data, self.o_data),
- ]
-
- def _connect_out(self, nxt):
- """ internal helper function to connect stage to an output source.
- do not use to connect stage-to-stage!
- """
- return [nxt.o_valid.eq(self.o_valid),
- self.i_ready.eq(nxt.i_ready_test),
- eq(nxt.o_data, self.o_data),
- ]
-
-
-def eq(o, i):
- """ makes signals equal: a helper routine which identifies if it is being
- passed a list (or tuple) of objects, or signals, or Records, and calls
- the objects' eq function.
-
- complex objects (classes) can be used: they must follow the
- convention of having an eq member function, which takes the
- responsibility of further calling eq and returning a list of
- eq assignments
-
- Record is a special (unusual, recursive) case, where the input may be
- specified as a dictionary (which may contain further dictionaries,
- recursively), where the field names of the dictionary must match
- the Record's field spec. Alternatively, an object with the same
- member names as the Record may be assigned: it does not have to
- *be* a Record.
-
- ArrayProxy is also special-cased, it's a bit messy: whilst ArrayProxy
- has an eq function, the object being assigned to it (e.g. a python
- object) might not. despite the *input* having an eq function,
- that doesn't help us, because it's the *ArrayProxy* that's being
- assigned to. so.... we cheat. use the ports() function of the
- python object, enumerate them, find out the list of Signals that way,
- and assign them.
- """
- res = []
- if isinstance(o, dict):
- for (k, v) in o.items():
- print ("d-eq", v, i[k])
- res.append(v.eq(i[k]))
- return res
-
- if not isinstance(o, Sequence):
- o, i = [o], [i]
- for (ao, ai) in zip(o, i):
- #print ("eq", ao, ai)
- if isinstance(ao, Record):
- for idx, (field_name, field_shape, _) in enumerate(ao.layout):
- if isinstance(field_shape, Layout):
- val = ai.fields
- else:
- val = ai
- if hasattr(val, field_name): # check for attribute
- val = getattr(val, field_name)
- else:
- val = val[field_name] # dictionary-style specification
- rres = eq(ao.fields[field_name], val)
- res += rres
- elif isinstance(ao, ArrayProxy) and not isinstance(ai, Value):
- for p in ai.ports():
- op = getattr(ao, p.name)
- #print (op, p, p.name)
- rres = op.eq(p)
- if not isinstance(rres, Sequence):
- rres = [rres]
- res += rres
- else:
- rres = ao.eq(ai)
- if not isinstance(rres, Sequence):
- rres = [rres]
- res += rres
- return res
-
-
-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 # REQUIRED
-
-
-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
+from queue import Queue
+import inspect
+from iocontrol import (PrevControl, NextControl, Object, RecordObject)
+from stageapi import (_spec, StageCls, Stage, StageChain, StageHelper)
+import nmoperator
+
class RecordBasedStage(Stage):
""" convenience class which provides a Records-based layout.
def setup(seif, m, i): return self.__setup(m, i)
-class StageChain(StageCls):
- """ pass in a list of stages, and they will automatically be
- chained together via their input and output specs into a
- combinatorial chain.
-
- the end result basically conforms to the exact same Stage API.
+class PassThroughStage(StageCls):
+ """ a pass-through stage with its input data spec identical to its output,
+ and "passes through" its data from input to output (does nothing).
- * 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
+ use this basically to explicitly make any data spec Stage-compliant.
+ (many APIs would potentially use a static "wrap" method in e.g.
+ StageCls to achieve a similar effect)
"""
- def __init__(self, chain, specallocate=False):
- self.chain = chain
- self.specallocate = specallocate
-
- def ispec(self):
- return self.chain[0].ispec()
-
- def ospec(self):
- return self.chain[-1].ospec()
-
- def _specallocate_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
- o = self.chain[idx].ospec() # last assignment survives
- m.d.comb += eq(o, c.process(i)) # process input into "o"
- if idx == len(self.chain)-1:
- break
- i = self.chain[idx+1].ispec() # new input on next loop
- m.d.comb += eq(i, o) # assign to next input
- return o # last loop is the output
-
- def _noallocate_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"
- return o # last loop is the output
-
- def setup(self, m, i):
- if self.specallocate:
- self.o = self._specallocate_setup(m, i)
- else:
- self.o = self._noallocate_setup(m, i)
+ def __init__(self, iospecfn): self.iospecfn = iospecfn
+ def ispec(self): return self.iospecfn()
+ def ospec(self): return self.iospecfn()
- def process(self, i):
- return self.o # conform to Stage API: return last-loop output
+class ControlBase(StageHelper, Elaboratable):
+ """ Common functions for Pipeline API. Note: a "pipeline stage" only
+ exists (conceptually) when a ControlBase derivative is handed
+ a Stage (combinatorial block)
-class ControlBase:
- """ Common functions for Pipeline API
+ NOTE: ControlBase derives from StageHelper, making it accidentally
+ compliant with the Stage API. Using those functions directly
+ *BYPASSES* a ControlBase instance ready/valid signalling, which
+ clearly should not be done without a really, really good reason.
"""
- def __init__(self, in_multi=None, stage_ctl=False):
+ def __init__(self, stage=None, in_multi=None, stage_ctl=False):
""" Base class containing ready/valid/data to previous and next stages
* p: contains ready/valid to the previous stage
* n: contains ready/valid to the next stage
Except when calling Controlbase.connect(), user must also:
- * add i_data member to PrevControl (p) and
- * add o_data member to NextControl (n)
+ * add data_i member to PrevControl (p) and
+ * add data_o member to NextControl (n)
+ Calling ControlBase._new_data is a good way to do that.
"""
+ StageHelper.__init__(self, stage)
+
# set up input and output IO ACK (prev/next ready/valid)
self.p = PrevControl(in_multi, stage_ctl)
self.n = NextControl(stage_ctl)
+ # set up the input and output data
+ if stage is not None:
+ self._new_data("data")
+
+ def _new_data(self, name):
+ """ allocates new data_i and data_o
+ """
+ self.p.data_i, self.n.data_o = self.new_specs(name)
+
+ @property
+ def data_r(self):
+ return self.process(self.p.data_i)
+
def connect_to_next(self, nxt):
""" helper function to connect to the next stage data/valid/ready.
"""
v | v | v |
out---in out--in out---in
- Also takes care of allocating i_data/o_data, by looking up
+ Also takes care of allocating data_i/data_o, by looking up
the data spec for each end of the pipechain. i.e It is NOT
- necessary to allocate self.p.i_data or self.n.o_data manually:
+ necessary to allocate self.p.data_i or self.n.data_o manually:
this is handled AUTOMATICALLY, here.
Basically this function is the direct equivalent of StageChain,
Thus it becomes possible to build up larger chains recursively.
More complex chains (multi-input, multi-output) will have to be
done manually.
+
+ Argument:
+
+ * :pipechain: - a sequence of ControlBase-derived classes
+ (must be one or more in length)
+
+ Returns:
+
+ * a list of eq assignments that will need to be added in
+ an elaborate() to m.d.comb
"""
+ assert len(pipechain) > 0, "pipechain must be non-zero length"
+ assert self.stage is None, "do not use connect with a stage"
eqs = [] # collated list of assignment statements
# connect inter-chain
for i in range(len(pipechain)-1):
- pipe1 = pipechain[i]
- pipe2 = pipechain[i+1]
- eqs += pipe1.connect_to_next(pipe2)
-
- # connect front of chain to ourselves
- front = pipechain[0]
- self.p.i_data = front.stage.ispec()
- eqs += front._connect_in(self)
-
- # connect end of chain to ourselves
- end = pipechain[-1]
- self.n.o_data = end.stage.ospec()
- eqs += end._connect_out(self)
+ pipe1 = pipechain[i] # earlier
+ pipe2 = pipechain[i+1] # later (by 1)
+ eqs += pipe1.connect_to_next(pipe2) # earlier n to later p
+
+ # connect front and back of chain to ourselves
+ front = pipechain[0] # first in chain
+ end = pipechain[-1] # last in chain
+ self.set_specs(front, end) # sets up ispec/ospec functions
+ self._new_data("chain") # NOTE: REPLACES existing data
+ eqs += front._connect_in(self) # front p to our p
+ eqs += end._connect_out(self) # end n to our n
return eqs
def set_input(self, i):
- """ helper function to set the input data
+ """ helper function to set the input data (used in unit tests)
"""
- return eq(self.p.i_data, i)
+ return nmoperator.eq(self.p.data_i, i)
+
+ def __iter__(self):
+ yield from self.p # yields ready/valid/data (data also gets yielded)
+ yield from self.n # ditto
def ports(self):
- res = [self.p.i_valid, self.n.i_ready,
- self.n.o_valid, self.p.o_ready,
- ]
- if hasattr(self.p.i_data, "ports"):
- res += self.p.i_data.ports()
- else:
- res += self.p.i_data
- if hasattr(self.n.o_data, "ports"):
- res += self.n.o_data.ports()
- else:
- res += self.n.o_data
- return res
+ return list(self)
- def _elaborate(self, platform):
+ def elaborate(self, platform):
""" handles case where stage has dynamic ready/valid functions
"""
m = Module()
+ m.submodules.p = self.p
+ m.submodules.n = self.n
+
+ self.setup(m, self.p.data_i)
+
if not self.p.stage_ctl:
return m
# intercept the previous (outgoing) "ready", combine with stage ready
- m.d.comb += self.p.s_o_ready.eq(self.p._o_ready & self.stage.d_ready)
+ m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready)
# intercept the next (incoming) "ready" and combine it with data valid
- sdv = self.stage.d_valid(self.n.i_ready)
- m.d.comb += self.n.d_valid.eq(self.n.i_ready & sdv)
+ sdv = self.stage.d_valid(self.n.ready_i)
+ m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
return m
Argument: stage. see Stage API above
- stage-1 p.i_valid >>in stage n.o_valid out>> stage+1
- stage-1 p.o_ready <<out stage n.i_ready <<in stage+1
- stage-1 p.i_data >>in stage n.o_data out>> stage+1
+ stage-1 p.valid_i >>in stage n.valid_o out>> stage+1
+ stage-1 p.ready_o <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>in stage n.data_o out>> stage+1
| |
process --->----^
| |
+-- r_data ->-+
- input data p.i_data is read (only), is processed and goes into an
+ input data p.data_i is read (only), is processed and goes into an
intermediate result store [process()]. this is updated combinatorially.
in a non-stall condition, the intermediate result will go into the
on the next cycle (as long as stall is not raised again) the
input may begin to be processed and transferred directly to output.
-
"""
- def __init__(self, stage, stage_ctl=False):
- ControlBase.__init__(self, stage_ctl=stage_ctl)
- self.stage = stage
-
- # set up the input and output data
- self.p.i_data = stage.ispec() # input type
- self.n.o_data = stage.ospec()
def elaborate(self, platform):
+ self.m = ControlBase.elaborate(self, platform)
- self.m = ControlBase._elaborate(self, platform)
-
- result = self.stage.ospec()
- r_data = self.stage.ospec()
- if hasattr(self.stage, "setup"):
- self.stage.setup(self.m, self.p.i_data)
+ result = _spec(self.stage.ospec, "r_tmp")
+ r_data = _spec(self.stage.ospec, "r_data")
# establish some combinatorial temporaries
o_n_validn = Signal(reset_less=True)
- n_i_ready = Signal(reset_less=True, name="n_i_rdy_data")
- i_p_valid_o_p_ready = Signal(reset_less=True)
- p_i_valid = Signal(reset_less=True)
- self.m.d.comb += [p_i_valid.eq(self.p.i_valid_test),
- o_n_validn.eq(~self.n.o_valid),
- i_p_valid_o_p_ready.eq(p_i_valid & self.p.o_ready),
- n_i_ready.eq(self.n.i_ready_test),
+ n_ready_i = Signal(reset_less=True, name="n_i_rdy_data")
+ nir_por = Signal(reset_less=True)
+ nir_por_n = Signal(reset_less=True)
+ p_valid_i = Signal(reset_less=True)
+ nir_novn = Signal(reset_less=True)
+ nirn_novn = Signal(reset_less=True)
+ por_pivn = Signal(reset_less=True)
+ npnn = Signal(reset_less=True)
+ self.m.d.comb += [p_valid_i.eq(self.p.valid_i_test),
+ o_n_validn.eq(~self.n.valid_o),
+ n_ready_i.eq(self.n.ready_i_test),
+ nir_por.eq(n_ready_i & self.p._ready_o),
+ nir_por_n.eq(n_ready_i & ~self.p._ready_o),
+ nir_novn.eq(n_ready_i | o_n_validn),
+ nirn_novn.eq(~n_ready_i & o_n_validn),
+ npnn.eq(nir_por | nirn_novn),
+ por_pivn.eq(self.p._ready_o & ~p_valid_i)
]
# store result of processing in combinatorial temporary
- self.m.d.comb += eq(result, self.stage.process(self.p.i_data))
+ self.m.d.comb += nmoperator.eq(result, self.data_r)
# if not in stall condition, update the temporary register
- with self.m.If(self.p.o_ready): # not stalled
- self.m.d.sync += eq(r_data, result) # update buffer
-
- with self.m.If(n_i_ready): # next stage is ready
- with self.m.If(self.p._o_ready): # not stalled
- # nothing in buffer: send (processed) input direct to output
- self.m.d.sync += [self.n.o_valid.eq(p_i_valid),
- eq(self.n.o_data, result), # update output
- ]
- with self.m.Else(): # p.o_ready is false, and data in buffer
- # Flush the [already processed] buffer to the output port.
- self.m.d.sync += [self.n.o_valid.eq(1), # reg empty
- eq(self.n.o_data, r_data), # flush buffer
- self.p._o_ready.eq(1), # clear stall
- ]
- # ignore input, since p.o_ready is also false.
-
- # (n.i_ready) is false here: next stage is ready
- with self.m.Elif(o_n_validn): # next stage being told "ready"
- self.m.d.sync += [self.n.o_valid.eq(p_i_valid),
- self.p._o_ready.eq(1), # Keep the buffer empty
- eq(self.n.o_data, result), # set output data
- ]
-
- # (n.i_ready) false and (n.o_valid) true:
- with self.m.Elif(i_p_valid_o_p_ready):
- # If next stage *is* ready, and not stalled yet, accept input
- self.m.d.sync += self.p._o_ready.eq(~(p_i_valid & self.n.o_valid))
+ with self.m.If(self.p.ready_o): # not stalled
+ self.m.d.sync += nmoperator.eq(r_data, result) # update buffer
+
+ # data pass-through conditions
+ with self.m.If(npnn):
+ data_o = self._postprocess(result) # XXX TBD, does nothing right now
+ self.m.d.sync += [self.n.valid_o.eq(p_valid_i), # valid if p_valid
+ nmoperator.eq(self.n.data_o, data_o), # update out
+ ]
+ # buffer flush conditions (NOTE: can override data passthru conditions)
+ with self.m.If(nir_por_n): # not stalled
+ # Flush the [already processed] buffer to the output port.
+ data_o = self._postprocess(r_data) # XXX TBD, does nothing right now
+ self.m.d.sync += [self.n.valid_o.eq(1), # reg empty
+ nmoperator.eq(self.n.data_o, data_o), # flush
+ ]
+ # output ready conditions
+ self.m.d.sync += self.p._ready_o.eq(nir_novn | por_pivn)
return self.m
Argument: stage. see Stage API above
- stage-1 p.i_valid >>in stage n.o_valid out>> stage+1
- stage-1 p.o_ready <<out stage n.i_ready <<in stage+1
- stage-1 p.i_data >>in stage n.o_data out>> stage+1
+ stage-1 p.valid_i >>in stage n.valid_o out>> stage+1
+ stage-1 p.ready_o <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>in stage n.data_o out>> stage+1
| |
+--process->--^
+ Truth Table
+
+ Inputs Temporary Output Data
+ ------- ---------- ----- ----
+ P P N N PiV& ~NiR& N P
+ i o i o PoR NoV o o
+ V R R V V R
+
+ ------- - - - -
+ 0 0 0 0 0 0 >0 0 reg
+ 0 0 0 1 0 1 >1 0 reg
+ 0 0 1 0 0 0 0 1 process(data_i)
+ 0 0 1 1 0 0 0 1 process(data_i)
+ ------- - - - -
+ 0 1 0 0 0 0 >0 0 reg
+ 0 1 0 1 0 1 >1 0 reg
+ 0 1 1 0 0 0 0 1 process(data_i)
+ 0 1 1 1 0 0 0 1 process(data_i)
+ ------- - - - -
+ 1 0 0 0 0 0 >0 0 reg
+ 1 0 0 1 0 1 >1 0 reg
+ 1 0 1 0 0 0 0 1 process(data_i)
+ 1 0 1 1 0 0 0 1 process(data_i)
+ ------- - - - -
+ 1 1 0 0 1 0 1 0 process(data_i)
+ 1 1 0 1 1 1 1 0 process(data_i)
+ 1 1 1 0 1 0 1 1 process(data_i)
+ 1 1 1 1 1 0 1 1 process(data_i)
+ ------- - - - -
"""
- def __init__(self, stage, stage_ctl=False):
- ControlBase.__init__(self, stage_ctl=stage_ctl)
- self.stage = stage
-
- # set up the input and output data
- self.p.i_data = stage.ispec() # input type
- self.n.o_data = stage.ospec()
def elaborate(self, platform):
-
- self.m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
r_busy = Signal()
- result = self.stage.ospec()
- if hasattr(self.stage, "setup"):
- self.stage.setup(self.m, self.p.i_data)
+ result = _spec(self.stage.ospec, "r_tmp")
# establish some combinatorial temporaries
- n_i_ready = Signal(reset_less=True, name="n_i_rdy_data")
- p_i_valid_p_o_ready = Signal(reset_less=True)
- p_i_valid = Signal(reset_less=True)
- self.m.d.comb += [p_i_valid.eq(self.p.i_valid_test),
- n_i_ready.eq(self.n.i_ready_test),
- p_i_valid_p_o_ready.eq(p_i_valid & self.p.o_ready),
+ n_ready_i = Signal(reset_less=True, name="n_i_rdy_data")
+ p_valid_i_p_ready_o = Signal(reset_less=True)
+ p_valid_i = Signal(reset_less=True)
+ m.d.comb += [p_valid_i.eq(self.p.valid_i_test),
+ n_ready_i.eq(self.n.ready_i_test),
+ p_valid_i_p_ready_o.eq(p_valid_i & self.p.ready_o),
]
# store result of processing in combinatorial temporary
- self.m.d.comb += eq(result, self.stage.process(self.p.i_data))
+ m.d.comb += nmoperator.eq(result, self.data_r)
# previous valid and ready
- with self.m.If(p_i_valid_p_o_ready):
- self.m.d.sync += [r_busy.eq(1), # output valid
- #self.n.o_valid.eq(1), # output valid
- eq(self.n.o_data, result), # update output
- ]
+ with m.If(p_valid_i_p_ready_o):
+ data_o = self._postprocess(result) # XXX TBD, does nothing right now
+ m.d.sync += [r_busy.eq(1), # output valid
+ nmoperator.eq(self.n.data_o, data_o), # update output
+ ]
# previous invalid or not ready, however next is accepting
- with self.m.Elif(n_i_ready):
- self.m.d.sync += [ eq(self.n.o_data, result)]
+ with m.Elif(n_ready_i):
+ data_o = self._postprocess(result) # XXX TBD, does nothing right now
+ m.d.sync += [nmoperator.eq(self.n.data_o, data_o)]
# TODO: could still send data here (if there was any)
- #self.m.d.sync += self.n.o_valid.eq(0) # ...so set output invalid
- self.m.d.sync += r_busy.eq(0) # ...so set output invalid
+ #m.d.sync += self.n.valid_o.eq(0) # ...so set output invalid
+ m.d.sync += r_busy.eq(0) # ...so set output invalid
- self.m.d.comb += self.n.o_valid.eq(r_busy)
+ m.d.comb += self.n.valid_o.eq(r_busy)
# if next is ready, so is previous
- self.m.d.comb += self.p._o_ready.eq(n_i_ready)
+ m.d.comb += self.p._ready_o.eq(n_ready_i)
return self.m
Argument: stage. see Stage API, above
- stage-1 p.i_valid >>in stage n.o_valid out>> stage+1
- stage-1 p.o_ready <<out stage n.i_ready <<in stage+1
- stage-1 p.i_data >>in stage n.o_data out>> stage+1
+ stage-1 p.valid_i >>in stage n.valid_o out>> stage+1
+ stage-1 p.ready_o <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>in stage n.data_o out>> stage+1
| |
r_data result
| |
Attributes:
-----------
- p.i_data : StageInput, shaped according to ispec
+ p.data_i : StageInput, shaped according to ispec
The pipeline input
- p.o_data : StageOutput, shaped according to ospec
+ p.data_o : StageOutput, shaped according to ospec
The pipeline output
r_data : input_shape according to ispec
A temporary (buffered) copy of a prior (valid) input.
result: output_shape according to ospec
The output of the combinatorial logic. it is updated
COMBINATORIALLY (no clock dependence).
- """
- def __init__(self, stage, stage_ctl=False):
- ControlBase.__init__(self, stage_ctl=stage_ctl)
- self.stage = stage
-
- # set up the input and output data
- self.p.i_data = stage.ispec() # input type
- self.n.o_data = stage.ospec() # output type
+ Truth Table
+
+ Inputs Temp Output Data
+ ------- - ----- ----
+ P P N N ~NiR& N P
+ i o i o NoV o o
+ V R R V V R
+
+ ------- - - -
+ 0 0 0 0 0 0 1 reg
+ 0 0 0 1 1 1 0 reg
+ 0 0 1 0 0 0 1 reg
+ 0 0 1 1 0 0 1 reg
+ ------- - - -
+ 0 1 0 0 0 0 1 reg
+ 0 1 0 1 1 1 0 reg
+ 0 1 1 0 0 0 1 reg
+ 0 1 1 1 0 0 1 reg
+ ------- - - -
+ 1 0 0 0 0 1 1 reg
+ 1 0 0 1 1 1 0 reg
+ 1 0 1 0 0 1 1 reg
+ 1 0 1 1 0 1 1 reg
+ ------- - - -
+ 1 1 0 0 0 1 1 process(data_i)
+ 1 1 0 1 1 1 0 process(data_i)
+ 1 1 1 0 0 1 1 process(data_i)
+ 1 1 1 1 0 1 1 process(data_i)
+ ------- - - -
+
+ Note: PoR is *NOT* involved in the above decision-making.
+ """
def elaborate(self, platform):
- self.m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
data_valid = Signal() # is data valid or not
- r_data = self.stage.ispec() # input type
- if hasattr(self.stage, "setup"):
- self.stage.setup(self.m, r_data)
+ r_data = _spec(self.stage.ospec, "r_tmp") # output type
# some temporaries
- p_i_valid = Signal(reset_less=True)
+ p_valid_i = Signal(reset_less=True)
pv = Signal(reset_less=True)
- self.m.d.comb += p_i_valid.eq(self.p.i_valid_test)
- self.m.d.comb += pv.eq(self.p.i_valid & self.p.o_ready)
-
- self.m.d.comb += self.n.o_valid.eq(data_valid)
- self.m.d.comb += self.p._o_ready.eq(~data_valid | self.n.i_ready_test)
- self.m.d.sync += data_valid.eq(p_i_valid | \
- (~self.n.i_ready_test & data_valid))
- with self.m.If(pv):
- self.m.d.sync += eq(r_data, self.p.i_data)
- self.m.d.comb += eq(self.n.o_data, self.stage.process(r_data))
- return self.m
+ buf_full = Signal(reset_less=True)
+ m.d.comb += p_valid_i.eq(self.p.valid_i_test)
+ m.d.comb += pv.eq(self.p.valid_i & self.p.ready_o)
+ m.d.comb += buf_full.eq(~self.n.ready_i_test & data_valid)
+ m.d.comb += self.n.valid_o.eq(data_valid)
+ m.d.comb += self.p._ready_o.eq(~data_valid | self.n.ready_i_test)
+ m.d.sync += data_valid.eq(p_valid_i | buf_full)
+
+ with m.If(pv):
+ m.d.sync += nmoperator.eq(r_data, self.data_r)
+ data_o = self._postprocess(r_data) # XXX TBD, does nothing right now
+ m.d.comb += nmoperator.eq(self.n.data_o, data_o)
+
+ return self.m
class UnbufferedPipeline2(ControlBase):
""" A simple pipeline stage with single-clock synchronisation
Argument: stage. see Stage API, above
- stage-1 p.i_valid >>in stage n.o_valid out>> stage+1
- stage-1 p.o_ready <<out stage n.i_ready <<in stage+1
- stage-1 p.i_data >>in stage n.o_data out>> stage+1
- | |
- r_data result
- | |
- +--process ->-+
-
+ stage-1 p.valid_i >>in stage n.valid_o out>> stage+1
+ stage-1 p.ready_o <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>in stage n.data_o out>> stage+1
+ | | |
+ +- process-> buf <-+
Attributes:
-----------
- p.i_data : StageInput, shaped according to ispec
+ p.data_i : StageInput, shaped according to ispec
The pipeline input
- p.o_data : StageOutput, shaped according to ospec
+ p.data_o : StageOutput, shaped according to ospec
The pipeline output
buf : output_shape according to ospec
A temporary (buffered) copy of a valid output
This is HELD if the output is not ready. It is updated
SYNCHRONOUSLY.
- """
- def __init__(self, stage, stage_ctl=False):
- ControlBase.__init__(self, stage_ctl=stage_ctl)
- self.stage = stage
-
- # set up the input and output data
- self.p.i_data = stage.ispec() # input type
- self.n.o_data = stage.ospec() # output type
+ Inputs Temp Output Data
+ ------- - -----
+ P P N N ~NiR& N P (buf_full)
+ i o i o NoV o o
+ V R R V V R
+
+ ------- - - -
+ 0 0 0 0 0 0 1 process(data_i)
+ 0 0 0 1 1 1 0 reg (odata, unchanged)
+ 0 0 1 0 0 0 1 process(data_i)
+ 0 0 1 1 0 0 1 process(data_i)
+ ------- - - -
+ 0 1 0 0 0 0 1 process(data_i)
+ 0 1 0 1 1 1 0 reg (odata, unchanged)
+ 0 1 1 0 0 0 1 process(data_i)
+ 0 1 1 1 0 0 1 process(data_i)
+ ------- - - -
+ 1 0 0 0 0 1 1 process(data_i)
+ 1 0 0 1 1 1 0 reg (odata, unchanged)
+ 1 0 1 0 0 1 1 process(data_i)
+ 1 0 1 1 0 1 1 process(data_i)
+ ------- - - -
+ 1 1 0 0 0 1 1 process(data_i)
+ 1 1 0 1 1 1 0 reg (odata, unchanged)
+ 1 1 1 0 0 1 1 process(data_i)
+ 1 1 1 1 0 1 1 process(data_i)
+ ------- - - -
+
+ Note: PoR is *NOT* involved in the above decision-making.
+ """
def elaborate(self, platform):
- self.m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
buf_full = Signal() # is data valid or not
- buf = self.stage.ospec() # output type
- if hasattr(self.stage, "setup"):
- self.stage.setup(self.m, self.p.i_data)
+ buf = _spec(self.stage.ospec, "r_tmp") # output type
# some temporaries
- p_i_valid = Signal(reset_less=True)
- self.m.d.comb += p_i_valid.eq(self.p.i_valid_test)
+ p_valid_i = Signal(reset_less=True)
+ m.d.comb += p_valid_i.eq(self.p.valid_i_test)
- self.m.d.comb += self.n.o_valid.eq(buf_full | p_i_valid)
- self.m.d.comb += self.p._o_ready.eq(~buf_full)
- self.m.d.sync += buf_full.eq(~self.n.i_ready_test & self.n.o_valid)
+ m.d.comb += self.n.valid_o.eq(buf_full | p_valid_i)
+ m.d.comb += self.p._ready_o.eq(~buf_full)
+ m.d.sync += buf_full.eq(~self.n.ready_i_test & self.n.valid_o)
- odata = Mux(buf_full, buf, self.stage.process(self.p.i_data))
- self.m.d.comb += eq(self.n.o_data, odata)
- self.m.d.sync += eq(buf, self.n.o_data)
+ data_o = Mux(buf_full, buf, self.data_r)
+ data_o = self._postprocess(data_o) # XXX TBD, does nothing right now
+ m.d.comb += nmoperator.eq(self.n.data_o, data_o)
+ m.d.sync += nmoperator.eq(buf, self.n.data_o)
return self.m
-class PassThroughStage(StageCls):
- """ a pass-through stage which has its input data spec equal to its output,
- and "passes through" its data from input to output.
+class PassThroughHandshake(ControlBase):
+ """ A control block that delays by one clock cycle.
+
+ Inputs Temporary Output Data
+ ------- ------------------ ----- ----
+ P P N N PiV& PiV| NiR| pvr N P (pvr)
+ i o i o PoR ~PoR ~NoV o o
+ V R R V V R
+
+ ------- - - - - - -
+ 0 0 0 0 0 1 1 0 1 1 odata (unchanged)
+ 0 0 0 1 0 1 0 0 1 0 odata (unchanged)
+ 0 0 1 0 0 1 1 0 1 1 odata (unchanged)
+ 0 0 1 1 0 1 1 0 1 1 odata (unchanged)
+ ------- - - - - - -
+ 0 1 0 0 0 0 1 0 0 1 odata (unchanged)
+ 0 1 0 1 0 0 0 0 0 0 odata (unchanged)
+ 0 1 1 0 0 0 1 0 0 1 odata (unchanged)
+ 0 1 1 1 0 0 1 0 0 1 odata (unchanged)
+ ------- - - - - - -
+ 1 0 0 0 0 1 1 1 1 1 process(in)
+ 1 0 0 1 0 1 0 0 1 0 odata (unchanged)
+ 1 0 1 0 0 1 1 1 1 1 process(in)
+ 1 0 1 1 0 1 1 1 1 1 process(in)
+ ------- - - - - - -
+ 1 1 0 0 1 1 1 1 1 1 process(in)
+ 1 1 0 1 1 1 0 0 1 0 odata (unchanged)
+ 1 1 1 0 1 1 1 1 1 1 process(in)
+ 1 1 1 1 1 1 1 1 1 1 process(in)
+ ------- - - - - - -
+
"""
- def __init__(self, iospecfn):
- self.iospecfn = iospecfn
- def ispec(self): return self.iospecfn()
- def ospec(self): return self.iospecfn()
- def process(self, i): return i
+
+ def elaborate(self, platform):
+ self.m = m = ControlBase.elaborate(self, platform)
+
+ r_data = _spec(self.stage.ospec, "r_tmp") # output type
+
+ # temporaries
+ p_valid_i = Signal(reset_less=True)
+ pvr = Signal(reset_less=True)
+ m.d.comb += p_valid_i.eq(self.p.valid_i_test)
+ m.d.comb += pvr.eq(p_valid_i & self.p.ready_o)
+
+ m.d.comb += self.p.ready_o.eq(~self.n.valid_o | self.n.ready_i_test)
+ m.d.sync += self.n.valid_o.eq(p_valid_i | ~self.p.ready_o)
+
+ odata = Mux(pvr, self.data_r, r_data)
+ m.d.sync += nmoperator.eq(r_data, odata)
+ r_data = self._postprocess(r_data) # XXX TBD, does nothing right now
+ m.d.comb += nmoperator.eq(self.n.data_o, r_data)
+
+ return m
class RegisterPipeline(UnbufferedPipeline):
""" A pipeline stage that delays by one clock cycle, creating a
- sync'd latch out of o_data and o_valid as an indirect byproduct
+ sync'd latch out of data_o and valid_o as an indirect byproduct
of using PassThroughStage
"""
def __init__(self, iospecfn):
UnbufferedPipeline.__init__(self, PassThroughStage(iospecfn))
+
+class FIFOControl(ControlBase):
+ """ FIFO Control. Uses Queue to store data, coincidentally
+ happens to have same valid/ready signalling as Stage API.
+
+ data_i -> fifo.din -> FIFO -> fifo.dout -> data_o
+ """
+ def __init__(self, depth, stage, in_multi=None, stage_ctl=False,
+ fwft=True, pipe=False):
+ """ FIFO Control
+
+ * :depth: number of entries in the FIFO
+ * :stage: data processing block
+ * :fwft: first word fall-thru mode (non-fwft introduces delay)
+ * :pipe: specifies pipe mode.
+
+ when fwft = True it indicates that transfers may occur
+ combinatorially through stage processing in the same clock cycle.
+ This requires that the Stage be a Moore FSM:
+ https://en.wikipedia.org/wiki/Moore_machine
+
+ when fwft = False it indicates that all output signals are
+ produced only from internal registers or memory, i.e. that the
+ Stage is a Mealy FSM:
+ https://en.wikipedia.org/wiki/Mealy_machine
+
+ data is processed (and located) as follows:
+
+ self.p self.stage temp fn temp fn temp fp self.n
+ data_i->process()->result->cat->din.FIFO.dout->cat(data_o)
+
+ yes, really: cat produces a Cat() which can be assigned to.
+ this is how the FIFO gets de-catted without needing a de-cat
+ function
+ """
+ self.fwft = fwft
+ self.pipe = pipe
+ self.fdepth = depth
+ ControlBase.__init__(self, stage, in_multi, stage_ctl)
+
+ def elaborate(self, platform):
+ self.m = m = ControlBase.elaborate(self, platform)
+
+ # make a FIFO with a signal of equal width to the data_o.
+ (fwidth, _) = nmoperator.shape(self.n.data_o)
+ fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe)
+ m.submodules.fifo = fifo
+
+ # store result of processing in combinatorial temporary
+ result = _spec(self.stage.ospec, "r_temp")
+ m.d.comb += nmoperator.eq(result, self.data_r)
+
+ # connect previous rdy/valid/data - do cat on data_i
+ # NOTE: cannot do the PrevControl-looking trick because
+ # of need to process the data. shaaaame....
+ m.d.comb += [fifo.we.eq(self.p.valid_i_test),
+ self.p.ready_o.eq(fifo.writable),
+ nmoperator.eq(fifo.din, nmoperator.cat(result)),
+ ]
+
+ # connect next rdy/valid/data - do cat on data_o (further below)
+ connections = [self.n.valid_o.eq(fifo.readable),
+ fifo.re.eq(self.n.ready_i_test),
+ ]
+ if self.fwft:
+ m.d.comb += connections # combinatorial on next ready/valid
+ else:
+ m.d.sync += connections # unbuffered fwft mode needs sync
+ data_o = nmoperator.cat(self.n.data_o).eq(fifo.dout)
+ data_o = self._postprocess(data_o) # XXX TBD, does nothing right now
+ m.d.comb += data_o
+
+ return m
+
+
+# aka "RegStage".
+class UnbufferedPipeline(FIFOControl):
+ def __init__(self, stage, in_multi=None, stage_ctl=False):
+ FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl,
+ fwft=True, pipe=False)
+
+# aka "BreakReadyStage" XXX had to set fwft=True to get it to work
+class PassThroughHandshake(FIFOControl):
+ def __init__(self, stage, in_multi=None, stage_ctl=False):
+ FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl,
+ fwft=True, pipe=True)
+
+# this is *probably* BufferedHandshake, although test #997 now succeeds.
+class BufferedHandshake(FIFOControl):
+ def __init__(self, stage, in_multi=None, stage_ctl=False):
+ FIFOControl.__init__(self, 2, stage, in_multi, stage_ctl,
+ fwft=True, pipe=False)
+
+
+"""
+# this is *probably* SimpleHandshake (note: memory cell size=0)
+class SimpleHandshake(FIFOControl):
+ def __init__(self, stage, in_multi=None, stage_ctl=False):
+ FIFOControl.__init__(self, 0, stage, in_multi, stage_ctl,
+ fwft=True, pipe=False)
+"""