chain cannot be empty
[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 StageChain(StageCls):
360 """ pass in a list of stages, and they will automatically be
361 chained together via their input and output specs into a
362 combinatorial chain, to create one giant combinatorial block.
363
364 the end result basically conforms to the exact same Stage API.
365
366 * input to this class will be the input of the first stage
367 * output of first stage goes into input of second
368 * output of second goes into input into third
369 * ... (etc. etc.)
370 * the output of this class will be the output of the last stage
371
372 NOTE: whilst this is very similar to ControlBase.connect(), it is
373 *really* important to appreciate that StageChain is pure
374 combinatorial and bypasses (does not involve, at all, ready/valid
375 signalling of any kind).
376
377 ControlBase.connect on the other hand respects, connects, and uses
378 ready/valid signalling.
379
380 Arguments:
381
382 * :chain: a chain of combinatorial blocks conforming to the Stage API
383 NOTE: StageChain.ispec and ospect have to have something
384 to return (beginning and end specs of the chain),
385 therefore the chain argument must be non-zero length
386
387 * :specallocate: if set, new input and output data will be allocated
388 and connected (eq'd) to each chained Stage.
389 in some cases if this is not done, the nmigen warning
390 "driving from two sources, module is being flattened"
391 will be issued.
392
393 NOTE: do NOT use StageChain with combinatorial blocks that have
394 side-effects (state-based / clock-based input) or conditional
395 (inter-chain) dependencies, unless you really know what you are doing.
396 """
397 def __init__(self, chain, specallocate=False):
398 assert len(chain) > 0, "stage chain must be non-zero length"
399 self.chain = chain
400 self.specallocate = specallocate
401
402 def ispec(self):
403 """ returns the ispec of the first of the chain
404 """
405 return _spec(self.chain[0].ispec, "chainin")
406
407 def ospec(self):
408 """ returns the ospec of the last of the chain
409 """
410 return _spec(self.chain[-1].ospec, "chainout")
411
412 def _specallocate_setup(self, m, i):
413 for (idx, c) in enumerate(self.chain):
414 if hasattr(c, "setup"):
415 c.setup(m, i) # stage may have some module stuff
416 ofn = self.chain[idx].ospec # last assignment survives
417 o = _spec(ofn, 'chainin%d' % idx)
418 m.d.comb += nmoperator.eq(o, c.process(i)) # process input into "o"
419 if idx == len(self.chain)-1:
420 break
421 ifn = self.chain[idx+1].ispec # new input on next loop
422 i = _spec(ifn, 'chainin%d' % (idx+1))
423 m.d.comb += nmoperator.eq(i, o) # assign to next input
424 return o # last loop is the output
425
426 def _noallocate_setup(self, m, i):
427 for (idx, c) in enumerate(self.chain):
428 if hasattr(c, "setup"):
429 c.setup(m, i) # stage may have some module stuff
430 i = o = c.process(i) # store input into "o"
431 return o # last loop is the output
432
433 def setup(self, m, i):
434 if self.specallocate:
435 self.o = self._specallocate_setup(m, i)
436 else:
437 self.o = self._noallocate_setup(m, i)
438
439 def process(self, i):
440 return self.o # conform to Stage API: return last-loop output
441
442
443 class StageHelper:
444 """ a convenience wrapper around something that is Stage-API-compliant.
445 (that "something" may be a static class, for example).
446 """
447 def __init__(self, stage):
448 self.stage = stage
449
450 def ospec(self, name):
451 assert self.stage is not None
452 return _spec(self.stage.ospec, name)
453
454 def ispec(self, name):
455 assert self.stage is not None
456 return _spec(self.stage.ispec, name)
457
458 def process(self, i):
459 if self.stage and hasattr(self.stage, "process"):
460 return self.stage.process(i)
461 return i
462
463 def setup(self, m, i):
464 if self.stage is not None and hasattr(self.stage, "setup"):
465 self.stage.setup(m, i)
466
467 def _postprocess(self, i): # XXX DISABLED
468 return i # RETURNS INPUT
469 if hasattr(self.stage, "postprocess"):
470 return self.stage.postprocess(i)
471 return i
472
473
474 class ControlBase(StageHelper, Elaboratable):
475 """ Common functions for Pipeline API. Note: a "pipeline stage" only
476 exists (conceptually) when a ControlBase derivative is handed
477 a Stage (combinatorial block)
478
479 NOTE: ControlBase derives from StageHelper, making it accidentally
480 compliant with the Stage API. Using those functions directly
481 *BYPASSES* a ControlBase instance ready/valid signalling, which
482 clearly should not be done without a really, really good reason.
483 """
484 def __init__(self, stage=None, in_multi=None, stage_ctl=False):
485 """ Base class containing ready/valid/data to previous and next stages
486
487 * p: contains ready/valid to the previous stage
488 * n: contains ready/valid to the next stage
489
490 Except when calling Controlbase.connect(), user must also:
491 * add data_i member to PrevControl (p) and
492 * add data_o member to NextControl (n)
493 Calling ControlBase._new_data is a good way to do that.
494 """
495 StageHelper.__init__(self, stage)
496
497 # set up input and output IO ACK (prev/next ready/valid)
498 self.p = PrevControl(in_multi, stage_ctl)
499 self.n = NextControl(stage_ctl)
500
501 # set up the input and output data
502 if stage is not None:
503 self._new_data(self, self, "data")
504
505 def _new_data(self, p, n, name):
506 """ allocates new data_i and data_o
507 """
508 self.p.data_i = _spec(p.stage.ispec, "%s_i" % name)
509 self.n.data_o = _spec(n.stage.ospec, "%s_o" % name)
510
511 @property
512 def data_r(self):
513 return self.process(self.p.data_i)
514
515 def connect_to_next(self, nxt):
516 """ helper function to connect to the next stage data/valid/ready.
517 """
518 return self.n.connect_to_next(nxt.p)
519
520 def _connect_in(self, prev):
521 """ internal helper function to connect stage to an input source.
522 do not use to connect stage-to-stage!
523 """
524 return self.p._connect_in(prev.p)
525
526 def _connect_out(self, nxt):
527 """ internal helper function to connect stage to an output source.
528 do not use to connect stage-to-stage!
529 """
530 return self.n._connect_out(nxt.n)
531
532 def connect(self, pipechain):
533 """ connects a chain (list) of Pipeline instances together and
534 links them to this ControlBase instance:
535
536 in <----> self <---> out
537 | ^
538 v |
539 [pipe1, pipe2, pipe3, pipe4]
540 | ^ | ^ | ^
541 v | v | v |
542 out---in out--in out---in
543
544 Also takes care of allocating data_i/data_o, by looking up
545 the data spec for each end of the pipechain. i.e It is NOT
546 necessary to allocate self.p.data_i or self.n.data_o manually:
547 this is handled AUTOMATICALLY, here.
548
549 Basically this function is the direct equivalent of StageChain,
550 except that unlike StageChain, the Pipeline logic is followed.
551
552 Just as StageChain presents an object that conforms to the
553 Stage API from a list of objects that also conform to the
554 Stage API, an object that calls this Pipeline connect function
555 has the exact same pipeline API as the list of pipline objects
556 it is called with.
557
558 Thus it becomes possible to build up larger chains recursively.
559 More complex chains (multi-input, multi-output) will have to be
560 done manually.
561
562 Argument:
563
564 * :pipechain: - a sequence of ControlBase-derived classes
565 (must be one or more in length)
566
567 Returns:
568
569 * a list of eq assignments that will need to be added in
570 an elaborate() to m.d.comb
571 """
572 assert len(pipechain) > 0, "pipechain must be non-zero length"
573 eqs = [] # collated list of assignment statements
574
575 # connect inter-chain
576 for i in range(len(pipechain)-1):
577 pipe1 = pipechain[i]
578 pipe2 = pipechain[i+1]
579 eqs += pipe1.connect_to_next(pipe2)
580
581 # connect front and back of chain to ourselves
582 front = pipechain[0]
583 end = pipechain[-1]
584 self._new_data(front, end, "chain") # NOTE: REPLACES existing data
585 eqs += front._connect_in(self)
586 eqs += end._connect_out(self)
587
588 return eqs
589
590 def set_input(self, i):
591 """ helper function to set the input data
592 """
593 return nmoperator.eq(self.p.data_i, i)
594
595 def __iter__(self):
596 yield from self.p
597 yield from self.n
598
599 def ports(self):
600 return list(self)
601
602 def elaborate(self, platform):
603 """ handles case where stage has dynamic ready/valid functions
604 """
605 m = Module()
606 m.submodules.p = self.p
607 m.submodules.n = self.n
608
609 self.setup(m, self.p.data_i)
610
611 if not self.p.stage_ctl:
612 return m
613
614 # intercept the previous (outgoing) "ready", combine with stage ready
615 m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready)
616
617 # intercept the next (incoming) "ready" and combine it with data valid
618 sdv = self.stage.d_valid(self.n.ready_i)
619 m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv)
620
621 return m
622