refactor the buffered pipeline to a cleaner API with better separation
authorLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Mon, 18 Mar 2019 11:31:40 +0000 (11:31 +0000)
committerLuke Kenneth Casson Leighton <lkcl@lkcl.net>
Mon, 18 Mar 2019 11:31:40 +0000 (11:31 +0000)
src/add/example_buf_pipe.py
src/add/test_buf_pipe.py

index 00eecc3ecd37db7b603c1dd4f50ce84e1b98ca52..45bed19301a5c46b3213c8ae0217405182f6a215 100644 (file)
 
 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,87 +98,57 @@ 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 are best of the same type.
-            however this is not strictly necessary.  an intermediate
-            transformation process could hypothetically be applied, however
-            it is result and r_data that definitively need to be of the same
-            (intermediary) type, as it is both result and r_data that
-            are transferred into o_data:
-
-            i_data -> process() -> result --> o_data
+    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 -+
         """
                                      |           ^
                                      |           |
                                      +-> r_data -+
         """
-        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)
-        """
-        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):
         """ copies the result into the intermediate register r_data,
             which will need to be outputted on a subsequent cycle
             prior to allowing "normal" operation.
         """
 
     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 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 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
-
-
-class BufferedPipeline:
-    """ buffered pipeline stage
-
-        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 ---+
-    """
-    def __init__(self):
-        # 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
+        return [self.i.data, self.o.data]
 
     def elaborate(self, platform):
         m = Module()
 
     def elaborate(self, platform):
         m = Module()
@@ -160,10 +162,10 @@ class BufferedPipeline:
 
         # store result of processing in combinatorial temporary
         with m.If(self.i.p_valid): # input is valid: process it
 
         # store result of processing in combinatorial temporary
         with m.If(self.i.p_valid): # input is valid: process it
-            m.d.comb += self.stage.process()
+            m.d.comb += eq(self.result, self.stage.process(self.i.data))
         # if not in stall condition, update the temporary register
         with m.If(self.o.p_ready): # not stalled
         # if not in stall condition, update the temporary register
         with m.If(self.o.p_ready): # not stalled
-            m.d.sync += self.stage.update_buffer()
+            m.d.sync += self.update_buffer()
 
         #with m.If(self.i.p_rst): # reset
         #    m.d.sync += self.o.n_valid.eq(0)
 
         #with m.If(self.i.p_rst): # reset
         #    m.d.sync += self.o.n_valid.eq(0)
@@ -172,12 +174,12 @@ class BufferedPipeline:
             with m.If(self.o.p_ready): # not stalled
                 # nothing in buffer: send (processed) input direct to output
                 m.d.sync += [self.o.n_valid.eq(self.i.p_valid),
             with m.If(self.o.p_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.stage.update_output(),
+                             self.update_output(),
                             ]
             with m.Else(): # o.p_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),
                             ]
             with m.Else(): # o.p_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.stage.flush_buffer(),
+                             self.flush_buffer(),
                              # clear stall condition, declare register empty.
                              self.o.p_ready.eq(1),
                             ]
                              # clear stall condition, declare register empty.
                              self.o.p_ready.eq(1),
                             ]
@@ -188,7 +190,7 @@ class BufferedPipeline:
             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)
             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)
-                         self.stage.update_output(),
+                         self.update_output(),
                         ]
         # (i.n_ready) false and (o.n_valid) true:
         with m.Elif(i_p_valid_o_p_ready):
                         ]
         # (i.n_ready) false and (o.n_valid) true:
         with m.Elif(i_p_valid_o_p_ready):
@@ -203,18 +205,63 @@ class BufferedPipeline:
                ]
 
 
                ]
 
 
-class ExampleBufPipe(BufferedPipeline):
+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)
index fa23eac0a79b7ae205d56b624db3d562682bc5e3..0b23d63732ad301233c5962421c5bf801b2dcbf2 100644 (file)
@@ -1,6 +1,6 @@
 from nmigen import Module, Signal
 from nmigen.compat.sim import run_simulation
 from nmigen import Module, Signal
 from nmigen.compat.sim import run_simulation
-from example_buf_pipe import ExampleBufPipe
+from example_buf_pipe import ExampleBufPipe, ExampleBufPipeAdd
 from random import randint
 
 
 from random import randint
 
 
@@ -21,24 +21,24 @@ def testbench(dut):
     yield
     #yield dut.i_p_rst.eq(0)
     yield dut.i.n_ready.eq(1)
     yield
     #yield dut.i_p_rst.eq(0)
     yield dut.i.n_ready.eq(1)
-    yield dut.stage.i_data.eq(5)
+    yield dut.i.data.eq(5)
     yield dut.i.p_valid.eq(1)
     yield
 
     yield dut.i.p_valid.eq(1)
     yield
 
-    yield dut.stage.i_data.eq(7)
+    yield dut.i.data.eq(7)
     yield from check_o_n_valid(dut, 0) # effects of i_p_valid delayed
     yield
     yield from check_o_n_valid(dut, 1) # ok *now* i_p_valid effect is felt
 
     yield from check_o_n_valid(dut, 0) # effects of i_p_valid delayed
     yield
     yield from check_o_n_valid(dut, 1) # ok *now* i_p_valid effect is felt
 
-    yield dut.stage.i_data.eq(2)
+    yield dut.i.data.eq(2)
     yield
     yield dut.i.n_ready.eq(0) # begin going into "stall" (next stage says ready)
     yield
     yield dut.i.n_ready.eq(0) # begin going into "stall" (next stage says ready)
-    yield dut.stage.i_data.eq(9)
+    yield dut.i.data.eq(9)
     yield
     yield dut.i.p_valid.eq(0)
     yield
     yield dut.i.p_valid.eq(0)
-    yield dut.stage.i_data.eq(12)
+    yield dut.i.data.eq(12)
     yield
     yield
-    yield dut.stage.i_data.eq(32)
+    yield dut.i.data.eq(32)
     yield dut.i.n_ready.eq(1)
     yield
     yield from check_o_n_valid(dut, 1) # buffer still needs to output
     yield dut.i.n_ready.eq(1)
     yield
     yield from check_o_n_valid(dut, 1) # buffer still needs to output
@@ -94,7 +94,7 @@ class Test3:
     def __init__(self, dut):
         self.dut = dut
         self.data = []
     def __init__(self, dut):
         self.dut = dut
         self.data = []
-        for i in range(10000):
+        for i in range(num_tests):
             #data.append(randint(0, 1<<16-1))
             self.data.append(i+1)
         self.i = 0
             #data.append(randint(0, 1<<16-1))
             self.data.append(i+1)
         self.i = 0
@@ -114,7 +114,7 @@ class Test3:
                     continue
                 if send and self.i != len(self.data):
                     yield self.dut.i.p_valid.eq(1)
                     continue
                 if send and self.i != len(self.data):
                     yield self.dut.i.p_valid.eq(1)
-                    yield self.dut.stage.i_data.eq(self.data[self.i])
+                    yield self.dut.i.data.eq(self.data[self.i])
                     self.i += 1
                 else:
                     yield self.dut.i.p_valid.eq(0)
                     self.i += 1
                 else:
                     yield self.dut.i.p_valid.eq(0)
@@ -131,7 +131,7 @@ class Test3:
                 i_n_ready = yield self.dut.i.n_ready
                 if not o_n_valid or not i_n_ready:
                     continue
                 i_n_ready = yield self.dut.i.n_ready
                 if not o_n_valid or not i_n_ready:
                     continue
-                o_data = yield self.dut.stage.o_data
+                o_data = yield self.dut.o.data
                 assert o_data == self.data[self.o] + 1, \
                             "%d-%d data %x not match %x\n" \
                             % (self.i, self.o, o_data, self.data[self.o])
                 assert o_data == self.data[self.o] + 1, \
                             "%d-%d data %x not match %x\n" \
                             % (self.i, self.o, o_data, self.data[self.o])
@@ -140,9 +140,60 @@ class Test3:
                     break
 
 
                     break
 
 
+class Test5:
+    def __init__(self, dut):
+        self.dut = dut
+        self.data = []
+        for i in range(num_tests):
+            self.data.append((randint(0, 1<<14), randint(0, 1<<14)))
+        self.i = 0
+        self.o = 0
+
+    def send(self):
+        while self.o != len(self.data):
+            send_range = randint(0, 3)
+            for j in range(randint(1,10)):
+                if send_range == 0:
+                    send = True
+                else:
+                    send = randint(0, send_range) != 0
+                o_p_ready = yield self.dut.o.p_ready
+                if not o_p_ready:
+                    yield
+                    continue
+                if send and self.i != len(self.data):
+                    yield self.dut.i.p_valid.eq(1)
+                    for v in self.dut.set_input(self.data[self.i]):
+                        yield v
+                    self.i += 1
+                else:
+                    yield self.dut.i.p_valid.eq(0)
+                yield
+
+    def rcv(self):
+        while self.o != len(self.data):
+            stall_range = randint(0, 3)
+            for j in range(randint(1,10)):
+                stall = randint(0, stall_range) != 0
+                yield self.dut.i.n_ready.eq(stall)
+                yield
+                o_n_valid = yield self.dut.o.n_valid
+                i_n_ready = yield self.dut.i.n_ready
+                if not o_n_valid or not i_n_ready:
+                    continue
+                o_data = yield self.dut.o.data
+                res = self.data[self.o][0] + self.data[self.o][1]
+                assert o_data == res, \
+                            "%d-%d data %x not match %s\n" \
+                            % (self.i, self.o, o_data, repr(self.data[self.o]))
+                self.o += 1
+                if self.o == len(self.data):
+                    break
+
+
 def testbench4(dut):
     data = []
 def testbench4(dut):
     data = []
-    for i in range(10000):
+    for i in range(num_tests):
         #data.append(randint(0, 1<<16-1))
         data.append(i+1)
     i = 0
         #data.append(randint(0, 1<<16-1))
         data.append(i+1)
     i = 0
@@ -177,7 +228,7 @@ class ExampleBufPipe2:
                               v               v
         i_p_valid  >>in  pipe1 o_n_valid  out>> i_p_valid  >>in  pipe2
         o_p_ready <<out pipe1 i_n_ready <<in  o_p_ready <<out pipe2
                               v               v
         i_p_valid  >>in  pipe1 o_n_valid  out>> i_p_valid  >>in  pipe2
         o_p_ready <<out pipe1 i_n_ready <<in  o_p_ready <<out pipe2
-        stage.i_data   >>in  pipe1 o_data   out>> stage.i_data   >>in  pipe2
+        i_data   >>in  pipe1 o_data   out>> i_data   >>in  pipe2
     """
     def __init__(self):
         self.pipe1 = ExampleBufPipe()
     """
     def __init__(self):
         self.pipe1 = ExampleBufPipe()
@@ -201,20 +252,22 @@ class ExampleBufPipe2:
         # connect inter-pipe input/output valid/ready/data
         m.d.comb += self.pipe2.i.p_valid.eq(self.pipe1.o.n_valid)
         m.d.comb += self.pipe1.i.n_ready.eq(self.pipe2.o.p_ready)
         # connect inter-pipe input/output valid/ready/data
         m.d.comb += self.pipe2.i.p_valid.eq(self.pipe1.o.n_valid)
         m.d.comb += self.pipe1.i.n_ready.eq(self.pipe2.o.p_ready)
-        m.d.comb += self.pipe2.stage.i_data.eq(self.pipe1.stage.o_data)
+        m.d.comb += self.pipe2.i.data.eq(self.pipe1.o.data)
 
         # inputs/outputs to the module: pipe1 connections here (LHS)
         m.d.comb += self.pipe1.i.p_valid.eq(self.i_p_valid)
         m.d.comb += self.o_p_ready.eq(self.pipe1.o.p_ready)
 
         # inputs/outputs to the module: pipe1 connections here (LHS)
         m.d.comb += self.pipe1.i.p_valid.eq(self.i_p_valid)
         m.d.comb += self.o_p_ready.eq(self.pipe1.o.p_ready)
-        m.d.comb += self.pipe1.stage.i_data.eq(self.i_data)
+        m.d.comb += self.pipe1.i.data.eq(self.i_data)
 
         # now pipe2 connections (RHS)
         m.d.comb += self.o_n_valid.eq(self.pipe2.o.n_valid)
         m.d.comb += self.pipe2.i.n_ready.eq(self.i_n_ready)
 
         # now pipe2 connections (RHS)
         m.d.comb += self.o_n_valid.eq(self.pipe2.o.n_valid)
         m.d.comb += self.pipe2.i.n_ready.eq(self.i_n_ready)
-        m.d.comb += self.o_data.eq(self.pipe2.stage.o_data)
+        m.d.comb += self.o_data.eq(self.pipe2.o.data)
 
         return m
 
 
         return m
 
+num_tests = 10000
+
 if __name__ == '__main__':
     print ("test 1")
     dut = ExampleBufPipe()
 if __name__ == '__main__':
     print ("test 1")
     dut = ExampleBufPipe()
@@ -232,3 +285,9 @@ if __name__ == '__main__':
     print ("test 4")
     dut = ExampleBufPipe2()
     run_simulation(dut, testbench4(dut), vcd_name="test_bufpipe4.vcd")
     print ("test 4")
     dut = ExampleBufPipe2()
     run_simulation(dut, testbench4(dut), vcd_name="test_bufpipe4.vcd")
+
+    print ("test 5")
+    dut = ExampleBufPipeAdd()
+    test = Test5(dut)
+    run_simulation(dut, [test.send, test.rcv], vcd_name="test_bufpipe5.vcd")
+