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