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