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