create example pipeline buffer "StageChain" example,
[ieee754fpu.git] / src / add / example_buf_pipe.py
index 45bed19301a5c46b3213c8ae0217405182f6a215..b83d5035cc01c7ec338c1589359323b213e3099f 100644 (file)
     where data will flow on *every* clock when the conditions are right.
 
     input acceptance conditions are when:
-        * incoming previous-stage strobe (i.p_valid) is HIGH
-        * outgoing previous-stage ready   (o.p_ready) is LOW
+        * incoming previous-stage strobe (p.i_valid) is HIGH
+        * outgoing previous-stage ready   (p.o_ready) is LOW
 
     output transmission conditions are when:
-        * outgoing next-stage strobe (o.n_valid) is HIGH
-        * outgoing next-stage ready   (i.n_ready) is LOW
+        * outgoing next-stage strobe (n.o_valid) is HIGH
+        * outgoing next-stage ready   (n.i_ready) 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
 
 from nmigen import Signal, Cat, Const, Mux, Module
 from nmigen.cli import verilog, rtlil
+from nmigen.hdl.rec import Record, Layout
+
 from collections.abc import Sequence
 
 
-class IOAckIn:
+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):
-        self.p_valid = Signal() # >>in - comes in from PREVIOUS stage
-        self.n_ready = Signal() # in<< - comes in from the NEXT stage
+    def __init__(self, i_width=1):
+        self.i_valid = Signal(i_width, name="p_i_valid") # prev   >>in  self
+        self.o_ready = Signal(name="p_o_ready") # prev   <<out self
 
+    def connect_in(self, prev):
+        """ 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),
+                prev.o_ready.eq(self.o_ready),
+                eq(self.i_data, prev.i_data),
+               ]
+
+    def i_valid_logic(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))
+            return self.i_valid == all1s
+        # single-bit i_valid case
+        return self.i_valid
 
-class IOAckOut:
 
+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):
-        self.n_valid = Signal() # out>> - goes out to the NEXT stage
-        self.p_ready = Signal() # <<out - goes out to the PREVIOUS stage
+        self.o_valid = Signal(name="n_o_valid") # self out>>  next
+        self.i_ready = Signal(name="n_i_ready") # self <<in   next
+
+    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.
+        """
+        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):
+        """ helper function to connect stage to an output source.  do not
+            use to connect stage-to-stage!
+        """
+        return [nxt.o_valid.eq(self.o_valid),
+                self.i_ready.eq(nxt.i_ready),
+                eq(nxt.o_data, self.o_data),
+               ]
 
 
 def eq(o, i):
+    """ makes signals equal: a helper routine which identifies if it is being
+        passed a list (or tuple) of objects, or signals, or Records, and calls
+        the objects' eq function.
+
+        complex objects (classes) can be used: they must follow the
+        convention of having an eq member function, which takes the
+        responsibility of further calling eq and returning a list of
+        eq assignments
+
+        Record is a special (unusual, recursive) case, where the input
+        is specified as a dictionary (which may contain further dictionaries,
+        recursively), where the field names of the dictionary must match
+        the Record's field spec.
+    """
     if not isinstance(o, Sequence):
         o, i = [o], [i]
     res = []
     for (ao, ai) in zip(o, i):
-        res.append(ao.eq(ai))
+        #print ("eq", ao, ai)
+        if isinstance(ao, Record):
+            for idx, (field_name, field_shape, _) in enumerate(ao.layout):
+                if isinstance(field_shape, Layout):
+                    rres = eq(ao.fields[field_name], ai.fields[field_name])
+                else:
+                    rres = eq(ao.fields[field_name], ai[field_name])
+                res += rres
+        else:
+            rres = ao.eq(ai)
+            if not isinstance(rres, Sequence):
+                rres = [rres]
+            res += rres
     return res
 
 
-class BufferedPipeline:
+class StageChain:
+    """ pass in a list of stages, and they will automatically be
+        chained together via their input and output specs into a
+        combinatorial chain.
+
+        * 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
+    """
+    def __init__(self, chain):
+        self.chain = chain
+
+    def ispec(self):
+        return self.chain[0].ispec()
+
+    def ospec(self):
+        return self.chain[-1].ospec()
+
+    def 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
+            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
+
+    def process(self, i):
+        return self.o
+
+
+class PipelineBase:
+    """ Common functions for Pipeline API
+    """
+    def __init__(self, stage, in_multi=None):
+        """ pass in a "stage" which may be either a static class or a class
+            instance, which has four functions (one optional):
+            * ispec: returns input signals according to the input specification
+            * ispec: returns output signals to the output specification
+            * process: takes an input instance and returns processed data
+            * setup: performs any module linkage if the stage uses one.
+
+            User must also:
+            * add i_data member to PrevControl and
+            * add o_data member to NextControl
+        """
+        self.stage = stage
+
+        # set up input and output IO ACK (prev/next ready/valid)
+        self.p = PrevControl(in_multi)
+        self.n = NextControl()
+
+    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):
+        """ 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):
+        """ 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 set_input(self, i):
+        """ helper function to set the input data
+        """
+        return eq(self.p.i_data, i)
+
+    def ports(self):
+        return [self.p.i_valid, self.n.i_ready,
+                self.n.o_valid, self.p.o_ready,
+                self.p.i_data, self.n.o_data   # XXX need flattening!
+               ]
+
+
+class BufferedPipeline(PipelineBase):
     """ buffered pipeline stage.  data and strobe signals travel in sync.
         if ever the input is ready and the output is not, processed data
         is stored in a temporary register.
 
-        stage-1   i.p_valid >>in   stage   o.n_valid out>>   stage+1
-        stage-1   o.p_ready <<out  stage   i.n_ready <<in    stage+1
-        stage-1   i.data    >>in   stage   o.data    out>>   stage+1
+        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 --->----^
                               |             |
                               +-- r_data ->-+
 
-        input data i_data is read (only), is processed and goes into an
+        input data p.i_data 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
@@ -99,111 +258,68 @@ class BufferedPipeline:
         input may begin to be processed and transferred directly to output.
     """
     def __init__(self, stage):
-        """ pass in a "stage" which may be either a static class or a class
-            instance, which has three functions:
-            * ispec: returns input signals according to the input specification
-            * ispec: returns output signals to the output specification
-            * process: takes an input instance and returns processed data
-
-            i_data -> process() -> result --> o.data
-                                     |           ^
-                                     |           |
-                                     +-> r_data -+
-        """
-        # input: strobe comes in from previous stage, ready comes in from next
-        self.i = IOAckIn()
-        #self.i.p_valid = Signal()    # >>in - comes in from PREVIOUS stage
-        #self.i.n_ready = Signal()   # in<< - comes in from the NEXT stage
-
-        # output: strobe goes out to next stage, ready comes in from previous
-        self.o = IOAckOut()
-        #self.o.n_valid = Signal()    # out>> - goes out to the NEXT stage
-        #self.o.p_ready = Signal()   # <<out - goes out to the PREVIOUS stage
+        PipelineBase.__init__(self, stage)
 
         # set up the input and output data
-        self.i.data = stage.ispec() # input type
-        self.r_data = stage.ospec() # all these are output type
-        self.result = stage.ospec()
-        self.o.data = stage.ospec()
-        self.stage = stage
-
-    def set_input(self, i):
-        return eq(self.i.data, i)
-
-    def update_buffer(self):
-        """ copies the result into the intermediate register r_data,
-            which will need to be outputted on a subsequent cycle
-            prior to allowing "normal" operation.
-        """
-        return eq(self.r_data, self.result)
-
-    def update_output(self):
-        """ copies the (combinatorial) result into the output
-        """
-        return eq(self.o.data, self.result)
-
-    def flush_buffer(self):
-        """ copies the *intermediate* register r_data into the output
-        """
-        return eq(self.o.data, self.r_data)
-
-    def ports(self):
-        return [self.i.data, self.o.data]
+        self.p.i_data = stage.ispec() # input type
+        self.n.o_data = stage.ospec()
 
     def elaborate(self, platform):
         m = Module()
 
+        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)
-        m.d.comb += [o_n_validn.eq(~self.o.n_valid),
-                     i_p_valid_o_p_ready.eq(self.i.p_valid & self.o.p_ready),
+        p_i_valid = Signal(reset_less=True)
+        m.d.comb += [p_i_valid.eq(self.p.i_valid_logic()),
+                     o_n_validn.eq(~self.n.o_valid),
+                     i_p_valid_o_p_ready.eq(p_i_valid & self.p.o_ready),
         ]
 
         # store result of processing in combinatorial temporary
-        with m.If(self.i.p_valid): # input is valid: process it
-            m.d.comb += eq(self.result, self.stage.process(self.i.data))
+        #with m.If(self.p.i_valid): # input is valid: process it
+        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.o.p_ready): # not stalled
-            m.d.sync += self.update_buffer()
-
-        #with m.If(self.i.p_rst): # reset
-        #    m.d.sync += self.o.n_valid.eq(0)
-        #    m.d.sync += self.o.p_ready.eq(0)
-        with m.If(self.i.n_ready): # next stage is ready
-            with m.If(self.o.p_ready): # not stalled
+        with m.If(self.p.o_ready): # not stalled
+            m.d.sync += eq(r_data, result) # update buffer
+
+        #with m.If(self.p.i_rst): # reset
+        #    m.d.sync += self.n.o_valid.eq(0)
+        #    m.d.sync += self.p.o_ready.eq(0)
+        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.o.n_valid.eq(self.i.p_valid),
-                             self.update_output(),
+                m.d.sync += [self.n.o_valid.eq(p_i_valid),
+                             eq(self.n.o_data, result), # update output
                             ]
-            with m.Else(): # o.p_ready is false, and something is in buffer.
+            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.o.n_valid.eq(1),
-                             self.flush_buffer(),
+                m.d.sync += [self.n.o_valid.eq(1),
+                             eq(self.n.o_data, r_data), # flush buffer
                              # clear stall condition, declare register empty.
-                             self.o.p_ready.eq(1),
+                             self.p.o_ready.eq(1),
                             ]
-                # ignore input, since o.p_ready is also false.
+                # ignore input, since p.o_ready is also false.
 
-        # (i.n_ready) is false here: next stage is ready
+        # (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.o.n_valid.eq(self.i.p_valid),
-                         self.o.p_ready.eq(1), # Keep the buffer empty
+            m.d.sync += [self.n.o_valid.eq(p_i_valid),
+                         self.p.o_ready.eq(1), # Keep the buffer empty
                          # set the output data (from comb result)
-                         self.update_output(),
+                         eq(self.n.o_data, result),
                         ]
-        # (i.n_ready) false and (o.n_valid) true:
+        # (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.o.p_ready.eq(~(self.i.p_valid & self.o.n_valid))
+            m.d.sync += self.p.o_ready.eq(~(p_i_valid & self.n.o_valid))
 
         return m
 
-    def ports(self):
-        return [self.i.p_valid, self.i.n_ready,
-                self.o.n_valid, self.o.p_ready,
-               ]
-
 
 class ExampleAddStage:
     """ an example of how to use the buffered pipeline, as a class instance
@@ -241,10 +357,10 @@ class ExampleStage:
     """
 
     def ispec():
-        return Signal(16)
+        return Signal(16, name="example_input_signal")
 
     def ospec():
-        return Signal(16)
+        return Signal(16, name="example_output_signal")
 
     def process(i):
         """ process the input data and returns it (adds 1)
@@ -252,6 +368,23 @@ class ExampleStage:
         return i + 1
 
 
+class ExampleStageCls:
+    """ an example of how to use the buffered pipeline, in a static class
+        fashion
+    """
+
+    def ispec(self):
+        return Signal(16, name="example_input_signal")
+
+    def ospec(self):
+        return Signal(16, name="example_output_signal")
+
+    def process(self, i):
+        """ process the input data and returns it (adds 1)
+        """
+        return i + 1
+
+
 class ExampleBufPipe(BufferedPipeline):
     """ an example of how to use the buffered pipeline.
     """
@@ -260,8 +393,66 @@ class ExampleBufPipe(BufferedPipeline):
         BufferedPipeline.__init__(self, ExampleStage)
 
 
+class CombPipe(PipelineBase):
+    """A simple pipeline stage containing combinational logic that can execute
+    completely in one clock cycle.
+
+    Attributes:
+    -----------
+    input : StageInput
+        The pipeline input
+    output : StageOutput
+        The pipeline output
+    r_data : Signal, input_shape
+        A temporary (buffered) copy of a prior (valid) input
+    result: Signal, output_shape
+        The output of the combinatorial logic
+    """
+
+    def __init__(self, stage):
+        PipelineBase.__init__(self, stage)
+        self._data_valid = Signal()
+
+        # set up the input and output data
+        self.p.i_data = stage.ispec() # input type
+        self.n.o_data = stage.ospec() # output type
+
+    def elaborate(self, platform):
+        m = Module()
+
+        r_data = self.stage.ispec() # input type
+        result = self.stage.ospec() # output data
+        if hasattr(self.stage, "setup"):
+            self.stage.setup(m, r_data)
+
+        p_i_valid = Signal(reset_less=True)
+        m.d.comb += p_i_valid.eq(self.p.i_valid_logic())
+        m.d.comb += eq(result, self.stage.process(r_data))
+        m.d.comb += self.n.o_valid.eq(self._data_valid)
+        m.d.comb += self.p.o_ready.eq(~self._data_valid | self.n.i_ready)
+        m.d.sync += self._data_valid.eq(p_i_valid | \
+                                        (~self.n.i_ready & self._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, result)
+        return m
+
+
+class ExampleCombPipe(CombPipe):
+    """ an example of how to use the combinatorial pipeline.
+    """
+
+    def __init__(self):
+        CombPipe.__init__(self, ExampleStage)
+
+
 if __name__ == '__main__':
     dut = ExampleBufPipe()
     vl = rtlil.convert(dut, ports=dut.ports())
     with open("test_bufpipe.il", "w") as f:
         f.write(vl)
+
+    dut = ExampleCombPipe()
+    vl = rtlil.convert(dut, ports=dut.ports())
+    with open("test_combpipe.il", "w") as f:
+        f.write(vl)