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