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 (i.p_valid) is HIGH
16 * outgoing previous-stage busy (o.p_busy) is LOW
18 output transmission conditions are when:
19 * outgoing next-stage strobe (o.n_valid) is HIGH
20 * outgoing next-stage busy (i.n_busy) 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 busy, 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
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.
55 input data i_data is read (only), is processed and goes into an
56 intermediate result store [process()]. this is updated combinatorially.
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()].
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
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.
71 """ i_data can be a DIFFERENT type from everything else
72 o_data, r_data and result must be of the same type
74 self
.i_data
= Signal(16)
75 self
.r_data
= Signal(16)
76 self
.o_data
= Signal(16)
77 self
.result
= Signal(16)
80 """ process the input data and store it in result.
81 (not needed to be known: result is combinatorial)
83 return self
.result
.eq(self
.i_data
+ 1)
85 def update_buffer(self
):
86 """ copies the result into the intermediate register r_data
88 return self
.r_data
.eq(self
.result
)
90 def update_output(self
):
91 """ copies the (combinatorial) result into the output
93 return self
.o_data
.eq(self
.result
)
95 def flush_buffer(self
):
96 """ copies the *intermediate* register r_data into the output
98 return self
.o_data
.eq(self
.r_data
)
101 return [self
.i_data
, self
.o_data
]
106 self
.p_valid
= Signal() # >>in - comes in from PREVIOUS stage
107 self
.n_busy
= Signal() # in<< - comes in from the NEXT stage
113 self
.n_valid
= Signal() # out>> - goes out to the NEXT stage
114 self
.p_busy
= Signal() # <<out - goes out to the PREVIOUS stage
117 class BufferedPipeline
:
118 """ buffered pipeline stage
120 stage-1 i.p_valid >>in stage o.n_valid out>> stage+1
121 stage-1 o.p_busy <<out stage i.n_busy <<in stage+1
122 stage-1 i_data >>in stage o_data out>> stage+1
129 # input: strobe comes in from previous stage, busy comes in from next
131 #self.i.p_valid = Signal() # >>in - comes in from PREVIOUS stage
132 #self.i.n_busy = Signal() # in<< - comes in from the NEXT stage
134 # output: strobe goes out to next stage, busy comes in from previous
136 #self.o.n_valid = Signal() # out>> - goes out to the NEXT stage
137 #self.o.p_busy = Signal() # <<out - goes out to the PREVIOUS stage
139 def elaborate(self
, platform
):
142 # establish some combinatorial temporaries
143 o_p_busyn
= Signal(reset_less
=True)
144 o_n_validn
= Signal(reset_less
=True)
145 i_n_busyn
= Signal(reset_less
=True)
146 i_p_valid_o_p_busyn
= Signal(reset_less
=True)
147 m
.d
.comb
+= [i_n_busyn
.eq(~self
.i
.n_busy
),
148 o_n_validn
.eq(~self
.o
.n_valid
),
149 o_p_busyn
.eq(~self
.o
.p_busy
),
150 i_p_valid_o_p_busyn
.eq(self
.i
.p_valid
& o_p_busyn
),
153 # store result of processing in combinatorial temporary
154 with m
.If(self
.i
.p_valid
): # input is valid: process it
155 m
.d
.comb
+= self
.stage
.process()
156 # if not in stall condition, update the temporary register
157 with m
.If(o_p_busyn
): # not stalled
158 m
.d
.sync
+= self
.stage
.update_buffer()
160 #with m.If(self.i.p_rst): # reset
161 # m.d.sync += self.o.n_valid.eq(0)
162 # m.d.sync += self.o.p_busy.eq(0)
163 with m
.If(i_n_busyn
): # next stage is not busy
164 with m
.If(o_p_busyn
): # not stalled
165 # nothing in buffer: send (processed) input direct to output
166 m
.d
.sync
+= [self
.o
.n_valid
.eq(self
.i
.p_valid
),
167 self
.stage
.update_output(),
169 with m
.Else(): # o.p_busy is true, and something is in our buffer.
170 # Flush the [already processed] buffer to the output port.
171 m
.d
.sync
+= [self
.o
.n_valid
.eq(1),
172 self
.stage
.flush_buffer(),
173 # clear stall condition, declare register empty.
176 # ignore input, since o.p_busy is also true.
178 # (i.n_busy) is true here: next stage is busy
179 with m
.Elif(o_n_validn
): # next stage being told "not busy"
180 m
.d
.sync
+= [self
.o
.n_valid
.eq(self
.i
.p_valid
),
181 self
.o
.p_busy
.eq(0), # Keep the buffer empty
182 # set the output data (from comb result)
183 self
.stage
.update_output(),
185 # (i.n_busy) and (o.n_valid) both true:
186 with m
.Elif(i_p_valid_o_p_busyn
):
187 # If next stage *is* busy, and not stalled yet, accept input
188 m
.d
.sync
+= self
.o
.p_busy
.eq(self
.i
.p_valid
& self
.o
.n_valid
)
193 return [self
.i
.p_valid
, self
.i
.n_busy
,
194 self
.o
.n_valid
, self
.o
.p_busy
,
198 class BufPipe(BufferedPipeline
):
201 BufferedPipeline
.__init
__(self
)
202 self
.stage
= ExampleStage()
205 return self
.stage
.ports() + BufferedPipeline
.ports(self
)
208 if __name__
== '__main__':
210 vl
= rtlil
.convert(dut
, ports
=dut
.ports())
211 with
open("test_bufpipe.il", "w") as f
: