add docstrings
[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():
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):
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():
152 if isinstance(x, Iterable):
153 yield from x
154 else:
155 yield x
156
157 def ports(self):
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 ControlBase(Elaboratable):
440 """ Common functions for Pipeline API. Note: a "pipeline stage" only
441 exists (conceptually) when a ControlBase derivative is handed
442 a Stage (combinatorial block)
443 """
444 def __init__(self, stage=None, in_multi=None, stage_ctl=False):
445 """ Base class containing ready/valid/data to previous and next stages
446
447 * p: contains ready/valid to the previous stage
448 * n: contains ready/valid to the next stage
449
450 Except when calling Controlbase.connect(), user must also:
451 * add data_i member to PrevControl (p) and
452 * add data_o member to NextControl (n)
453 """
454 self.stage = stage
455
456 # set up input and output IO ACK (prev/next ready/valid)
457 self.p = PrevControl(in_multi, stage_ctl)
458 self.n = NextControl(stage_ctl)
459
460 # set up the input and output data
461 if stage is not None:
462 self.p.data_i = _spec(stage.ispec, "data_i") # input type
463 self.n.data_o = _spec(stage.ospec, "data_o") # output type
464
465 def connect_to_next(self, nxt):
466 """ helper function to connect to the next stage data/valid/ready.
467 """
468 return self.n.connect_to_next(nxt.p)
469
470 def _connect_in(self, prev):
471 """ internal helper function to connect stage to an input source.
472 do not use to connect stage-to-stage!
473 """
474 return self.p._connect_in(prev.p)
475
476 def _connect_out(self, nxt):
477 """ internal helper function to connect stage to an output source.
478 do not use to connect stage-to-stage!
479 """
480 return self.n._connect_out(nxt.n)
481
482 def connect(self, pipechain):
483 """ connects a chain (list) of Pipeline instances together and
484 links them to this ControlBase instance:
485
486 in <----> self <---> out
487 | ^
488 v |
489 [pipe1, pipe2, pipe3, pipe4]
490 | ^ | ^ | ^
491 v | v | v |
492 out---in out--in out---in
493
494 Also takes care of allocating data_i/data_o, by looking up
495 the data spec for each end of the pipechain. i.e It is NOT
496 necessary to allocate self.p.data_i or self.n.data_o manually:
497 this is handled AUTOMATICALLY, here.
498
499 Basically this function is the direct equivalent of StageChain,
500 except that unlike StageChain, the Pipeline logic is followed.
501
502 Just as StageChain presents an object that conforms to the
503 Stage API from a list of objects that also conform to the
504 Stage API, an object that calls this Pipeline connect function
505 has the exact same pipeline API as the list of pipline objects
506 it is called with.
507
508 Thus it becomes possible to build up larger chains recursively.
509 More complex chains (multi-input, multi-output) will have to be
510 done manually.
511
512 Argument:
513
514 * :pipechain: - a sequence of ControlBase-derived classes
515 (must be one or more in length)
516
517 Returns:
518
519 * a list of eq assignments that will need to be added in
520 an elaborate() to m.d.comb
521 """
522 assert len(pipechain) > 0, "pipechain must be non-zero length"
523 eqs = [] # collated list of assignment statements
524
525 # connect inter-chain
526 for i in range(len(pipechain)-1):
527 pipe1 = pipechain[i]
528 pipe2 = pipechain[i+1]
529 eqs += pipe1.connect_to_next(pipe2)
530
531 # connect front of chain to ourselves
532 front = pipechain[0]
533 self.p.data_i = _spec(front.stage.ispec, "chainin")
534 eqs += front._connect_in(self)
535
536 # connect end of chain to ourselves
537 end = pipechain[-1]
538 self.n.data_o = _spec(end.stage.ospec, "chainout")
539 eqs += end._connect_out(self)
540
541 return eqs
542
543 def _postprocess(self, i): # XXX DISABLED
544 return i # RETURNS INPUT
545 if hasattr(self.stage, "postprocess"):
546 return self.stage.postprocess(i)
547 return i
548
549 def set_input(self, i):
550 """ helper function to set the input data
551 """
552 return nmoperator.eq(self.p.data_i, i)
553
554 def __iter__(self):
555 yield from self.p
556 yield from self.n
557
558 def ports(self):
559 return list(self)
560
561 def elaborate(self, platform):
562 """ handles case where stage has dynamic ready/valid functions
563 """
564 m = Module()
565 m.submodules.p = self.p
566 m.submodules.n = self.n
567
568 if self.stage is not None and hasattr(self.stage, "setup"):
569 self.stage.setup(m, self.p.data_i)
570
571 if not self.p.stage_ctl:
572 return m
573
574 # intercept the previous (outgoing) "ready", combine with stage ready
575 m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready)
576
577 # intercept the next (incoming) "ready" and combine it with data valid
578 sdv = self.stage.d_valid(self.n.ready_i)
579 m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
580
581 return m
582