From c04ee65fdb5a6c1fb9129211484d1f71a7a15f1d Mon Sep 17 00:00:00 2001 From: Luke Kenneth Casson Leighton Date: Sat, 27 Apr 2019 16:06:23 +0100 Subject: [PATCH] split out IO control classes to separate module --- src/add/iocontrol.py | 514 ++++++++++++++++++++++++++++++++++++++++++ src/add/singlepipe.py | 508 +---------------------------------------- 2 files changed, 518 insertions(+), 504 deletions(-) create mode 100644 src/add/iocontrol.py diff --git a/src/add/iocontrol.py b/src/add/iocontrol.py new file mode 100644 index 00000000..b74d299f --- /dev/null +++ b/src/add/iocontrol.py @@ -0,0 +1,514 @@ +""" IO Control API + + Associated development bugs: + * http://bugs.libre-riscv.org/show_bug.cgi?id=64 + * http://bugs.libre-riscv.org/show_bug.cgi?id=57 + + Stage API: + --------- + + stage requires compliance with a strict API that may be + implemented in several means, including as a static class. + the methods of a stage instance must be as follows: + + * ispec() - Input data format specification + returns an object or a list or tuple of objects, or + a Record, each object having an "eq" function which + takes responsibility for copying by assignment all + sub-objects + * ospec() - Output data format specification + requirements as for ospec + * process(m, i) - Processes an ispec-formatted object + returns a combinatorial block of a result that + may be assigned to the output, by way of the "eq" + function + * setup(m, i) - Optional function for setting up submodules + may be used for more complex stages, to link + the input (i) to submodules. must take responsibility + for adding those submodules to the module (m). + the submodules must be combinatorial blocks and + must have their inputs and output linked combinatorially. + + Both StageCls (for use with non-static classes) and Stage (for use + by static classes) are abstract classes from which, for convenience + and as a courtesy to other developers, anything conforming to the + Stage API may *choose* to derive. + + StageChain: + ---------- + + A useful combinatorial wrapper around stages that chains them together + and then presents a Stage-API-conformant interface. By presenting + the same API as the stages it wraps, it can clearly be used recursively. + + ControlBase: + ----------- + + The base class for pipelines. Contains previous and next ready/valid/data. + Also has an extremely useful "connect" function that can be used to + connect a chain of pipelines and present the exact same prev/next + ready/valid/data API. +""" + +from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable +from nmigen.cli import verilog, rtlil +from nmigen.hdl.rec import Record + +from abc import ABCMeta, abstractmethod +from collections.abc import Sequence, Iterable +from collections import OrderedDict +import inspect + +from nmoperator import eq, cat, shape + + +class Object: + def __init__(self): + self.fields = OrderedDict() + + def __setattr__(self, k, v): + print ("kv", k, v) + if (k.startswith('_') or k in ["fields", "name", "src_loc"] or + k in dir(Object) or "fields" not in self.__dict__): + return object.__setattr__(self, k, v) + self.fields[k] = v + + def __getattr__(self, k): + if k in self.__dict__: + return object.__getattr__(self, k) + try: + return self.fields[k] + except KeyError as e: + raise AttributeError(e) + + def __iter__(self): + for x in self.fields.values(): + if isinstance(x, Iterable): + yield from x + else: + yield x + + def eq(self, inp): + res = [] + for (k, o) in self.fields.items(): + i = getattr(inp, k) + print ("eq", o, i) + rres = o.eq(i) + if isinstance(rres, Sequence): + res += rres + else: + res.append(rres) + print (res) + return res + + def ports(self): + return list(self) + + +class RecordObject(Record): + def __init__(self, layout=None, name=None): + Record.__init__(self, layout=layout or [], name=None) + + def __setattr__(self, k, v): + #print (dir(Record)) + if (k.startswith('_') or k in ["fields", "name", "src_loc"] or + k in dir(Record) or "fields" not in self.__dict__): + return object.__setattr__(self, k, v) + self.fields[k] = v + #print ("RecordObject setattr", k, v) + if isinstance(v, Record): + newlayout = {k: (k, v.layout)} + elif isinstance(v, Value): + newlayout = {k: (k, v.shape())} + else: + newlayout = {k: (k, shape(v))} + self.layout.fields.update(newlayout) + + def __iter__(self): + for x in self.fields.values(): + if isinstance(x, Iterable): + yield from x + else: + yield x + + def ports(self): + return list(self) + + +def _spec(fn, name=None): + if name is None: + return fn() + varnames = dict(inspect.getmembers(fn.__code__))['co_varnames'] + if 'name' in varnames: + return fn(name=name) + return fn() + + +class PrevControl(Elaboratable): + """ contains signals that come *from* the previous stage (both in and out) + * valid_i: previous stage indicating all incoming data is valid. + may be a multi-bit signal, where all bits are required + to be asserted to indicate "valid". + * ready_o: output to next stage indicating readiness to accept data + * data_i : an input - added by the user of this class + """ + + def __init__(self, i_width=1, stage_ctl=False): + self.stage_ctl = stage_ctl + self.valid_i = Signal(i_width, name="p_valid_i") # prev >>in self + self._ready_o = Signal(name="p_ready_o") # prev < 1: + # multi-bit case: valid only when valid_i is all 1s + all1s = Const(-1, (len(self.valid_i), False)) + valid_i = (self.valid_i == all1s) + else: + # single-bit valid_i case + valid_i = self.valid_i + + # when stage indicates not ready, incoming data + # must "appear" to be not ready too + if self.stage_ctl: + valid_i = valid_i & self.s_ready_o + + return valid_i + + def elaborate(self, platform): + m = Module() + m.d.comb += self.trigger.eq(self.valid_i_test & self.ready_o) + return m + + def eq(self, i): + return [self.data_i.eq(i.data_i), + self.ready_o.eq(i.ready_o), + self.valid_i.eq(i.valid_i)] + + def __iter__(self): + yield self.valid_i + yield self.ready_o + if hasattr(self.data_i, "ports"): + yield from self.data_i.ports() + elif isinstance(self.data_i, Sequence): + yield from self.data_i + else: + yield self.data_i + + def ports(self): + return list(self) + + +class NextControl(Elaboratable): + """ contains the signals that go *to* the next stage (both in and out) + * valid_o: output indicating to next stage that data is valid + * ready_i: input from next stage indicating that it can accept data + * data_o : an output - added by the user of this class + """ + def __init__(self, stage_ctl=False): + self.stage_ctl = stage_ctl + self.valid_o = Signal(name="n_valid_o") # self out>> next + self.ready_i = Signal(name="n_ready_i") # self < self <---> out + | ^ + v | + [pipe1, pipe2, pipe3, pipe4] + | ^ | ^ | ^ + v | v | v | + out---in out--in out---in + + Also takes care of allocating data_i/data_o, by looking up + the data spec for each end of the pipechain. i.e It is NOT + necessary to allocate self.p.data_i or self.n.data_o manually: + this is handled AUTOMATICALLY, here. + + Basically this function is the direct equivalent of StageChain, + except that unlike StageChain, the Pipeline logic is followed. + + Just as StageChain presents an object that conforms to the + Stage API from a list of objects that also conform to the + Stage API, an object that calls this Pipeline connect function + has the exact same pipeline API as the list of pipline objects + it is called with. + + Thus it becomes possible to build up larger chains recursively. + More complex chains (multi-input, multi-output) will have to be + done manually. + """ + eqs = [] # collated list of assignment statements + + # connect inter-chain + for i in range(len(pipechain)-1): + pipe1 = pipechain[i] + pipe2 = pipechain[i+1] + eqs += pipe1.connect_to_next(pipe2) + + # connect front of chain to ourselves + front = pipechain[0] + self.p.data_i = _spec(front.stage.ispec, "chainin") + eqs += front._connect_in(self) + + # connect end of chain to ourselves + end = pipechain[-1] + self.n.data_o = _spec(end.stage.ospec, "chainout") + eqs += end._connect_out(self) + + return eqs + + def _postprocess(self, i): # XXX DISABLED + return i # RETURNS INPUT + if hasattr(self.stage, "postprocess"): + return self.stage.postprocess(i) + return i + + def set_input(self, i): + """ helper function to set the input data + """ + return eq(self.p.data_i, i) + + def __iter__(self): + yield from self.p + yield from self.n + + def ports(self): + return list(self) + + def elaborate(self, platform): + """ handles case where stage has dynamic ready/valid functions + """ + m = Module() + m.submodules.p = self.p + m.submodules.n = self.n + + if self.stage is not None and hasattr(self.stage, "setup"): + self.stage.setup(m, self.p.data_i) + + if not self.p.stage_ctl: + return m + + # intercept the previous (outgoing) "ready", combine with stage ready + m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready) + + # intercept the next (incoming) "ready" and combine it with data valid + sdv = self.stage.d_valid(self.n.ready_i) + m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv) + + return m + + diff --git a/src/add/singlepipe.py b/src/add/singlepipe.py index be7fce7c..ae5d2e64 100644 --- a/src/add/singlepipe.py +++ b/src/add/singlepipe.py @@ -5,51 +5,6 @@ * http://bugs.libre-riscv.org/show_bug.cgi?id=64 * http://bugs.libre-riscv.org/show_bug.cgi?id=57 - eq: - -- - - a strategically very important function that is identical in function - to nmigen's Signal.eq function, except it may take objects, or a list - of objects, or a tuple of objects, and where objects may also be - Records. - - Stage API: - --------- - - stage requires compliance with a strict API that may be - implemented in several means, including as a static class. - the methods of a stage instance must be as follows: - - * ispec() - Input data format specification - returns an object or a list or tuple of objects, or - a Record, each object having an "eq" function which - takes responsibility for copying by assignment all - sub-objects - * ospec() - Output data format specification - requirements as for ospec - * process(m, i) - Processes an ispec-formatted object - returns a combinatorial block of a result that - may be assigned to the output, by way of the "eq" - function - * setup(m, i) - Optional function for setting up submodules - may be used for more complex stages, to link - the input (i) to submodules. must take responsibility - for adding those submodules to the module (m). - the submodules must be combinatorial blocks and - must have their inputs and output linked combinatorially. - - Both StageCls (for use with non-static classes) and Stage (for use - by static classes) are abstract classes from which, for convenience - and as a courtesy to other developers, anything conforming to the - Stage API may *choose* to derive. - - StageChain: - ---------- - - A useful combinatorial wrapper around stages that chains them together - and then presents a Stage-API-conformant interface. By presenting - the same API as the stages it wraps, it can clearly be used recursively. - RecordBasedStage: ---------------- @@ -70,14 +25,6 @@ StageChain, however when passed to UnbufferedPipeline they can be used to introduce a single clock delay. - ControlBase: - ----------- - - The base class for pipelines. Contains previous and next ready/valid/data. - Also has an extremely useful "connect" function that can be used to - connect a chain of pipelines and present the exact same prev/next - ready/valid/data API. - UnbufferedPipeline: ------------------ @@ -181,272 +128,10 @@ from queue import Queue import inspect from nmoperator import eq, cat, shape - - -class Object: - def __init__(self): - self.fields = OrderedDict() - - def __setattr__(self, k, v): - print ("kv", k, v) - if (k.startswith('_') or k in ["fields", "name", "src_loc"] or - k in dir(Object) or "fields" not in self.__dict__): - return object.__setattr__(self, k, v) - self.fields[k] = v - - def __getattr__(self, k): - if k in self.__dict__: - return object.__getattr__(self, k) - try: - return self.fields[k] - except KeyError as e: - raise AttributeError(e) - - def __iter__(self): - for x in self.fields.values(): - if isinstance(x, Iterable): - yield from x - else: - yield x - - def eq(self, inp): - res = [] - for (k, o) in self.fields.items(): - i = getattr(inp, k) - print ("eq", o, i) - rres = o.eq(i) - if isinstance(rres, Sequence): - res += rres - else: - res.append(rres) - print (res) - return res - - def ports(self): - return list(self) - - -class RecordObject(Record): - def __init__(self, layout=None, name=None): - Record.__init__(self, layout=layout or [], name=None) - - def __setattr__(self, k, v): - #print (dir(Record)) - if (k.startswith('_') or k in ["fields", "name", "src_loc"] or - k in dir(Record) or "fields" not in self.__dict__): - return object.__setattr__(self, k, v) - self.fields[k] = v - #print ("RecordObject setattr", k, v) - if isinstance(v, Record): - newlayout = {k: (k, v.layout)} - elif isinstance(v, Value): - newlayout = {k: (k, v.shape())} - else: - newlayout = {k: (k, shape(v))} - self.layout.fields.update(newlayout) - - def __iter__(self): - for x in self.fields.values(): - if isinstance(x, Iterable): - yield from x - else: - yield x - - def ports(self): - return list(self) - - -def _spec(fn, name=None): - if name is None: - return fn() - varnames = dict(inspect.getmembers(fn.__code__))['co_varnames'] - if 'name' in varnames: - return fn(name=name) - return fn() - - -class PrevControl(Elaboratable): - """ contains signals that come *from* the previous stage (both in and out) - * valid_i: previous stage indicating all incoming data is valid. - may be a multi-bit signal, where all bits are required - to be asserted to indicate "valid". - * ready_o: output to next stage indicating readiness to accept data - * data_i : an input - added by the user of this class - """ - - def __init__(self, i_width=1, stage_ctl=False): - self.stage_ctl = stage_ctl - self.valid_i = Signal(i_width, name="p_valid_i") # prev >>in self - self._ready_o = Signal(name="p_ready_o") # prev < 1: - # multi-bit case: valid only when valid_i is all 1s - all1s = Const(-1, (len(self.valid_i), False)) - valid_i = (self.valid_i == all1s) - else: - # single-bit valid_i case - valid_i = self.valid_i - - # when stage indicates not ready, incoming data - # must "appear" to be not ready too - if self.stage_ctl: - valid_i = valid_i & self.s_ready_o - - return valid_i - - def elaborate(self, platform): - m = Module() - m.d.comb += self.trigger.eq(self.valid_i_test & self.ready_o) - return m - - def eq(self, i): - return [self.data_i.eq(i.data_i), - self.ready_o.eq(i.ready_o), - self.valid_i.eq(i.valid_i)] - - def __iter__(self): - yield self.valid_i - yield self.ready_o - if hasattr(self.data_i, "ports"): - yield from self.data_i.ports() - elif isinstance(self.data_i, Sequence): - yield from self.data_i - else: - yield self.data_i - - def ports(self): - return list(self) - - -class NextControl(Elaboratable): - """ contains the signals that go *to* the next stage (both in and out) - * valid_o: output indicating to next stage that data is valid - * ready_i: input from next stage indicating that it can accept data - * data_o : an output - added by the user of this class - """ - def __init__(self, stage_ctl=False): - self.stage_ctl = stage_ctl - self.valid_o = Signal(name="n_valid_o") # self out>> next - self.ready_i = Signal(name="n_ready_i") # self < self <---> out - | ^ - v | - [pipe1, pipe2, pipe3, pipe4] - | ^ | ^ | ^ - v | v | v | - out---in out--in out---in - - Also takes care of allocating data_i/data_o, by looking up - the data spec for each end of the pipechain. i.e It is NOT - necessary to allocate self.p.data_i or self.n.data_o manually: - this is handled AUTOMATICALLY, here. - - Basically this function is the direct equivalent of StageChain, - except that unlike StageChain, the Pipeline logic is followed. - - Just as StageChain presents an object that conforms to the - Stage API from a list of objects that also conform to the - Stage API, an object that calls this Pipeline connect function - has the exact same pipeline API as the list of pipline objects - it is called with. - - Thus it becomes possible to build up larger chains recursively. - More complex chains (multi-input, multi-output) will have to be - done manually. - """ - eqs = [] # collated list of assignment statements - - # connect inter-chain - for i in range(len(pipechain)-1): - pipe1 = pipechain[i] - pipe2 = pipechain[i+1] - eqs += pipe1.connect_to_next(pipe2) - - # connect front of chain to ourselves - front = pipechain[0] - self.p.data_i = _spec(front.stage.ispec, "chainin") - eqs += front._connect_in(self) - - # connect end of chain to ourselves - end = pipechain[-1] - self.n.data_o = _spec(end.stage.ospec, "chainout") - eqs += end._connect_out(self) - - return eqs - - def _postprocess(self, i): # XXX DISABLED - return i # RETURNS INPUT - if hasattr(self.stage, "postprocess"): - return self.stage.postprocess(i) - return i - - def set_input(self, i): - """ helper function to set the input data - """ - return eq(self.p.data_i, i) - - def __iter__(self): - yield from self.p - yield from self.n - - def ports(self): - return list(self) - - def elaborate(self, platform): - """ handles case where stage has dynamic ready/valid functions - """ - m = Module() - m.submodules.p = self.p - m.submodules.n = self.n - - if self.stage is not None and hasattr(self.stage, "setup"): - self.stage.setup(m, self.p.data_i) - - if not self.p.stage_ctl: - return m - - # intercept the previous (outgoing) "ready", combine with stage ready - m.d.comb += self.p.s_ready_o.eq(self.p._ready_o & self.stage.d_ready) - - # intercept the next (incoming) "ready" and combine it with data valid - sdv = self.stage.d_valid(self.n.ready_i) - m.d.comb += self.n.d_valid.eq(self.n.ready_i & sdv) - - return m - - class BufferedHandshake(ControlBase): """ buffered pipeline stage. data and strobe signals travel in sync. if ever the input is ready and the output is not, processed data -- 2.30.2