brackets round imports looks cleaner?
[soc.git] / src / soc / experiment / alu_fsm.py
1 """Simple example of a FSM-based ALU
2
3 This demonstrates a design that follows the valid/ready protocol of the
4 ALU, but with a FSM implementation, instead of a pipeline. It is also
5 intended to comply with both the CompALU API and the nmutil Pipeline API
6 (Liskov Substitution Principle)
7
8 The basic rules are:
9
10 1) p.ready_o is asserted on the initial ("Idle") state, otherwise it keeps low.
11 2) n.valid_o is asserted on the final ("Done") state, otherwise it keeps low.
12 3) The FSM stays in the Idle state while p.valid_i is low, otherwise
13 it accepts the input data and moves on.
14 4) The FSM stays in the Done state while n.ready_i is low, otherwise
15 it releases the output data and goes back to the Idle state.
16
17 """
18
19 from nmigen import Elaboratable, Signal, Module, Cat
20 from nmigen.cli import rtlil
21 from math import log2
22 import os
23
24 from nmutil.iocontrol import PrevControl, NextControl
25
26 from soc.fu.base_input_record import CompOpSubsetBase
27 from soc.decoder.power_enums import (MicrOp, Function)
28
29 from nmutil.gtkw import write_gtkw
30 from nmutil.sim_tmp_alternative import (Simulator, is_engine_pysim,
31 nmigen_sim_top_module)
32
33
34 class CompFSMOpSubset(CompOpSubsetBase):
35 def __init__(self, name=None):
36 layout = (('sdir', 1),
37 )
38
39 super().__init__(layout, name=name)
40
41
42
43 class Dummy:
44 pass
45
46
47 class Shifter(Elaboratable):
48 """Simple sequential shifter
49
50 Prev port data:
51 * p.data_i.data: value to be shifted
52 * p.data_i.shift: shift amount
53 * When zero, no shift occurs.
54 * On POWER, range is 0 to 63 for 32-bit,
55 * and 0 to 127 for 64-bit.
56 * Other values wrap around.
57
58 Operation type
59 * op.sdir: shift direction (0 = left, 1 = right)
60
61 Next port data:
62 * n.data_o.data: shifted value
63 """
64 class PrevData:
65 def __init__(self, width):
66 self.data = Signal(width, name="p_data_i")
67 self.shift = Signal(width, name="p_shift_i")
68 self.ctx = Dummy() # comply with CompALU API
69
70 def _get_data(self):
71 return [self.data, self.shift]
72
73 class NextData:
74 def __init__(self, width):
75 self.data = Signal(width, name="n_data_o")
76
77 def _get_data(self):
78 return [self.data]
79
80 def __init__(self, width):
81 self.width = width
82 self.p = PrevControl()
83 self.n = NextControl()
84 self.p.data_i = Shifter.PrevData(width)
85 self.n.data_o = Shifter.NextData(width)
86
87 # more pieces to make this example class comply with the CompALU API
88 self.op = CompFSMOpSubset(name="op")
89 self.p.data_i.ctx.op = self.op
90 self.i = self.p.data_i._get_data()
91 self.out = self.n.data_o._get_data()
92
93 def elaborate(self, platform):
94 m = Module()
95
96 m.submodules.p = self.p
97 m.submodules.n = self.n
98
99 # Note:
100 # It is good practice to design a sequential circuit as
101 # a data path and a control path.
102
103 # Data path
104 # ---------
105 # The idea is to have a register that can be
106 # loaded or shifted (left and right).
107
108 # the control signals
109 load = Signal()
110 shift = Signal()
111 direction = Signal()
112 # the data flow
113 shift_in = Signal(self.width)
114 shift_left_by_1 = Signal(self.width)
115 shift_right_by_1 = Signal(self.width)
116 next_shift = Signal(self.width)
117 # the register
118 shift_reg = Signal(self.width, reset_less=True)
119 # build the data flow
120 m.d.comb += [
121 # connect input and output
122 shift_in.eq(self.p.data_i.data),
123 self.n.data_o.data.eq(shift_reg),
124 # generate shifted views of the register
125 shift_left_by_1.eq(Cat(0, shift_reg[:-1])),
126 shift_right_by_1.eq(Cat(shift_reg[1:], 0)),
127 ]
128 # choose the next value of the register according to the
129 # control signals
130 # default is no change
131 m.d.comb += next_shift.eq(shift_reg)
132 with m.If(load):
133 m.d.comb += next_shift.eq(shift_in)
134 with m.Elif(shift):
135 with m.If(direction):
136 m.d.comb += next_shift.eq(shift_right_by_1)
137 with m.Else():
138 m.d.comb += next_shift.eq(shift_left_by_1)
139
140 # register the next value
141 m.d.sync += shift_reg.eq(next_shift)
142
143 # Control path
144 # ------------
145 # The idea is to have a SHIFT state where the shift register
146 # is shifted every cycle, while a counter decrements.
147 # This counter is loaded with shift amount in the initial state.
148 # The SHIFT state is left when the counter goes to zero.
149
150 # Shift counter
151 shift_width = int(log2(self.width)) + 1
152 next_count = Signal(shift_width)
153 count = Signal(shift_width, reset_less=True)
154 m.d.sync += count.eq(next_count)
155
156 with m.FSM():
157 with m.State("IDLE"):
158 m.d.comb += [
159 # keep p.ready_o active on IDLE
160 self.p.ready_o.eq(1),
161 # keep loading the shift register and shift count
162 load.eq(1),
163 next_count.eq(self.p.data_i.shift),
164 ]
165 # capture the direction bit as well
166 m.d.sync += direction.eq(self.op.sdir)
167 with m.If(self.p.valid_i):
168 # Leave IDLE when data arrives
169 with m.If(next_count == 0):
170 # short-circuit for zero shift
171 m.next = "DONE"
172 with m.Else():
173 m.next = "SHIFT"
174 with m.State("SHIFT"):
175 m.d.comb += [
176 # keep shifting, while counter is not zero
177 shift.eq(1),
178 # decrement the shift counter
179 next_count.eq(count - 1),
180 ]
181 with m.If(next_count == 0):
182 # exit when shift counter goes to zero
183 m.next = "DONE"
184 with m.State("DONE"):
185 # keep n.valid_o active while the data is not accepted
186 m.d.comb += self.n.valid_o.eq(1)
187 with m.If(self.n.ready_i):
188 # go back to IDLE when the data is accepted
189 m.next = "IDLE"
190
191 return m
192
193 def __iter__(self):
194 yield self.op.sdir
195 yield self.p.data_i.data
196 yield self.p.data_i.shift
197 yield self.p.valid_i
198 yield self.p.ready_o
199 yield self.n.ready_i
200 yield self.n.valid_o
201 yield self.n.data_o.data
202
203 def ports(self):
204 return list(self)
205
206
207 def test_shifter():
208 m = Module()
209 m.submodules.shf = dut = Shifter(8)
210 print("Shifter port names:")
211 for port in dut:
212 print("-", port.name)
213 # generate RTLIL
214 # try "proc; show" in yosys to check the data path
215 il = rtlil.convert(dut, ports=dut.ports())
216 with open("test_shifter.il", "w") as f:
217 f.write(il)
218
219 gtkwave_style = {
220 'in': {'color': 'orange'},
221 'out': {'color': 'yellow'},
222 }
223
224 gtkwave_desc = [
225 'clk',
226 {'comment': 'Shifter Demonstration'},
227 ('prev port', [
228 ('op__sdir', 'in'),
229 ('p_data_i[7:0]', 'in'),
230 ('p_shift_i[7:0]', 'in'),
231 ('p_valid_i', 'in'),
232 ('p_ready_o' if is_engine_pysim() else 'p_p_ready_o', 'out'),
233 ]),
234 ('internal', [
235 'fsm_state' if is_engine_pysim() else 'fsm_state[1:0]',
236 'count[3:0]',
237 'shift_reg[7:0]',
238 ]),
239 ('next port', [
240 ('n_data_o[7:0]', 'out'),
241 ('n_valid_o' if is_engine_pysim() else 'n_n_valid_o', 'out'),
242 ('n_ready_i', 'in'),
243 ]),
244 ]
245
246 module = nmigen_sim_top_module + "shf"
247 write_gtkw("test_shifter.gtkw", "test_shifter.vcd",
248 gtkwave_desc, gtkwave_style,
249 module=module, loc=__file__, base='dec')
250
251 sim = Simulator(m)
252 sim.add_clock(1e-6)
253
254 def send(data, shift, direction):
255 # present input data and assert valid_i
256 yield dut.p.data_i.data.eq(data)
257 yield dut.p.data_i.shift.eq(shift)
258 yield dut.op.sdir.eq(direction)
259 yield dut.p.valid_i.eq(1)
260 yield
261 # wait for p.ready_o to be asserted
262 while not (yield dut.p.ready_o):
263 yield
264 # show current operation operation
265 # force dump of the above message by toggling the
266 # underlying signal
267 # clear input data and negate p.valid_i
268 yield dut.p.valid_i.eq(0)
269 yield dut.p.data_i.data.eq(0)
270 yield dut.p.data_i.shift.eq(0)
271 yield dut.op.sdir.eq(0)
272
273 def receive(expected):
274 # signal readiness to receive data
275 yield dut.n.ready_i.eq(1)
276 yield
277 # wait for n.valid_o to be asserted
278 while not (yield dut.n.valid_o):
279 yield
280 # read result
281 result = yield dut.n.data_o.data
282 # negate n.ready_i
283 yield dut.n.ready_i.eq(0)
284 # check result
285 assert result == expected
286 # finish displaying the current operation
287
288 def producer():
289 # 13 >> 2
290 yield from send(13, 2, 1)
291 # 3 << 4
292 yield from send(3, 4, 0)
293 # 21 << 0
294 # use a debug signal to mark an interesting operation
295 # in this case, it is a shift by zero
296 yield from send(21, 0, 0)
297
298 def consumer():
299 # the consumer is not in step with the producer, but the
300 # order of the results are preserved
301 # 13 >> 2 = 3
302 yield from receive(3)
303 # 3 << 4 = 48
304 yield from receive(48)
305 # 21 << 0 = 21
306 # you can look for the rising edge of this signal to quickly
307 # locate this point in the traces
308 yield from receive(21)
309
310 sim.add_sync_process(producer)
311 sim.add_sync_process(consumer)
312 sim_writer = sim.write_vcd("test_shifter.vcd")
313 with sim_writer:
314 sim.run()
315
316
317 if __name__ == "__main__":
318 test_shifter()