update comments
[ieee754fpu.git] / src / add / singlepipe.py
index 5be2d919d5a7fed75a29d4900141f978c46e4942..68b62e432d4fc6022b99361c3358d01638414fce 100644 (file)
@@ -1,11 +1,10 @@
-""" Pipeline and BufferedHandshake implementation, conforming to the same API.
-    For multi-input and multi-output variants, see multipipe.
+""" 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 (iocontrol.py) in combination with below
+    Important: see Stage API (stageapi.py) in combination with below
 
     RecordBasedStage:
     ----------------
     StageChain, however when passed to UnbufferedPipeline they
     can be used to introduce a single clock delay.
 
+    ControlBase:
+    -----------
+
+    The base class for pipelines.  Contains previous and next ready/valid/data.
+    Also has an extremely useful "connect" function that can be used to
+    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:
     ------------------
 
     https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v
 """
 
-from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable
+from nmigen import Signal, Mux, Module, Elaboratable
 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 nmigen.hdl.rec import Record
 
-from abc import ABCMeta, abstractmethod
-from collections.abc import Sequence, Iterable
-from collections import OrderedDict
 from queue import Queue
 import inspect
 
+from iocontrol import (PrevControl, NextControl, Object, RecordObject)
+from stageapi import (_spec, StageCls, Stage, StageChain, StageHelper)
 import nmoperator
-from iocontrol import (Object, RecordObject, _spec,
-                       PrevControl, NextControl, StageCls, Stage,
-                       ControlBase, StageChain)
                       
 
 class RecordBasedStage(Stage):
@@ -151,6 +156,170 @@ class RecordBasedStage(Stage):
     def setup(seif, m, i): return self.__setup(m, i)
 
 
+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).
+
+        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, iospecfn): self.iospecfn = iospecfn
+    def ispec(self): return self.iospecfn()
+    def ospec(self): return self.iospecfn()
+
+
+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)
+
+        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
+
+            * 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 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.
+        """
+        return self.n.connect_to_next(nxt.p)
+
+    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.p._connect_in(prev.p)
+
+    def _connect_out(self, nxt):
+        """ internal helper function to connect stage to an output source.
+            do not use to connect stage-to-stage!
+        """
+        return self.n._connect_out(nxt.n)
+
+    def connect(self, pipechain):
+        """ connects a chain (list) of Pipeline instances together and
+            links them to this ControlBase instance:
+
+                      in <----> self <---> out
+                       |                   ^
+                       v                   |
+                    [pipe1, pipe2, pipe3, pipe4]
+                       |    ^  |    ^  |     ^
+                       v    |  v    |  v     |
+                     out---in out--in out---in
+
+            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.data_i or self.n.data_o manually:
+            this is handled AUTOMATICALLY, here.
+
+            Basically this function is the direct equivalent of StageChain,
+            except that unlike StageChain, the Pipeline logic is followed.
+
+            Just as StageChain presents an object that conforms to the
+            Stage API from a list of objects that also conform to the
+            Stage API, an object that calls this Pipeline connect function
+            has the exact same pipeline API as the list of pipline objects
+            it is called with.
+
+            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]                # 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 (used in unit tests)
+        """
+        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):
+        return list(self)
+
+    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_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.ready_i)
+        m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
+
+        return m
+
+
 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
@@ -184,8 +353,8 @@ class BufferedHandshake(ControlBase):
     def elaborate(self, platform):
         self.m = ControlBase.elaborate(self, platform)
 
-        result = _spec(self.sh.ospec, "r_tmp")
-        r_data = _spec(self.sh.ospec, "r_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)
@@ -280,7 +449,7 @@ class SimpleHandshake(ControlBase):
         self.m = m = ControlBase.elaborate(self, platform)
 
         r_busy = Signal()
-        result = _spec(self.sh.ospec, "r_tmp")
+        result = _spec(self.stage.ospec, "r_tmp")
 
         # establish some combinatorial temporaries
         n_ready_i = Signal(reset_less=True, name="n_i_rdy_data")
@@ -388,7 +557,7 @@ class UnbufferedPipeline(ControlBase):
         self.m = m = ControlBase.elaborate(self, platform)
 
         data_valid = Signal() # is data valid or not
-        r_data = _spec(self.sh.ospec, "r_tmp") # output type
+        r_data = _spec(self.stage.ospec, "r_tmp") # output type
 
         # some temporaries
         p_valid_i = Signal(reset_less=True)
@@ -474,7 +643,7 @@ class UnbufferedPipeline2(ControlBase):
         self.m = m = ControlBase.elaborate(self, platform)
 
         buf_full = Signal() # is data valid or not
-        buf = _spec(self.sh.ospec, "r_tmp") # output type
+        buf = _spec(self.stage.ospec, "r_tmp") # output type
 
         # some temporaries
         p_valid_i = Signal(reset_less=True)
@@ -492,21 +661,6 @@ class UnbufferedPipeline2(ControlBase):
         return self.m
 
 
-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).
-
-        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, 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.
 
@@ -543,7 +697,7 @@ class PassThroughHandshake(ControlBase):
     def elaborate(self, platform):
         self.m = m = ControlBase.elaborate(self, platform)
 
-        r_data = _spec(self.sh.ospec, "r_tmp") # output type
+        r_data = _spec(self.stage.ospec, "r_tmp") # output type
 
         # temporaries
         p_valid_i = Signal(reset_less=True)
@@ -572,23 +726,29 @@ class RegisterPipeline(UnbufferedPipeline):
 
 
 class FIFOControl(ControlBase):
-    """ FIFO Control.  Uses SyncFIFO to store data, coincidentally
+    """ 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, buffered=False, pipe=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)
-            * :buffered: use buffered FIFO (introduces extra cycle delay)
+            * :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.
 
-            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.
+            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:
 
@@ -599,12 +759,7 @@ class FIFOControl(ControlBase):
             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
         ControlBase.__init__(self, stage, in_multi, stage_ctl)
@@ -614,33 +769,32 @@ class FIFOControl(ControlBase):
 
         # 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)
+        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.sh.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
+        def processfn(data_i):
+            # store result of processing in combinatorial temporary
+            result = _spec(self.stage.ospec, "r_temp")
+            m.d.comb += nmoperator.eq(result, self.process(data_i))
+            return nmoperator.cat(result)
+
+        ## prev: make the FIFO (Queue object) "look" like a PrevControl...
+        m.submodules.fp = fp = PrevControl()
+        fp.valid_i, fp._ready_o, fp.data_i = fifo.we, fifo.writable, fifo.din
+        m.d.comb += fp._connect_in(self.p, fn=processfn)
+
+        # next: make the FIFO (Queue object) "look" like a NextControl...
+        m.submodules.fn = fn = NextControl()
+        fn.valid_o, fn.ready_i, fn.data_o  = fifo.readable, fifo.re, fifo.dout
+        connections = fn._connect_out(self.n, fn=nmoperator.cat)
+
+        # ok ok so we can't just do the ready/valid eqs straight:
+        # first 2 from connections are the ready/valid, 3rd is data.
+        if self.fwft:
+            m.d.comb += connections[:2] # 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)
+            m.d.sync += connections[:2]  # non-fwft mode needs sync
+        data_o = connections[2] # get the data
         data_o = self._postprocess(data_o) # XXX TBD, does nothing right now
         m.d.comb += data_o