00eecc3ecd37db7b603c1dd4f50ce84e1b98ca52
[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_valid) is HIGH
16 * outgoing previous-stage ready (o.p_ready) is LOW
17
18 output transmission conditions are when:
19 * outgoing next-stage strobe (o.n_valid) is HIGH
20 * outgoing next-stage ready (i.n_ready) 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: that
29 is the responsibility / contract that this stage *must* accept.
30 on the same clock, it's possible to tell the input that it must
31 not send any more data. this is the "stall" condition.
32
33 we now effectively have *two* possible pieces of data to "choose" from:
34 the buffered data, and the incoming data. the decision as to which
35 to process and output is based on whether we are in "stall" or not.
36 i.e. when the next stage is no longer ready, the output comes from
37 the buffer if a stall had previously occurred, otherwise it comes
38 direct from processing the input.
39
40 this allows us to respect a synchronous "travelling STB" with what
41 dan calls a "buffered handshake".
42
43 it's quite a complex state machine!
44 """
45
46 from nmigen import Signal, Cat, Const, Mux, Module
47 from nmigen.cli import verilog, rtlil
48
49
50 class ExampleStage:
51 """ an example of how to use the buffered pipeline. actual names of
52 variables (i_data, r_data, o_data, result) below do not matter:
53 the functions however do.
54
55 input data i_data is read (only), is processed and goes into an
56 intermediate result store [process()]. this is updated combinatorially.
57
58 in a non-stall condition, the intermediate result will go into the
59 output (update_output). however if ever there is a stall, it goes
60 into r_data instead [update_buffer()].
61
62 when the non-stall condition is released, r_data is the first
63 to be transferred to the output [flush_buffer()], and the stall
64 condition cleared.
65
66 on the next cycle (as long as stall is not raised again) the
67 input may begin to be processed and transferred directly to output.
68 """
69
70 def __init__(self):
71 """ i_data can be a DIFFERENT type from everything else
72 o_data, r_data and result are best of the same type.
73 however this is not strictly necessary. an intermediate
74 transformation process could hypothetically be applied, however
75 it is result and r_data that definitively need to be of the same
76 (intermediary) type, as it is both result and r_data that
77 are transferred into o_data:
78
79 i_data -> process() -> result --> o_data
80 | ^
81 | |
82 +-> r_data -+
83 """
84 self.i_data = Signal(16)
85 self.r_data = Signal(16)
86 self.o_data = Signal(16)
87 self.result = Signal(16)
88
89 def process(self):
90 """ process the input data and store it in result.
91 (not needed to be known: result is combinatorial)
92 """
93 return self.result.eq(self.i_data + 1)
94
95 def update_buffer(self):
96 """ copies the result into the intermediate register r_data,
97 which will need to be outputted on a subsequent cycle
98 prior to allowing "normal" operation.
99 """
100 return self.r_data.eq(self.result)
101
102 def update_output(self):
103 """ copies the (combinatorial) result into the output
104 """
105 return self.o_data.eq(self.result)
106
107 def flush_buffer(self):
108 """ copies the *intermediate* register r_data into the output
109 """
110 return self.o_data.eq(self.r_data)
111
112 def ports(self):
113 return [self.i_data, self.o_data]
114
115 class IOAckIn:
116
117 def __init__(self):
118 self.p_valid = Signal() # >>in - comes in from PREVIOUS stage
119 self.n_ready = Signal() # in<< - comes in from the NEXT stage
120
121
122 class IOAckOut:
123
124 def __init__(self):
125 self.n_valid = Signal() # out>> - goes out to the NEXT stage
126 self.p_ready = Signal() # <<out - goes out to the PREVIOUS stage
127
128
129 class BufferedPipeline:
130 """ buffered pipeline stage
131
132 stage-1 i.p_valid >>in stage o.n_valid out>> stage+1
133 stage-1 o.p_ready <<out stage i.n_ready <<in stage+1
134 stage-1 i_data >>in stage o_data out>> stage+1
135 | |
136 +-------> process
137 | |
138 +-- r_data ---+
139 """
140 def __init__(self):
141 # input: strobe comes in from previous stage, ready comes in from next
142 self.i = IOAckIn()
143 #self.i.p_valid = Signal() # >>in - comes in from PREVIOUS stage
144 #self.i.n_ready = Signal() # in<< - comes in from the NEXT stage
145
146 # output: strobe goes out to next stage, ready comes in from previous
147 self.o = IOAckOut()
148 #self.o.n_valid = Signal() # out>> - goes out to the NEXT stage
149 #self.o.p_ready = Signal() # <<out - goes out to the PREVIOUS stage
150
151 def elaborate(self, platform):
152 m = Module()
153
154 # establish some combinatorial temporaries
155 o_n_validn = Signal(reset_less=True)
156 i_p_valid_o_p_ready = Signal(reset_less=True)
157 m.d.comb += [o_n_validn.eq(~self.o.n_valid),
158 i_p_valid_o_p_ready.eq(self.i.p_valid & self.o.p_ready),
159 ]
160
161 # store result of processing in combinatorial temporary
162 with m.If(self.i.p_valid): # input is valid: process it
163 m.d.comb += self.stage.process()
164 # if not in stall condition, update the temporary register
165 with m.If(self.o.p_ready): # not stalled
166 m.d.sync += self.stage.update_buffer()
167
168 #with m.If(self.i.p_rst): # reset
169 # m.d.sync += self.o.n_valid.eq(0)
170 # m.d.sync += self.o.p_ready.eq(0)
171 with m.If(self.i.n_ready): # next stage is ready
172 with m.If(self.o.p_ready): # not stalled
173 # nothing in buffer: send (processed) input direct to output
174 m.d.sync += [self.o.n_valid.eq(self.i.p_valid),
175 self.stage.update_output(),
176 ]
177 with m.Else(): # o.p_ready is false, and something is in buffer.
178 # Flush the [already processed] buffer to the output port.
179 m.d.sync += [self.o.n_valid.eq(1),
180 self.stage.flush_buffer(),
181 # clear stall condition, declare register empty.
182 self.o.p_ready.eq(1),
183 ]
184 # ignore input, since o.p_ready is also false.
185
186 # (i.n_ready) is false here: next stage is ready
187 with m.Elif(o_n_validn): # next stage being told "ready"
188 m.d.sync += [self.o.n_valid.eq(self.i.p_valid),
189 self.o.p_ready.eq(1), # Keep the buffer empty
190 # set the output data (from comb result)
191 self.stage.update_output(),
192 ]
193 # (i.n_ready) false and (o.n_valid) true:
194 with m.Elif(i_p_valid_o_p_ready):
195 # If next stage *is* ready, and not stalled yet, accept input
196 m.d.sync += self.o.p_ready.eq(~(self.i.p_valid & self.o.n_valid))
197
198 return m
199
200 def ports(self):
201 return [self.i.p_valid, self.i.n_ready,
202 self.o.n_valid, self.o.p_ready,
203 ]
204
205
206 class ExampleBufPipe(BufferedPipeline):
207
208 def __init__(self):
209 BufferedPipeline.__init__(self)
210 self.stage = ExampleStage()
211
212 def ports(self):
213 return self.stage.ports() + BufferedPipeline.ports(self)
214
215
216 if __name__ == '__main__':
217 dut = BufPipe()
218 vl = rtlil.convert(dut, ports=dut.ports())
219 with open("test_bufpipe.il", "w") as f:
220 f.write(vl)