""" 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.
+ 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 (iocontrol.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:
------------------
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.lib.fifo import SyncFIFO
-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
- """
-
- 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, direct=False):
- """ internal helper function to connect stage to an input source.
- do not use to connect stage-to-stage!
- """
- if direct:
- i_valid = prev.i_valid
- else:
- i_valid = prev.i_valid_test
- return [self.i_valid.eq(i_valid),
- 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
+from nmigen.lib.fifo import SyncFIFOBuffered
+from nmigen.hdl.rec import Record
- # 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, direct=False):
- """ internal helper function to connect stage to an output source.
- do not use to connect stage-to-stage!
- """
- if direct:
- i_ready = nxt.i_ready
- else:
- i_ready = nxt.i_ready_test
- return [nxt.o_valid.eq(self.o_valid),
- self.i_ready.eq(i_ready),
- eq(nxt.o_data, self.o_data),
- ]
-
-
-class Visitor:
- """ a helper routine which identifies if it is being passed a list
- (or tuple) of objects, or signals, or Records, and calls
- a visitor function.
-
- the visiting fn is called when an object is identified.
-
- 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.
- """
- def visit(self, o, i, act):
- if isinstance(o, dict):
- return self.dict_visit(o, i, act)
-
- res = act.prepare()
- if not isinstance(o, Sequence):
- o, i = [o], [i]
- for (ao, ai) in zip(o, i):
- #print ("visit", fn, ao, ai)
- if isinstance(ao, Record):
- rres = self.record_visit(ao, ai, act)
- elif isinstance(ao, ArrayProxy) and not isinstance(ai, Value):
- rres = self.arrayproxy_visit(ao, ai, act)
- else:
- rres = act.fn(ao, ai)
- res += rres
- return res
-
- def dict_visit(self, o, i, act):
- res = act.prepare()
- for (k, v) in o.items():
- print ("d-eq", v, i[k])
- res.append(act.fn(v, i[k]))
- return res
-
- def record_visit(self, ao, ai, act):
- res = act.prepare()
- 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
- res += self.visit(ao.fields[field_name], val, act)
- return res
-
- def arrayproxy_visit(self, ao, ai, act):
- res = act.prepare()
- for p in ai.ports():
- op = getattr(ao, p.name)
- #print (op, p, p.name)
- res.append(fn(op, p))
- return res
-
-
-class Eq(Visitor):
- def __init__(self):
- self.res = []
- def prepare(self):
- return []
- def fn(self, o, i):
- rres = o.eq(i)
- if not isinstance(rres, Sequence):
- rres = [rres]
- return rres
- def __call__(self, o, i):
- return self.visit(o, i, self)
-
-
-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.
- """
- return Eq()(o, i)
-
-
-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, stage=None, in_multi=None, stage_ctl=False):
""" Base class containing ready/valid/data to previous and next stages
* 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.
"""
- self.stage = stage
+ StageHelper.__init__(self, stage)
# set up input and output IO ACK (prev/next ready/valid)
self.p = PrevControl(in_multi, stage_ctl)
# set up the input and output data
if stage is not None:
- self.p.i_data = stage.ispec() # input type
- self.n.o_data = stage.ospec()
+ self._new_data(self, self, "data")
+
+ def _new_data(self, p, n, name):
+ """ allocates new data_i and data_o
+ """
+ self.p.data_i = _spec(p.stage.ispec, "%s_i" % name)
+ self.n.data_o = _spec(n.stage.ospec, "%s_o" % 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"
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)
+ pipe1 = pipechain[i] # earlier
+ pipe2 = pipechain[i+1] # later (by 1)
+ eqs += pipe1.connect_to_next(pipe2) # earlier n to later p
- # 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)
+ # connect front and back of chain to ourselves
+ front = pipechain[0] # first in chain
+ end = pipechain[-1] # last in chain
+ self._new_data(front, end, "chain") # NOTE: REPLACES existing data
+ eqs += front._connect_in(self) # front p to our p
+ eqs += end._connect_out(self) # end n to out 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
- if self.stage is not None and hasattr(self.stage, "setup"):
- self.stage.setup(m, self.p.i_data)
+ 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
"""
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()
+ 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")
+ 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_i_valid = 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_i_valid.eq(self.p.i_valid_test),
- o_n_validn.eq(~self.n.o_valid),
- n_i_ready.eq(self.n.i_ready_test),
- nir_por.eq(n_i_ready & self.p._o_ready),
- nir_por_n.eq(n_i_ready & ~self.p._o_ready),
- nir_novn.eq(n_i_ready | o_n_validn),
- nirn_novn.eq(~n_i_ready & o_n_validn),
+ 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._o_ready & ~p_i_valid)
+ 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(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):
- self.m.d.sync += [self.n.o_valid.eq(p_i_valid), # valid if p_valid
- eq(self.n.o_data, result), # update output
+ 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.
- self.m.d.sync += [self.n.o_valid.eq(1), # reg empty
- eq(self.n.o_data, r_data), # flush buffer
+ 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._o_ready.eq(nir_novn | por_pivn)
+ 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
- ------- ---------- -----
- P P N N PiV& ~NiV& N P
+ 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
- 0 0 0 1 0 1 >1 0
- 0 0 1 0 0 0 0 1
- 0 0 1 1 0 0 0 1
+ 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
- 0 1 0 1 0 1 >1 0
- 0 1 1 0 0 0 0 1
- 0 1 1 1 0 0 0 1
+ 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
- 1 0 0 1 0 1 >1 0
- 1 0 1 0 0 0 0 1
- 1 0 1 1 0 0 0 1
+ 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
- 1 1 0 1 1 1 1 0
- 1 1 1 0 1 0 1 1
- 1 1 1 1 1 0 1 1
+ 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 elaborate(self, platform):
- self.m = m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
r_busy = Signal()
- result = self.stage.ospec()
+ 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)
- 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
- 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 m.If(p_i_valid_p_o_ready):
+ 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
- eq(self.n.o_data, result), # update output
+ nmoperator.eq(self.n.data_o, data_o), # update output
]
# previous invalid or not ready, however next is accepting
- with m.Elif(n_i_ready):
- 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)
- #m.d.sync += self.n.o_valid.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
- 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
- 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.
Truth Table
- Inputs Temp Output
- ------- - -----
+ 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
- 0 0 0 1 1 1 0
- 0 0 1 0 0 0 1
- 0 0 1 1 0 0 1
+ 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
- 0 1 0 1 1 1 0
- 0 1 1 0 0 0 1
- 0 1 1 1 0 0 1
+ 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
- 1 0 0 1 1 1 0
- 1 0 1 0 0 1 1
- 1 0 1 1 0 1 1
+ 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
- 1 1 0 1 1 1 0
- 1 1 1 0 0 1 1
- 1 1 1 1 0 1 1
+ 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 = m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
data_valid = Signal() # is data valid or not
- r_data = self.stage.ospec() # output type
+ 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)
- m.d.comb += p_i_valid.eq(self.p.i_valid_test)
- m.d.comb += pv.eq(self.p.i_valid & self.p.o_ready)
+ 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)
- m.d.comb += self.n.o_valid.eq(data_valid)
- m.d.comb += self.p._o_ready.eq(~data_valid | self.n.i_ready_test)
- m.d.sync += data_valid.eq(p_i_valid | \
- (~self.n.i_ready_test & data_valid))
with m.If(pv):
- m.d.sync += eq(r_data, self.stage.process(self.p.i_data))
- m.d.comb += eq(self.n.o_data, r_data)
+ 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
and two-way valid/ready synchronised signalling.
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-> 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.
+
+ 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 = 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
+ buf = _spec(self.stage.ospec, "r_tmp") # output type
# some temporaries
- p_i_valid = Signal(reset_less=True)
- 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)
- m.d.comb += self.n.o_valid.eq(buf_full | p_i_valid)
- m.d.comb += self.p._o_ready.eq(~buf_full)
- 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))
- m.d.comb += eq(self.n.o_data, odata)
- 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.
- """
- 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
-
-
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 elaborate(self, platform):
- self.m = m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
+
+ r_data = _spec(self.stage.ospec, "r_tmp") # output type
# temporaries
- p_i_valid = Signal(reset_less=True)
+ p_valid_i = Signal(reset_less=True)
pvr = Signal(reset_less=True)
- m.d.comb += p_i_valid.eq(self.p.i_valid_test)
- m.d.comb += pvr.eq(p_i_valid & self.p.o_ready)
+ 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.o_ready.eq(~self.n.o_valid | self.n.i_ready_test)
- m.d.sync += self.n.o_valid.eq(p_i_valid | ~self.p.o_ready)
+ 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.stage.process(self.p.i_data), self.n.o_data)
- m.d.sync += eq(self.n.o_data, odata)
+ 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 FIFOtest(ControlBase):
- """ A test of using a SyncFIFO to see if it will work.
- Note: the only things it will accept is a Signal of width "width".
+class FIFOControl(ControlBase):
+ """ FIFO Control. Uses Queue to store data, coincidentally
+ happens to have same valid/ready signalling as Stage API.
+ (TODO: remove use of SyncFIFOBuffered)
+
+ data_i -> fifo.din -> FIFO -> fifo.dout -> data_o
"""
+ def __init__(self, depth, stage, in_multi=None, stage_ctl=False,
+ fwft=True, buffered=False, 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)
+ * :buffered: use buffered FIFO (introduces extra cycle delay)
- def __init__(self, width, depth):
+ NOTE 1: FPGAs may have trouble with the defaults for SyncFIFO
+ (fwft=True, buffered=False). XXX TODO: fix this by
+ using Queue in all cases instead.
- self.fwidth = width
+ 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
+ """
+
+ assert not (fwft and buffered), "buffered cannot do fwft"
+ if buffered:
+ depth += 1
+ self.fwft = fwft
+ self.buffered = buffered
+ self.pipe = pipe
self.fdepth = depth
- def iospecfn():
- return Signal(width, name="data")
- stage = PassThroughStage(iospecfn)
- ControlBase.__init__(self, stage=stage)
+ ControlBase.__init__(self, stage, in_multi, stage_ctl)
def elaborate(self, platform):
- self.m = m = ControlBase._elaborate(self, platform)
+ self.m = m = ControlBase.elaborate(self, platform)
- fifo = SyncFIFO(self.fwidth, self.fdepth)
+ # make a FIFO with a signal of equal width to the data_o.
+ (fwidth, _) = nmoperator.shape(self.n.data_o)
+ if self.buffered:
+ fifo = SyncFIFOBuffered(fwidth, self.fdepth)
+ else:
+ fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe)
m.submodules.fifo = fifo
- # prev: make the FIFO "look" like a PrevControl...
- fp = PrevControl()
- fp.i_valid = fifo.we
- fp._o_ready = fifo.writable
- fp.i_data = fifo.din
- # ... so we can do this!
- m.d.comb += fp._connect_in(self.p, True)
-
- # next: make the FIFO "look" like a NextControl...
- fn = NextControl()
- fn.o_valid = fifo.readable
- fn.i_ready = fifo.re
- fn.o_data = fifo.dout
- # ... so we can do this!
- m.d.comb += fn._connect_out(self.n)
-
- # err... that should be all!
+ # 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 or self.buffered:
+ 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)
+"""