update comments
[ieee754fpu.git] / src / add / example_buf_pipe.py
1 """ nmigen implementation of buffered pipeline stage, based on zipcpu:
2 https://zipcpu.com/blog/2017/08/14/strategies-for-pipelining.html
3
4 this module requires quite a bit of thought to understand how it works
5 (and why it is needed in the first place). reading the above is
6 *strongly* recommended.
7
8 unlike john dawson's IEEE754 FPU STB/ACK signalling, which requires
9 the STB / ACK signals to raise and lower (on separate clocks) before
10 data may proceeed (thus only allowing one piece of data to proceed
11 on *ALTERNATE* cycles), the signalling here is a true pipeline
12 where data will flow on *every* clock when the conditions are right.
13
14 input acceptance conditions are when:
15 * incoming previous-stage strobe (i_p_stb) is HIGH
16 * outgoing previous-stage busy (o_p_busy) is LOW
17
18 output transmission conditions are when:
19 * outgoing next-stage strobe (o_n_stb) is HIGH
20 * outgoing next-stage busy (i_n_busy) is LOW
21
22 the tricky bit is when the input has valid data and the output is not
23 ready to accept it. if it wasn't for the clock synchronisation, it
24 would be possible to tell the input "hey don't send that data, we're
25 not ready". unfortunately, it's not possible to "change the past":
26 the previous stage *has no choice* but to pass on its data.
27
28 therefore, the incoming data *must* be accepted - and stored.
29 on the same clock, it's possible to tell the input that it must
30 not send any more data. this is the "stall" condition.
31
32 we now effectively have *two* possible pieces of data to "choose" from:
33 the buffered data, and the incoming data. the decision as to which
34 to process and output is based on whether we are in "stall" or not.
35 i.e. when the next stage is no longer busy, the output comes from
36 the buffer if a stall had previously occurred, otherwise it comes
37 direct from processing the input.
38
39 it's quite a complex state machine!
40 """
41
42 from nmigen import Signal, Cat, Const, Mux, Module
43 from nmigen.compat.sim import run_simulation
44 from nmigen.cli import verilog, rtlil
45
46 class BufPipe:
47 """ buffered pipeline stage
48
49 stage-1 i_p_stb >>in stage o_n_stb out>> stage+1
50 stage-1 o_p_busy <<out stage i_n_busy <<in stage+1
51 stage-1 i_data >>in stage o_data out>> stage+1
52 | |
53 +-------> process
54 | |
55 +-- r_data ---+
56 """
57 def __init__(self):
58 # input
59 #self.i_p_rst = Signal() # >>in - comes in from PREVIOUS stage
60 self.i_p_stb = Signal() # >>in - comes in from PREVIOUS stage
61 self.i_n_busy = Signal() # in<< - comes in from the NEXT stage
62 self.i_data = Signal(32) # >>in - comes in from the PREVIOUS stage
63 #self.i_rst = Signal()
64
65 # buffered
66 self.r_data = Signal(32)
67
68 # output
69 self.o_n_stb = Signal() # out>> - goes out to the NEXT stage
70 self.o_p_busy = Signal() # <<out - goes out to the PREVIOUS stage
71 self.o_data = Signal(32) # out>> - goes out to the NEXT stage
72
73 def pre_process(self, d_in):
74 return d_in | 0xf0000
75
76 def process(self, d_in):
77 return d_in + 1
78
79 def elaborate(self, platform):
80 m = Module()
81
82 o_p_busyn = Signal(reset_less=True)
83 o_n_stbn = Signal(reset_less=True)
84 i_n_busyn = Signal(reset_less=True)
85 i_p_stb_o_p_busyn = Signal(reset_less=True)
86 m.d.comb += i_n_busyn.eq(~self.i_n_busy)
87 m.d.comb += o_n_stbn.eq(~self.o_n_stb)
88 m.d.comb += o_p_busyn.eq(~self.o_p_busy)
89 m.d.comb += i_p_stb_o_p_busyn.eq(self.i_p_stb & o_p_busyn)
90
91 result = Signal(32)
92 m.d.comb += result.eq(self.process(self.i_data))
93 with m.If(o_p_busyn): # not stalled
94 m.d.sync += self.r_data.eq(result)
95
96 #with m.If(self.i_p_rst): # reset
97 # m.d.sync += self.o_n_stb.eq(0)
98 # m.d.sync += self.o_p_busy.eq(0)
99 with m.If(i_n_busyn): # next stage is not busy
100 with m.If(o_p_busyn): # not stalled
101 # nothing in buffer: send input direct to output
102 m.d.sync += self.o_n_stb.eq(self.i_p_stb)
103 m.d.sync += self.o_data.eq(result)
104 with m.Else(): # o_p_busy is true, and something is in our buffer.
105 # Flush the [already processed] buffer to the output port.
106 m.d.sync += self.o_n_stb.eq(1)
107 m.d.sync += self.o_data.eq(self.r_data)
108 # ignore input, since o_p_busy is also true.
109 # also clear stall condition, declare register to be empty.
110 m.d.sync += self.o_p_busy.eq(0)
111
112 # (i_n_busy) is true here: next stage is busy
113 with m.Elif(o_n_stbn): # next stage being told "not busy"
114 m.d.sync += self.o_n_stb.eq(self.i_p_stb)
115 m.d.sync += self.o_p_busy.eq(0) # Keep the buffer empty
116 # Apply the logic to the input data, and set the output data
117 m.d.sync += self.o_data.eq(result)
118
119 # (i_n_busy) and (o_n_stb) both true:
120 with m.Elif(i_p_stb_o_p_busyn):
121 # If next stage *is* busy, and not stalled yet, accept input
122 m.d.sync += self.o_p_busy.eq(self.i_p_stb & self.o_n_stb)
123
124 with m.If(o_p_busyn): # not stalled
125 # turns out that from all of the above conditions, just
126 # always put result into buffer if not busy
127 m.d.sync += self.r_data.eq(result)
128
129 return m
130
131 def ports(self):
132 return [self.i_p_stb, self.i_n_busy, self.i_data,
133 self.r_data,
134 self.o_n_stb, self.o_p_busy, self.o_data
135 ]
136
137
138 def testbench(dut):
139 #yield dut.i_p_rst.eq(1)
140 yield dut.i_n_busy.eq(1)
141 yield dut.o_p_busy.eq(1)
142 yield
143 yield
144 #yield dut.i_p_rst.eq(0)
145 yield dut.i_n_busy.eq(0)
146 yield dut.i_data.eq(5)
147 yield dut.i_p_stb.eq(1)
148 yield
149 yield dut.i_data.eq(7)
150 yield
151 yield dut.i_data.eq(2)
152 yield
153 yield dut.i_n_busy.eq(1)
154 yield dut.i_data.eq(9)
155 yield
156 yield dut.i_p_stb.eq(0)
157 yield dut.i_data.eq(12)
158 yield
159 yield dut.i_data.eq(32)
160 yield dut.i_n_busy.eq(0)
161 yield
162 yield
163 yield
164 yield
165
166
167 if __name__ == '__main__':
168 dut = BufPipe()
169 vl = rtlil.convert(dut, ports=dut.ports())
170 with open("test_bufpipe.il", "w") as f:
171 f.write(vl)
172 run_simulation(dut, testbench(dut), vcd_name="test_bufpipe.vcd")
173