Move new write_gtkw and its example to nmutil
[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 cxxsim = False
21 if cxxsim:
22 from nmigen.sim.cxxsim import Simulator, Settle
23 else:
24 from nmigen.back.pysim import Simulator, Settle
25 from nmigen.cli import rtlil
26 from math import log2
27 from nmutil.iocontrol import PrevControl, NextControl
28
29 from soc.fu.base_input_record import CompOpSubsetBase
30 from soc.decoder.power_enums import (MicrOp, Function)
31
32 from nmutil.gtkw import write_gtkw
33
34
35 class CompFSMOpSubset(CompOpSubsetBase):
36 def __init__(self, name=None):
37 layout = (('sdir', 1),
38 )
39
40 super().__init__(layout, name=name)
41
42
43
44 class Dummy:
45 pass
46
47
48 class Shifter(Elaboratable):
49 """Simple sequential shifter
50
51 Prev port data:
52 * p.data_i.data: value to be shifted
53 * p.data_i.shift: shift amount
54 * When zero, no shift occurs.
55 * On POWER, range is 0 to 63 for 32-bit,
56 * and 0 to 127 for 64-bit.
57 * Other values wrap around.
58
59 Operation type
60 * op.sdir: shift direction (0 = left, 1 = right)
61
62 Next port data:
63 * n.data_o.data: shifted value
64 """
65 class PrevData:
66 def __init__(self, width):
67 self.data = Signal(width, name="p_data_i")
68 self.shift = Signal(width, name="p_shift_i")
69 self.ctx = Dummy() # comply with CompALU API
70
71 def _get_data(self):
72 return [self.data, self.shift]
73
74 class NextData:
75 def __init__(self, width):
76 self.data = Signal(width, name="n_data_o")
77
78 def _get_data(self):
79 return [self.data]
80
81 def __init__(self, width):
82 self.width = width
83 self.p = PrevControl()
84 self.n = NextControl()
85 self.p.data_i = Shifter.PrevData(width)
86 self.n.data_o = Shifter.NextData(width)
87
88 # more pieces to make this example class comply with the CompALU API
89 self.op = CompFSMOpSubset(name="op")
90 self.p.data_i.ctx.op = self.op
91 self.i = self.p.data_i._get_data()
92 self.out = self.n.data_o._get_data()
93
94 def elaborate(self, platform):
95 m = Module()
96
97 m.submodules.p = self.p
98 m.submodules.n = self.n
99
100 # Note:
101 # It is good practice to design a sequential circuit as
102 # a data path and a control path.
103
104 # Data path
105 # ---------
106 # The idea is to have a register that can be
107 # loaded or shifted (left and right).
108
109 # the control signals
110 load = Signal()
111 shift = Signal()
112 direction = Signal()
113 # the data flow
114 shift_in = Signal(self.width)
115 shift_left_by_1 = Signal(self.width)
116 shift_right_by_1 = Signal(self.width)
117 next_shift = Signal(self.width)
118 # the register
119 shift_reg = Signal(self.width, reset_less=True)
120 # build the data flow
121 m.d.comb += [
122 # connect input and output
123 shift_in.eq(self.p.data_i.data),
124 self.n.data_o.data.eq(shift_reg),
125 # generate shifted views of the register
126 shift_left_by_1.eq(Cat(0, shift_reg[:-1])),
127 shift_right_by_1.eq(Cat(shift_reg[1:], 0)),
128 ]
129 # choose the next value of the register according to the
130 # control signals
131 # default is no change
132 m.d.comb += next_shift.eq(shift_reg)
133 with m.If(load):
134 m.d.comb += next_shift.eq(shift_in)
135 with m.Elif(shift):
136 with m.If(direction):
137 m.d.comb += next_shift.eq(shift_right_by_1)
138 with m.Else():
139 m.d.comb += next_shift.eq(shift_left_by_1)
140
141 # register the next value
142 m.d.sync += shift_reg.eq(next_shift)
143
144 # Control path
145 # ------------
146 # The idea is to have a SHIFT state where the shift register
147 # is shifted every cycle, while a counter decrements.
148 # This counter is loaded with shift amount in the initial state.
149 # The SHIFT state is left when the counter goes to zero.
150
151 # Shift counter
152 shift_width = int(log2(self.width)) + 1
153 next_count = Signal(shift_width)
154 count = Signal(shift_width, reset_less=True)
155 m.d.sync += count.eq(next_count)
156
157 with m.FSM():
158 with m.State("IDLE"):
159 m.d.comb += [
160 # keep p.ready_o active on IDLE
161 self.p.ready_o.eq(1),
162 # keep loading the shift register and shift count
163 load.eq(1),
164 next_count.eq(self.p.data_i.shift),
165 ]
166 # capture the direction bit as well
167 m.d.sync += direction.eq(self.op.sdir)
168 with m.If(self.p.valid_i):
169 # Leave IDLE when data arrives
170 with m.If(next_count == 0):
171 # short-circuit for zero shift
172 m.next = "DONE"
173 with m.Else():
174 m.next = "SHIFT"
175 with m.State("SHIFT"):
176 m.d.comb += [
177 # keep shifting, while counter is not zero
178 shift.eq(1),
179 # decrement the shift counter
180 next_count.eq(count - 1),
181 ]
182 with m.If(next_count == 0):
183 # exit when shift counter goes to zero
184 m.next = "DONE"
185 with m.State("DONE"):
186 # keep n.valid_o active while the data is not accepted
187 m.d.comb += self.n.valid_o.eq(1)
188 with m.If(self.n.ready_i):
189 # go back to IDLE when the data is accepted
190 m.next = "IDLE"
191
192 return m
193
194 def __iter__(self):
195 yield self.op.sdir
196 yield self.p.data_i.data
197 yield self.p.data_i.shift
198 yield self.p.valid_i
199 yield self.p.ready_o
200 yield self.n.ready_i
201 yield self.n.valid_o
202 yield self.n.data_o.data
203
204 def ports(self):
205 return list(self)
206
207
208 def test_shifter():
209 m = Module()
210 m.submodules.shf = dut = Shifter(8)
211 print("Shifter port names:")
212 for port in dut:
213 print("-", port.name)
214 # generate RTLIL
215 # try "proc; show" in yosys to check the data path
216 il = rtlil.convert(dut, ports=dut.ports())
217 with open("test_shifter.il", "w") as f:
218 f.write(il)
219
220 # Describe a GTKWave document
221
222 # Style for signals, classes and groups
223 gtkwave_style = {
224 # Root selector. Gives default attributes for every signal.
225 '': {'base': 'dec'},
226 # color the traces, according to class
227 # class names are not hardcoded, they are just strings
228 'in': {'color': 'orange'},
229 'out': {'color': 'yellow'},
230 # signals in the debug group have a common color and module path
231 'debug': {'module': 'top', 'color': 'red'},
232 # display a different string replacing the signal name
233 'test_case': {'display': 'test case'},
234 }
235
236 # DOM style description for the trace pane
237 gtkwave_desc = [
238 # simple signal, without a class
239 # even so, it inherits the top-level root attributes
240 'clk',
241 # comment
242 {'comment': 'Shifter Demonstration'},
243 # collapsible signal group
244 ('prev port', [
245 # attach a class style for each signal
246 ('op__sdir', 'in'),
247 ('p_data_i[7:0]', 'in'),
248 ('p_shift_i[7:0]', 'in'),
249 ('p_valid_i', 'in'),
250 ('p_ready_o', 'out'),
251 ]),
252 # Signals in a signal group inherit the group attributes.
253 # In this case, a different module path and color.
254 ('debug', [
255 {'comment': 'Some debug statements'},
256 # inline attributes, instead of a class name
257 ('zero', {'display': 'zero delay shift'}),
258 'interesting',
259 'test_case',
260 'msg',
261 ]),
262 ('internal', [
263 'fsm_state',
264 'count[3:0]',
265 'shift_reg[7:0]',
266 ]),
267 ('next port', [
268 ('n_data_o[7:0]', 'out'),
269 ('n_valid_o', 'out'),
270 ('n_ready_i', 'in'),
271 ]),
272 ]
273
274 write_gtkw("test_shifter.gtkw", "test_shifter.vcd",
275 gtkwave_desc, gtkwave_style,
276 module="top.shf", loc=__file__, marker=10500000)
277
278 sim = Simulator(m)
279 sim.add_clock(1e-6)
280
281 # demonstrates adding extra debug signal traces
282 # they end up in the top module
283 #
284 zero = Signal() # mark an interesting place
285 #
286 # demonstrates string traces
287 #
288 # display a message when the signal is high
289 # the low level is just an horizontal line
290 interesting = Signal(decoder=lambda v: 'interesting!' if v else '')
291 # choose between alternate strings based on numerical value
292 test_cases = ['', '13>>2', '3<<4', '21<<0']
293 test_case = Signal(8, decoder=lambda v: test_cases[v])
294 # hack to display arbitrary strings, like debug statements
295 msg = Signal(decoder=lambda _: msg.str)
296 msg.str = ''
297
298 def send(data, shift, direction):
299 # present input data and assert valid_i
300 yield dut.p.data_i.data.eq(data)
301 yield dut.p.data_i.shift.eq(shift)
302 yield dut.op.sdir.eq(direction)
303 yield dut.p.valid_i.eq(1)
304 yield
305 # wait for p.ready_o to be asserted
306 while not (yield dut.p.ready_o):
307 yield
308 # show current operation operation
309 if direction:
310 msg.str = f'{data}>>{shift}'
311 else:
312 msg.str = f'{data}<<{shift}'
313 # force dump of the above message by toggling the
314 # underlying signal
315 yield msg.eq(0)
316 yield msg.eq(1)
317 # clear input data and negate p.valid_i
318 yield dut.p.valid_i.eq(0)
319 yield dut.p.data_i.data.eq(0)
320 yield dut.p.data_i.shift.eq(0)
321 yield dut.op.sdir.eq(0)
322
323 def receive(expected):
324 # signal readiness to receive data
325 yield dut.n.ready_i.eq(1)
326 yield
327 # wait for n.valid_o to be asserted
328 while not (yield dut.n.valid_o):
329 yield
330 # read result
331 result = yield dut.n.data_o.data
332 # negate n.ready_i
333 yield dut.n.ready_i.eq(0)
334 # check result
335 assert result == expected
336 # finish displaying the current operation
337 msg.str = ''
338 yield msg.eq(0)
339 yield msg.eq(1)
340
341 def producer():
342 # 13 >> 2
343 yield from send(13, 2, 1)
344 # 3 << 4
345 yield from send(3, 4, 0)
346 # 21 << 0
347 # use a debug signal to mark an interesting operation
348 # in this case, it is a shift by zero
349 yield interesting.eq(1)
350 yield from send(21, 0, 0)
351 yield interesting.eq(0)
352
353 def consumer():
354 # the consumer is not in step with the producer, but the
355 # order of the results are preserved
356 # 13 >> 2 = 3
357 yield test_case.eq(1)
358 yield from receive(3)
359 # 3 << 4 = 48
360 yield test_case.eq(2)
361 yield from receive(48)
362 # 21 << 0 = 21
363 yield test_case.eq(3)
364 # you can look for the rising edge of this signal to quickly
365 # locate this point in the traces
366 yield zero.eq(1)
367 yield from receive(21)
368 yield zero.eq(0)
369 yield test_case.eq(0)
370
371 sim.add_sync_process(producer)
372 sim.add_sync_process(consumer)
373 sim_writer = sim.write_vcd(
374 "test_shifter.vcd",
375 # include additional signals in the trace dump
376 traces=[zero, interesting, test_case, msg],
377 )
378 with sim_writer:
379 sim.run()
380
381
382 if __name__ == "__main__":
383 test_shifter()