1 """ nmigen implementation of buffered pipeline stage, based on zipcpu:
2 https://zipcpu.com/blog/2017/08/14/strategies-for-pipelining.html
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.
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.
14 input acceptance conditions are when:
15 * incoming previous-stage strobe (p.i_valid) is HIGH
16 * outgoing previous-stage ready (p.o_ready) is LOW
18 output transmission conditions are when:
19 * outgoing next-stage strobe (n.o_valid) is HIGH
20 * outgoing next-stage ready (n.i_ready) is LOW
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.
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.
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.
40 this allows us to respect a synchronous "travelling STB" with what
41 dan calls a "buffered handshake".
43 it's quite a complex state machine!
46 from nmigen
import Signal
, Cat
, Const
, Mux
, Module
47 from nmigen
.cli
import verilog
, rtlil
48 from nmigen
.hdl
.rec
import Record
, Layout
50 from collections
.abc
import Sequence
54 """ contains signals that come *from* the previous stage (both in and out)
55 * i_valid: previous stage indicating all incoming data is valid.
56 may be a multi-bit signal, where all bits are required
57 to be asserted to indicate "valid".
58 * o_ready: output to next stage indicating readiness to accept data
59 * i_data : an input - added by the user of this class
62 def __init__(self
, i_width
=1):
63 self
.i_valid
= Signal(i_width
, name
="p_i_valid") # prev >>in self
64 self
.o_ready
= Signal(name
="p_o_ready") # prev <<out self
66 def connect_in(self
, prev
):
67 """ helper function to connect stage to an input source. do not
68 use to connect stage-to-stage!
70 return [self
.i_valid
.eq(prev
.i_valid
),
71 prev
.o_ready
.eq(self
.o_ready
),
72 eq(self
.i_data
, prev
.i_data
),
75 def i_valid_logic(self
):
76 vlen
= len(self
.i_valid
)
77 if vlen
> 1: # multi-bit case: valid only when i_valid is all 1s
78 all1s
= Const(-1, (len(self
.i_valid
), False))
79 return self
.i_valid
== all1s
80 # single-bit i_valid case
85 """ contains the signals that go *to* the next stage (both in and out)
86 * o_valid: output indicating to next stage that data is valid
87 * i_ready: input from next stage indicating that it can accept data
88 * o_data : an output - added by the user of this class
91 self
.o_valid
= Signal(name
="n_o_valid") # self out>> next
92 self
.i_ready
= Signal(name
="n_i_ready") # self <<in next
94 def connect_to_next(self
, nxt
):
95 """ helper function to connect to the next stage data/valid/ready.
96 data/valid is passed *TO* nxt, and ready comes *IN* from nxt.
98 return [nxt
.i_valid
.eq(self
.o_valid
),
99 self
.i_ready
.eq(nxt
.o_ready
),
100 eq(nxt
.i_data
, self
.o_data
),
103 def connect_out(self
, nxt
):
104 """ helper function to connect stage to an output source. do not
105 use to connect stage-to-stage!
107 return [nxt
.o_valid
.eq(self
.o_valid
),
108 self
.i_ready
.eq(nxt
.i_ready
),
109 eq(nxt
.o_data
, self
.o_data
),
114 """ makes signals equal: a helper routine which identifies if it is being
115 passed a list (or tuple) of objects, or signals, or Records, and calls
116 the objects' eq function.
118 complex objects (classes) can be used: they must follow the
119 convention of having an eq member function, which takes the
120 responsibility of further calling eq and returning a list of
123 Record is a special (unusual, recursive) case, where the input
124 is specified as a dictionary (which may contain further dictionaries,
125 recursively), where the field names of the dictionary must match
126 the Record's field spec.
128 if not isinstance(o
, Sequence
):
131 for (ao
, ai
) in zip(o
, i
):
132 #print ("eq", ao, ai)
133 if isinstance(ao
, Record
):
134 for idx
, (field_name
, field_shape
, _
) in enumerate(ao
.layout
):
135 if isinstance(field_shape
, Layout
):
136 rres
= eq(ao
.fields
[field_name
], ai
.fields
[field_name
])
138 rres
= eq(ao
.fields
[field_name
], ai
[field_name
])
142 if not isinstance(rres
, Sequence
):
149 """ Common functions for Pipeline API
151 def __init__(self
, stage
, in_multi
=None):
152 """ pass in a "stage" which may be either a static class or a class
153 instance, which has four functions (one optional):
154 * ispec: returns input signals according to the input specification
155 * ispec: returns output signals to the output specification
156 * process: takes an input instance and returns processed data
157 * setup: performs any module linkage if the stage uses one.
160 * add i_data member to PrevControl and
161 * add o_data member to NextControl
165 # set up input and output IO ACK (prev/next ready/valid)
166 self
.p
= PrevControl(in_multi
)
167 self
.n
= NextControl()
169 def connect_to_next(self
, nxt
):
170 """ helper function to connect to the next stage data/valid/ready.
172 return self
.n
.connect_to_next(nxt
.p
)
174 def connect_in(self
, prev
):
175 """ helper function to connect stage to an input source. do not
176 use to connect stage-to-stage!
178 return self
.p
.connect_in(prev
.p
)
180 def connect_out(self
, nxt
):
181 """ helper function to connect stage to an output source. do not
182 use to connect stage-to-stage!
184 return self
.n
.connect_out(nxt
.n
)
186 def set_input(self
, i
):
187 """ helper function to set the input data
189 return eq(self
.p
.i_data
, i
)
192 return [self
.p
.i_valid
, self
.n
.i_ready
,
193 self
.n
.o_valid
, self
.p
.o_ready
,
194 self
.p
.i_data
, self
.n
.o_data
# XXX need flattening!
198 class BufferedPipeline(PipelineBase
):
199 """ buffered pipeline stage. data and strobe signals travel in sync.
200 if ever the input is ready and the output is not, processed data
201 is stored in a temporary register.
203 stage-1 p.i_valid >>in stage n.o_valid out>> stage+1
204 stage-1 p.o_ready <<out stage n.i_ready <<in stage+1
205 stage-1 p.i_data >>in stage n.o_data out>> stage+1
211 input data p.i_data is read (only), is processed and goes into an
212 intermediate result store [process()]. this is updated combinatorially.
214 in a non-stall condition, the intermediate result will go into the
215 output (update_output). however if ever there is a stall, it goes
216 into r_data instead [update_buffer()].
218 when the non-stall condition is released, r_data is the first
219 to be transferred to the output [flush_buffer()], and the stall
222 on the next cycle (as long as stall is not raised again) the
223 input may begin to be processed and transferred directly to output.
225 def __init__(self
, stage
):
226 PipelineBase
.__init
__(self
, stage
)
228 # set up the input and output data
229 self
.p
.i_data
= stage
.ispec() # input type
230 self
.n
.o_data
= stage
.ospec()
232 def elaborate(self
, platform
):
235 result
= self
.stage
.ospec()
236 r_data
= self
.stage
.ospec()
237 if hasattr(self
.stage
, "setup"):
238 self
.stage
.setup(m
, self
.p
.i_data
)
240 # establish some combinatorial temporaries
241 o_n_validn
= Signal(reset_less
=True)
242 i_p_valid_o_p_ready
= Signal(reset_less
=True)
243 p_i_valid
= Signal(reset_less
=True)
244 m
.d
.comb
+= [p_i_valid
.eq(self
.p
.i_valid_logic()),
245 o_n_validn
.eq(~self
.n
.o_valid
),
246 i_p_valid_o_p_ready
.eq(p_i_valid
& self
.p
.o_ready
),
249 # store result of processing in combinatorial temporary
250 #with m.If(self.p.i_valid): # input is valid: process it
251 m
.d
.comb
+= eq(result
, self
.stage
.process(self
.p
.i_data
))
252 # if not in stall condition, update the temporary register
253 with m
.If(self
.p
.o_ready
): # not stalled
254 m
.d
.sync
+= eq(r_data
, result
) # update buffer
256 #with m.If(self.p.i_rst): # reset
257 # m.d.sync += self.n.o_valid.eq(0)
258 # m.d.sync += self.p.o_ready.eq(0)
259 with m
.If(self
.n
.i_ready
): # next stage is ready
260 with m
.If(self
.p
.o_ready
): # not stalled
261 # nothing in buffer: send (processed) input direct to output
262 m
.d
.sync
+= [self
.n
.o_valid
.eq(p_i_valid
),
263 eq(self
.n
.o_data
, result
), # update output
265 with m
.Else(): # p.o_ready is false, and something is in buffer.
266 # Flush the [already processed] buffer to the output port.
267 m
.d
.sync
+= [self
.n
.o_valid
.eq(1),
268 eq(self
.n
.o_data
, r_data
), # flush buffer
269 # clear stall condition, declare register empty.
270 self
.p
.o_ready
.eq(1),
272 # ignore input, since p.o_ready is also false.
274 # (n.i_ready) is false here: next stage is ready
275 with m
.Elif(o_n_validn
): # next stage being told "ready"
276 m
.d
.sync
+= [self
.n
.o_valid
.eq(p_i_valid
),
277 self
.p
.o_ready
.eq(1), # Keep the buffer empty
278 # set the output data (from comb result)
279 eq(self
.n
.o_data
, result
),
281 # (n.i_ready) false and (n.o_valid) true:
282 with m
.Elif(i_p_valid_o_p_ready
):
283 # If next stage *is* ready, and not stalled yet, accept input
284 m
.d
.sync
+= self
.p
.o_ready
.eq(~
(p_i_valid
& self
.n
.o_valid
))
289 class ExampleAddStage
:
290 """ an example of how to use the buffered pipeline, as a class instance
294 """ returns a tuple of input signals which will be the incoming data
296 return (Signal(16), Signal(16))
299 """ returns an output signal which will happen to contain the sum
304 def process(self
, i
):
305 """ process the input data (sums the values in the tuple) and returns it
310 class ExampleBufPipeAdd(BufferedPipeline
):
311 """ an example of how to use the buffered pipeline, using a class instance
315 addstage
= ExampleAddStage()
316 BufferedPipeline
.__init
__(self
, addstage
)
320 """ an example of how to use the buffered pipeline, in a static class
331 """ process the input data and returns it (adds 1)
336 class ExampleBufPipe(BufferedPipeline
):
337 """ an example of how to use the buffered pipeline.
341 BufferedPipeline
.__init
__(self
, ExampleStage
)
344 class CombPipe(PipelineBase
):
345 """A simple pipeline stage containing combinational logic that can execute
346 completely in one clock cycle.
354 r_data : Signal, input_shape
355 A temporary (buffered) copy of a prior (valid) input
356 result: Signal, output_shape
357 The output of the combinatorial logic
360 def __init__(self
, stage
):
361 PipelineBase
.__init
__(self
, stage
)
362 self
._data
_valid
= Signal()
364 # set up the input and output data
365 self
.p
.i_data
= stage
.ispec() # input type
366 self
.n
.o_data
= stage
.ospec() # output type
368 def elaborate(self
, platform
):
371 r_data
= self
.stage
.ispec() # input type
372 result
= self
.stage
.ospec() # output data
373 if hasattr(self
.stage
, "setup"):
374 self
.stage
.setup(m
, r_data
)
376 p_i_valid
= Signal(reset_less
=True)
377 m
.d
.comb
+= p_i_valid
.eq(self
.p
.i_valid_logic())
378 m
.d
.comb
+= eq(result
, self
.stage
.process(r_data
))
379 m
.d
.comb
+= self
.n
.o_valid
.eq(self
._data
_valid
)
380 m
.d
.comb
+= self
.p
.o_ready
.eq(~self
._data
_valid | self
.n
.i_ready
)
381 m
.d
.sync
+= self
._data
_valid
.eq(p_i_valid | \
382 (~self
.n
.i_ready
& self
._data
_valid
))
383 with m
.If(self
.p
.i_valid
& self
.p
.o_ready
):
384 m
.d
.sync
+= eq(r_data
, self
.p
.i_data
)
385 m
.d
.comb
+= eq(self
.n
.o_data
, result
)
389 class ExampleCombPipe(CombPipe
):
390 """ an example of how to use the combinatorial pipeline.
394 CombPipe
.__init
__(self
, ExampleStage
)
397 if __name__
== '__main__':
398 dut
= ExampleBufPipe()
399 vl
= rtlil
.convert(dut
, ports
=dut
.ports())
400 with
open("test_bufpipe.il", "w") as f
:
403 dut
= ExampleCombPipe()
404 vl
= rtlil
.convert(dut
, ports
=dut
.ports())
405 with
open("test_combpipe.il", "w") as f
: