Revert "move wrapping of stage into StageHandler"
[ieee754fpu.git] / src / add / iocontrol.py
1 """ IO Control API
2
3 Associated development bugs:
4 * http://bugs.libre-riscv.org/show_bug.cgi?id=64
5 * http://bugs.libre-riscv.org/show_bug.cgi?id=57
6
7 Stage API:
8 ---------
9
10 stage requires compliance with a strict API that may be
11 implemented in several means, including as a static class.
12
13 Stage Blocks really must be combinatorial blocks. It would be ok
14 to have input come in from sync'd sources (clock-driven) however by
15 doing so they would no longer be deterministic, and chaining such
16 blocks with such side-effects together could result in unexpected,
17 unpredictable, unreproduceable behaviour.
18 So generally to be avoided, then unless you know what you are doing.
19
20 the methods of a stage instance must be as follows:
21
22 * ispec() - Input data format specification. Takes a bit of explaining.
23 The requirements are: something that eventually derives from
24 nmigen Value must be returned *OR* an iterator or iterable
25 or sequence (list, tuple etc.) or generator must *yield*
26 thing(s) that (eventually) derive from the nmigen Value
27 class. Complex to state, very simple in practice:
28 see test_buf_pipe.py for over 25 working examples.
29
30 * ospec() - Output data format specification.
31 requirements identical to ispec
32
33 * process(m, i) - Processes an ispec-formatted object/sequence
34 returns a combinatorial block of a result that
35 may be assigned to the output, by way of the "nmoperator.eq"
36 function. Note that what is returned here can be
37 extremely flexible. Even a dictionary can be returned
38 as long as it has fields that match precisely with the
39 Record into which its values is intended to be assigned.
40 Again: see example unit tests for details.
41
42 * setup(m, i) - Optional function for setting up submodules
43 may be used for more complex stages, to link
44 the input (i) to submodules. must take responsibility
45 for adding those submodules to the module (m).
46 the submodules must be combinatorial blocks and
47 must have their inputs and output linked combinatorially.
48
49 Both StageCls (for use with non-static classes) and Stage (for use
50 by static classes) are abstract classes from which, for convenience
51 and as a courtesy to other developers, anything conforming to the
52 Stage API may *choose* to derive. See Liskov Substitution Principle:
53 https://en.wikipedia.org/wiki/Liskov_substitution_principle
54
55 StageChain:
56 ----------
57
58 A useful combinatorial wrapper around stages that chains them together
59 and then presents a Stage-API-conformant interface. By presenting
60 the same API as the stages it wraps, it can clearly be used recursively.
61
62 ControlBase:
63 -----------
64
65 The base class for pipelines. Contains previous and next ready/valid/data.
66 Also has an extremely useful "connect" function that can be used to
67 connect a chain of pipelines and present the exact same prev/next
68 ready/valid/data API.
69
70 Note: pipelines basically do not become pipelines as such until
71 handed to a derivative of ControlBase. ControlBase itself is *not*
72 strictly considered a pipeline class. Wishbone and AXI4 (master or
73 slave) could be derived from ControlBase, for example.
74 """
75
76 from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable
77 from nmigen.cli import verilog, rtlil
78 from nmigen.hdl.rec import Record
79
80 from abc import ABCMeta, abstractmethod
81 from collections.abc import Sequence, Iterable
82 from collections import OrderedDict
83 import inspect
84
85 import nmoperator
86
87
88 class Object:
89 def __init__(self):
90 self.fields = OrderedDict()
91
92 def __setattr__(self, k, v):
93 print ("kv", k, v)
94 if (k.startswith('_') or k in ["fields", "name", "src_loc"] or
95 k in dir(Object) or "fields" not in self.__dict__):
96 return object.__setattr__(self, k, v)
97 self.fields[k] = v
98
99 def __getattr__(self, k):
100 if k in self.__dict__:
101 return object.__getattr__(self, k)
102 try:
103 return self.fields[k]
104 except KeyError as e:
105 raise AttributeError(e)
106
107 def __iter__(self):
108 for x in self.fields.values(): # OrderedDict so order is preserved
109 if isinstance(x, Iterable):
110 yield from x
111 else:
112 yield x
113
114 def eq(self, inp):
115 res = []
116 for (k, o) in self.fields.items():
117 i = getattr(inp, k)
118 print ("eq", o, i)
119 rres = o.eq(i)
120 if isinstance(rres, Sequence):
121 res += rres
122 else:
123 res.append(rres)
124 print (res)
125 return res
126
127 def ports(self): # being called "keys" would be much better
128 return list(self)
129
130
131 class RecordObject(Record):
132 def __init__(self, layout=None, name=None):
133 Record.__init__(self, layout=layout or [], name=None)
134
135 def __setattr__(self, k, v):
136 #print (dir(Record))
137 if (k.startswith('_') or k in ["fields", "name", "src_loc"] or
138 k in dir(Record) or "fields" not in self.__dict__):
139 return object.__setattr__(self, k, v)
140 self.fields[k] = v
141 #print ("RecordObject setattr", k, v)
142 if isinstance(v, Record):
143 newlayout = {k: (k, v.layout)}
144 elif isinstance(v, Value):
145 newlayout = {k: (k, v.shape())}
146 else:
147 newlayout = {k: (k, nmoperator.shape(v))}
148 self.layout.fields.update(newlayout)
149
150 def __iter__(self):
151 for x in self.fields.values(): # remember: fields is an OrderedDict
152 if isinstance(x, Iterable):
153 yield from x # a bit like flatten (nmigen.tools)
154 else:
155 yield x
156
157 def ports(self): # would be better being called "keys"
158 return list(self)
159
160
161 def _spec(fn, name=None):
162 if name is None:
163 return fn()
164 varnames = dict(inspect.getmembers(fn.__code__))['co_varnames']
165 if 'name' in varnames:
166 return fn(name=name)
167 return fn()
168
169
170 class PrevControl(Elaboratable):
171 """ contains signals that come *from* the previous stage (both in and out)
172 * valid_i: previous stage indicating all incoming data is valid.
173 may be a multi-bit signal, where all bits are required
174 to be asserted to indicate "valid".
175 * ready_o: output to next stage indicating readiness to accept data
176 * data_i : an input - MUST be added by the USER of this class
177 """
178
179 def __init__(self, i_width=1, stage_ctl=False):
180 self.stage_ctl = stage_ctl
181 self.valid_i = Signal(i_width, name="p_valid_i") # prev >>in self
182 self._ready_o = Signal(name="p_ready_o") # prev <<out self
183 self.data_i = None # XXX MUST BE ADDED BY USER
184 if stage_ctl:
185 self.s_ready_o = Signal(name="p_s_o_rdy") # prev <<out self
186 self.trigger = Signal(reset_less=True)
187
188 @property
189 def ready_o(self):
190 """ public-facing API: indicates (externally) that stage is ready
191 """
192 if self.stage_ctl:
193 return self.s_ready_o # set dynamically by stage
194 return self._ready_o # return this when not under dynamic control
195
196 def _connect_in(self, prev, direct=False, fn=None):
197 """ internal helper function to connect stage to an input source.
198 do not use to connect stage-to-stage!
199 """
200 valid_i = prev.valid_i if direct else prev.valid_i_test
201 data_i = fn(prev.data_i) if fn is not None else prev.data_i
202 return [self.valid_i.eq(valid_i),
203 prev.ready_o.eq(self.ready_o),
204 nmoperator.eq(self.data_i, data_i),
205 ]
206
207 @property
208 def valid_i_test(self):
209 vlen = len(self.valid_i)
210 if vlen > 1:
211 # multi-bit case: valid only when valid_i is all 1s
212 all1s = Const(-1, (len(self.valid_i), False))
213 valid_i = (self.valid_i == all1s)
214 else:
215 # single-bit valid_i case
216 valid_i = self.valid_i
217
218 # when stage indicates not ready, incoming data
219 # must "appear" to be not ready too
220 if self.stage_ctl:
221 valid_i = valid_i & self.s_ready_o
222
223 return valid_i
224
225 def elaborate(self, platform):
226 m = Module()
227 m.d.comb += self.trigger.eq(self.valid_i_test & self.ready_o)
228 return m
229
230 def eq(self, i):
231 return [self.data_i.eq(i.data_i),
232 self.ready_o.eq(i.ready_o),
233 self.valid_i.eq(i.valid_i)]
234
235 def __iter__(self):
236 yield self.valid_i
237 yield self.ready_o
238 if hasattr(self.data_i, "ports"):
239 yield from self.data_i.ports()
240 elif isinstance(self.data_i, Sequence):
241 yield from self.data_i
242 else:
243 yield self.data_i
244
245 def ports(self):
246 return list(self)
247
248
249 class NextControl(Elaboratable):
250 """ contains the signals that go *to* the next stage (both in and out)
251 * valid_o: output indicating to next stage that data is valid
252 * ready_i: input from next stage indicating that it can accept data
253 * data_o : an output - MUST be added by the USER of this class
254 """
255 def __init__(self, stage_ctl=False):
256 self.stage_ctl = stage_ctl
257 self.valid_o = Signal(name="n_valid_o") # self out>> next
258 self.ready_i = Signal(name="n_ready_i") # self <<in next
259 self.data_o = None # XXX MUST BE ADDED BY USER
260 #if self.stage_ctl:
261 self.d_valid = Signal(reset=1) # INTERNAL (data valid)
262 self.trigger = Signal(reset_less=True)
263
264 @property
265 def ready_i_test(self):
266 if self.stage_ctl:
267 return self.ready_i & self.d_valid
268 return self.ready_i
269
270 def connect_to_next(self, nxt):
271 """ helper function to connect to the next stage data/valid/ready.
272 data/valid is passed *TO* nxt, and ready comes *IN* from nxt.
273 use this when connecting stage-to-stage
274 """
275 return [nxt.valid_i.eq(self.valid_o),
276 self.ready_i.eq(nxt.ready_o),
277 nmoperator.eq(nxt.data_i, self.data_o),
278 ]
279
280 def _connect_out(self, nxt, direct=False, fn=None):
281 """ internal helper function to connect stage to an output source.
282 do not use to connect stage-to-stage!
283 """
284 ready_i = nxt.ready_i if direct else nxt.ready_i_test
285 data_o = fn(nxt.data_o) if fn is not None else nxt.data_o
286 return [nxt.valid_o.eq(self.valid_o),
287 self.ready_i.eq(ready_i),
288 nmoperator.eq(data_o, self.data_o),
289 ]
290
291 def elaborate(self, platform):
292 m = Module()
293 m.d.comb += self.trigger.eq(self.ready_i_test & self.valid_o)
294 return m
295
296 def __iter__(self):
297 yield self.ready_i
298 yield self.valid_o
299 if hasattr(self.data_o, "ports"):
300 yield from self.data_o.ports()
301 elif isinstance(self.data_o, Sequence):
302 yield from self.data_o
303 else:
304 yield self.data_o
305
306 def ports(self):
307 return list(self)
308
309
310 class StageCls(metaclass=ABCMeta):
311 """ Class-based "Stage" API. requires instantiation (after derivation)
312
313 see "Stage API" above.. Note: python does *not* require derivation
314 from this class. All that is required is that the pipelines *have*
315 the functions listed in this class. Derivation from this class
316 is therefore merely a "courtesy" to maintainers.
317 """
318 @abstractmethod
319 def ispec(self): pass # REQUIRED
320 @abstractmethod
321 def ospec(self): pass # REQUIRED
322 #@abstractmethod
323 #def setup(self, m, i): pass # OPTIONAL
324 @abstractmethod
325 def process(self, i): pass # REQUIRED
326
327
328 class Stage(metaclass=ABCMeta):
329 """ Static "Stage" API. does not require instantiation (after derivation)
330
331 see "Stage API" above. Note: python does *not* require derivation
332 from this class. All that is required is that the pipelines *have*
333 the functions listed in this class. Derivation from this class
334 is therefore merely a "courtesy" to maintainers.
335 """
336 @staticmethod
337 @abstractmethod
338 def ispec(): pass
339
340 @staticmethod
341 @abstractmethod
342 def ospec(): pass
343
344 #@staticmethod
345 #@abstractmethod
346 #def setup(m, i): pass
347
348 @staticmethod
349 @abstractmethod
350 def process(i): pass
351
352
353 class StageChain(StageCls):
354 """ pass in a list of stages, and they will automatically be
355 chained together via their input and output specs into a
356 combinatorial chain, to create one giant combinatorial block.
357
358 the end result basically conforms to the exact same Stage API.
359
360 * input to this class will be the input of the first stage
361 * output of first stage goes into input of second
362 * output of second goes into input into third
363 * ... (etc. etc.)
364 * the output of this class will be the output of the last stage
365
366 NOTE: whilst this is very similar to ControlBase.connect(), it is
367 *really* important to appreciate that StageChain is pure
368 combinatorial and bypasses (does not involve, at all, ready/valid
369 signalling of any kind).
370
371 ControlBase.connect on the other hand respects, connects, and uses
372 ready/valid signalling.
373
374 Arguments:
375
376 * :chain: a chain of combinatorial blocks conforming to the Stage API
377 NOTE: StageChain.ispec and ospect have to have something
378 to return (beginning and end specs of the chain),
379 therefore the chain argument must be non-zero length
380
381 * :specallocate: if set, new input and output data will be allocated
382 and connected (eq'd) to each chained Stage.
383 in some cases if this is not done, the nmigen warning
384 "driving from two sources, module is being flattened"
385 will be issued.
386
387 NOTE: do NOT use StageChain with combinatorial blocks that have
388 side-effects (state-based / clock-based input) or conditional
389 (inter-chain) dependencies, unless you really know what you are doing.
390 """
391 def __init__(self, chain, specallocate=False):
392 assert len(chain) > 0, "stage chain must be non-zero length"
393 self.chain = chain
394 self.specallocate = specallocate
395
396 def ispec(self):
397 """ returns the ispec of the first of the chain
398 """
399 return _spec(self.chain[0].ispec, "chainin")
400
401 def ospec(self):
402 """ returns the ospec of the last of the chain
403 """
404 return _spec(self.chain[-1].ospec, "chainout")
405
406 def _specallocate_setup(self, m, i):
407 o = i # in case chain is empty
408 for (idx, c) in enumerate(self.chain):
409 if hasattr(c, "setup"):
410 c.setup(m, i) # stage may have some module stuff
411 ofn = self.chain[idx].ospec # last assignment survives
412 o = _spec(ofn, 'chainin%d' % idx)
413 m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
414 if idx == len(self.chain)-1:
415 break
416 ifn = self.chain[idx+1].ispec # new input on next loop
417 i = _spec(ifn, 'chainin%d' % (idx+1))
418 m.d.comb += nmoperator.eq(i, o) # assign to next input
419 return o # last loop is the output
420
421 def _noallocate_setup(self, m, i):
422 o = i # in case chain is empty
423 for (idx, c) in enumerate(self.chain):
424 if hasattr(c, "setup"):
425 c.setup(m, i) # stage may have some module stuff
426 i = o = c.process(i) # store input into "o"
427 return o # last loop is the output
428
429 def setup(self, m, i):
430 if self.specallocate:
431 self.o = self._specallocate_setup(m, i)
432 else:
433 self.o = self._noallocate_setup(m, i)
434
435 def process(self, i):
436 return self.o # conform to Stage API: return last-loop output
437
438
439 class StageHandler(Elaboratable):
440 """ Stage handling class
441 """
442 def __init__(self, ctrl, stage):
443 """
444 """
445 if stage is not None:
446 self.new_data(self, self, "data")
447
448 @property
449 def data_r(self):
450 return self.stage.process(self.p.data_i)
451
452 def _postprocess(self, i): # XXX DISABLED
453 return i # RETURNS INPUT
454 if hasattr(self.stage, "postprocess"):
455 return self.stage.postprocess(i)
456 return i
457
458 def new_data(self, p, n, name):
459 """ allocates new data_i and data_o
460 """
461 self.p.data_i = _spec(p.stage.ispec, "%s_i" % name)
462 self.n.data_o = _spec(n.stage.ospec, "%s_o" % name)
463
464 def elaborate(self, platform):
465 """ handles case where stage has dynamic ready/valid functions
466 """
467 m = Module()
468 m.submodules.p = self.p
469 m.submodules.n = self.n
470
471 if self.stage is not None and hasattr(self.stage, "setup"):
472 self.stage.setup(m, self.p.data_i)
473
474 if not self.p.stage_ctl:
475 return m
476
477 # intercept the previous (outgoing) "ready", combine with stage ready
478 m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready)
479
480 # intercept the next (incoming) "ready" and combine it with data valid
481 sdv = self.stage.d_valid(self.n.ready_i)
482 m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
483
484 return m
485
486
487 class ControlBase(StageHandler):
488 """ Common functions for Pipeline API. Note: a "pipeline stage" only
489 exists (conceptually) when a ControlBase derivative is handed
490 a Stage (combinatorial block)
491 """
492 def __init__(self, stage=None, in_multi=None, stage_ctl=False):
493 """ Base class containing ready/valid/data to previous and next stages
494
495 * p: contains ready/valid to the previous stage
496 * n: contains ready/valid to the next stage
497
498 Except when calling Controlbase.connect(), user must also:
499 * add data_i member to PrevControl (p) and
500 * add data_o member to NextControl (n)
501 """
502 self.stage = stage
503
504 # set up input and output IO ACK (prev/next ready/valid)
505 self.p = PrevControl(in_multi, stage_ctl)
506 self.n = NextControl(stage_ctl)
507
508 StageHandler.__init__(self, self, stage)
509
510 def connect_to_next(self, nxt):
511 """ helper function to connect to the next stage data/valid/ready.
512 """
513 return self.n.connect_to_next(nxt.p)
514
515 def _connect_in(self, prev):
516 """ internal helper function to connect stage to an input source.
517 do not use to connect stage-to-stage!
518 """
519 return self.p._connect_in(prev.p)
520
521 def _connect_out(self, nxt):
522 """ internal helper function to connect stage to an output source.
523 do not use to connect stage-to-stage!
524 """
525 return self.n._connect_out(nxt.n)
526
527 def connect(self, pipechain):
528 """ connects a chain (list) of Pipeline instances together and
529 links them to this ControlBase instance:
530
531 in <----> self <---> out
532 | ^
533 v |
534 [pipe1, pipe2, pipe3, pipe4]
535 | ^ | ^ | ^
536 v | v | v |
537 out---in out--in out---in
538
539 Also takes care of allocating data_i/data_o, by looking up
540 the data spec for each end of the pipechain. i.e It is NOT
541 necessary to allocate self.p.data_i or self.n.data_o manually:
542 this is handled AUTOMATICALLY, here.
543
544 Basically this function is the direct equivalent of StageChain,
545 except that unlike StageChain, the Pipeline logic is followed.
546
547 Just as StageChain presents an object that conforms to the
548 Stage API from a list of objects that also conform to the
549 Stage API, an object that calls this Pipeline connect function
550 has the exact same pipeline API as the list of pipline objects
551 it is called with.
552
553 Thus it becomes possible to build up larger chains recursively.
554 More complex chains (multi-input, multi-output) will have to be
555 done manually.
556
557 Argument:
558
559 * :pipechain: - a sequence of ControlBase-derived classes
560 (must be one or more in length)
561
562 Returns:
563
564 * a list of eq assignments that will need to be added in
565 an elaborate() to m.d.comb
566 """
567 assert len(pipechain) > 0, "pipechain must be non-zero length"
568 eqs = [] # collated list of assignment statements
569
570 # connect inter-chain
571 for i in range(len(pipechain)-1):
572 pipe1 = pipechain[i]
573 pipe2 = pipechain[i+1]
574 eqs += pipe1.connect_to_next(pipe2)
575
576 # connect front and back of chain to ourselves
577 front = pipechain[0]
578 end = pipechain[-1]
579 self.new_data(front, end, "chain") # NOTE: REPLACES existing data
580 eqs += front._connect_in(self)
581 eqs += end._connect_out(self)
582
583 return eqs
584
585 def set_input(self, i):
586 """ helper function to set the input data
587 """
588 return nmoperator.eq(self.p.data_i, i)
589
590 def __iter__(self):
591 yield from self.p
592 yield from self.n
593
594 def ports(self):
595 return list(self)
596