looking for replacements of the hard-coded control blocks
[ieee754fpu.git] / src / add / singlepipe.py
index 3150d389269d798a1b875c4dd21977ebec8a04f1..fe052be72a3e70fde85e56058cf708ea9e7cd341 100644 (file)
@@ -1,4 +1,4 @@
-""" Pipeline and BufferedPipeline implementation, conforming to the same API.
+""" Pipeline and BufferedHandshake implementation, conforming to the same API.
     For multi-input and multi-output variants, see multipipe.
 
     eq:
     ------------------
 
     A simple stalling clock-synchronised pipeline that has no buffering
-    (unlike BufferedPipeline).  Data flows on *every* clock cycle when
+    (unlike BufferedHandshake).  Data flows on *every* clock cycle when
     the conditions are right (this is nominally when the input is valid
     and the output is ready).
 
     A stall anywhere along the line will result in a stall back-propagating
-    down the entire chain.  The BufferedPipeline by contrast will buffer
+    down the entire chain.  The BufferedHandshake by contrast will buffer
     incoming data, allowing previous stages one clock cycle's grace before
     also having to stall.
 
     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:
     ----------------
 
     clock delay, when its stage is a PassThroughStage, it results in a Pipeline
     stage that, duh, delays its (unmodified) input by one clock cycle.
 
-    BufferedPipeline:
+    BufferedHandshake:
     ----------------
 
     nmigen implementation of buffered pipeline stage, based on zipcpu:
     dan calls a "buffered handshake".
 
     it's quite a complex state machine!
+
+    SimpleHandshake
+    ---------------
+
+    Synchronised pipeline, Based on:
+    https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v
 """
 
 from nmigen import Signal, Cat, Const, Mux, Module, Value
 from nmigen.cli import verilog, rtlil
+from nmigen.lib.fifo import SyncFIFO, SyncFIFOBuffered
 from nmigen.hdl.ast import ArrayProxy
 from nmigen.hdl.rec import Record, Layout
 
 from abc import ABCMeta, abstractmethod
 from collections.abc import Sequence
+from queue import Queue
+
+
+class RecordObject(Record):
+    def __init__(self, layout=None, name=None):
+        Record.__init__(self, layout=layout or [], name=None)
+
+    def __setattr__(self, k, v):
+        if k in dir(Record) or "fields" not in self.__dict__:
+            return object.__setattr__(self, k, v)
+        self.fields[k] = v
+        if isinstance(v, Record):
+            newlayout = {k: (k, v.layout)}
+        else:
+            newlayout = {k: (k, v.shape())}
+        self.layout.fields.update(newlayout)
+
 
 
 class PrevControl:
@@ -168,27 +200,50 @@ class PrevControl:
         * i_data : an input - added by the user of this class
     """
 
-    def __init__(self, i_width=1):
+    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._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
 
-    def _connect_in(self, prev):
+    @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, fn=None):
         """ 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),
+        i_valid = prev.i_valid if direct else prev.i_valid_test
+        i_data = fn(prev.i_data) if fn is not None else prev.i_data
+        return [self.i_valid.eq(i_valid),
                 prev.o_ready.eq(self.o_ready),
-                eq(self.i_data, prev.i_data),
+                eq(self.i_data, i_data),
                ]
 
-    def i_valid_logic(self):
+    @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
+        if vlen > 1:
+            # multi-bit case: valid only when i_valid is all 1s
             all1s = Const(-1, (len(self.i_valid), False))
-            return self.i_valid == all1s
-        # single-bit i_valid case
-        return self.i_valid
+            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:
@@ -197,10 +252,19 @@ class NextControl:
         * 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):
+    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.
@@ -212,25 +276,24 @@ class NextControl:
                 eq(nxt.i_data, self.o_data),
                ]
 
-    def _connect_out(self, nxt):
+    def _connect_out(self, nxt, direct=False, fn=None):
         """ internal helper function to connect stage to an output source.
             do not use to connect stage-to-stage!
         """
+        i_ready = nxt.i_ready if direct else nxt.i_ready_test
+        o_data = fn(nxt.o_data) if fn is not None else nxt.o_data
         return [nxt.o_valid.eq(self.o_valid),
-                self.i_ready.eq(nxt.i_ready),
-                eq(nxt.o_data, self.o_data),
+                self.i_ready.eq(i_ready),
+                eq(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.
+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.
 
-        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
+        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,
@@ -247,13 +310,92 @@ def eq(o, i):
         python object, enumerate them, find out the list of Signals that way,
         and assign them.
     """
-    if not isinstance(o, Sequence):
-        o, i = [o], [i]
+    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
+            val = self.visit(ao.fields[field_name], val, act)
+            if isinstance(val, Sequence):
+                res += val
+            else:
+                res.append(val)
+        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)
+
+
+def flatten(i):
+    """ flattens a compound structure recursively using Cat
+    """
+    if not isinstance(i, Sequence):
+        i = [i]
     res = []
-    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):
+    for ai in i:
+        print ("flatten", ai)
+        if isinstance(ai, Record):
+            print ("record", list(ai.layout))
+            rres = []
+            for idx, (field_name, field_shape, _) in enumerate(ai.layout):
                 if isinstance(field_shape, Layout):
                     val = ai.fields
                 else:
@@ -262,22 +404,28 @@ def eq(o, i):
                     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):
+                print ("recidx", idx, field_name, field_shape, val)
+                val = flatten(val)
+                print ("recidx flat", idx, val)
+                if isinstance(val, Sequence):
+                    rres += val
+                else:
+                    rres.append(val)
+
+        elif isinstance(ai, ArrayProxy) and not isinstance(ai, Value):
+            rres = []
             for p in ai.ports():
-                op = getattr(ao, p.name)
+                op = getattr(ai, p.name)
                 #print (op, p, p.name)
-                rres = op.eq(p)
-                if not isinstance(rres, Sequence):
-                    rres = [rres]
-                res += rres
+                rres.append(flatten(p))
         else:
-            rres = ao.eq(ai)
-            if not isinstance(rres, Sequence):
-                rres = [rres]
-            res += rres
-    return res
+            rres = ai
+        if not isinstance(rres, Sequence):
+            rres = [rres]
+        res += rres
+        print ("flatten res", res)
+    return Cat(*res)
+
 
 
 class StageCls(metaclass=ABCMeta):
@@ -351,8 +499,9 @@ class StageChain(StageCls):
         * output of second goes into input into third (etc. etc.)
         * the output of this class will be the output of the last stage
     """
-    def __init__(self, chain):
+    def __init__(self, chain, specallocate=False):
         self.chain = chain
+        self.specallocate = specallocate
 
     def ispec(self):
         return self.chain[0].ispec()
@@ -360,17 +509,30 @@ class StageChain(StageCls):
     def ospec(self):
         return self.chain[-1].ospec()
 
-    def setup(self, m, i):
+    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()     # only the last assignment survives
+            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:
-                ni = self.chain[idx+1].ispec() # becomes new input on next loop
-                m.d.comb += eq(ni, o)          # assign output to next input
-                i = ni
-        self.o = o                             # last loop is the output
+            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 process(self, i):
         return self.o # conform to Stage API: return last-loop output
@@ -379,7 +541,7 @@ class StageChain(StageCls):
 class ControlBase:
     """ Common functions for Pipeline API
     """
-    def __init__(self, in_multi=None):
+    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
@@ -389,10 +551,16 @@ class ControlBase:
             * add i_data member to PrevControl (p) and
             * add o_data member to NextControl (n)
         """
+        self.stage = stage
 
         # set up input and output IO ACK (prev/next ready/valid)
-        self.p = PrevControl(in_multi)
-        self.n = NextControl()
+        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.p.i_data = stage.ispec() # input type
+            self.n.o_data = stage.ospec()
 
     def connect_to_next(self, nxt):
         """ helper function to connect to the next stage data/valid/ready.
@@ -411,7 +579,7 @@ class ControlBase:
         """
         return self.n._connect_out(nxt.n)
 
-    def connect(self, m, pipechain):
+    def connect(self, pipechain):
         """ connects a chain (list) of Pipeline instances together and
             links them to this ControlBase instance:
 
@@ -451,16 +619,21 @@ class ControlBase:
 
         # connect front of chain to ourselves
         front = pipechain[0]
-        #self.p.i_data = front.stage.ispec()
+        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()
+        self.n.o_data = end.stage.ospec()
         eqs += end._connect_out(self)
 
-        # activate the assignments
-        m.d.comb += eqs
+        return eqs
+
+    def _postprocess(self, i): # XXX DISABLED
+        return i # RETURNS INPUT
+        if hasattr(self.stage, "postprocess"):
+            return self.stage.postprocess(i)
+        return i
 
     def set_input(self, i):
         """ helper function to set the input data
@@ -481,8 +654,28 @@ class ControlBase:
             res += self.n.o_data
         return res
 
+    def _elaborate(self, platform):
+        """ handles case where stage has dynamic ready/valid functions
+        """
+        m = Module()
+
+        if self.stage is not None and hasattr(self.stage, "setup"):
+            self.stage.setup(m, self.p.i_data)
+
+        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)
+
+        # 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)
+
+        return m
+
 
-class BufferedPipeline(ControlBase):
+class BufferedHandshake(ControlBase):
     """ buffered pipeline stage.  data and strobe signals travel in sync.
         if ever the input is ready and the output is not, processed data
         is shunted in a temporary register.
@@ -510,67 +703,140 @@ class BufferedPipeline(ControlBase):
 
         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):
-        ControlBase.__init__(self)
-        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):
-        m = Module()
+        self.m = ControlBase._elaborate(self, platform)
 
         result = self.stage.ospec()
         r_data = self.stage.ospec()
-        if hasattr(self.stage, "setup"):
-            self.stage.setup(m, self.p.i_data)
 
         # establish some combinatorial temporaries
         o_n_validn = Signal(reset_less=True)
-        i_p_valid_o_p_ready = Signal(reset_less=True)
+        n_i_ready = 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)
-        m.d.comb += [p_i_valid.eq(self.p.i_valid_logic()),
+        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),
-                     i_p_valid_o_p_ready.eq(p_i_valid & self.p.o_ready),
+                     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),
+                     npnn.eq(nir_por | nirn_novn),
+                     por_pivn.eq(self.p._o_ready & ~p_i_valid)
         ]
 
         # store result of processing in combinatorial temporary
-        m.d.comb += eq(result, self.stage.process(self.p.i_data))
+        self.m.d.comb += eq(result, self.stage.process(self.p.i_data))
 
         # if not in stall condition, update the temporary register
-        with m.If(self.p.o_ready): # not stalled
-            m.d.sync += eq(r_data, result) # update buffer
-
-        with m.If(self.n.i_ready): # next stage is ready
-            with m.If(self.p.o_ready): # not stalled
-                # nothing in buffer: send (processed) input direct to output
-                m.d.sync += [self.n.o_valid.eq(p_i_valid),
-                             eq(self.n.o_data, result), # update output
-                            ]
-            with m.Else(): # p.o_ready is false, and something is in buffer.
-                # Flush the [already processed] buffer to the output port.
-                m.d.sync += [self.n.o_valid.eq(1),      # declare reg empty
-                             eq(self.n.o_data, r_data), # flush buffer
-                             self.p.o_ready.eq(1),      # clear stall condition
-                            ]
-                # ignore input, since p.o_ready is also false.
-
-        # (n.i_ready) is false here: next stage is ready
-        with m.Elif(o_n_validn): # next stage being told "ready"
-            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
+        with self.m.If(self.p.o_ready): # not stalled
+            self.m.d.sync += eq(r_data, result) # update buffer
+
+        # data pass-through conditions
+        with self.m.If(npnn):
+            o_data = self._postprocess(result)
+            self.m.d.sync += [self.n.o_valid.eq(p_i_valid), # valid if p_valid
+                              eq(self.n.o_data, o_data),    # update output
+                             ]
+        # 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.
+            o_data = self._postprocess(r_data)
+            self.m.d.sync += [self.n.o_valid.eq(1),  # reg empty
+                              eq(self.n.o_data, o_data), # flush buffer
+                             ]
+        # output ready conditions
+        self.m.d.sync += self.p._o_ready.eq(nir_novn | por_pivn)
+
+        return self.m
+
+
+class SimpleHandshake(ControlBase):
+    """ simple handshake control.  data and strobe signals travel in sync.
+        implements the protocol used by Wishbone and AXI4.
+
+        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
+                              |             |
+                              +--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(i_data)
+        0 0 1 1   0    0     0 1    process(i_data)
+        -------   -    -     - -
+        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(i_data)
+        0 1 1 1   0    0     0 1    process(i_data)
+        -------   -    -     - -
+        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(i_data)
+        1 0 1 1   0    0     0 1    process(i_data)
+        -------   -    -     - -
+        1 1 0 0   1    0     1 0    process(i_data)
+        1 1 0 1   1    1     1 0    process(i_data)
+        1 1 1 0   1    0     1 1    process(i_data)
+        1 1 1 1   1    0     1 1    process(i_data)
+        -------   -    -     - -
+    """
+
+    def elaborate(self, platform):
+        self.m = m = ControlBase._elaborate(self, platform)
+
+        r_busy = Signal()
+        result = self.stage.ospec()
+
+        # 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),
+        ]
+
+        # store result of processing in combinatorial temporary
+        m.d.comb += eq(result, self.stage.process(self.p.i_data))
+
+        # previous valid and ready
+        with m.If(p_i_valid_p_o_ready):
+            o_data = self._postprocess(result)
+            m.d.sync += [r_busy.eq(1),      # output valid
+                         eq(self.n.o_data, o_data), # update output
                         ]
+        # previous invalid or not ready, however next is accepting
+        with m.Elif(n_i_ready):
+            o_data = self._postprocess(result)
+            m.d.sync += [eq(self.n.o_data, o_data)]
+            # 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 += r_busy.eq(0) # ...so set output invalid
 
-        # (n.i_ready) false and (n.o_valid) true:
-        with m.Elif(i_p_valid_o_p_ready):
-            # If next stage *is* ready, and not stalled yet, accept input
-            m.d.sync += self.p.o_ready.eq(~(p_i_valid & self.n.o_valid))
+        m.d.comb += self.n.o_valid.eq(r_busy)
+        # if next is ready, so is previous
+        m.d.comb += self.p._o_ready.eq(n_i_ready)
 
-        return m
+        return self.m
 
 
 class UnbufferedPipeline(ControlBase):
@@ -580,7 +846,7 @@ class UnbufferedPipeline(ControlBase):
         Note that a stall in one stage will result in the entire pipeline
         chain stalling.
 
-        Also that unlike BufferedPipeline, the valid/ready signalling does NOT
+        Also that unlike BufferedHandshake, the valid/ready signalling does NOT
         travel synchronously with the data: the valid/ready signalling
         combines in a *combinatorial* fashion.  Therefore, a long pipeline
         chain will lengthen propagation delays.
@@ -608,47 +874,214 @@ class UnbufferedPipeline(ControlBase):
         result: output_shape according to ospec
             The output of the combinatorial logic.  it is updated
             COMBINATORIALLY (no clock dependence).
-    """
 
-    def __init__(self, stage):
-        ControlBase.__init__(self)
-        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(i_data)
+        1 1 0 1   1    1 0    process(i_data)
+        1 1 1 0   0    1 1    process(i_data)
+        1 1 1 1   0    1 1    process(i_data)
+        -------   -    - -
+
+        Note: PoR is *NOT* involved in the above decision-making.
+    """
 
     def elaborate(self, platform):
-        m = Module()
+        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(m, r_data)
+        r_data = self.stage.ospec() # output type
 
+        # some temporaries
         p_i_valid = Signal(reset_less=True)
-        m.d.comb += p_i_valid.eq(self.p.i_valid_logic())
+        pv = Signal(reset_less=True)
+        buf_full = 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)
+        m.d.comb += buf_full.eq(~self.n.i_ready_test & data_valid)
+
         m.d.comb += self.n.o_valid.eq(data_valid)
-        m.d.comb += self.p.o_ready.eq(~data_valid | self.n.i_ready)
-        m.d.sync += data_valid.eq(p_i_valid | \
-                                        (~self.n.i_ready & data_valid))
-        with m.If(self.p.i_valid & self.p.o_ready):
-            m.d.sync += eq(r_data, self.p.i_data)
-        m.d.comb += eq(self.n.o_data, self.stage.process(r_data))
-        return m
+        m.d.comb += self.p._o_ready.eq(~data_valid | self.n.i_ready_test)
+        m.d.sync += data_valid.eq(p_i_valid | buf_full)
+
+        with m.If(pv):
+            m.d.sync += eq(r_data, self.stage.process(self.p.i_data))
+        o_data = self._postprocess(r_data)
+        m.d.comb += eq(self.n.o_data, o_data)
+
+        return self.m
+
+class UnbufferedPipeline2(ControlBase):
+    """ A simple pipeline stage with single-clock synchronisation
+        and two-way valid/ready synchronised signalling.
+
+        Note that a stall in one stage will result in the entire pipeline
+        chain stalling.
+
+        Also that unlike BufferedHandshake, the valid/ready signalling does NOT
+        travel synchronously with the data: the valid/ready signalling
+        combines in a *combinatorial* fashion.  Therefore, a long pipeline
+        chain will lengthen propagation delays.
+
+        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
+                              |             |    |
+                              +- process-> buf <-+
+        Attributes:
+        -----------
+        p.i_data : StageInput, shaped according to ispec
+            The pipeline input
+        p.o_data : 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(i_data)
+        0 0 0 1   1    1 0   reg (odata, unchanged)
+        0 0 1 0   0    0 1   process(i_data)
+        0 0 1 1   0    0 1   process(i_data)
+        -------   -    - -
+        0 1 0 0   0    0 1   process(i_data)
+        0 1 0 1   1    1 0   reg (odata, unchanged)
+        0 1 1 0   0    0 1   process(i_data)
+        0 1 1 1   0    0 1   process(i_data)
+        -------   -    - -
+        1 0 0 0   0    1 1   process(i_data)
+        1 0 0 1   1    1 0   reg (odata, unchanged)
+        1 0 1 0   0    1 1   process(i_data)
+        1 0 1 1   0    1 1   process(i_data)
+        -------   -    - -
+        1 1 0 0   0    1 1   process(i_data)
+        1 1 0 1   1    1 0   reg (odata, unchanged)
+        1 1 1 0   0    1 1   process(i_data)
+        1 1 1 1   0    1 1   process(i_data)
+        -------   -    - -
+
+        Note: PoR is *NOT* involved in the above decision-making.
+    """
+
+    def elaborate(self, platform):
+        self.m = m = ControlBase._elaborate(self, platform)
+
+        buf_full = Signal() # is data valid or not
+        buf = self.stage.ospec() # output type
+
+        # some temporaries
+        p_i_valid = Signal(reset_less=True)
+        m.d.comb += p_i_valid.eq(self.p.i_valid_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)
+
+        o_data = Mux(buf_full, buf, self.stage.process(self.p.i_data))
+        o_data = self._postprocess(o_data)
+        m.d.comb += eq(self.n.o_data, o_data)
+        m.d.sync += eq(buf, self.n.o_data)
+
+        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, iospec):
+    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)
+
+        r_data = self.stage.ospec() # output type
+
+        # temporaries
+        p_i_valid = 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 += 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)
+
+        odata = Mux(pvr, self.stage.process(self.p.i_data), r_data)
+        m.d.sync += eq(r_data, odata)
+        r_data = self._postprocess(r_data)
+        m.d.comb += eq(self.n.o_data, 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
@@ -657,3 +1090,107 @@ class RegisterPipeline(UnbufferedPipeline):
     def __init__(self, iospecfn):
         UnbufferedPipeline.__init__(self, PassThroughStage(iospecfn))
 
+
+class FIFOControl(ControlBase):
+    """ FIFO Control.  Uses SyncFIFO to store data, coincidentally
+        happens to have same valid/ready signalling as Stage API.
+
+        i_data -> fifo.din -> FIFO -> fifo.dout -> o_data
+    """
+
+    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)
+
+            NOTE 1: FPGAs may have trouble with the defaults for SyncFIFO
+                    (fwft=True, buffered=False)
+
+            NOTE 2: i_data *must* have a shape function.  it can therefore
+                    be a Signal, or a Record, or a RecordObject.
+
+            data is processed (and located) as follows:
+
+            self.p  self.stage temp    fn temp  fn  temp  fp   self.n
+            i_data->process()->result->flatten->din.FIFO.dout->flatten(o_data)
+
+            yes, really: flatten produces a Cat() which can be assigned to.
+            this is how the FIFO gets de-flattened without needing a de-flatten
+            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
+        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 o_data.
+        (fwidth, _) = self.n.o_data.shape()
+        if self.buffered:
+            fifo = SyncFIFOBuffered(fwidth, self.fdepth)
+        else:
+            fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe)
+        m.submodules.fifo = fifo
+
+        # store result of processing in combinatorial temporary
+        result = self.stage.ospec()
+        m.d.comb += eq(result, self.stage.process(self.p.i_data))
+
+        # connect previous rdy/valid/data - do flatten on i_data
+        # NOTE: cannot do the PrevControl-looking trick because
+        # of need to process the data.  shaaaame....
+        m.d.comb += [fifo.we.eq(self.p.i_valid_test),
+                     self.p.o_ready.eq(fifo.writable),
+                     eq(fifo.din, flatten(result)),
+                   ]
+
+        # connect next rdy/valid/data - do flatten on o_data
+        connections = [self.n.o_valid.eq(fifo.readable),
+                     fifo.re.eq(self.n.i_ready_test),
+                   ]
+        if self.fwft or self.buffered:
+            m.d.comb += connections
+        else:
+            m.d.sync += connections # unbuffered fwft mode needs sync
+        o_data = flatten(self.n.o_data).eq(fifo.dout)
+        o_data = self._postprocess(o_data)
+        m.d.comb += o_data
+
+        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)