From: Luke Kenneth Casson Leighton Date: Thu, 2 May 2019 12:57:20 +0000 (+0100) Subject: move singlepipe, multipipe, nmoperator and pipeline.py to nmutil X-Git-Tag: ls180-24jan2020~1083 X-Git-Url: https://git.libre-soc.org/?p=ieee754fpu.git;a=commitdiff_plain;h=919e349e32ec226a14c8c01ba6b3c0112d4495a3;hp=da918b3777224f5fe562d23eebc3b110261a8c8f move singlepipe, multipipe, nmoperator and pipeline.py to nmutil --- diff --git a/src/ieee754/add/multipipe.py b/src/ieee754/add/multipipe.py deleted file mode 100644 index e24703f8..00000000 --- a/src/ieee754/add/multipipe.py +++ /dev/null @@ -1,358 +0,0 @@ -""" Combinatorial Multi-input and Multi-output multiplexer blocks - conforming to Pipeline API - - Multi-input is complex because if any one input is ready, the output - can be ready, and the decision comes from a separate module. - - Multi-output is simple (pretty much identical to UnbufferedPipeline), - and the selection is just a mux. The only proviso (difference) being: - the outputs not being selected have to have their ready_o signals - DEASSERTED. -""" - -from math import log -from nmigen import Signal, Cat, Const, Mux, Module, Array, Elaboratable -from nmigen.cli import verilog, rtlil -from nmigen.lib.coding import PriorityEncoder -from nmigen.hdl.rec import Record, Layout -from stageapi import _spec - -from collections.abc import Sequence - -from example_buf_pipe import eq, NextControl, PrevControl, ExampleStage - - -class MultiInControlBase(Elaboratable): - """ Common functions for Pipeline API - """ - def __init__(self, in_multi=None, p_len=1): - """ Multi-input Control class. Conforms to same API as ControlBase... - mostly. has additional indices to the *multiple* input stages - - * p: contains ready/valid to the previous stages PLURAL - * n: contains ready/valid to the next stage - - User must also: - * add data_i members to PrevControl and - * add data_o member to NextControl - """ - # set up input and output IO ACK (prev/next ready/valid) - p = [] - for i in range(p_len): - p.append(PrevControl(in_multi)) - self.p = Array(p) - self.n = NextControl() - - def connect_to_next(self, nxt, p_idx=0): - """ helper function to connect to the next stage data/valid/ready. - """ - return self.n.connect_to_next(nxt.p[p_idx]) - - def _connect_in(self, prev, idx=0, prev_idx=None): - """ helper function to connect stage to an input source. do not - use to connect stage-to-stage! - """ - if prev_idx is None: - return self.p[idx]._connect_in(prev.p) - return self.p[idx]._connect_in(prev.p[prev_idx]) - - def _connect_out(self, nxt): - """ helper function to connect stage to an output source. do not - use to connect stage-to-stage! - """ - if nxt_idx is None: - return self.n._connect_out(nxt.n) - return self.n._connect_out(nxt.n) - - def set_input(self, i, idx=0): - """ helper function to set the input data - """ - return eq(self.p[idx].data_i, i) - - def elaborate(self, platform): - m = Module() - for i, p in enumerate(self.p): - setattr(m.submodules, "p%d" % i, p) - m.submodules.n = self.n - return m - - def __iter__(self): - for p in self.p: - yield from p - yield from self.n - - def ports(self): - return list(self) - - -class MultiOutControlBase(Elaboratable): - """ Common functions for Pipeline API - """ - def __init__(self, n_len=1, in_multi=None): - """ Multi-output Control class. Conforms to same API as ControlBase... - mostly. has additional indices to the multiple *output* stages - [MultiInControlBase has multiple *input* stages] - - * p: contains ready/valid to the previou stage - * n: contains ready/valid to the next stages PLURAL - - User must also: - * add data_i member to PrevControl and - * add data_o members to NextControl - """ - - # set up input and output IO ACK (prev/next ready/valid) - self.p = PrevControl(in_multi) - n = [] - for i in range(n_len): - n.append(NextControl()) - self.n = Array(n) - - def connect_to_next(self, nxt, n_idx=0): - """ helper function to connect to the next stage data/valid/ready. - """ - return self.n[n_idx].connect_to_next(nxt.p) - - def _connect_in(self, prev, idx=0): - """ helper function to connect stage to an input source. do not - use to connect stage-to-stage! - """ - return self.n[idx]._connect_in(prev.p) - - def _connect_out(self, nxt, idx=0, nxt_idx=None): - """ helper function to connect stage to an output source. do not - use to connect stage-to-stage! - """ - if nxt_idx is None: - return self.n[idx]._connect_out(nxt.n) - return self.n[idx]._connect_out(nxt.n[nxt_idx]) - - def elaborate(self, platform): - m = Module() - m.submodules.p = self.p - for i, n in enumerate(self.n): - setattr(m.submodules, "n%d" % i, n) - return m - - 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 - for n in self.n: - yield from n - - def ports(self): - return list(self) - - -class CombMultiOutPipeline(MultiOutControlBase): - """ A multi-input Combinatorial block conforming to the Pipeline API - - Attributes: - ----------- - p.data_i : stage input data (non-array). shaped according to ispec - n.data_o : stage output data array. shaped according to ospec - """ - - def __init__(self, stage, n_len, n_mux): - MultiOutControlBase.__init__(self, n_len=n_len) - self.stage = stage - self.n_mux = n_mux - - # set up the input and output data - self.p.data_i = _spec(stage.ispec, 'data_i') # input type - for i in range(n_len): - name = 'data_o_%d' % i - self.n[i].data_o = _spec(stage.ospec, name) # output type - - def process(self, i): - if hasattr(self.stage, "process"): - return self.stage.process(i) - return i - - def elaborate(self, platform): - m = MultiOutControlBase.elaborate(self, platform) - - if hasattr(self.n_mux, "elaborate"): # TODO: identify submodule? - m.submodules += self.n_mux - - # need buffer register conforming to *input* spec - r_data = _spec(self.stage.ispec, 'r_data') # input type - if hasattr(self.stage, "setup"): - self.stage.setup(m, r_data) - - # multiplexer id taken from n_mux - mid = self.n_mux.m_id - - # temporaries - p_valid_i = Signal(reset_less=True) - pv = Signal(reset_less=True) - m.d.comb += p_valid_i.eq(self.p.valid_i_test) - m.d.comb += pv.eq(self.p.valid_i & self.p.ready_o) - - # all outputs to next stages first initialised to zero (invalid) - # the only output "active" is then selected by the muxid - for i in range(len(self.n)): - m.d.comb += self.n[i].valid_o.eq(0) - data_valid = self.n[mid].valid_o - m.d.comb += self.p.ready_o.eq(~data_valid | self.n[mid].ready_i) - m.d.comb += data_valid.eq(p_valid_i | \ - (~self.n[mid].ready_i & data_valid)) - with m.If(pv): - m.d.comb += eq(r_data, self.p.data_i) - m.d.comb += eq(self.n[mid].data_o, self.process(r_data)) - - return m - - -class CombMultiInPipeline(MultiInControlBase): - """ A multi-input Combinatorial block conforming to the Pipeline API - - Attributes: - ----------- - p.data_i : StageInput, shaped according to ispec - The pipeline input - p.data_o : StageOutput, shaped according to ospec - The pipeline output - r_data : input_shape according to ispec - A temporary (buffered) copy of a prior (valid) input. - This is HELD if the output is not ready. It is updated - SYNCHRONOUSLY. - """ - - def __init__(self, stage, p_len, p_mux): - MultiInControlBase.__init__(self, p_len=p_len) - self.stage = stage - self.p_mux = p_mux - - # set up the input and output data - for i in range(p_len): - name = 'data_i_%d' % i - self.p[i].data_i = _spec(stage.ispec, name) # input type - self.n.data_o = _spec(stage.ospec, 'data_o') - - def process(self, i): - if hasattr(self.stage, "process"): - return self.stage.process(i) - return i - - def elaborate(self, platform): - m = MultiInControlBase.elaborate(self, platform) - - m.submodules += self.p_mux - - # need an array of buffer registers conforming to *input* spec - r_data = [] - data_valid = [] - p_valid_i = [] - n_ready_in = [] - p_len = len(self.p) - for i in range(p_len): - name = 'r_%d' % i - r = _spec(self.stage.ispec, name) # input type - r_data.append(r) - data_valid.append(Signal(name="data_valid", reset_less=True)) - p_valid_i.append(Signal(name="p_valid_i", reset_less=True)) - n_ready_in.append(Signal(name="n_ready_in", reset_less=True)) - if hasattr(self.stage, "setup"): - self.stage.setup(m, r) - if len(r_data) > 1: - r_data = Array(r_data) - p_valid_i = Array(p_valid_i) - n_ready_in = Array(n_ready_in) - data_valid = Array(data_valid) - - nirn = Signal(reset_less=True) - m.d.comb += nirn.eq(~self.n.ready_i) - mid = self.p_mux.m_id - for i in range(p_len): - m.d.comb += data_valid[i].eq(0) - m.d.comb += n_ready_in[i].eq(1) - m.d.comb += p_valid_i[i].eq(0) - m.d.comb += self.p[i].ready_o.eq(0) - m.d.comb += p_valid_i[mid].eq(self.p_mux.active) - m.d.comb += self.p[mid].ready_o.eq(~data_valid[mid] | self.n.ready_i) - m.d.comb += n_ready_in[mid].eq(nirn & data_valid[mid]) - anyvalid = Signal(i, reset_less=True) - av = [] - for i in range(p_len): - av.append(data_valid[i]) - anyvalid = Cat(*av) - m.d.comb += self.n.valid_o.eq(anyvalid.bool()) - m.d.comb += data_valid[mid].eq(p_valid_i[mid] | \ - (n_ready_in[mid] & data_valid[mid])) - - for i in range(p_len): - vr = Signal(reset_less=True) - m.d.comb += vr.eq(self.p[i].valid_i & self.p[i].ready_o) - with m.If(vr): - m.d.comb += eq(r_data[i], self.p[i].data_i) - - m.d.comb += eq(self.n.data_o, self.process(r_data[mid])) - - return m - - -class CombMuxOutPipe(CombMultiOutPipeline): - def __init__(self, stage, n_len): - # HACK: stage is also the n-way multiplexer - CombMultiOutPipeline.__init__(self, stage, n_len=n_len, n_mux=stage) - - # HACK: n-mux is also the stage... so set the muxid equal to input mid - stage.m_id = self.p.data_i.mid - - - -class InputPriorityArbiter(Elaboratable): - """ arbitration module for Input-Mux pipe, baed on PriorityEncoder - """ - def __init__(self, pipe, num_rows): - self.pipe = pipe - self.num_rows = num_rows - self.mmax = int(log(self.num_rows) / log(2)) - self.m_id = Signal(self.mmax, reset_less=True) # multiplex id - self.active = Signal(reset_less=True) - - def elaborate(self, platform): - m = Module() - - assert len(self.pipe.p) == self.num_rows, \ - "must declare input to be same size" - pe = PriorityEncoder(self.num_rows) - m.submodules.selector = pe - - # connect priority encoder - in_ready = [] - for i in range(self.num_rows): - p_valid_i = Signal(reset_less=True) - m.d.comb += p_valid_i.eq(self.pipe.p[i].valid_i_test) - in_ready.append(p_valid_i) - m.d.comb += pe.i.eq(Cat(*in_ready)) # array of input "valids" - m.d.comb += self.active.eq(~pe.n) # encoder active (one input valid) - m.d.comb += self.m_id.eq(pe.o) # output one active input - - return m - - def ports(self): - return [self.m_id, self.active] - - - -class PriorityCombMuxInPipe(CombMultiInPipeline): - """ an example of how to use the combinatorial pipeline. - """ - - def __init__(self, stage, p_len=2): - p_mux = InputPriorityArbiter(self, p_len) - CombMultiInPipeline.__init__(self, stage, p_len, p_mux) - - -if __name__ == '__main__': - - dut = PriorityCombMuxInPipe(ExampleStage) - vl = rtlil.convert(dut, ports=dut.ports()) - with open("test_combpipe.il", "w") as f: - f.write(vl) diff --git a/src/ieee754/add/nmoperator.py b/src/ieee754/add/nmoperator.py deleted file mode 100644 index bd5e5544..00000000 --- a/src/ieee754/add/nmoperator.py +++ /dev/null @@ -1,171 +0,0 @@ -""" nmigen operator functions / utils - - 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. -""" - -from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable -from nmigen.cli import verilog, rtlil -from nmigen.lib.fifo import SyncFIFO, SyncFIFOBuffered -from nmigen.hdl.ast import ArrayProxy -from nmigen.hdl.rec import Record, Layout - -from abc import ABCMeta, abstractmethod -from collections.abc import Sequence, Iterable -from collections import OrderedDict -from queue import Queue -import inspect - - -class Visitor2: - """ a helper class for iterating twin-argument compound data structures. - - Record is a special (unusual, recursive) case, where the input may be - specified as a dictionary (which may contain further dictionaries, - recursively), where the field names of the dictionary must match - the Record's field spec. Alternatively, an object with the same - member names as the Record may be assigned: it does not have to - *be* a Record. - - ArrayProxy is also special-cased, it's a bit messy: whilst ArrayProxy - has an eq function, the object being assigned to it (e.g. a python - object) might not. despite the *input* having an eq function, - that doesn't help us, because it's the *ArrayProxy* that's being - assigned to. so.... we cheat. use the ports() function of the - python object, enumerate them, find out the list of Signals that way, - and assign them. - """ - def iterator2(self, o, i): - if isinstance(o, dict): - yield from self.dict_iter2(o, i) - - if not isinstance(o, Sequence): - o, i = [o], [i] - for (ao, ai) in zip(o, i): - #print ("visit", fn, ao, ai) - if isinstance(ao, Record): - yield from self.record_iter2(ao, ai) - elif isinstance(ao, ArrayProxy) and not isinstance(ai, Value): - yield from self.arrayproxy_iter2(ao, ai) - else: - yield (ao, ai) - - def dict_iter2(self, o, i): - for (k, v) in o.items(): - print ("d-iter", v, i[k]) - yield (v, i[k]) - return res - - def _not_quite_working_with_all_unit_tests_record_iter2(self, ao, ai): - print ("record_iter2", ao, ai, type(ao), type(ai)) - if isinstance(ai, Value): - if isinstance(ao, Sequence): - ao, ai = [ao], [ai] - for o, i in zip(ao, ai): - yield (o, i) - return - for idx, (field_name, field_shape, _) in enumerate(ao.layout): - if isinstance(field_shape, Layout): - val = ai.fields - else: - val = ai - if hasattr(val, field_name): # check for attribute - val = getattr(val, field_name) - else: - val = val[field_name] # dictionary-style specification - yield from self.iterator2(ao.fields[field_name], val) - - def record_iter2(self, ao, ai): - for idx, (field_name, field_shape, _) in enumerate(ao.layout): - if isinstance(field_shape, Layout): - val = ai.fields - else: - val = ai - if hasattr(val, field_name): # check for attribute - val = getattr(val, field_name) - else: - val = val[field_name] # dictionary-style specification - yield from self.iterator2(ao.fields[field_name], val) - - def arrayproxy_iter2(self, ao, ai): - for p in ai.ports(): - op = getattr(ao, p.name) - print ("arrayproxy - p", p, p.name) - yield from self.iterator2(op, p) - - -class Visitor: - """ a helper class for iterating single-argument compound data structures. - similar to Visitor2. - """ - def iterate(self, i): - """ iterate a compound structure recursively using yield - """ - if not isinstance(i, Sequence): - i = [i] - for ai in i: - #print ("iterate", ai) - if isinstance(ai, Record): - #print ("record", list(ai.layout)) - yield from self.record_iter(ai) - elif isinstance(ai, ArrayProxy) and not isinstance(ai, Value): - yield from self.array_iter(ai) - else: - yield ai - - def record_iter(self, ai): - for idx, (field_name, field_shape, _) in enumerate(ai.layout): - if isinstance(field_shape, Layout): - val = ai.fields - else: - val = ai - if hasattr(val, field_name): # check for attribute - val = getattr(val, field_name) - else: - val = val[field_name] # dictionary-style specification - #print ("recidx", idx, field_name, field_shape, val) - yield from self.iterate(val) - - def array_iter(self, ai): - for p in ai.ports(): - yield from self.iterate(p) - - -def eq(o, i): - """ makes signals equal: a helper routine which identifies if it is being - passed a list (or tuple) of objects, or signals, or Records, and calls - the objects' eq function. - """ - res = [] - for (ao, ai) in Visitor2().iterator2(o, i): - rres = ao.eq(ai) - if not isinstance(rres, Sequence): - rres = [rres] - res += rres - return res - - -def shape(i): - #print ("shape", i) - r = 0 - for part in list(i): - #print ("shape?", part) - s, _ = part.shape() - r += s - return r, False - - -def cat(i): - """ flattens a compound structure recursively using Cat - """ - from nmigen.tools import flatten - #res = list(flatten(i)) # works (as of nmigen commit f22106e5) HOWEVER... - res = list(Visitor().iterate(i)) # needed because input may be a sequence - return Cat(*res) - - diff --git a/src/ieee754/add/pipeline.py b/src/ieee754/add/pipeline.py deleted file mode 100644 index afcee743..00000000 --- a/src/ieee754/add/pipeline.py +++ /dev/null @@ -1,394 +0,0 @@ -""" Example 5: Making use of PyRTL and Introspection. """ - -from collections.abc import Sequence - -from nmigen import Signal -from nmigen.hdl.rec import Record -from nmigen import tracer -from nmigen.compat.fhdl.bitcontainer import value_bits_sign -from contextlib import contextmanager - -from nmoperator import eq -from singlepipe import StageCls, ControlBase, BufferedHandshake -from singlepipe import UnbufferedPipeline - - -# The following example shows how pyrtl can be used to make some interesting -# hardware structures using python introspection. In particular, this example -# makes a N-stage pipeline structure. Any specific pipeline is then a derived -# class of SimplePipeline where methods with names starting with "stage" are -# stages, and new members with names not starting with "_" are to be registered -# for the next stage. - -def like(value, rname, pipe, pipemode=False): - if isinstance(value, ObjectProxy): - return ObjectProxy.like(pipe, value, pipemode=pipemode, - name=rname, reset_less=True) - else: - return Signal(value_bits_sign(value), name=rname, - reset_less=True) - return Signal.like(value, name=rname, reset_less=True) - -def get_assigns(_assigns): - assigns = [] - for e in _assigns: - if isinstance(e, ObjectProxy): - assigns += get_assigns(e._assigns) - else: - assigns.append(e) - return assigns - - -def get_eqs(_eqs): - eqs = [] - for e in _eqs: - if isinstance(e, ObjectProxy): - eqs += get_eqs(e._eqs) - else: - eqs.append(e) - return eqs - - -class ObjectProxy: - def __init__(self, m, name=None, pipemode=False, syncmode=True): - self._m = m - if name is None: - name = tracer.get_var_name(default=None) - self.name = name - self._pipemode = pipemode - self._syncmode = syncmode - self._eqs = {} - self._assigns = [] - self._preg_map = {} - - @classmethod - def like(cls, m, value, pipemode=False, name=None, src_loc_at=0, **kwargs): - name = name or tracer.get_var_name(depth=2 + src_loc_at, - default="$like") - - src_loc_at_1 = 1 + src_loc_at - r = ObjectProxy(m, value.name, pipemode) - #for a, aname in value._preg_map.items(): - # r._preg_map[aname] = like(a, aname, m, pipemode) - for a in value.ports(): - aname = a.name - r._preg_map[aname] = like(a, aname, m, pipemode) - return r - - def __repr__(self): - subobjs = [] - for a in self.ports(): - aname = a.name - ai = self._preg_map[aname] - subobjs.append(repr(ai)) - return "" % subobjs - - def get_specs(self, liked=False): - res = [] - for k, v in self._preg_map.items(): - #v = like(v, k, stage._m) - res.append(v) - if isinstance(v, ObjectProxy): - res += v.get_specs() - return res - - def eq(self, i): - print ("ObjectProxy eq", self, i) - res = [] - for a in self.ports(): - aname = a.name - ai = i._preg_map[aname] - res.append(a.eq(ai)) - return res - - def ports(self): - res = [] - for aname, a in self._preg_map.items(): - if isinstance(a, Signal) or isinstance(a, ObjectProxy) or \ - isinstance(a, Record): - res.append(a) - #print ("ObjectPorts", res) - return res - - def __getattr__(self, name): - try: - v = self._preg_map[name] - return v - #return like(v, name, self._m) - except KeyError: - raise AttributeError( - 'error, no pipeline register "%s" defined for OP %s' - % (name, self.name)) - - def __setattr__(self, name, value): - if name.startswith('_') or name in ['name', 'ports', 'eq', 'like']: - # do not do anything tricky with variables starting with '_' - object.__setattr__(self, name, value) - return - #rname = "%s_%s" % (self.name, name) - rname = name - new_pipereg = like(value, rname, self._m, self._pipemode) - self._preg_map[name] = new_pipereg - #object.__setattr__(self, name, new_pipereg) - if self._pipemode: - #print ("OP pipemode", self._syncmode, new_pipereg, value) - assign = eq(new_pipereg, value) - if self._syncmode: - self._m.d.sync += assign - else: - self._m.d.comb += assign - elif self._m: - #print ("OP !pipemode assign", new_pipereg, value, type(value)) - self._m.d.comb += eq(new_pipereg, value) - else: - #print ("OP !pipemode !m", new_pipereg, value, type(value)) - self._assigns += eq(new_pipereg, value) - if isinstance(value, ObjectProxy): - #print ("OP, defer assigns:", value._assigns) - self._assigns += value._assigns - self._eqs.append(value._eqs) - - -class PipelineStage: - """ Pipeline builder stage with auto generation of pipeline registers. - """ - - def __init__(self, name, m, prev=None, pipemode=False, ispec=None): - self._m = m - self._stagename = name - self._preg_map = {'__nextstage__': {}} - self._prev_stage = prev - self._ispec = ispec - if ispec: - self._preg_map[self._stagename] = ispec - if prev: - print ("prev", prev._stagename, prev._preg_map) - #if prev._stagename in prev._preg_map: - # m = prev._preg_map[prev._stagename] - # self._preg_map[prev._stagename] = m - if '__nextstage__' in prev._preg_map: - m = prev._preg_map['__nextstage__'] - m = likedict(m) - self._preg_map[self._stagename] = m - #for k, v in m.items(): - #m[k] = like(v, k, self._m) - print ("make current", self._stagename, m) - self._pipemode = pipemode - self._eqs = {} - self._assigns = [] - - def __getattribute__(self, name): - if name.startswith('_'): - return object.__getattribute__(self, name) - #if name in self._preg_map['__nextstage__']: - # return self._preg_map['__nextstage__'][name] - try: - print ("getattr", name, object.__getattribute__(self, '_preg_map')) - v = self._preg_map[self._stagename][name] - return v - #return like(v, name, self._m) - except KeyError: - raise AttributeError( - 'error, no pipeline register "%s" defined for stage %s' - % (name, self._stagename)) - - def __setattr__(self, name, value): - if name.startswith('_'): - # do not do anything tricky with variables starting with '_' - object.__setattr__(self, name, value) - return - pipereg_id = self._stagename - rname = 'pipereg_' + pipereg_id + '_' + name - new_pipereg = like(value, rname, self._m, self._pipemode) - next_stage = '__nextstage__' - if next_stage not in self._preg_map: - self._preg_map[next_stage] = {} - self._preg_map[next_stage][name] = new_pipereg - print ("setattr", name, value, self._preg_map) - if self._pipemode: - self._eqs[name] = new_pipereg - assign = eq(new_pipereg, value) - print ("pipemode: append", new_pipereg, value, assign) - if isinstance(value, ObjectProxy): - print ("OP, assigns:", value._assigns) - self._assigns += value._assigns - self._eqs[name]._eqs = value._eqs - #self._m.d.comb += assign - self._assigns += assign - elif self._m: - print ("!pipemode: assign", new_pipereg, value) - assign = eq(new_pipereg, value) - self._m.d.sync += assign - else: - print ("!pipemode !m: defer assign", new_pipereg, value) - assign = eq(new_pipereg, value) - self._eqs[name] = new_pipereg - self._assigns += assign - if isinstance(value, ObjectProxy): - print ("OP, defer assigns:", value._assigns) - self._assigns += value._assigns - self._eqs[name]._eqs = value._eqs - -def likelist(specs): - res = [] - for v in specs: - res.append(like(v, v.name, None, pipemode=True)) - return res - -def likedict(specs): - if not isinstance(specs, dict): - return like(specs, specs.name, None, pipemode=True) - res = {} - for k, v in specs.items(): - res[k] = likedict(v) - return res - - -class AutoStage(StageCls): - def __init__(self, inspecs, outspecs, eqs, assigns): - self.inspecs, self.outspecs = inspecs, outspecs - self.eqs, self.assigns = eqs, assigns - #self.o = self.ospec() - def ispec(self): return likedict(self.inspecs) - def ospec(self): return likedict(self.outspecs) - - def process(self, i): - print ("stage process", i) - return self.eqs - - def setup(self, m, i): - print ("stage setup i", i, m) - print ("stage setup inspecs", self.inspecs) - print ("stage setup outspecs", self.outspecs) - print ("stage setup eqs", self.eqs) - #self.o = self.ospec() - m.d.comb += eq(self.inspecs, i) - #m.d.comb += eq(self.outspecs, self.eqs) - #m.d.comb += eq(self.o, i) - - -class AutoPipe(UnbufferedPipeline): - def __init__(self, stage, assigns): - UnbufferedPipeline.__init__(self, stage) - self.assigns = assigns - - def elaborate(self, platform): - m = UnbufferedPipeline.elaborate(self, platform) - m.d.comb += self.assigns - print ("assigns", self.assigns, m) - return m - - -class PipeManager: - def __init__(self, m, pipemode=False, pipetype=None): - self.m = m - self.pipemode = pipemode - self.pipetype = pipetype - - @contextmanager - def Stage(self, name, prev=None, ispec=None): - if ispec: - ispec = likedict(ispec) - print ("start stage", name, ispec) - stage = PipelineStage(name, None, prev, self.pipemode, ispec=ispec) - try: - yield stage, self.m #stage._m - finally: - pass - if self.pipemode: - if stage._ispec: - print ("use ispec", stage._ispec) - inspecs = stage._ispec - else: - inspecs = self.get_specs(stage, name) - #inspecs = likedict(inspecs) - outspecs = self.get_specs(stage, '__nextstage__', liked=True) - print ("stage inspecs", name, inspecs) - print ("stage outspecs", name, outspecs) - eqs = stage._eqs # get_eqs(stage._eqs) - assigns = get_assigns(stage._assigns) - print ("stage eqs", name, eqs) - print ("stage assigns", name, assigns) - s = AutoStage(inspecs, outspecs, eqs, assigns) - self.stages.append(s) - print ("end stage", name, self.pipemode, "\n") - - def get_specs(self, stage, name, liked=False): - return stage._preg_map[name] - if name in stage._preg_map: - res = [] - for k, v in stage._preg_map[name].items(): - #v = like(v, k, stage._m) - res.append(v) - #if isinstance(v, ObjectProxy): - # res += v.get_specs() - return res - return {} - - def __enter__(self): - self.stages = [] - return self - - def __exit__(self, *args): - print ("exit stage", args) - pipes = [] - cb = ControlBase() - for s in self.stages: - print ("stage specs", s, s.inspecs, s.outspecs) - if self.pipetype == 'buffered': - p = BufferedHandshake(s) - else: - p = AutoPipe(s, s.assigns) - pipes.append(p) - self.m.submodules += p - - self.m.d.comb += cb.connect(pipes) - - -class SimplePipeline: - """ Pipeline builder with auto generation of pipeline registers. - """ - - def __init__(self, m): - self._m = m - self._pipeline_register_map = {} - self._current_stage_num = 0 - - def _setup(self): - stage_list = [] - for method in dir(self): - if method.startswith('stage'): - stage_list.append(method) - for stage in sorted(stage_list): - stage_method = getattr(self, stage) - stage_method() - self._current_stage_num += 1 - - def __getattr__(self, name): - try: - return self._pipeline_register_map[self._current_stage_num][name] - except KeyError: - raise AttributeError( - 'error, no pipeline register "%s" defined for stage %d' - % (name, self._current_stage_num)) - - def __setattr__(self, name, value): - if name.startswith('_'): - # do not do anything tricky with variables starting with '_' - object.__setattr__(self, name, value) - return - next_stage = self._current_stage_num + 1 - pipereg_id = str(self._current_stage_num) + 'to' + str(next_stage) - rname = 'pipereg_' + pipereg_id + '_' + name - #new_pipereg = Signal(value_bits_sign(value), name=rname, - # reset_less=True) - if isinstance(value, ObjectProxy): - new_pipereg = ObjectProxy.like(self._m, value, - name=rname, reset_less = True) - else: - new_pipereg = Signal.like(value, name=rname, reset_less = True) - if next_stage not in self._pipeline_register_map: - self._pipeline_register_map[next_stage] = {} - self._pipeline_register_map[next_stage][name] = new_pipereg - self._m.d.sync += eq(new_pipereg, value) - diff --git a/src/ieee754/add/singlepipe.py b/src/ieee754/add/singlepipe.py deleted file mode 100644 index 68b62e43..00000000 --- a/src/ieee754/add/singlepipe.py +++ /dev/null @@ -1,829 +0,0 @@ -""" Pipeline API. For multi-input and multi-output variants, see multipipe. - - Associated development bugs: - * http://bugs.libre-riscv.org/show_bug.cgi?id=64 - * http://bugs.libre-riscv.org/show_bug.cgi?id=57 - - Important: see Stage API (stageapi.py) in combination with below - - RecordBasedStage: - ---------------- - - A convenience class that takes an input shape, output shape, a - "processing" function and an optional "setup" function. Honestly - though, there's not much more effort to just... create a class - that returns a couple of Records (see ExampleAddRecordStage in - examples). - - PassThroughStage: - ---------------- - - A convenience class that takes a single function as a parameter, - that is chain-called to create the exact same input and output spec. - It has a process() function that simply returns its input. - - Instances of this class are completely redundant if handed to - 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. - - Note: pipelines basically do not become pipelines as such until - handed to a derivative of ControlBase. ControlBase itself is *not* - strictly considered a pipeline class. Wishbone and AXI4 (master or - slave) could be derived from ControlBase, for example. - UnbufferedPipeline: - ------------------ - - A simple stalling clock-synchronised pipeline that has no buffering - (unlike BufferedHandshake). Data flows on *every* clock cycle when - the conditions are right (this is nominally when the input is valid - and the output is ready). - - A stall anywhere along the line will result in a stall back-propagating - down the entire chain. The BufferedHandshake by contrast will buffer - incoming data, allowing previous stages one clock cycle's grace before - also having to stall. - - An advantage of the UnbufferedPipeline over the Buffered one is - that the amount of logic needed (number of gates) is greatly - reduced (no second set of buffers basically) - - The disadvantage of the UnbufferedPipeline is that the valid/ready - logic, if chained together, is *combinatorial*, resulting in - progressively larger gate delay. - - PassThroughHandshake: - ------------------ - - A Control class that introduces a single clock delay, passing its - data through unaltered. Unlike RegisterPipeline (which relies - on UnbufferedPipeline and PassThroughStage) it handles ready/valid - itself. - - RegisterPipeline: - ---------------- - - A convenience class that, because UnbufferedPipeline introduces a single - clock delay, when its stage is a PassThroughStage, it results in a Pipeline - stage that, duh, delays its (unmodified) input by one clock cycle. - - BufferedHandshake: - ---------------- - - nmigen implementation of buffered pipeline stage, based on zipcpu: - https://zipcpu.com/blog/2017/08/14/strategies-for-pipelining.html - - this module requires quite a bit of thought to understand how it works - (and why it is needed in the first place). reading the above is - *strongly* recommended. - - unlike john dawson's IEEE754 FPU STB/ACK signalling, which requires - the STB / ACK signals to raise and lower (on separate clocks) before - data may proceeed (thus only allowing one piece of data to proceed - on *ALTERNATE* cycles), the signalling here is a true pipeline - where data will flow on *every* clock when the conditions are right. - - input acceptance conditions are when: - * incoming previous-stage strobe (p.valid_i) is HIGH - * outgoing previous-stage ready (p.ready_o) is LOW - - output transmission conditions are when: - * outgoing next-stage strobe (n.valid_o) is HIGH - * outgoing next-stage ready (n.ready_i) is LOW - - the tricky bit is when the input has valid data and the output is not - ready to accept it. if it wasn't for the clock synchronisation, it - would be possible to tell the input "hey don't send that data, we're - not ready". unfortunately, it's not possible to "change the past": - the previous stage *has no choice* but to pass on its data. - - therefore, the incoming data *must* be accepted - and stored: that - is the responsibility / contract that this stage *must* accept. - on the same clock, it's possible to tell the input that it must - not send any more data. this is the "stall" condition. - - we now effectively have *two* possible pieces of data to "choose" from: - the buffered data, and the incoming data. the decision as to which - to process and output is based on whether we are in "stall" or not. - i.e. when the next stage is no longer ready, the output comes from - the buffer if a stall had previously occurred, otherwise it comes - direct from processing the input. - - this allows us to respect a synchronous "travelling STB" with what - dan calls a "buffered handshake". - - it's quite a complex state machine! - - SimpleHandshake - --------------- - - Synchronised pipeline, Based on: - https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v -""" - -from nmigen import Signal, Mux, Module, Elaboratable -from nmigen.cli import verilog, rtlil -from nmigen.hdl.rec import Record - -from queue import Queue -import inspect - -from iocontrol import (PrevControl, NextControl, Object, RecordObject) -from stageapi import (_spec, StageCls, Stage, StageChain, StageHelper) -import nmoperator - - -class RecordBasedStage(Stage): - """ convenience class which provides a Records-based layout. - honestly it's a lot easier just to create a direct Records-based - class (see ExampleAddRecordStage) - """ - def __init__(self, in_shape, out_shape, processfn, setupfn=None): - self.in_shape = in_shape - self.out_shape = out_shape - self.__process = processfn - self.__setup = setupfn - def ispec(self): return Record(self.in_shape) - def ospec(self): return Record(self.out_shape) - def process(seif, i): return self.__process(i) - def setup(seif, m, i): return self.__setup(m, i) - - -class PassThroughStage(StageCls): - """ a pass-through stage with its input data spec identical to its output, - and "passes through" its data from input to output (does nothing). - - use this basically to explicitly make any data spec Stage-compliant. - (many APIs would potentially use a static "wrap" method in e.g. - StageCls to achieve a similar effect) - """ - def __init__(self, iospecfn): self.iospecfn = iospecfn - def ispec(self): return self.iospecfn() - def ospec(self): return self.iospecfn() - - -class ControlBase(StageHelper, Elaboratable): - """ Common functions for Pipeline API. Note: a "pipeline stage" only - exists (conceptually) when a ControlBase derivative is handed - a Stage (combinatorial block) - - NOTE: ControlBase derives from StageHelper, making it accidentally - compliant with the Stage API. Using those functions directly - *BYPASSES* a ControlBase instance ready/valid signalling, which - clearly should not be done without a really, really good reason. - """ - def __init__(self, stage=None, in_multi=None, stage_ctl=False): - """ Base class containing ready/valid/data to previous and next stages - - * p: contains ready/valid to the previous stage - * n: contains ready/valid to the next stage - - Except when calling Controlbase.connect(), user must also: - * add data_i member to PrevControl (p) and - * add data_o member to NextControl (n) - Calling ControlBase._new_data is a good way to do that. - """ - StageHelper.__init__(self, stage) - - # set up input and output IO ACK (prev/next ready/valid) - self.p = PrevControl(in_multi, stage_ctl) - self.n = NextControl(stage_ctl) - - # set up the input and output data - if stage is not None: - self._new_data("data") - - def _new_data(self, name): - """ allocates new data_i and data_o - """ - self.p.data_i, self.n.data_o = self.new_specs(name) - - @property - def data_r(self): - return self.process(self.p.data_i) - - def connect_to_next(self, nxt): - """ helper function to connect to the next stage data/valid/ready. - """ - return self.n.connect_to_next(nxt.p) - - def _connect_in(self, prev): - """ internal helper function to connect stage to an input source. - do not use to connect stage-to-stage! - """ - return self.p._connect_in(prev.p) - - def _connect_out(self, nxt): - """ internal helper function to connect stage to an output source. - do not use to connect stage-to-stage! - """ - return self.n._connect_out(nxt.n) - - def connect(self, pipechain): - """ connects a chain (list) of Pipeline instances together and - links them to this ControlBase instance: - - in <----> 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. - - Argument: - - * :pipechain: - a sequence of ControlBase-derived classes - (must be one or more in length) - - Returns: - - * a list of eq assignments that will need to be added in - an elaborate() to m.d.comb - """ - assert len(pipechain) > 0, "pipechain must be non-zero length" - assert self.stage is None, "do not use connect with a stage" - eqs = [] # collated list of assignment statements - - # connect inter-chain - for i in range(len(pipechain)-1): - pipe1 = pipechain[i] # earlier - pipe2 = pipechain[i+1] # later (by 1) - eqs += pipe1.connect_to_next(pipe2) # earlier n to later p - - # connect front and back of chain to ourselves - front = pipechain[0] # first in chain - end = pipechain[-1] # last in chain - self.set_specs(front, end) # sets up ispec/ospec functions - self._new_data("chain") # NOTE: REPLACES existing data - eqs += front._connect_in(self) # front p to our p - eqs += end._connect_out(self) # end n to our n - - return eqs - - def set_input(self, i): - """ helper function to set the input data (used in unit tests) - """ - return nmoperator.eq(self.p.data_i, i) - - def __iter__(self): - yield from self.p # yields ready/valid/data (data also gets yielded) - yield from self.n # ditto - - 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 - - self.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 - is shunted in a temporary register. - - Argument: stage. see Stage API above - - stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 - stage-1 p.ready_o <>in stage n.data_o out>> stage+1 - | | - process --->----^ - | | - +-- r_data ->-+ - - input data p.data_i is read (only), is processed and goes into an - intermediate result store [process()]. this is updated combinatorially. - - in a non-stall condition, the intermediate result will go into the - output (update_output). however if ever there is a stall, it goes - into r_data instead [update_buffer()]. - - when the non-stall condition is released, r_data is the first - to be transferred to the output [flush_buffer()], and the stall - condition cleared. - - on the next cycle (as long as stall is not raised again) the - input may begin to be processed and transferred directly to output. - """ - - def elaborate(self, platform): - self.m = ControlBase.elaborate(self, platform) - - result = _spec(self.stage.ospec, "r_tmp") - r_data = _spec(self.stage.ospec, "r_data") - - # establish some combinatorial temporaries - o_n_validn = Signal(reset_less=True) - n_ready_i = Signal(reset_less=True, name="n_i_rdy_data") - nir_por = Signal(reset_less=True) - nir_por_n = Signal(reset_less=True) - p_valid_i = Signal(reset_less=True) - nir_novn = Signal(reset_less=True) - nirn_novn = Signal(reset_less=True) - por_pivn = Signal(reset_less=True) - npnn = Signal(reset_less=True) - self.m.d.comb += [p_valid_i.eq(self.p.valid_i_test), - o_n_validn.eq(~self.n.valid_o), - n_ready_i.eq(self.n.ready_i_test), - nir_por.eq(n_ready_i & self.p._ready_o), - nir_por_n.eq(n_ready_i & ~self.p._ready_o), - nir_novn.eq(n_ready_i | o_n_validn), - nirn_novn.eq(~n_ready_i & o_n_validn), - npnn.eq(nir_por | nirn_novn), - por_pivn.eq(self.p._ready_o & ~p_valid_i) - ] - - # store result of processing in combinatorial temporary - self.m.d.comb += nmoperator.eq(result, self.data_r) - - # if not in stall condition, update the temporary register - with self.m.If(self.p.ready_o): # not stalled - self.m.d.sync += nmoperator.eq(r_data, result) # update buffer - - # data pass-through conditions - with self.m.If(npnn): - data_o = self._postprocess(result) # XXX TBD, does nothing right now - self.m.d.sync += [self.n.valid_o.eq(p_valid_i), # valid if p_valid - nmoperator.eq(self.n.data_o, data_o), # update out - ] - # buffer flush conditions (NOTE: can override data passthru conditions) - with self.m.If(nir_por_n): # not stalled - # Flush the [already processed] buffer to the output port. - data_o = self._postprocess(r_data) # XXX TBD, does nothing right now - self.m.d.sync += [self.n.valid_o.eq(1), # reg empty - nmoperator.eq(self.n.data_o, data_o), # flush - ] - # output ready conditions - self.m.d.sync += self.p._ready_o.eq(nir_novn | por_pivn) - - return self.m - - -class SimpleHandshake(ControlBase): - """ simple handshake control. data and strobe signals travel in sync. - implements the protocol used by Wishbone and AXI4. - - Argument: stage. see Stage API above - - stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 - stage-1 p.ready_o <>in stage n.data_o out>> stage+1 - | | - +--process->--^ - Truth Table - - Inputs Temporary Output Data - ------- ---------- ----- ---- - P P N N PiV& ~NiR& N P - i o i o PoR NoV o o - V R R V V R - - ------- - - - - - 0 0 0 0 0 0 >0 0 reg - 0 0 0 1 0 1 >1 0 reg - 0 0 1 0 0 0 0 1 process(data_i) - 0 0 1 1 0 0 0 1 process(data_i) - ------- - - - - - 0 1 0 0 0 0 >0 0 reg - 0 1 0 1 0 1 >1 0 reg - 0 1 1 0 0 0 0 1 process(data_i) - 0 1 1 1 0 0 0 1 process(data_i) - ------- - - - - - 1 0 0 0 0 0 >0 0 reg - 1 0 0 1 0 1 >1 0 reg - 1 0 1 0 0 0 0 1 process(data_i) - 1 0 1 1 0 0 0 1 process(data_i) - ------- - - - - - 1 1 0 0 1 0 1 0 process(data_i) - 1 1 0 1 1 1 1 0 process(data_i) - 1 1 1 0 1 0 1 1 process(data_i) - 1 1 1 1 1 0 1 1 process(data_i) - ------- - - - - - """ - - def elaborate(self, platform): - self.m = m = ControlBase.elaborate(self, platform) - - r_busy = Signal() - result = _spec(self.stage.ospec, "r_tmp") - - # establish some combinatorial temporaries - n_ready_i = Signal(reset_less=True, name="n_i_rdy_data") - p_valid_i_p_ready_o = Signal(reset_less=True) - p_valid_i = Signal(reset_less=True) - m.d.comb += [p_valid_i.eq(self.p.valid_i_test), - n_ready_i.eq(self.n.ready_i_test), - p_valid_i_p_ready_o.eq(p_valid_i & self.p.ready_o), - ] - - # store result of processing in combinatorial temporary - m.d.comb += nmoperator.eq(result, self.data_r) - - # previous valid and ready - with m.If(p_valid_i_p_ready_o): - data_o = self._postprocess(result) # XXX TBD, does nothing right now - m.d.sync += [r_busy.eq(1), # output valid - nmoperator.eq(self.n.data_o, data_o), # update output - ] - # previous invalid or not ready, however next is accepting - with m.Elif(n_ready_i): - data_o = self._postprocess(result) # XXX TBD, does nothing right now - m.d.sync += [nmoperator.eq(self.n.data_o, data_o)] - # TODO: could still send data here (if there was any) - #m.d.sync += self.n.valid_o.eq(0) # ...so set output invalid - m.d.sync += r_busy.eq(0) # ...so set output invalid - - m.d.comb += self.n.valid_o.eq(r_busy) - # if next is ready, so is previous - m.d.comb += self.p._ready_o.eq(n_ready_i) - - return self.m - - -class UnbufferedPipeline(ControlBase): - """ A simple pipeline stage with single-clock synchronisation - and two-way valid/ready synchronised signalling. - - Note that a stall in one stage will result in the entire pipeline - chain stalling. - - Also that unlike BufferedHandshake, the valid/ready signalling does NOT - travel synchronously with the data: the valid/ready signalling - combines in a *combinatorial* fashion. Therefore, a long pipeline - chain will lengthen propagation delays. - - Argument: stage. see Stage API, above - - stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 - stage-1 p.ready_o <>in stage n.data_o out>> stage+1 - | | - r_data result - | | - +--process ->-+ - - Attributes: - ----------- - p.data_i : StageInput, shaped according to ispec - The pipeline input - p.data_o : StageOutput, shaped according to ospec - The pipeline output - r_data : input_shape according to ispec - A temporary (buffered) copy of a prior (valid) input. - This is HELD if the output is not ready. It is updated - SYNCHRONOUSLY. - result: output_shape according to ospec - The output of the combinatorial logic. it is updated - COMBINATORIALLY (no clock dependence). - - Truth Table - - Inputs Temp Output Data - ------- - ----- ---- - P P N N ~NiR& N P - i o i o NoV o o - V R R V V R - - ------- - - - - 0 0 0 0 0 0 1 reg - 0 0 0 1 1 1 0 reg - 0 0 1 0 0 0 1 reg - 0 0 1 1 0 0 1 reg - ------- - - - - 0 1 0 0 0 0 1 reg - 0 1 0 1 1 1 0 reg - 0 1 1 0 0 0 1 reg - 0 1 1 1 0 0 1 reg - ------- - - - - 1 0 0 0 0 1 1 reg - 1 0 0 1 1 1 0 reg - 1 0 1 0 0 1 1 reg - 1 0 1 1 0 1 1 reg - ------- - - - - 1 1 0 0 0 1 1 process(data_i) - 1 1 0 1 1 1 0 process(data_i) - 1 1 1 0 0 1 1 process(data_i) - 1 1 1 1 0 1 1 process(data_i) - ------- - - - - - Note: PoR is *NOT* involved in the above decision-making. - """ - - def elaborate(self, platform): - self.m = m = ControlBase.elaborate(self, platform) - - data_valid = Signal() # is data valid or not - r_data = _spec(self.stage.ospec, "r_tmp") # output type - - # some temporaries - p_valid_i = Signal(reset_less=True) - pv = Signal(reset_less=True) - buf_full = Signal(reset_less=True) - m.d.comb += p_valid_i.eq(self.p.valid_i_test) - m.d.comb += pv.eq(self.p.valid_i & self.p.ready_o) - m.d.comb += buf_full.eq(~self.n.ready_i_test & data_valid) - - m.d.comb += self.n.valid_o.eq(data_valid) - m.d.comb += self.p._ready_o.eq(~data_valid | self.n.ready_i_test) - m.d.sync += data_valid.eq(p_valid_i | buf_full) - - with m.If(pv): - m.d.sync += nmoperator.eq(r_data, self.data_r) - data_o = self._postprocess(r_data) # XXX TBD, does nothing right now - m.d.comb += nmoperator.eq(self.n.data_o, data_o) - - return self.m - -class UnbufferedPipeline2(ControlBase): - """ A simple pipeline stage with single-clock synchronisation - and two-way valid/ready synchronised signalling. - - Note that a stall in one stage will result in the entire pipeline - chain stalling. - - Also that unlike BufferedHandshake, the valid/ready signalling does NOT - travel synchronously with the data: the valid/ready signalling - combines in a *combinatorial* fashion. Therefore, a long pipeline - chain will lengthen propagation delays. - - Argument: stage. see Stage API, above - - stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 - stage-1 p.ready_o <>in stage n.data_o out>> stage+1 - | | | - +- process-> buf <-+ - Attributes: - ----------- - p.data_i : StageInput, shaped according to ispec - The pipeline input - p.data_o : StageOutput, shaped according to ospec - The pipeline output - buf : output_shape according to ospec - A temporary (buffered) copy of a valid output - This is HELD if the output is not ready. It is updated - SYNCHRONOUSLY. - - Inputs Temp Output Data - ------- - ----- - P P N N ~NiR& N P (buf_full) - i o i o NoV o o - V R R V V R - - ------- - - - - 0 0 0 0 0 0 1 process(data_i) - 0 0 0 1 1 1 0 reg (odata, unchanged) - 0 0 1 0 0 0 1 process(data_i) - 0 0 1 1 0 0 1 process(data_i) - ------- - - - - 0 1 0 0 0 0 1 process(data_i) - 0 1 0 1 1 1 0 reg (odata, unchanged) - 0 1 1 0 0 0 1 process(data_i) - 0 1 1 1 0 0 1 process(data_i) - ------- - - - - 1 0 0 0 0 1 1 process(data_i) - 1 0 0 1 1 1 0 reg (odata, unchanged) - 1 0 1 0 0 1 1 process(data_i) - 1 0 1 1 0 1 1 process(data_i) - ------- - - - - 1 1 0 0 0 1 1 process(data_i) - 1 1 0 1 1 1 0 reg (odata, unchanged) - 1 1 1 0 0 1 1 process(data_i) - 1 1 1 1 0 1 1 process(data_i) - ------- - - - - - Note: PoR is *NOT* involved in the above decision-making. - """ - - def elaborate(self, platform): - self.m = m = ControlBase.elaborate(self, platform) - - buf_full = Signal() # is data valid or not - buf = _spec(self.stage.ospec, "r_tmp") # output type - - # some temporaries - p_valid_i = Signal(reset_less=True) - m.d.comb += p_valid_i.eq(self.p.valid_i_test) - - m.d.comb += self.n.valid_o.eq(buf_full | p_valid_i) - m.d.comb += self.p._ready_o.eq(~buf_full) - m.d.sync += buf_full.eq(~self.n.ready_i_test & self.n.valid_o) - - data_o = Mux(buf_full, buf, self.data_r) - data_o = self._postprocess(data_o) # XXX TBD, does nothing right now - m.d.comb += nmoperator.eq(self.n.data_o, data_o) - m.d.sync += nmoperator.eq(buf, self.n.data_o) - - return self.m - - -class PassThroughHandshake(ControlBase): - """ A control block that delays by one clock cycle. - - Inputs Temporary Output Data - ------- ------------------ ----- ---- - P P N N PiV& PiV| NiR| pvr N P (pvr) - i o i o PoR ~PoR ~NoV o o - V R R V V R - - ------- - - - - - - - 0 0 0 0 0 1 1 0 1 1 odata (unchanged) - 0 0 0 1 0 1 0 0 1 0 odata (unchanged) - 0 0 1 0 0 1 1 0 1 1 odata (unchanged) - 0 0 1 1 0 1 1 0 1 1 odata (unchanged) - ------- - - - - - - - 0 1 0 0 0 0 1 0 0 1 odata (unchanged) - 0 1 0 1 0 0 0 0 0 0 odata (unchanged) - 0 1 1 0 0 0 1 0 0 1 odata (unchanged) - 0 1 1 1 0 0 1 0 0 1 odata (unchanged) - ------- - - - - - - - 1 0 0 0 0 1 1 1 1 1 process(in) - 1 0 0 1 0 1 0 0 1 0 odata (unchanged) - 1 0 1 0 0 1 1 1 1 1 process(in) - 1 0 1 1 0 1 1 1 1 1 process(in) - ------- - - - - - - - 1 1 0 0 1 1 1 1 1 1 process(in) - 1 1 0 1 1 1 0 0 1 0 odata (unchanged) - 1 1 1 0 1 1 1 1 1 1 process(in) - 1 1 1 1 1 1 1 1 1 1 process(in) - ------- - - - - - - - - """ - - def elaborate(self, platform): - self.m = m = ControlBase.elaborate(self, platform) - - r_data = _spec(self.stage.ospec, "r_tmp") # output type - - # temporaries - p_valid_i = Signal(reset_less=True) - pvr = Signal(reset_less=True) - m.d.comb += p_valid_i.eq(self.p.valid_i_test) - m.d.comb += pvr.eq(p_valid_i & self.p.ready_o) - - m.d.comb += self.p.ready_o.eq(~self.n.valid_o | self.n.ready_i_test) - m.d.sync += self.n.valid_o.eq(p_valid_i | ~self.p.ready_o) - - odata = Mux(pvr, self.data_r, r_data) - m.d.sync += nmoperator.eq(r_data, odata) - r_data = self._postprocess(r_data) # XXX TBD, does nothing right now - m.d.comb += nmoperator.eq(self.n.data_o, r_data) - - return m - - -class RegisterPipeline(UnbufferedPipeline): - """ A pipeline stage that delays by one clock cycle, creating a - sync'd latch out of data_o and valid_o as an indirect byproduct - of using PassThroughStage - """ - def __init__(self, iospecfn): - UnbufferedPipeline.__init__(self, PassThroughStage(iospecfn)) - - -class FIFOControl(ControlBase): - """ FIFO Control. Uses Queue to store data, coincidentally - happens to have same valid/ready signalling as Stage API. - - data_i -> fifo.din -> FIFO -> fifo.dout -> data_o - """ - def __init__(self, depth, stage, in_multi=None, stage_ctl=False, - fwft=True, pipe=False): - """ FIFO Control - - * :depth: number of entries in the FIFO - * :stage: data processing block - * :fwft: first word fall-thru mode (non-fwft introduces delay) - * :pipe: specifies pipe mode. - - when fwft = True it indicates that transfers may occur - combinatorially through stage processing in the same clock cycle. - This requires that the Stage be a Moore FSM: - https://en.wikipedia.org/wiki/Moore_machine - - when fwft = False it indicates that all output signals are - produced only from internal registers or memory, i.e. that the - Stage is a Mealy FSM: - https://en.wikipedia.org/wiki/Mealy_machine - - data is processed (and located) as follows: - - self.p self.stage temp fn temp fn temp fp self.n - data_i->process()->result->cat->din.FIFO.dout->cat(data_o) - - yes, really: cat produces a Cat() which can be assigned to. - this is how the FIFO gets de-catted without needing a de-cat - function - """ - self.fwft = fwft - self.pipe = pipe - self.fdepth = depth - ControlBase.__init__(self, stage, in_multi, stage_ctl) - - def elaborate(self, platform): - self.m = m = ControlBase.elaborate(self, platform) - - # make a FIFO with a signal of equal width to the data_o. - (fwidth, _) = nmoperator.shape(self.n.data_o) - fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe) - m.submodules.fifo = fifo - - def processfn(data_i): - # store result of processing in combinatorial temporary - result = _spec(self.stage.ospec, "r_temp") - m.d.comb += nmoperator.eq(result, self.process(data_i)) - return nmoperator.cat(result) - - ## prev: make the FIFO (Queue object) "look" like a PrevControl... - m.submodules.fp = fp = PrevControl() - fp.valid_i, fp._ready_o, fp.data_i = fifo.we, fifo.writable, fifo.din - m.d.comb += fp._connect_in(self.p, fn=processfn) - - # next: make the FIFO (Queue object) "look" like a NextControl... - m.submodules.fn = fn = NextControl() - fn.valid_o, fn.ready_i, fn.data_o = fifo.readable, fifo.re, fifo.dout - connections = fn._connect_out(self.n, fn=nmoperator.cat) - - # ok ok so we can't just do the ready/valid eqs straight: - # first 2 from connections are the ready/valid, 3rd is data. - if self.fwft: - m.d.comb += connections[:2] # combinatorial on next ready/valid - else: - m.d.sync += connections[:2] # non-fwft mode needs sync - data_o = connections[2] # get the data - data_o = self._postprocess(data_o) # XXX TBD, does nothing right now - m.d.comb += data_o - - return m - - -# aka "RegStage". -class UnbufferedPipeline(FIFOControl): - def __init__(self, stage, in_multi=None, stage_ctl=False): - FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl, - fwft=True, pipe=False) - -# aka "BreakReadyStage" XXX had to set fwft=True to get it to work -class PassThroughHandshake(FIFOControl): - def __init__(self, stage, in_multi=None, stage_ctl=False): - FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl, - fwft=True, pipe=True) - -# this is *probably* BufferedHandshake, although test #997 now succeeds. -class BufferedHandshake(FIFOControl): - def __init__(self, stage, in_multi=None, stage_ctl=False): - FIFOControl.__init__(self, 2, stage, in_multi, stage_ctl, - fwft=True, pipe=False) - - -""" -# this is *probably* SimpleHandshake (note: memory cell size=0) -class SimpleHandshake(FIFOControl): - def __init__(self, stage, in_multi=None, stage_ctl=False): - FIFOControl.__init__(self, 0, stage, in_multi, stage_ctl, - fwft=True, pipe=False) -""" diff --git a/src/nmutil/__init__.py b/src/nmutil/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/nmutil/multipipe.py b/src/nmutil/multipipe.py new file mode 100644 index 00000000..e24703f8 --- /dev/null +++ b/src/nmutil/multipipe.py @@ -0,0 +1,358 @@ +""" Combinatorial Multi-input and Multi-output multiplexer blocks + conforming to Pipeline API + + Multi-input is complex because if any one input is ready, the output + can be ready, and the decision comes from a separate module. + + Multi-output is simple (pretty much identical to UnbufferedPipeline), + and the selection is just a mux. The only proviso (difference) being: + the outputs not being selected have to have their ready_o signals + DEASSERTED. +""" + +from math import log +from nmigen import Signal, Cat, Const, Mux, Module, Array, Elaboratable +from nmigen.cli import verilog, rtlil +from nmigen.lib.coding import PriorityEncoder +from nmigen.hdl.rec import Record, Layout +from stageapi import _spec + +from collections.abc import Sequence + +from example_buf_pipe import eq, NextControl, PrevControl, ExampleStage + + +class MultiInControlBase(Elaboratable): + """ Common functions for Pipeline API + """ + def __init__(self, in_multi=None, p_len=1): + """ Multi-input Control class. Conforms to same API as ControlBase... + mostly. has additional indices to the *multiple* input stages + + * p: contains ready/valid to the previous stages PLURAL + * n: contains ready/valid to the next stage + + User must also: + * add data_i members to PrevControl and + * add data_o member to NextControl + """ + # set up input and output IO ACK (prev/next ready/valid) + p = [] + for i in range(p_len): + p.append(PrevControl(in_multi)) + self.p = Array(p) + self.n = NextControl() + + def connect_to_next(self, nxt, p_idx=0): + """ helper function to connect to the next stage data/valid/ready. + """ + return self.n.connect_to_next(nxt.p[p_idx]) + + def _connect_in(self, prev, idx=0, prev_idx=None): + """ helper function to connect stage to an input source. do not + use to connect stage-to-stage! + """ + if prev_idx is None: + return self.p[idx]._connect_in(prev.p) + return self.p[idx]._connect_in(prev.p[prev_idx]) + + def _connect_out(self, nxt): + """ helper function to connect stage to an output source. do not + use to connect stage-to-stage! + """ + if nxt_idx is None: + return self.n._connect_out(nxt.n) + return self.n._connect_out(nxt.n) + + def set_input(self, i, idx=0): + """ helper function to set the input data + """ + return eq(self.p[idx].data_i, i) + + def elaborate(self, platform): + m = Module() + for i, p in enumerate(self.p): + setattr(m.submodules, "p%d" % i, p) + m.submodules.n = self.n + return m + + def __iter__(self): + for p in self.p: + yield from p + yield from self.n + + def ports(self): + return list(self) + + +class MultiOutControlBase(Elaboratable): + """ Common functions for Pipeline API + """ + def __init__(self, n_len=1, in_multi=None): + """ Multi-output Control class. Conforms to same API as ControlBase... + mostly. has additional indices to the multiple *output* stages + [MultiInControlBase has multiple *input* stages] + + * p: contains ready/valid to the previou stage + * n: contains ready/valid to the next stages PLURAL + + User must also: + * add data_i member to PrevControl and + * add data_o members to NextControl + """ + + # set up input and output IO ACK (prev/next ready/valid) + self.p = PrevControl(in_multi) + n = [] + for i in range(n_len): + n.append(NextControl()) + self.n = Array(n) + + def connect_to_next(self, nxt, n_idx=0): + """ helper function to connect to the next stage data/valid/ready. + """ + return self.n[n_idx].connect_to_next(nxt.p) + + def _connect_in(self, prev, idx=0): + """ helper function to connect stage to an input source. do not + use to connect stage-to-stage! + """ + return self.n[idx]._connect_in(prev.p) + + def _connect_out(self, nxt, idx=0, nxt_idx=None): + """ helper function to connect stage to an output source. do not + use to connect stage-to-stage! + """ + if nxt_idx is None: + return self.n[idx]._connect_out(nxt.n) + return self.n[idx]._connect_out(nxt.n[nxt_idx]) + + def elaborate(self, platform): + m = Module() + m.submodules.p = self.p + for i, n in enumerate(self.n): + setattr(m.submodules, "n%d" % i, n) + return m + + 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 + for n in self.n: + yield from n + + def ports(self): + return list(self) + + +class CombMultiOutPipeline(MultiOutControlBase): + """ A multi-input Combinatorial block conforming to the Pipeline API + + Attributes: + ----------- + p.data_i : stage input data (non-array). shaped according to ispec + n.data_o : stage output data array. shaped according to ospec + """ + + def __init__(self, stage, n_len, n_mux): + MultiOutControlBase.__init__(self, n_len=n_len) + self.stage = stage + self.n_mux = n_mux + + # set up the input and output data + self.p.data_i = _spec(stage.ispec, 'data_i') # input type + for i in range(n_len): + name = 'data_o_%d' % i + self.n[i].data_o = _spec(stage.ospec, name) # output type + + def process(self, i): + if hasattr(self.stage, "process"): + return self.stage.process(i) + return i + + def elaborate(self, platform): + m = MultiOutControlBase.elaborate(self, platform) + + if hasattr(self.n_mux, "elaborate"): # TODO: identify submodule? + m.submodules += self.n_mux + + # need buffer register conforming to *input* spec + r_data = _spec(self.stage.ispec, 'r_data') # input type + if hasattr(self.stage, "setup"): + self.stage.setup(m, r_data) + + # multiplexer id taken from n_mux + mid = self.n_mux.m_id + + # temporaries + p_valid_i = Signal(reset_less=True) + pv = Signal(reset_less=True) + m.d.comb += p_valid_i.eq(self.p.valid_i_test) + m.d.comb += pv.eq(self.p.valid_i & self.p.ready_o) + + # all outputs to next stages first initialised to zero (invalid) + # the only output "active" is then selected by the muxid + for i in range(len(self.n)): + m.d.comb += self.n[i].valid_o.eq(0) + data_valid = self.n[mid].valid_o + m.d.comb += self.p.ready_o.eq(~data_valid | self.n[mid].ready_i) + m.d.comb += data_valid.eq(p_valid_i | \ + (~self.n[mid].ready_i & data_valid)) + with m.If(pv): + m.d.comb += eq(r_data, self.p.data_i) + m.d.comb += eq(self.n[mid].data_o, self.process(r_data)) + + return m + + +class CombMultiInPipeline(MultiInControlBase): + """ A multi-input Combinatorial block conforming to the Pipeline API + + Attributes: + ----------- + p.data_i : StageInput, shaped according to ispec + The pipeline input + p.data_o : StageOutput, shaped according to ospec + The pipeline output + r_data : input_shape according to ispec + A temporary (buffered) copy of a prior (valid) input. + This is HELD if the output is not ready. It is updated + SYNCHRONOUSLY. + """ + + def __init__(self, stage, p_len, p_mux): + MultiInControlBase.__init__(self, p_len=p_len) + self.stage = stage + self.p_mux = p_mux + + # set up the input and output data + for i in range(p_len): + name = 'data_i_%d' % i + self.p[i].data_i = _spec(stage.ispec, name) # input type + self.n.data_o = _spec(stage.ospec, 'data_o') + + def process(self, i): + if hasattr(self.stage, "process"): + return self.stage.process(i) + return i + + def elaborate(self, platform): + m = MultiInControlBase.elaborate(self, platform) + + m.submodules += self.p_mux + + # need an array of buffer registers conforming to *input* spec + r_data = [] + data_valid = [] + p_valid_i = [] + n_ready_in = [] + p_len = len(self.p) + for i in range(p_len): + name = 'r_%d' % i + r = _spec(self.stage.ispec, name) # input type + r_data.append(r) + data_valid.append(Signal(name="data_valid", reset_less=True)) + p_valid_i.append(Signal(name="p_valid_i", reset_less=True)) + n_ready_in.append(Signal(name="n_ready_in", reset_less=True)) + if hasattr(self.stage, "setup"): + self.stage.setup(m, r) + if len(r_data) > 1: + r_data = Array(r_data) + p_valid_i = Array(p_valid_i) + n_ready_in = Array(n_ready_in) + data_valid = Array(data_valid) + + nirn = Signal(reset_less=True) + m.d.comb += nirn.eq(~self.n.ready_i) + mid = self.p_mux.m_id + for i in range(p_len): + m.d.comb += data_valid[i].eq(0) + m.d.comb += n_ready_in[i].eq(1) + m.d.comb += p_valid_i[i].eq(0) + m.d.comb += self.p[i].ready_o.eq(0) + m.d.comb += p_valid_i[mid].eq(self.p_mux.active) + m.d.comb += self.p[mid].ready_o.eq(~data_valid[mid] | self.n.ready_i) + m.d.comb += n_ready_in[mid].eq(nirn & data_valid[mid]) + anyvalid = Signal(i, reset_less=True) + av = [] + for i in range(p_len): + av.append(data_valid[i]) + anyvalid = Cat(*av) + m.d.comb += self.n.valid_o.eq(anyvalid.bool()) + m.d.comb += data_valid[mid].eq(p_valid_i[mid] | \ + (n_ready_in[mid] & data_valid[mid])) + + for i in range(p_len): + vr = Signal(reset_less=True) + m.d.comb += vr.eq(self.p[i].valid_i & self.p[i].ready_o) + with m.If(vr): + m.d.comb += eq(r_data[i], self.p[i].data_i) + + m.d.comb += eq(self.n.data_o, self.process(r_data[mid])) + + return m + + +class CombMuxOutPipe(CombMultiOutPipeline): + def __init__(self, stage, n_len): + # HACK: stage is also the n-way multiplexer + CombMultiOutPipeline.__init__(self, stage, n_len=n_len, n_mux=stage) + + # HACK: n-mux is also the stage... so set the muxid equal to input mid + stage.m_id = self.p.data_i.mid + + + +class InputPriorityArbiter(Elaboratable): + """ arbitration module for Input-Mux pipe, baed on PriorityEncoder + """ + def __init__(self, pipe, num_rows): + self.pipe = pipe + self.num_rows = num_rows + self.mmax = int(log(self.num_rows) / log(2)) + self.m_id = Signal(self.mmax, reset_less=True) # multiplex id + self.active = Signal(reset_less=True) + + def elaborate(self, platform): + m = Module() + + assert len(self.pipe.p) == self.num_rows, \ + "must declare input to be same size" + pe = PriorityEncoder(self.num_rows) + m.submodules.selector = pe + + # connect priority encoder + in_ready = [] + for i in range(self.num_rows): + p_valid_i = Signal(reset_less=True) + m.d.comb += p_valid_i.eq(self.pipe.p[i].valid_i_test) + in_ready.append(p_valid_i) + m.d.comb += pe.i.eq(Cat(*in_ready)) # array of input "valids" + m.d.comb += self.active.eq(~pe.n) # encoder active (one input valid) + m.d.comb += self.m_id.eq(pe.o) # output one active input + + return m + + def ports(self): + return [self.m_id, self.active] + + + +class PriorityCombMuxInPipe(CombMultiInPipeline): + """ an example of how to use the combinatorial pipeline. + """ + + def __init__(self, stage, p_len=2): + p_mux = InputPriorityArbiter(self, p_len) + CombMultiInPipeline.__init__(self, stage, p_len, p_mux) + + +if __name__ == '__main__': + + dut = PriorityCombMuxInPipe(ExampleStage) + vl = rtlil.convert(dut, ports=dut.ports()) + with open("test_combpipe.il", "w") as f: + f.write(vl) diff --git a/src/nmutil/nmoperator.py b/src/nmutil/nmoperator.py new file mode 100644 index 00000000..bd5e5544 --- /dev/null +++ b/src/nmutil/nmoperator.py @@ -0,0 +1,171 @@ +""" nmigen operator functions / utils + + 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. +""" + +from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable +from nmigen.cli import verilog, rtlil +from nmigen.lib.fifo import SyncFIFO, SyncFIFOBuffered +from nmigen.hdl.ast import ArrayProxy +from nmigen.hdl.rec import Record, Layout + +from abc import ABCMeta, abstractmethod +from collections.abc import Sequence, Iterable +from collections import OrderedDict +from queue import Queue +import inspect + + +class Visitor2: + """ a helper class for iterating twin-argument compound data structures. + + Record is a special (unusual, recursive) case, where the input may be + specified as a dictionary (which may contain further dictionaries, + recursively), where the field names of the dictionary must match + the Record's field spec. Alternatively, an object with the same + member names as the Record may be assigned: it does not have to + *be* a Record. + + ArrayProxy is also special-cased, it's a bit messy: whilst ArrayProxy + has an eq function, the object being assigned to it (e.g. a python + object) might not. despite the *input* having an eq function, + that doesn't help us, because it's the *ArrayProxy* that's being + assigned to. so.... we cheat. use the ports() function of the + python object, enumerate them, find out the list of Signals that way, + and assign them. + """ + def iterator2(self, o, i): + if isinstance(o, dict): + yield from self.dict_iter2(o, i) + + if not isinstance(o, Sequence): + o, i = [o], [i] + for (ao, ai) in zip(o, i): + #print ("visit", fn, ao, ai) + if isinstance(ao, Record): + yield from self.record_iter2(ao, ai) + elif isinstance(ao, ArrayProxy) and not isinstance(ai, Value): + yield from self.arrayproxy_iter2(ao, ai) + else: + yield (ao, ai) + + def dict_iter2(self, o, i): + for (k, v) in o.items(): + print ("d-iter", v, i[k]) + yield (v, i[k]) + return res + + def _not_quite_working_with_all_unit_tests_record_iter2(self, ao, ai): + print ("record_iter2", ao, ai, type(ao), type(ai)) + if isinstance(ai, Value): + if isinstance(ao, Sequence): + ao, ai = [ao], [ai] + for o, i in zip(ao, ai): + yield (o, i) + return + for idx, (field_name, field_shape, _) in enumerate(ao.layout): + if isinstance(field_shape, Layout): + val = ai.fields + else: + val = ai + if hasattr(val, field_name): # check for attribute + val = getattr(val, field_name) + else: + val = val[field_name] # dictionary-style specification + yield from self.iterator2(ao.fields[field_name], val) + + def record_iter2(self, ao, ai): + for idx, (field_name, field_shape, _) in enumerate(ao.layout): + if isinstance(field_shape, Layout): + val = ai.fields + else: + val = ai + if hasattr(val, field_name): # check for attribute + val = getattr(val, field_name) + else: + val = val[field_name] # dictionary-style specification + yield from self.iterator2(ao.fields[field_name], val) + + def arrayproxy_iter2(self, ao, ai): + for p in ai.ports(): + op = getattr(ao, p.name) + print ("arrayproxy - p", p, p.name) + yield from self.iterator2(op, p) + + +class Visitor: + """ a helper class for iterating single-argument compound data structures. + similar to Visitor2. + """ + def iterate(self, i): + """ iterate a compound structure recursively using yield + """ + if not isinstance(i, Sequence): + i = [i] + for ai in i: + #print ("iterate", ai) + if isinstance(ai, Record): + #print ("record", list(ai.layout)) + yield from self.record_iter(ai) + elif isinstance(ai, ArrayProxy) and not isinstance(ai, Value): + yield from self.array_iter(ai) + else: + yield ai + + def record_iter(self, ai): + for idx, (field_name, field_shape, _) in enumerate(ai.layout): + if isinstance(field_shape, Layout): + val = ai.fields + else: + val = ai + if hasattr(val, field_name): # check for attribute + val = getattr(val, field_name) + else: + val = val[field_name] # dictionary-style specification + #print ("recidx", idx, field_name, field_shape, val) + yield from self.iterate(val) + + def array_iter(self, ai): + for p in ai.ports(): + yield from self.iterate(p) + + +def eq(o, i): + """ makes signals equal: a helper routine which identifies if it is being + passed a list (or tuple) of objects, or signals, or Records, and calls + the objects' eq function. + """ + res = [] + for (ao, ai) in Visitor2().iterator2(o, i): + rres = ao.eq(ai) + if not isinstance(rres, Sequence): + rres = [rres] + res += rres + return res + + +def shape(i): + #print ("shape", i) + r = 0 + for part in list(i): + #print ("shape?", part) + s, _ = part.shape() + r += s + return r, False + + +def cat(i): + """ flattens a compound structure recursively using Cat + """ + from nmigen.tools import flatten + #res = list(flatten(i)) # works (as of nmigen commit f22106e5) HOWEVER... + res = list(Visitor().iterate(i)) # needed because input may be a sequence + return Cat(*res) + + diff --git a/src/nmutil/pipeline.py b/src/nmutil/pipeline.py new file mode 100644 index 00000000..afcee743 --- /dev/null +++ b/src/nmutil/pipeline.py @@ -0,0 +1,394 @@ +""" Example 5: Making use of PyRTL and Introspection. """ + +from collections.abc import Sequence + +from nmigen import Signal +from nmigen.hdl.rec import Record +from nmigen import tracer +from nmigen.compat.fhdl.bitcontainer import value_bits_sign +from contextlib import contextmanager + +from nmoperator import eq +from singlepipe import StageCls, ControlBase, BufferedHandshake +from singlepipe import UnbufferedPipeline + + +# The following example shows how pyrtl can be used to make some interesting +# hardware structures using python introspection. In particular, this example +# makes a N-stage pipeline structure. Any specific pipeline is then a derived +# class of SimplePipeline where methods with names starting with "stage" are +# stages, and new members with names not starting with "_" are to be registered +# for the next stage. + +def like(value, rname, pipe, pipemode=False): + if isinstance(value, ObjectProxy): + return ObjectProxy.like(pipe, value, pipemode=pipemode, + name=rname, reset_less=True) + else: + return Signal(value_bits_sign(value), name=rname, + reset_less=True) + return Signal.like(value, name=rname, reset_less=True) + +def get_assigns(_assigns): + assigns = [] + for e in _assigns: + if isinstance(e, ObjectProxy): + assigns += get_assigns(e._assigns) + else: + assigns.append(e) + return assigns + + +def get_eqs(_eqs): + eqs = [] + for e in _eqs: + if isinstance(e, ObjectProxy): + eqs += get_eqs(e._eqs) + else: + eqs.append(e) + return eqs + + +class ObjectProxy: + def __init__(self, m, name=None, pipemode=False, syncmode=True): + self._m = m + if name is None: + name = tracer.get_var_name(default=None) + self.name = name + self._pipemode = pipemode + self._syncmode = syncmode + self._eqs = {} + self._assigns = [] + self._preg_map = {} + + @classmethod + def like(cls, m, value, pipemode=False, name=None, src_loc_at=0, **kwargs): + name = name or tracer.get_var_name(depth=2 + src_loc_at, + default="$like") + + src_loc_at_1 = 1 + src_loc_at + r = ObjectProxy(m, value.name, pipemode) + #for a, aname in value._preg_map.items(): + # r._preg_map[aname] = like(a, aname, m, pipemode) + for a in value.ports(): + aname = a.name + r._preg_map[aname] = like(a, aname, m, pipemode) + return r + + def __repr__(self): + subobjs = [] + for a in self.ports(): + aname = a.name + ai = self._preg_map[aname] + subobjs.append(repr(ai)) + return "" % subobjs + + def get_specs(self, liked=False): + res = [] + for k, v in self._preg_map.items(): + #v = like(v, k, stage._m) + res.append(v) + if isinstance(v, ObjectProxy): + res += v.get_specs() + return res + + def eq(self, i): + print ("ObjectProxy eq", self, i) + res = [] + for a in self.ports(): + aname = a.name + ai = i._preg_map[aname] + res.append(a.eq(ai)) + return res + + def ports(self): + res = [] + for aname, a in self._preg_map.items(): + if isinstance(a, Signal) or isinstance(a, ObjectProxy) or \ + isinstance(a, Record): + res.append(a) + #print ("ObjectPorts", res) + return res + + def __getattr__(self, name): + try: + v = self._preg_map[name] + return v + #return like(v, name, self._m) + except KeyError: + raise AttributeError( + 'error, no pipeline register "%s" defined for OP %s' + % (name, self.name)) + + def __setattr__(self, name, value): + if name.startswith('_') or name in ['name', 'ports', 'eq', 'like']: + # do not do anything tricky with variables starting with '_' + object.__setattr__(self, name, value) + return + #rname = "%s_%s" % (self.name, name) + rname = name + new_pipereg = like(value, rname, self._m, self._pipemode) + self._preg_map[name] = new_pipereg + #object.__setattr__(self, name, new_pipereg) + if self._pipemode: + #print ("OP pipemode", self._syncmode, new_pipereg, value) + assign = eq(new_pipereg, value) + if self._syncmode: + self._m.d.sync += assign + else: + self._m.d.comb += assign + elif self._m: + #print ("OP !pipemode assign", new_pipereg, value, type(value)) + self._m.d.comb += eq(new_pipereg, value) + else: + #print ("OP !pipemode !m", new_pipereg, value, type(value)) + self._assigns += eq(new_pipereg, value) + if isinstance(value, ObjectProxy): + #print ("OP, defer assigns:", value._assigns) + self._assigns += value._assigns + self._eqs.append(value._eqs) + + +class PipelineStage: + """ Pipeline builder stage with auto generation of pipeline registers. + """ + + def __init__(self, name, m, prev=None, pipemode=False, ispec=None): + self._m = m + self._stagename = name + self._preg_map = {'__nextstage__': {}} + self._prev_stage = prev + self._ispec = ispec + if ispec: + self._preg_map[self._stagename] = ispec + if prev: + print ("prev", prev._stagename, prev._preg_map) + #if prev._stagename in prev._preg_map: + # m = prev._preg_map[prev._stagename] + # self._preg_map[prev._stagename] = m + if '__nextstage__' in prev._preg_map: + m = prev._preg_map['__nextstage__'] + m = likedict(m) + self._preg_map[self._stagename] = m + #for k, v in m.items(): + #m[k] = like(v, k, self._m) + print ("make current", self._stagename, m) + self._pipemode = pipemode + self._eqs = {} + self._assigns = [] + + def __getattribute__(self, name): + if name.startswith('_'): + return object.__getattribute__(self, name) + #if name in self._preg_map['__nextstage__']: + # return self._preg_map['__nextstage__'][name] + try: + print ("getattr", name, object.__getattribute__(self, '_preg_map')) + v = self._preg_map[self._stagename][name] + return v + #return like(v, name, self._m) + except KeyError: + raise AttributeError( + 'error, no pipeline register "%s" defined for stage %s' + % (name, self._stagename)) + + def __setattr__(self, name, value): + if name.startswith('_'): + # do not do anything tricky with variables starting with '_' + object.__setattr__(self, name, value) + return + pipereg_id = self._stagename + rname = 'pipereg_' + pipereg_id + '_' + name + new_pipereg = like(value, rname, self._m, self._pipemode) + next_stage = '__nextstage__' + if next_stage not in self._preg_map: + self._preg_map[next_stage] = {} + self._preg_map[next_stage][name] = new_pipereg + print ("setattr", name, value, self._preg_map) + if self._pipemode: + self._eqs[name] = new_pipereg + assign = eq(new_pipereg, value) + print ("pipemode: append", new_pipereg, value, assign) + if isinstance(value, ObjectProxy): + print ("OP, assigns:", value._assigns) + self._assigns += value._assigns + self._eqs[name]._eqs = value._eqs + #self._m.d.comb += assign + self._assigns += assign + elif self._m: + print ("!pipemode: assign", new_pipereg, value) + assign = eq(new_pipereg, value) + self._m.d.sync += assign + else: + print ("!pipemode !m: defer assign", new_pipereg, value) + assign = eq(new_pipereg, value) + self._eqs[name] = new_pipereg + self._assigns += assign + if isinstance(value, ObjectProxy): + print ("OP, defer assigns:", value._assigns) + self._assigns += value._assigns + self._eqs[name]._eqs = value._eqs + +def likelist(specs): + res = [] + for v in specs: + res.append(like(v, v.name, None, pipemode=True)) + return res + +def likedict(specs): + if not isinstance(specs, dict): + return like(specs, specs.name, None, pipemode=True) + res = {} + for k, v in specs.items(): + res[k] = likedict(v) + return res + + +class AutoStage(StageCls): + def __init__(self, inspecs, outspecs, eqs, assigns): + self.inspecs, self.outspecs = inspecs, outspecs + self.eqs, self.assigns = eqs, assigns + #self.o = self.ospec() + def ispec(self): return likedict(self.inspecs) + def ospec(self): return likedict(self.outspecs) + + def process(self, i): + print ("stage process", i) + return self.eqs + + def setup(self, m, i): + print ("stage setup i", i, m) + print ("stage setup inspecs", self.inspecs) + print ("stage setup outspecs", self.outspecs) + print ("stage setup eqs", self.eqs) + #self.o = self.ospec() + m.d.comb += eq(self.inspecs, i) + #m.d.comb += eq(self.outspecs, self.eqs) + #m.d.comb += eq(self.o, i) + + +class AutoPipe(UnbufferedPipeline): + def __init__(self, stage, assigns): + UnbufferedPipeline.__init__(self, stage) + self.assigns = assigns + + def elaborate(self, platform): + m = UnbufferedPipeline.elaborate(self, platform) + m.d.comb += self.assigns + print ("assigns", self.assigns, m) + return m + + +class PipeManager: + def __init__(self, m, pipemode=False, pipetype=None): + self.m = m + self.pipemode = pipemode + self.pipetype = pipetype + + @contextmanager + def Stage(self, name, prev=None, ispec=None): + if ispec: + ispec = likedict(ispec) + print ("start stage", name, ispec) + stage = PipelineStage(name, None, prev, self.pipemode, ispec=ispec) + try: + yield stage, self.m #stage._m + finally: + pass + if self.pipemode: + if stage._ispec: + print ("use ispec", stage._ispec) + inspecs = stage._ispec + else: + inspecs = self.get_specs(stage, name) + #inspecs = likedict(inspecs) + outspecs = self.get_specs(stage, '__nextstage__', liked=True) + print ("stage inspecs", name, inspecs) + print ("stage outspecs", name, outspecs) + eqs = stage._eqs # get_eqs(stage._eqs) + assigns = get_assigns(stage._assigns) + print ("stage eqs", name, eqs) + print ("stage assigns", name, assigns) + s = AutoStage(inspecs, outspecs, eqs, assigns) + self.stages.append(s) + print ("end stage", name, self.pipemode, "\n") + + def get_specs(self, stage, name, liked=False): + return stage._preg_map[name] + if name in stage._preg_map: + res = [] + for k, v in stage._preg_map[name].items(): + #v = like(v, k, stage._m) + res.append(v) + #if isinstance(v, ObjectProxy): + # res += v.get_specs() + return res + return {} + + def __enter__(self): + self.stages = [] + return self + + def __exit__(self, *args): + print ("exit stage", args) + pipes = [] + cb = ControlBase() + for s in self.stages: + print ("stage specs", s, s.inspecs, s.outspecs) + if self.pipetype == 'buffered': + p = BufferedHandshake(s) + else: + p = AutoPipe(s, s.assigns) + pipes.append(p) + self.m.submodules += p + + self.m.d.comb += cb.connect(pipes) + + +class SimplePipeline: + """ Pipeline builder with auto generation of pipeline registers. + """ + + def __init__(self, m): + self._m = m + self._pipeline_register_map = {} + self._current_stage_num = 0 + + def _setup(self): + stage_list = [] + for method in dir(self): + if method.startswith('stage'): + stage_list.append(method) + for stage in sorted(stage_list): + stage_method = getattr(self, stage) + stage_method() + self._current_stage_num += 1 + + def __getattr__(self, name): + try: + return self._pipeline_register_map[self._current_stage_num][name] + except KeyError: + raise AttributeError( + 'error, no pipeline register "%s" defined for stage %d' + % (name, self._current_stage_num)) + + def __setattr__(self, name, value): + if name.startswith('_'): + # do not do anything tricky with variables starting with '_' + object.__setattr__(self, name, value) + return + next_stage = self._current_stage_num + 1 + pipereg_id = str(self._current_stage_num) + 'to' + str(next_stage) + rname = 'pipereg_' + pipereg_id + '_' + name + #new_pipereg = Signal(value_bits_sign(value), name=rname, + # reset_less=True) + if isinstance(value, ObjectProxy): + new_pipereg = ObjectProxy.like(self._m, value, + name=rname, reset_less = True) + else: + new_pipereg = Signal.like(value, name=rname, reset_less = True) + if next_stage not in self._pipeline_register_map: + self._pipeline_register_map[next_stage] = {} + self._pipeline_register_map[next_stage][name] = new_pipereg + self._m.d.sync += eq(new_pipereg, value) + diff --git a/src/nmutil/singlepipe.py b/src/nmutil/singlepipe.py new file mode 100644 index 00000000..68b62e43 --- /dev/null +++ b/src/nmutil/singlepipe.py @@ -0,0 +1,829 @@ +""" Pipeline API. For multi-input and multi-output variants, see multipipe. + + Associated development bugs: + * http://bugs.libre-riscv.org/show_bug.cgi?id=64 + * http://bugs.libre-riscv.org/show_bug.cgi?id=57 + + Important: see Stage API (stageapi.py) in combination with below + + RecordBasedStage: + ---------------- + + A convenience class that takes an input shape, output shape, a + "processing" function and an optional "setup" function. Honestly + though, there's not much more effort to just... create a class + that returns a couple of Records (see ExampleAddRecordStage in + examples). + + PassThroughStage: + ---------------- + + A convenience class that takes a single function as a parameter, + that is chain-called to create the exact same input and output spec. + It has a process() function that simply returns its input. + + Instances of this class are completely redundant if handed to + 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. + + Note: pipelines basically do not become pipelines as such until + handed to a derivative of ControlBase. ControlBase itself is *not* + strictly considered a pipeline class. Wishbone and AXI4 (master or + slave) could be derived from ControlBase, for example. + UnbufferedPipeline: + ------------------ + + A simple stalling clock-synchronised pipeline that has no buffering + (unlike BufferedHandshake). Data flows on *every* clock cycle when + the conditions are right (this is nominally when the input is valid + and the output is ready). + + A stall anywhere along the line will result in a stall back-propagating + down the entire chain. The BufferedHandshake by contrast will buffer + incoming data, allowing previous stages one clock cycle's grace before + also having to stall. + + An advantage of the UnbufferedPipeline over the Buffered one is + that the amount of logic needed (number of gates) is greatly + reduced (no second set of buffers basically) + + The disadvantage of the UnbufferedPipeline is that the valid/ready + logic, if chained together, is *combinatorial*, resulting in + progressively larger gate delay. + + PassThroughHandshake: + ------------------ + + A Control class that introduces a single clock delay, passing its + data through unaltered. Unlike RegisterPipeline (which relies + on UnbufferedPipeline and PassThroughStage) it handles ready/valid + itself. + + RegisterPipeline: + ---------------- + + A convenience class that, because UnbufferedPipeline introduces a single + clock delay, when its stage is a PassThroughStage, it results in a Pipeline + stage that, duh, delays its (unmodified) input by one clock cycle. + + BufferedHandshake: + ---------------- + + nmigen implementation of buffered pipeline stage, based on zipcpu: + https://zipcpu.com/blog/2017/08/14/strategies-for-pipelining.html + + this module requires quite a bit of thought to understand how it works + (and why it is needed in the first place). reading the above is + *strongly* recommended. + + unlike john dawson's IEEE754 FPU STB/ACK signalling, which requires + the STB / ACK signals to raise and lower (on separate clocks) before + data may proceeed (thus only allowing one piece of data to proceed + on *ALTERNATE* cycles), the signalling here is a true pipeline + where data will flow on *every* clock when the conditions are right. + + input acceptance conditions are when: + * incoming previous-stage strobe (p.valid_i) is HIGH + * outgoing previous-stage ready (p.ready_o) is LOW + + output transmission conditions are when: + * outgoing next-stage strobe (n.valid_o) is HIGH + * outgoing next-stage ready (n.ready_i) is LOW + + the tricky bit is when the input has valid data and the output is not + ready to accept it. if it wasn't for the clock synchronisation, it + would be possible to tell the input "hey don't send that data, we're + not ready". unfortunately, it's not possible to "change the past": + the previous stage *has no choice* but to pass on its data. + + therefore, the incoming data *must* be accepted - and stored: that + is the responsibility / contract that this stage *must* accept. + on the same clock, it's possible to tell the input that it must + not send any more data. this is the "stall" condition. + + we now effectively have *two* possible pieces of data to "choose" from: + the buffered data, and the incoming data. the decision as to which + to process and output is based on whether we are in "stall" or not. + i.e. when the next stage is no longer ready, the output comes from + the buffer if a stall had previously occurred, otherwise it comes + direct from processing the input. + + this allows us to respect a synchronous "travelling STB" with what + dan calls a "buffered handshake". + + it's quite a complex state machine! + + SimpleHandshake + --------------- + + Synchronised pipeline, Based on: + https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v +""" + +from nmigen import Signal, Mux, Module, Elaboratable +from nmigen.cli import verilog, rtlil +from nmigen.hdl.rec import Record + +from queue import Queue +import inspect + +from iocontrol import (PrevControl, NextControl, Object, RecordObject) +from stageapi import (_spec, StageCls, Stage, StageChain, StageHelper) +import nmoperator + + +class RecordBasedStage(Stage): + """ convenience class which provides a Records-based layout. + honestly it's a lot easier just to create a direct Records-based + class (see ExampleAddRecordStage) + """ + def __init__(self, in_shape, out_shape, processfn, setupfn=None): + self.in_shape = in_shape + self.out_shape = out_shape + self.__process = processfn + self.__setup = setupfn + def ispec(self): return Record(self.in_shape) + def ospec(self): return Record(self.out_shape) + def process(seif, i): return self.__process(i) + def setup(seif, m, i): return self.__setup(m, i) + + +class PassThroughStage(StageCls): + """ a pass-through stage with its input data spec identical to its output, + and "passes through" its data from input to output (does nothing). + + use this basically to explicitly make any data spec Stage-compliant. + (many APIs would potentially use a static "wrap" method in e.g. + StageCls to achieve a similar effect) + """ + def __init__(self, iospecfn): self.iospecfn = iospecfn + def ispec(self): return self.iospecfn() + def ospec(self): return self.iospecfn() + + +class ControlBase(StageHelper, Elaboratable): + """ Common functions for Pipeline API. Note: a "pipeline stage" only + exists (conceptually) when a ControlBase derivative is handed + a Stage (combinatorial block) + + NOTE: ControlBase derives from StageHelper, making it accidentally + compliant with the Stage API. Using those functions directly + *BYPASSES* a ControlBase instance ready/valid signalling, which + clearly should not be done without a really, really good reason. + """ + def __init__(self, stage=None, in_multi=None, stage_ctl=False): + """ Base class containing ready/valid/data to previous and next stages + + * p: contains ready/valid to the previous stage + * n: contains ready/valid to the next stage + + Except when calling Controlbase.connect(), user must also: + * add data_i member to PrevControl (p) and + * add data_o member to NextControl (n) + Calling ControlBase._new_data is a good way to do that. + """ + StageHelper.__init__(self, stage) + + # set up input and output IO ACK (prev/next ready/valid) + self.p = PrevControl(in_multi, stage_ctl) + self.n = NextControl(stage_ctl) + + # set up the input and output data + if stage is not None: + self._new_data("data") + + def _new_data(self, name): + """ allocates new data_i and data_o + """ + self.p.data_i, self.n.data_o = self.new_specs(name) + + @property + def data_r(self): + return self.process(self.p.data_i) + + def connect_to_next(self, nxt): + """ helper function to connect to the next stage data/valid/ready. + """ + return self.n.connect_to_next(nxt.p) + + def _connect_in(self, prev): + """ internal helper function to connect stage to an input source. + do not use to connect stage-to-stage! + """ + return self.p._connect_in(prev.p) + + def _connect_out(self, nxt): + """ internal helper function to connect stage to an output source. + do not use to connect stage-to-stage! + """ + return self.n._connect_out(nxt.n) + + def connect(self, pipechain): + """ connects a chain (list) of Pipeline instances together and + links them to this ControlBase instance: + + in <----> 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. + + Argument: + + * :pipechain: - a sequence of ControlBase-derived classes + (must be one or more in length) + + Returns: + + * a list of eq assignments that will need to be added in + an elaborate() to m.d.comb + """ + assert len(pipechain) > 0, "pipechain must be non-zero length" + assert self.stage is None, "do not use connect with a stage" + eqs = [] # collated list of assignment statements + + # connect inter-chain + for i in range(len(pipechain)-1): + pipe1 = pipechain[i] # earlier + pipe2 = pipechain[i+1] # later (by 1) + eqs += pipe1.connect_to_next(pipe2) # earlier n to later p + + # connect front and back of chain to ourselves + front = pipechain[0] # first in chain + end = pipechain[-1] # last in chain + self.set_specs(front, end) # sets up ispec/ospec functions + self._new_data("chain") # NOTE: REPLACES existing data + eqs += front._connect_in(self) # front p to our p + eqs += end._connect_out(self) # end n to our n + + return eqs + + def set_input(self, i): + """ helper function to set the input data (used in unit tests) + """ + return nmoperator.eq(self.p.data_i, i) + + def __iter__(self): + yield from self.p # yields ready/valid/data (data also gets yielded) + yield from self.n # ditto + + 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 + + self.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 + is shunted in a temporary register. + + Argument: stage. see Stage API above + + stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 + stage-1 p.ready_o <>in stage n.data_o out>> stage+1 + | | + process --->----^ + | | + +-- r_data ->-+ + + input data p.data_i is read (only), is processed and goes into an + intermediate result store [process()]. this is updated combinatorially. + + in a non-stall condition, the intermediate result will go into the + output (update_output). however if ever there is a stall, it goes + into r_data instead [update_buffer()]. + + when the non-stall condition is released, r_data is the first + to be transferred to the output [flush_buffer()], and the stall + condition cleared. + + on the next cycle (as long as stall is not raised again) the + input may begin to be processed and transferred directly to output. + """ + + def elaborate(self, platform): + self.m = ControlBase.elaborate(self, platform) + + result = _spec(self.stage.ospec, "r_tmp") + r_data = _spec(self.stage.ospec, "r_data") + + # establish some combinatorial temporaries + o_n_validn = Signal(reset_less=True) + n_ready_i = Signal(reset_less=True, name="n_i_rdy_data") + nir_por = Signal(reset_less=True) + nir_por_n = Signal(reset_less=True) + p_valid_i = Signal(reset_less=True) + nir_novn = Signal(reset_less=True) + nirn_novn = Signal(reset_less=True) + por_pivn = Signal(reset_less=True) + npnn = Signal(reset_less=True) + self.m.d.comb += [p_valid_i.eq(self.p.valid_i_test), + o_n_validn.eq(~self.n.valid_o), + n_ready_i.eq(self.n.ready_i_test), + nir_por.eq(n_ready_i & self.p._ready_o), + nir_por_n.eq(n_ready_i & ~self.p._ready_o), + nir_novn.eq(n_ready_i | o_n_validn), + nirn_novn.eq(~n_ready_i & o_n_validn), + npnn.eq(nir_por | nirn_novn), + por_pivn.eq(self.p._ready_o & ~p_valid_i) + ] + + # store result of processing in combinatorial temporary + self.m.d.comb += nmoperator.eq(result, self.data_r) + + # if not in stall condition, update the temporary register + with self.m.If(self.p.ready_o): # not stalled + self.m.d.sync += nmoperator.eq(r_data, result) # update buffer + + # data pass-through conditions + with self.m.If(npnn): + data_o = self._postprocess(result) # XXX TBD, does nothing right now + self.m.d.sync += [self.n.valid_o.eq(p_valid_i), # valid if p_valid + nmoperator.eq(self.n.data_o, data_o), # update out + ] + # buffer flush conditions (NOTE: can override data passthru conditions) + with self.m.If(nir_por_n): # not stalled + # Flush the [already processed] buffer to the output port. + data_o = self._postprocess(r_data) # XXX TBD, does nothing right now + self.m.d.sync += [self.n.valid_o.eq(1), # reg empty + nmoperator.eq(self.n.data_o, data_o), # flush + ] + # output ready conditions + self.m.d.sync += self.p._ready_o.eq(nir_novn | por_pivn) + + return self.m + + +class SimpleHandshake(ControlBase): + """ simple handshake control. data and strobe signals travel in sync. + implements the protocol used by Wishbone and AXI4. + + Argument: stage. see Stage API above + + stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 + stage-1 p.ready_o <>in stage n.data_o out>> stage+1 + | | + +--process->--^ + Truth Table + + Inputs Temporary Output Data + ------- ---------- ----- ---- + P P N N PiV& ~NiR& N P + i o i o PoR NoV o o + V R R V V R + + ------- - - - - + 0 0 0 0 0 0 >0 0 reg + 0 0 0 1 0 1 >1 0 reg + 0 0 1 0 0 0 0 1 process(data_i) + 0 0 1 1 0 0 0 1 process(data_i) + ------- - - - - + 0 1 0 0 0 0 >0 0 reg + 0 1 0 1 0 1 >1 0 reg + 0 1 1 0 0 0 0 1 process(data_i) + 0 1 1 1 0 0 0 1 process(data_i) + ------- - - - - + 1 0 0 0 0 0 >0 0 reg + 1 0 0 1 0 1 >1 0 reg + 1 0 1 0 0 0 0 1 process(data_i) + 1 0 1 1 0 0 0 1 process(data_i) + ------- - - - - + 1 1 0 0 1 0 1 0 process(data_i) + 1 1 0 1 1 1 1 0 process(data_i) + 1 1 1 0 1 0 1 1 process(data_i) + 1 1 1 1 1 0 1 1 process(data_i) + ------- - - - - + """ + + def elaborate(self, platform): + self.m = m = ControlBase.elaborate(self, platform) + + r_busy = Signal() + result = _spec(self.stage.ospec, "r_tmp") + + # establish some combinatorial temporaries + n_ready_i = Signal(reset_less=True, name="n_i_rdy_data") + p_valid_i_p_ready_o = Signal(reset_less=True) + p_valid_i = Signal(reset_less=True) + m.d.comb += [p_valid_i.eq(self.p.valid_i_test), + n_ready_i.eq(self.n.ready_i_test), + p_valid_i_p_ready_o.eq(p_valid_i & self.p.ready_o), + ] + + # store result of processing in combinatorial temporary + m.d.comb += nmoperator.eq(result, self.data_r) + + # previous valid and ready + with m.If(p_valid_i_p_ready_o): + data_o = self._postprocess(result) # XXX TBD, does nothing right now + m.d.sync += [r_busy.eq(1), # output valid + nmoperator.eq(self.n.data_o, data_o), # update output + ] + # previous invalid or not ready, however next is accepting + with m.Elif(n_ready_i): + data_o = self._postprocess(result) # XXX TBD, does nothing right now + m.d.sync += [nmoperator.eq(self.n.data_o, data_o)] + # TODO: could still send data here (if there was any) + #m.d.sync += self.n.valid_o.eq(0) # ...so set output invalid + m.d.sync += r_busy.eq(0) # ...so set output invalid + + m.d.comb += self.n.valid_o.eq(r_busy) + # if next is ready, so is previous + m.d.comb += self.p._ready_o.eq(n_ready_i) + + return self.m + + +class UnbufferedPipeline(ControlBase): + """ A simple pipeline stage with single-clock synchronisation + and two-way valid/ready synchronised signalling. + + Note that a stall in one stage will result in the entire pipeline + chain stalling. + + Also that unlike BufferedHandshake, the valid/ready signalling does NOT + travel synchronously with the data: the valid/ready signalling + combines in a *combinatorial* fashion. Therefore, a long pipeline + chain will lengthen propagation delays. + + Argument: stage. see Stage API, above + + stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 + stage-1 p.ready_o <>in stage n.data_o out>> stage+1 + | | + r_data result + | | + +--process ->-+ + + Attributes: + ----------- + p.data_i : StageInput, shaped according to ispec + The pipeline input + p.data_o : StageOutput, shaped according to ospec + The pipeline output + r_data : input_shape according to ispec + A temporary (buffered) copy of a prior (valid) input. + This is HELD if the output is not ready. It is updated + SYNCHRONOUSLY. + result: output_shape according to ospec + The output of the combinatorial logic. it is updated + COMBINATORIALLY (no clock dependence). + + Truth Table + + Inputs Temp Output Data + ------- - ----- ---- + P P N N ~NiR& N P + i o i o NoV o o + V R R V V R + + ------- - - - + 0 0 0 0 0 0 1 reg + 0 0 0 1 1 1 0 reg + 0 0 1 0 0 0 1 reg + 0 0 1 1 0 0 1 reg + ------- - - - + 0 1 0 0 0 0 1 reg + 0 1 0 1 1 1 0 reg + 0 1 1 0 0 0 1 reg + 0 1 1 1 0 0 1 reg + ------- - - - + 1 0 0 0 0 1 1 reg + 1 0 0 1 1 1 0 reg + 1 0 1 0 0 1 1 reg + 1 0 1 1 0 1 1 reg + ------- - - - + 1 1 0 0 0 1 1 process(data_i) + 1 1 0 1 1 1 0 process(data_i) + 1 1 1 0 0 1 1 process(data_i) + 1 1 1 1 0 1 1 process(data_i) + ------- - - - + + Note: PoR is *NOT* involved in the above decision-making. + """ + + def elaborate(self, platform): + self.m = m = ControlBase.elaborate(self, platform) + + data_valid = Signal() # is data valid or not + r_data = _spec(self.stage.ospec, "r_tmp") # output type + + # some temporaries + p_valid_i = Signal(reset_less=True) + pv = Signal(reset_less=True) + buf_full = Signal(reset_less=True) + m.d.comb += p_valid_i.eq(self.p.valid_i_test) + m.d.comb += pv.eq(self.p.valid_i & self.p.ready_o) + m.d.comb += buf_full.eq(~self.n.ready_i_test & data_valid) + + m.d.comb += self.n.valid_o.eq(data_valid) + m.d.comb += self.p._ready_o.eq(~data_valid | self.n.ready_i_test) + m.d.sync += data_valid.eq(p_valid_i | buf_full) + + with m.If(pv): + m.d.sync += nmoperator.eq(r_data, self.data_r) + data_o = self._postprocess(r_data) # XXX TBD, does nothing right now + m.d.comb += nmoperator.eq(self.n.data_o, data_o) + + return self.m + +class UnbufferedPipeline2(ControlBase): + """ A simple pipeline stage with single-clock synchronisation + and two-way valid/ready synchronised signalling. + + Note that a stall in one stage will result in the entire pipeline + chain stalling. + + Also that unlike BufferedHandshake, the valid/ready signalling does NOT + travel synchronously with the data: the valid/ready signalling + combines in a *combinatorial* fashion. Therefore, a long pipeline + chain will lengthen propagation delays. + + Argument: stage. see Stage API, above + + stage-1 p.valid_i >>in stage n.valid_o out>> stage+1 + stage-1 p.ready_o <>in stage n.data_o out>> stage+1 + | | | + +- process-> buf <-+ + Attributes: + ----------- + p.data_i : StageInput, shaped according to ispec + The pipeline input + p.data_o : StageOutput, shaped according to ospec + The pipeline output + buf : output_shape according to ospec + A temporary (buffered) copy of a valid output + This is HELD if the output is not ready. It is updated + SYNCHRONOUSLY. + + Inputs Temp Output Data + ------- - ----- + P P N N ~NiR& N P (buf_full) + i o i o NoV o o + V R R V V R + + ------- - - - + 0 0 0 0 0 0 1 process(data_i) + 0 0 0 1 1 1 0 reg (odata, unchanged) + 0 0 1 0 0 0 1 process(data_i) + 0 0 1 1 0 0 1 process(data_i) + ------- - - - + 0 1 0 0 0 0 1 process(data_i) + 0 1 0 1 1 1 0 reg (odata, unchanged) + 0 1 1 0 0 0 1 process(data_i) + 0 1 1 1 0 0 1 process(data_i) + ------- - - - + 1 0 0 0 0 1 1 process(data_i) + 1 0 0 1 1 1 0 reg (odata, unchanged) + 1 0 1 0 0 1 1 process(data_i) + 1 0 1 1 0 1 1 process(data_i) + ------- - - - + 1 1 0 0 0 1 1 process(data_i) + 1 1 0 1 1 1 0 reg (odata, unchanged) + 1 1 1 0 0 1 1 process(data_i) + 1 1 1 1 0 1 1 process(data_i) + ------- - - - + + Note: PoR is *NOT* involved in the above decision-making. + """ + + def elaborate(self, platform): + self.m = m = ControlBase.elaborate(self, platform) + + buf_full = Signal() # is data valid or not + buf = _spec(self.stage.ospec, "r_tmp") # output type + + # some temporaries + p_valid_i = Signal(reset_less=True) + m.d.comb += p_valid_i.eq(self.p.valid_i_test) + + m.d.comb += self.n.valid_o.eq(buf_full | p_valid_i) + m.d.comb += self.p._ready_o.eq(~buf_full) + m.d.sync += buf_full.eq(~self.n.ready_i_test & self.n.valid_o) + + data_o = Mux(buf_full, buf, self.data_r) + data_o = self._postprocess(data_o) # XXX TBD, does nothing right now + m.d.comb += nmoperator.eq(self.n.data_o, data_o) + m.d.sync += nmoperator.eq(buf, self.n.data_o) + + return self.m + + +class PassThroughHandshake(ControlBase): + """ A control block that delays by one clock cycle. + + Inputs Temporary Output Data + ------- ------------------ ----- ---- + P P N N PiV& PiV| NiR| pvr N P (pvr) + i o i o PoR ~PoR ~NoV o o + V R R V V R + + ------- - - - - - - + 0 0 0 0 0 1 1 0 1 1 odata (unchanged) + 0 0 0 1 0 1 0 0 1 0 odata (unchanged) + 0 0 1 0 0 1 1 0 1 1 odata (unchanged) + 0 0 1 1 0 1 1 0 1 1 odata (unchanged) + ------- - - - - - - + 0 1 0 0 0 0 1 0 0 1 odata (unchanged) + 0 1 0 1 0 0 0 0 0 0 odata (unchanged) + 0 1 1 0 0 0 1 0 0 1 odata (unchanged) + 0 1 1 1 0 0 1 0 0 1 odata (unchanged) + ------- - - - - - - + 1 0 0 0 0 1 1 1 1 1 process(in) + 1 0 0 1 0 1 0 0 1 0 odata (unchanged) + 1 0 1 0 0 1 1 1 1 1 process(in) + 1 0 1 1 0 1 1 1 1 1 process(in) + ------- - - - - - - + 1 1 0 0 1 1 1 1 1 1 process(in) + 1 1 0 1 1 1 0 0 1 0 odata (unchanged) + 1 1 1 0 1 1 1 1 1 1 process(in) + 1 1 1 1 1 1 1 1 1 1 process(in) + ------- - - - - - - + + """ + + def elaborate(self, platform): + self.m = m = ControlBase.elaborate(self, platform) + + r_data = _spec(self.stage.ospec, "r_tmp") # output type + + # temporaries + p_valid_i = Signal(reset_less=True) + pvr = Signal(reset_less=True) + m.d.comb += p_valid_i.eq(self.p.valid_i_test) + m.d.comb += pvr.eq(p_valid_i & self.p.ready_o) + + m.d.comb += self.p.ready_o.eq(~self.n.valid_o | self.n.ready_i_test) + m.d.sync += self.n.valid_o.eq(p_valid_i | ~self.p.ready_o) + + odata = Mux(pvr, self.data_r, r_data) + m.d.sync += nmoperator.eq(r_data, odata) + r_data = self._postprocess(r_data) # XXX TBD, does nothing right now + m.d.comb += nmoperator.eq(self.n.data_o, r_data) + + return m + + +class RegisterPipeline(UnbufferedPipeline): + """ A pipeline stage that delays by one clock cycle, creating a + sync'd latch out of data_o and valid_o as an indirect byproduct + of using PassThroughStage + """ + def __init__(self, iospecfn): + UnbufferedPipeline.__init__(self, PassThroughStage(iospecfn)) + + +class FIFOControl(ControlBase): + """ FIFO Control. Uses Queue to store data, coincidentally + happens to have same valid/ready signalling as Stage API. + + data_i -> fifo.din -> FIFO -> fifo.dout -> data_o + """ + def __init__(self, depth, stage, in_multi=None, stage_ctl=False, + fwft=True, pipe=False): + """ FIFO Control + + * :depth: number of entries in the FIFO + * :stage: data processing block + * :fwft: first word fall-thru mode (non-fwft introduces delay) + * :pipe: specifies pipe mode. + + when fwft = True it indicates that transfers may occur + combinatorially through stage processing in the same clock cycle. + This requires that the Stage be a Moore FSM: + https://en.wikipedia.org/wiki/Moore_machine + + when fwft = False it indicates that all output signals are + produced only from internal registers or memory, i.e. that the + Stage is a Mealy FSM: + https://en.wikipedia.org/wiki/Mealy_machine + + data is processed (and located) as follows: + + self.p self.stage temp fn temp fn temp fp self.n + data_i->process()->result->cat->din.FIFO.dout->cat(data_o) + + yes, really: cat produces a Cat() which can be assigned to. + this is how the FIFO gets de-catted without needing a de-cat + function + """ + self.fwft = fwft + self.pipe = pipe + self.fdepth = depth + ControlBase.__init__(self, stage, in_multi, stage_ctl) + + def elaborate(self, platform): + self.m = m = ControlBase.elaborate(self, platform) + + # make a FIFO with a signal of equal width to the data_o. + (fwidth, _) = nmoperator.shape(self.n.data_o) + fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe) + m.submodules.fifo = fifo + + def processfn(data_i): + # store result of processing in combinatorial temporary + result = _spec(self.stage.ospec, "r_temp") + m.d.comb += nmoperator.eq(result, self.process(data_i)) + return nmoperator.cat(result) + + ## prev: make the FIFO (Queue object) "look" like a PrevControl... + m.submodules.fp = fp = PrevControl() + fp.valid_i, fp._ready_o, fp.data_i = fifo.we, fifo.writable, fifo.din + m.d.comb += fp._connect_in(self.p, fn=processfn) + + # next: make the FIFO (Queue object) "look" like a NextControl... + m.submodules.fn = fn = NextControl() + fn.valid_o, fn.ready_i, fn.data_o = fifo.readable, fifo.re, fifo.dout + connections = fn._connect_out(self.n, fn=nmoperator.cat) + + # ok ok so we can't just do the ready/valid eqs straight: + # first 2 from connections are the ready/valid, 3rd is data. + if self.fwft: + m.d.comb += connections[:2] # combinatorial on next ready/valid + else: + m.d.sync += connections[:2] # non-fwft mode needs sync + data_o = connections[2] # get the data + data_o = self._postprocess(data_o) # XXX TBD, does nothing right now + m.d.comb += data_o + + return m + + +# aka "RegStage". +class UnbufferedPipeline(FIFOControl): + def __init__(self, stage, in_multi=None, stage_ctl=False): + FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl, + fwft=True, pipe=False) + +# aka "BreakReadyStage" XXX had to set fwft=True to get it to work +class PassThroughHandshake(FIFOControl): + def __init__(self, stage, in_multi=None, stage_ctl=False): + FIFOControl.__init__(self, 1, stage, in_multi, stage_ctl, + fwft=True, pipe=True) + +# this is *probably* BufferedHandshake, although test #997 now succeeds. +class BufferedHandshake(FIFOControl): + def __init__(self, stage, in_multi=None, stage_ctl=False): + FIFOControl.__init__(self, 2, stage, in_multi, stage_ctl, + fwft=True, pipe=False) + + +""" +# this is *probably* SimpleHandshake (note: memory cell size=0) +class SimpleHandshake(FIFOControl): + def __init__(self, stage, in_multi=None, stage_ctl=False): + FIFOControl.__init__(self, 0, stage, in_multi, stage_ctl, + fwft=True, pipe=False) +"""