refactor the buffered pipeline to a cleaner API with better separation
[ieee754fpu.git] / src / add / example_buf_pipe.py
index 6bd4d062e9fbddc32582792d970407fc5aa4d3b7..45bed19301a5c46b3213c8ae0217405182f6a215 100644 (file)
     where data will flow on *every* clock when the conditions are right.
 
     input acceptance conditions are when:
     where data will flow on *every* clock when the conditions are right.
 
     input acceptance conditions are when:
-        * incoming previous-stage strobe (i_p_stb) is HIGH
-        * outgoing previous-stage busy   (o_p_busy) is LOW
+        * incoming previous-stage strobe (i.p_valid) is HIGH
+        * outgoing previous-stage ready   (o.p_ready) is LOW
 
     output transmission conditions are when:
 
     output transmission conditions are when:
-        * outgoing next-stage strobe (o_n_stb) is HIGH
-        * outgoing next-stage busy   (i_n_busy) is LOW
+        * outgoing next-stage strobe (o.n_valid) is HIGH
+        * outgoing next-stage ready   (i.n_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
 
     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
@@ -33,7 +33,7 @@
     we now effectively have *two* possible pieces of data to "choose" from:
     the buffered data, and the incoming data.  the decision as to which
     to process and output is based on whether we are in "stall" or not.
     we now effectively have *two* possible pieces of data to "choose" from:
     the buffered data, and the incoming data.  the decision as to which
     to process and output is based on whether we are in "stall" or not.
-    i.e. when the next stage is no longer busy, the output comes from
+    i.e. when the next stage is no longer ready, the output comes from
     the buffer if a stall had previously occurred, otherwise it comes
     direct from processing the input.
 
     the buffer if a stall had previously occurred, otherwise it comes
     direct from processing the input.
 
 
 from nmigen import Signal, Cat, Const, Mux, Module
 from nmigen.cli import verilog, rtlil
 
 from nmigen import Signal, Cat, Const, Mux, Module
 from nmigen.cli import verilog, rtlil
+from collections.abc import Sequence
 
 
 
 
-class ExampleStage:
-    """ an example of how to use the buffered pipeline.  actual names of
-        variables (i_data, r_data, o_data, result) below do not matter:
-        the functions however do.
+class IOAckIn:
+
+    def __init__(self):
+        self.p_valid = Signal() # >>in - comes in from PREVIOUS stage
+        self.n_ready = Signal() # in<< - comes in from the NEXT stage
+
+
+class IOAckOut:
+
+    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
+
+
+def eq(o, i):
+    if not isinstance(o, Sequence):
+        o, i = [o], [i]
+    res = []
+    for (ao, ai) in zip(o, i):
+        res.append(ao.eq(ai))
+    return res
+
+
+class BufferedPipeline:
+    """ 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
+                              |             |
+                            process --->----^
+                              |             |
+                              +-- r_data ->-+
 
         input data i_data is read (only), is processed and goes into an
         intermediate result store [process()].  this is updated combinatorially.
 
         input data i_data is read (only), is processed and goes into an
         intermediate result store [process()].  this is updated combinatorially.
@@ -66,133 +98,170 @@ class ExampleStage:
         on the next cycle (as long as stall is not raised again) the
         input may begin to be processed and transferred directly to output.
     """
         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):
-        """ i_data can be a DIFFERENT type from everything else
-            o_data, r_data and result must be of the same type
-        """
-        self.i_data = Signal(16)
-        self.r_data = Signal(16)
-        self.o_data = Signal(16)
-        self.result = Signal(16)
-
-    def process(self):
-        """ process the input data and store it in result.
-            (not needed to be known: result is combinatorial)
+    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 -+
         """
         """
-        return self.result.eq(self.i_data + 1)
+        # 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
+
+        # 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):
 
     def update_buffer(self):
-        """ copies the result into the intermediate register r_data
+        """ 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 self.r_data.eq(self.result)
+        return eq(self.r_data, self.result)
 
     def update_output(self):
         """ copies the (combinatorial) result into the output
         """
 
     def update_output(self):
         """ copies the (combinatorial) result into the output
         """
-        return self.o_data.eq(self.result)
+        return eq(self.o.data, self.result)
 
     def flush_buffer(self):
         """ copies the *intermediate* register r_data into the output
         """
 
     def flush_buffer(self):
         """ copies the *intermediate* register r_data into the output
         """
-        return self.o_data.eq(self.r_data)
+        return eq(self.o.data, self.r_data)
 
     def ports(self):
 
     def ports(self):
-        return [self.i_data, self.o_data]
-
-
-class BufferedPipeline:
-    """ buffered pipeline stage
-
-        stage-1   i_p_stb  >>in   stage   o_n_stb  out>>   stage+1
-        stage-1   o_p_busy <<out  stage   i_n_busy <<in    stage+1
-        stage-1   i_data   >>in   stage   o_data   out>>   stage+1
-                              |             |
-                              +------->  process
-                              |             |
-                              +-- r_data ---+
-    """
-    def __init__(self):
-        # input: strobe comes in from previous stage, busy comes in from next
-        #self.i_p_rst = Signal()    # >>in - comes in from PREVIOUS stage
-        self.i_p_stb = Signal()    # >>in - comes in from PREVIOUS stage
-        self.i_n_busy = Signal()   # in<< - comes in from the NEXT stage
-
-        # output: strobe goes out to next stage, busy comes in from previous
-        self.o_n_stb = Signal()    # out>> - goes out to the NEXT stage
-        self.o_p_busy = Signal()   # <<out - goes out to the PREVIOUS stage
+        return [self.i.data, self.o.data]
 
     def elaborate(self, platform):
         m = Module()
 
         # establish some combinatorial temporaries
 
     def elaborate(self, platform):
         m = Module()
 
         # establish some combinatorial temporaries
-        o_p_busyn = Signal(reset_less=True)
-        o_n_stbn = Signal(reset_less=True)
-        i_n_busyn = Signal(reset_less=True)
-        i_p_stb_o_p_busyn = Signal(reset_less=True)
-        m.d.comb += [i_n_busyn.eq(~self.i_n_busy),
-                     o_n_stbn.eq(~self.o_n_stb),
-                     o_p_busyn.eq(~self.o_p_busy),
-                     i_p_stb_o_p_busyn.eq(self.i_p_stb & o_p_busyn),
+        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),
         ]
 
         # store result of processing in combinatorial temporary
         ]
 
         # store result of processing in combinatorial temporary
-        with m.If(self.i_p_stb): # input is valid: process it
-            m.d.comb += self.stage.process()
+        with m.If(self.i.p_valid): # input is valid: process it
+            m.d.comb += eq(self.result, self.stage.process(self.i.data))
         # if not in stall condition, update the temporary register
         # if not in stall condition, update the temporary register
-        with m.If(o_p_busyn): # not stalled
-            m.d.sync += self.stage.update_buffer()
-
-        #with m.If(self.i_p_rst): # reset
-        #    m.d.sync += self.o_n_stb.eq(0)
-        #    m.d.sync += self.o_p_busy.eq(0)
-        with m.If(i_n_busyn): # next stage is not busy
-            with m.If(o_p_busyn): # not stalled
+        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
                 # nothing in buffer: send (processed) input direct to output
                 # nothing in buffer: send (processed) input direct to output
-                m.d.sync += [self.o_n_stb.eq(self.i_p_stb),
-                             self.stage.update_output(),
+                m.d.sync += [self.o.n_valid.eq(self.i.p_valid),
+                             self.update_output(),
                             ]
                             ]
-            with m.Else(): # o_p_busy is true, and something is in our buffer.
+            with m.Else(): # o.p_ready is false, and something is in buffer.
                 # Flush the [already processed] buffer to the output port.
                 # Flush the [already processed] buffer to the output port.
-                m.d.sync += [self.o_n_stb.eq(1),
-                             self.stage.flush_buffer(),
+                m.d.sync += [self.o.n_valid.eq(1),
+                             self.flush_buffer(),
                              # clear stall condition, declare register empty.
                              # clear stall condition, declare register empty.
-                             self.o_p_busy.eq(0),
+                             self.o.p_ready.eq(1),
                             ]
                             ]
-                # ignore input, since o_p_busy is also true.
+                # ignore input, since o.p_ready is also false.
 
 
-        # (i_n_busy) is true here: next stage is busy
-        with m.Elif(o_n_stbn): # next stage being told "not busy"
-            m.d.sync += [self.o_n_stb.eq(self.i_p_stb),
-                         self.o_p_busy.eq(0), # Keep the buffer empty
+        # (i.n_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
                          # set the output data (from comb result)
                          # set the output data (from comb result)
-                         self.stage.update_output(),
+                         self.update_output(),
                         ]
                         ]
-        # (i_n_busy) and (o_n_stb) both true:
-        with m.Elif(i_p_stb_o_p_busyn):
-            # If next stage *is* busy, and not stalled yet, accept input
-            m.d.sync += self.o_p_busy.eq(self.i_p_stb & self.o_n_stb)
+        # (i.n_ready) false and (o.n_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))
 
         return m
 
     def ports(self):
 
         return m
 
     def ports(self):
-        return [self.i_p_stb, self.i_n_busy,
-                self.o_n_stb, self.o_p_busy,
+        return [self.i.p_valid, self.i.n_ready,
+                self.o.n_valid, self.o.p_ready,
                ]
 
 
                ]
 
 
-class BufPipe(BufferedPipeline, ExampleStage):
+class ExampleAddStage:
+    """ an example of how to use the buffered pipeline, as a class instance
+    """
+
+    def ispec(self):
+        """ returns a tuple of input signals which will be the incoming data
+        """
+        return (Signal(16), Signal(16))
+
+    def ospec(self):
+        """ returns an output signal which will happen to contain the sum
+            of the two inputs
+        """
+        return Signal(16)
+
+    def process(self, i):
+        """ process the input data (sums the values in the tuple) and returns it
+        """
+        return i[0] + i[1]
+
+
+class ExampleBufPipeAdd(BufferedPipeline):
+    """ an example of how to use the buffered pipeline, using a class instance
+    """
 
     def __init__(self):
 
     def __init__(self):
-        BufferedPipeline.__init__(self)
-        self.stage = ExampleStage()
+        addstage = ExampleAddStage()
+        BufferedPipeline.__init__(self, addstage)
 
 
-    def ports(self):
-        return self.stage.ports() + BufferedPipeline.ports(self)
+
+class ExampleStage:
+    """ an example of how to use the buffered pipeline, in a static class
+        fashion
+    """
+
+    def ispec():
+        return Signal(16)
+
+    def ospec():
+        return Signal(16)
+
+    def process(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.
+    """
+
+    def __init__(self):
+        BufferedPipeline.__init__(self, ExampleStage)
 
 
 if __name__ == '__main__':
 
 
 if __name__ == '__main__':
-    dut = BufPipe()
+    dut = ExampleBufPipe()
     vl = rtlil.convert(dut, ports=dut.ports())
     with open("test_bufpipe.il", "w") as f:
         f.write(vl)
     vl = rtlil.convert(dut, ports=dut.ports())
     with open("test_bufpipe.il", "w") as f:
         f.write(vl)