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