split stageapi into separate module, move ControlBase to singlepipe
[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 class PrevControl(Elaboratable):
168 """ contains signals that come *from* the previous stage (both in and out)
169 * valid_i: previous stage indicating all incoming data is valid.
170 may be a multi-bit signal, where all bits are required
171 to be asserted to indicate "valid".
172 * ready_o: output to next stage indicating readiness to accept data
173 * data_i : an input - MUST be added by the USER of this class
174 """
175
176 def __init__(self, i_width=1, stage_ctl=False):
177 self.stage_ctl = stage_ctl
178 self.valid_i = Signal(i_width, name="p_valid_i") # prev >>in self
179 self._ready_o = Signal(name="p_ready_o") # prev <<out self
180 self.data_i = None # XXX MUST BE ADDED BY USER
181 if stage_ctl:
182 self.s_ready_o = Signal(name="p_s_o_rdy") # prev <<out self
183 self.trigger = Signal(reset_less=True)
184
185 @property
186 def ready_o(self):
187 """ public-facing API: indicates (externally) that stage is ready
188 """
189 if self.stage_ctl:
190 return self.s_ready_o # set dynamically by stage
191 return self._ready_o # return this when not under dynamic control
192
193 def _connect_in(self, prev, direct=False, fn=None):
194 """ internal helper function to connect stage to an input source.
195 do not use to connect stage-to-stage!
196 """
197 valid_i = prev.valid_i if direct else prev.valid_i_test
198 data_i = fn(prev.data_i) if fn is not None else prev.data_i
199 return [self.valid_i.eq(valid_i),
200 prev.ready_o.eq(self.ready_o),
201 nmoperator.eq(self.data_i, data_i),
202 ]
203
204 @property
205 def valid_i_test(self):
206 vlen = len(self.valid_i)
207 if vlen > 1:
208 # multi-bit case: valid only when valid_i is all 1s
209 all1s = Const(-1, (len(self.valid_i), False))
210 valid_i = (self.valid_i == all1s)
211 else:
212 # single-bit valid_i case
213 valid_i = self.valid_i
214
215 # when stage indicates not ready, incoming data
216 # must "appear" to be not ready too
217 if self.stage_ctl:
218 valid_i = valid_i & self.s_ready_o
219
220 return valid_i
221
222 def elaborate(self, platform):
223 m = Module()
224 m.d.comb += self.trigger.eq(self.valid_i_test & self.ready_o)
225 return m
226
227 def eq(self, i):
228 return [self.data_i.eq(i.data_i),
229 self.ready_o.eq(i.ready_o),
230 self.valid_i.eq(i.valid_i)]
231
232 def __iter__(self):
233 yield self.valid_i
234 yield self.ready_o
235 if hasattr(self.data_i, "ports"):
236 yield from self.data_i.ports()
237 elif isinstance(self.data_i, Sequence):
238 yield from self.data_i
239 else:
240 yield self.data_i
241
242 def ports(self):
243 return list(self)
244
245
246 class NextControl(Elaboratable):
247 """ contains the signals that go *to* the next stage (both in and out)
248 * valid_o: output indicating to next stage that data is valid
249 * ready_i: input from next stage indicating that it can accept data
250 * data_o : an output - MUST be added by the USER of this class
251 """
252 def __init__(self, stage_ctl=False):
253 self.stage_ctl = stage_ctl
254 self.valid_o = Signal(name="n_valid_o") # self out>> next
255 self.ready_i = Signal(name="n_ready_i") # self <<in next
256 self.data_o = None # XXX MUST BE ADDED BY USER
257 #if self.stage_ctl:
258 self.d_valid = Signal(reset=1) # INTERNAL (data valid)
259 self.trigger = Signal(reset_less=True)
260
261 @property
262 def ready_i_test(self):
263 if self.stage_ctl:
264 return self.ready_i & self.d_valid
265 return self.ready_i
266
267 def connect_to_next(self, nxt):
268 """ helper function to connect to the next stage data/valid/ready.
269 data/valid is passed *TO* nxt, and ready comes *IN* from nxt.
270 use this when connecting stage-to-stage
271 """
272 return [nxt.valid_i.eq(self.valid_o),
273 self.ready_i.eq(nxt.ready_o),
274 nmoperator.eq(nxt.data_i, self.data_o),
275 ]
276
277 def _connect_out(self, nxt, direct=False, fn=None):
278 """ internal helper function to connect stage to an output source.
279 do not use to connect stage-to-stage!
280 """
281 ready_i = nxt.ready_i if direct else nxt.ready_i_test
282 data_o = fn(nxt.data_o) if fn is not None else nxt.data_o
283 return [nxt.valid_o.eq(self.valid_o),
284 self.ready_i.eq(ready_i),
285 nmoperator.eq(data_o, self.data_o),
286 ]
287
288 def elaborate(self, platform):
289 m = Module()
290 m.d.comb += self.trigger.eq(self.ready_i_test & self.valid_o)
291 return m
292
293 def __iter__(self):
294 yield self.ready_i
295 yield self.valid_o
296 if hasattr(self.data_o, "ports"):
297 yield from self.data_o.ports()
298 elif isinstance(self.data_o, Sequence):
299 yield from self.data_o
300 else:
301 yield self.data_o
302
303 def ports(self):
304 return list(self)
305