remove unneeded functions, make result and r_data local to elaborate()
[ieee754fpu.git] / src / add / example_buf_pipe.py
index 5faee7876c83e602fecb47e6c9ba366a7ec7aa90..f760ce3948e0e96f3b7d18d18636f50d0f93c2c7 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_stb) is HIGH
-        * outgoing previous-stage busy   (o_p_busy) 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_stb) is HIGH
-        * outgoing next-stage busy   (i_n_busy) 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
     not ready".  unfortunately, it's not possible to "change the past":
     the previous stage *has no choice* but to pass on its data.
 
-    therefore, the incoming data *must* be accepted - and stored.
+    therefore, the incoming data *must* be accepted - and stored: that
+    is the responsibility / contract that this stage *must* accept.
     on the same clock, it's possible to tell the input that it must
     not send any more data.  this is the "stall" condition.
 
     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.
 
+    this allows us to respect a synchronous "travelling STB" with what
+    dan calls a "buffered handshake".
+
     it's quite a complex state machine!
 """
 
 from nmigen import Signal, Cat, Const, Mux, Module
 from nmigen.cli import verilog, rtlil
+from collections.abc import Sequence
 
-class BufPipe:
-    """ 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 ---+
+class PrevControl:
+    """ contains signals that come *from* the previous stage (both in and out)
+        * i_valid: input from previous stage indicating incoming data is 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.i_valid = Signal(name="p_i_valid") # >>in
+        self.o_ready = Signal(name="p_o_ready") # <<out
+
+    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),
+               ]
+
+
+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):
-        # input
-        #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
-        self.i_data = Signal(16) # >>in - comes in from the PREVIOUS stage
-        #self.i_rst = Signal()
+        self.o_valid = Signal(name="n_o_valid") # out>>
+        self.i_ready = Signal(name="n_i_ready") # <<in
+
+    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
+        passsed a list (or tuple) of objects, and calls the objects' eq
+        function.
+    """
+    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 PipelineBase:
+    """ Common functions for Pipeline API
+    """
+    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
 
-        # buffered
-        self.r_data = Signal(16)
+            User must also:
+            * add i_data member to PrevControl and
+            * add o_data member to NextControl
+        """
+        self.stage = stage
 
-        # output
-        self.o_n_stb = Signal()    # out>> - goes out to the NEXT stage
-        self.o_p_busy = Signal()   # <<out - goes out to the PREVIOUS stage
-        self.o_data = Signal(16) # out>> - goes out to the NEXT stage
+        # set up input and output IO ACK (prev/next ready/valid)
+        self.p = PrevControl()
+        self.n = NextControl()
 
-    def pre_process(self, d_in):
-        return d_in | 0xf0000
+    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 process(self, d_in):
-        return d_in + 1
+    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
+               ]
+
+
+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   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 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
+        output (update_output).  however if ever there is a stall, it goes
+        into r_data instead [update_buffer()].
+
+        when the non-stall condition is released, r_data is the first
+        to be transferred to the output [flush_buffer()], and the stall
+        condition cleared.
+
+        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):
+        PipelineBase.__init__(self, 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()
 
+        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_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.n.o_valid),
+                     i_p_valid_o_p_ready.eq(self.p.i_valid & self.p.o_ready),
         ]
 
         # store result of processing in combinatorial temporary
-        result = Signal(16)
-        with m.If(self.i_p_stb): # input is valid: process it
-            m.d.comb += result.eq(self.process(self.i_data))
-        with m.If(o_p_busyn): # not stalled
-            m.d.sync += self.r_data.eq(result)
-
-        #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
-                # nothing in buffer: send input direct to output
-                m.d.sync += [self.o_n_stb.eq(self.i_p_stb),
-                             self.o_data.eq(result),
+        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.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.n.o_valid.eq(self.p.i_valid),
+                             eq(self.n.o_data, result), # update output
                             ]
-            with m.Else(): # o_p_busy is true, and something is in our 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_stb.eq(1),
-                             self.o_data.eq(self.r_data),
+                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_busy.eq(0),
+                             self.p.o_ready.eq(1),
                             ]
-                # ignore input, since o_p_busy is also true.
+                # ignore input, since p.o_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
+        # (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(self.p.i_valid),
+                         self.p.o_ready.eq(1), # Keep the buffer empty
                          # set the output data (from comb result)
-                         self.o_data.eq(result),
+                         eq(self.n.o_data, result),
                         ]
-        # (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)
+        # (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(~(self.p.i_valid & self.n.o_valid))
+
+        return m
 
-        with m.If(o_p_busyn): # not stalled
-            # turns out that from all of the above conditions, just
-            # always put result into buffer if not busy
-            m.d.sync += self.r_data.eq(result)
 
+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):
+        addstage = ExampleAddStage()
+        BufferedPipeline.__init__(self, addstage)
+
+
+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)
+
+
+class CombPipe(PipelineBase):
+    """A simple pipeline stage containing combinational logic that can execute
+    completely in one clock cycle.
+
+    Parameters:
+    -----------
+    input_shape : int or tuple or None
+        the shape of ``input.data`` and ``comb_input``
+    output_shape : int or tuple or None
+        the shape of ``output.data`` and ``comb_output``
+    name : str
+        the name
+
+    Attributes:
+    -----------
+    input : StageInput
+        The pipeline input
+    output : StageOutput
+        The pipeline output
+    comb_input : Signal, input_shape
+        The input to the combinatorial logic
+    comb_output: 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)
+
+        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(self.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
 
-    def ports(self):
-        return [self.i_p_stb, self.i_n_busy, self.i_data,
-                self.r_data,
-                self.o_n_stb, self.o_p_busy, self.o_data
-               ]
+
+class ExampleCombPipe(CombPipe):
+    """ an example of how to use the combinatorial pipeline.
+    """
+
+    def __init__(self):
+        CombPipe.__init__(self, ExampleStage)
 
 
 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)
 
+    dut = ExampleCombPipe()
+    vl = rtlil.convert(dut, ports=dut.ports())
+    with open("test_combpipe.il", "w") as f:
+        f.write(vl)