add CompFSMOpSubset, also change dir to sdir
[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.back.pysim import Simulator
21 from nmigen.cli import rtlil
22 from math import log2
23 from nmutil.iocontrol import PrevControl, NextControl
24
25 from soc.fu.base_input_record import CompOpSubsetBase
26 from soc.decoder.power_enums import (MicrOp, Function)
27
28
29 class CompFSMOpSubset(CompOpSubsetBase):
30 def __init__(self, name=None):
31 layout = (('dir', 1),
32 )
33
34 super().__init__(layout, name=name)
35
36
37
38 class Dummy:
39 pass
40
41
42 class Shifter(Elaboratable):
43 """Simple sequential shifter
44
45 Prev port data:
46 * p.data_i.data: value to be shifted
47 * p.data_i.shift: shift amount
48 * When zero, no shift occurs.
49 * On POWER, range is 0 to 63 for 32-bit,
50 * and 0 to 127 for 64-bit.
51 * Other values wrap around.
52 * p.data_i.sdir: shift direction (0 = left, 1 = right)
53
54 Next port data:
55 * n.data_o.data: shifted value
56 """
57 class PrevData:
58 def __init__(self, width):
59 self.data = Signal(width, name="p_data_i")
60 self.shift = Signal(width, name="p_shift_i")
61 self.sdir = Signal(name="p_sdir_i")
62 self.ctx = Dummy() # comply with CompALU API
63
64 def _get_data(self):
65 return [self.data, self.shift]
66
67 class NextData:
68 def __init__(self, width):
69 self.data = Signal(width, name="n_data_o")
70
71 def _get_data(self):
72 return [self.data]
73
74 def __init__(self, width):
75 self.width = width
76 self.p = PrevControl()
77 self.n = NextControl()
78 self.p.data_i = Shifter.PrevData(width)
79 self.n.data_o = Shifter.NextData(width)
80
81 # more pieces to make this example class comply with the CompALU API
82 self.op = CompFSMOpSubset()
83 self.p.data_i.ctx.op = self.op
84 self.i = self.p.data_i._get_data()
85 self.out = self.n.data_o._get_data()
86
87 def elaborate(self, platform):
88 m = Module()
89
90 m.submodules.p = self.p
91 m.submodules.n = self.n
92
93 # Note:
94 # It is good practice to design a sequential circuit as
95 # a data path and a control path.
96
97 # Data path
98 # ---------
99 # The idea is to have a register that can be
100 # loaded or shifted (left and right).
101
102 # the control signals
103 load = Signal()
104 shift = Signal()
105 direction = Signal()
106 # the data flow
107 shift_in = Signal(self.width)
108 shift_left_by_1 = Signal(self.width)
109 shift_right_by_1 = Signal(self.width)
110 next_shift = Signal(self.width)
111 # the register
112 shift_reg = Signal(self.width, reset_less=True)
113 # build the data flow
114 m.d.comb += [
115 # connect input and output
116 shift_in.eq(self.p.data_i.data),
117 self.n.data_o.data.eq(shift_reg),
118 # generate shifted views of the register
119 shift_left_by_1.eq(Cat(0, shift_reg[:-1])),
120 shift_right_by_1.eq(Cat(shift_reg[1:], 0)),
121 ]
122 # choose the next value of the register according to the
123 # control signals
124 # default is no change
125 m.d.comb += next_shift.eq(shift_reg)
126 with m.If(load):
127 m.d.comb += next_shift.eq(shift_in)
128 with m.Elif(shift):
129 with m.If(direction):
130 m.d.comb += next_shift.eq(shift_right_by_1)
131 with m.Else():
132 m.d.comb += next_shift.eq(shift_left_by_1)
133
134 # register the next value
135 m.d.sync += shift_reg.eq(next_shift)
136
137 # Control path
138 # ------------
139 # The idea is to have a SHIFT state where the shift register
140 # is shifted every cycle, while a counter decrements.
141 # This counter is loaded with shift amount in the initial state.
142 # The SHIFT state is left when the counter goes to zero.
143
144 # Shift counter
145 shift_width = int(log2(self.width)) + 1
146 next_count = Signal(shift_width)
147 count = Signal(shift_width, reset_less=True)
148 m.d.sync += count.eq(next_count)
149
150 with m.FSM():
151 with m.State("IDLE"):
152 m.d.comb += [
153 # keep p.ready_o active on IDLE
154 self.p.ready_o.eq(1),
155 # keep loading the shift register and shift count
156 load.eq(1),
157 next_count.eq(self.p.data_i.shift),
158 ]
159 # capture the direction bit as well
160 m.d.sync += direction.eq(self.p.data_i.sdir)
161 with m.If(self.p.valid_i):
162 # Leave IDLE when data arrives
163 with m.If(next_count == 0):
164 # short-circuit for zero shift
165 m.next = "DONE"
166 with m.Else():
167 m.next = "SHIFT"
168 with m.State("SHIFT"):
169 m.d.comb += [
170 # keep shifting, while counter is not zero
171 shift.eq(1),
172 # decrement the shift counter
173 next_count.eq(count - 1),
174 ]
175 with m.If(next_count == 0):
176 # exit when shift counter goes to zero
177 m.next = "DONE"
178 with m.State("DONE"):
179 # keep n.valid_o active while the data is not accepted
180 m.d.comb += self.n.valid_o.eq(1)
181 with m.If(self.n.ready_i):
182 # go back to IDLE when the data is accepted
183 m.next = "IDLE"
184
185 return m
186
187 def __iter__(self):
188 yield self.p.data_i.data
189 yield self.p.data_i.shift
190 yield self.p.data_i.sdir
191 yield self.p.valid_i
192 yield self.p.ready_o
193 yield self.n.ready_i
194 yield self.n.valid_o
195 yield self.n.data_o.data
196
197 def ports(self):
198 return list(self)
199
200
201 def test_shifter():
202 m = Module()
203 m.submodules.shf = dut = Shifter(8)
204 print("Shifter port names:")
205 for port in dut:
206 print("-", port.name)
207 # generate RTLIL
208 # try "proc; show" in yosys to check the data path
209 il = rtlil.convert(dut, ports=dut.ports())
210 with open("test_shifter.il", "w") as f:
211 f.write(il)
212 sim = Simulator(m)
213 sim.add_clock(1e-6)
214
215 def send(data, shift, direction):
216 # present input data and assert valid_i
217 yield dut.p.data_i.data.eq(data)
218 yield dut.p.data_i.shift.eq(shift)
219 yield dut.p.data_i.sdir.eq(direction)
220 yield dut.p.valid_i.eq(1)
221 yield
222 # wait for p.ready_o to be asserted
223 while not (yield dut.p.ready_o):
224 yield
225 # clear input data and negate p.valid_i
226 yield dut.p.valid_i.eq(0)
227 yield dut.p.data_i.data.eq(0)
228 yield dut.p.data_i.shift.eq(0)
229 yield dut.p.data_i.sdir.eq(0)
230
231 def receive(expected):
232 # signal readiness to receive data
233 yield dut.n.ready_i.eq(1)
234 yield
235 # wait for n.valid_o to be asserted
236 while not (yield dut.n.valid_o):
237 yield
238 # read result
239 result = yield dut.n.data_o.data
240 # negate n.ready_i
241 yield dut.n.ready_i.eq(0)
242 # check result
243 assert result == expected
244
245 def producer():
246 # 13 >> 2
247 yield from send(13, 2, 1)
248 # 3 << 4
249 yield from send(3, 4, 0)
250 # 21 << 0
251 yield from send(21, 0, 0)
252
253 def consumer():
254 # the consumer is not in step with the producer, but the
255 # order of the results are preserved
256 # 13 >> 2 = 3
257 yield from receive(3)
258 # 3 << 4 = 48
259 yield from receive(48)
260 # 21 << 0 = 21
261 yield from receive(21)
262
263 sim.add_sync_process(producer)
264 sim.add_sync_process(consumer)
265 sim_writer = sim.write_vcd(
266 "test_shifter.vcd",
267 "test_shifter.gtkw",
268 traces=dut.ports()
269 )
270 with sim_writer:
271 sim.run()
272
273
274 if __name__ == "__main__":
275 test_shifter()