X-Git-Url: https://git.libre-soc.org/?a=blobdiff_plain;f=src%2Fadd%2Fsinglepipe.py;h=68b62e432d4fc6022b99361c3358d01638414fce;hb=6bff1a997f3846872cf489c24b5c01426c4dc97c;hp=be7fce7ce0ce5806da9e8c2797ab3b042db4dd64;hpb=0e045b3dcb3cbae5a8d503046a49c0802a4ca7b4;p=ieee754fpu.git diff --git a/src/add/singlepipe.py b/src/add/singlepipe.py index be7fce7c..68b62e43 100644 --- a/src/add/singlepipe.py +++ b/src/add/singlepipe.py @@ -1,54 +1,10 @@ -""" Pipeline and BufferedHandshake implementation, conforming to the same API. - For multi-input and multi-output variants, see multipipe. +""" 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 - eq: - -- - - a strategically very important function that is identical in function - to nmigen's Signal.eq function, except it may take objects, or a list - of objects, or a tuple of objects, and where objects may also be - Records. - - Stage API: - --------- - - stage requires compliance with a strict API that may be - implemented in several means, including as a static class. - the methods of a stage instance must be as follows: - - * ispec() - Input data format specification - returns an object or a list or tuple of objects, or - a Record, each object having an "eq" function which - takes responsibility for copying by assignment all - sub-objects - * ospec() - Output data format specification - requirements as for ospec - * process(m, i) - Processes an ispec-formatted object - returns a combinatorial block of a result that - may be assigned to the output, by way of the "eq" - function - * setup(m, i) - Optional function for setting up submodules - may be used for more complex stages, to link - the input (i) to submodules. must take responsibility - for adding those submodules to the module (m). - the submodules must be combinatorial blocks and - must have their inputs and output linked combinatorially. - - Both StageCls (for use with non-static classes) and Stage (for use - by static classes) are abstract classes from which, for convenience - and as a courtesy to other developers, anything conforming to the - Stage API may *choose* to derive. - - StageChain: - ---------- - - A useful combinatorial wrapper around stages that chains them together - and then presents a Stage-API-conformant interface. By presenting - the same API as the stages it wraps, it can clearly be used recursively. + Important: see Stage API (stageapi.py) in combination with below RecordBasedStage: ---------------- @@ -78,6 +34,10 @@ 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: ------------------ @@ -168,285 +128,17 @@ https://github.com/ZipCPU/dbgbus/blob/master/hexbus/rtl/hbdeword.v """ -from nmigen import Signal, Cat, Const, Mux, Module, Value, Elaboratable +from nmigen import Signal, Mux, Module, 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 nmigen.hdl.rec import Record -from abc import ABCMeta, abstractmethod -from collections.abc import Sequence, Iterable -from collections import OrderedDict from queue import Queue import inspect -from nmoperator import eq, cat, shape - - -class Object: - def __init__(self): - self.fields = OrderedDict() - - def __setattr__(self, k, v): - print ("kv", k, v) - if (k.startswith('_') or k in ["fields", "name", "src_loc"] or - k in dir(Object) or "fields" not in self.__dict__): - return object.__setattr__(self, k, v) - self.fields[k] = v - - def __getattr__(self, k): - if k in self.__dict__: - return object.__getattr__(self, k) - try: - return self.fields[k] - except KeyError as e: - raise AttributeError(e) - - def __iter__(self): - for x in self.fields.values(): - if isinstance(x, Iterable): - yield from x - else: - yield x - - def eq(self, inp): - res = [] - for (k, o) in self.fields.items(): - i = getattr(inp, k) - print ("eq", o, i) - rres = o.eq(i) - if isinstance(rres, Sequence): - res += rres - else: - res.append(rres) - print (res) - return res - - def ports(self): - return list(self) - - -class RecordObject(Record): - def __init__(self, layout=None, name=None): - Record.__init__(self, layout=layout or [], name=None) - - def __setattr__(self, k, v): - #print (dir(Record)) - if (k.startswith('_') or k in ["fields", "name", "src_loc"] or - k in dir(Record) or "fields" not in self.__dict__): - return object.__setattr__(self, k, v) - self.fields[k] = v - #print ("RecordObject setattr", k, v) - if isinstance(v, Record): - newlayout = {k: (k, v.layout)} - elif isinstance(v, Value): - newlayout = {k: (k, v.shape())} - else: - newlayout = {k: (k, shape(v))} - self.layout.fields.update(newlayout) - - def __iter__(self): - for x in self.fields.values(): - if isinstance(x, Iterable): - yield from x - else: - yield x - - def ports(self): - return list(self) - - -def _spec(fn, name=None): - if name is None: - return fn() - varnames = dict(inspect.getmembers(fn.__code__))['co_varnames'] - if 'name' in varnames: - return fn(name=name) - return fn() - - -class PrevControl(Elaboratable): - """ contains signals that come *from* the previous stage (both in and out) - * valid_i: previous stage indicating all incoming data is valid. - may be a multi-bit signal, where all bits are required - to be asserted to indicate "valid". - * ready_o: output to next stage indicating readiness to accept data - * data_i : an input - added by the user of this class - """ - - def __init__(self, i_width=1, stage_ctl=False): - self.stage_ctl = stage_ctl - self.valid_i = Signal(i_width, name="p_valid_i") # prev >>in self - self._ready_o = Signal(name="p_ready_o") # prev < 1: - # multi-bit case: valid only when valid_i is all 1s - all1s = Const(-1, (len(self.valid_i), False)) - valid_i = (self.valid_i == all1s) - else: - # single-bit valid_i case - valid_i = self.valid_i - - # when stage indicates not ready, incoming data - # must "appear" to be not ready too - if self.stage_ctl: - valid_i = valid_i & self.s_ready_o - - return valid_i - - def elaborate(self, platform): - m = Module() - m.d.comb += self.trigger.eq(self.valid_i_test & self.ready_o) - return m - - def eq(self, i): - return [self.data_i.eq(i.data_i), - self.ready_o.eq(i.ready_o), - self.valid_i.eq(i.valid_i)] - - def __iter__(self): - yield self.valid_i - yield self.ready_o - if hasattr(self.data_i, "ports"): - yield from self.data_i.ports() - elif isinstance(self.data_i, Sequence): - yield from self.data_i - else: - yield self.data_i - - def ports(self): - return list(self) - - -class NextControl(Elaboratable): - """ contains the signals that go *to* the next stage (both in and out) - * valid_o: output indicating to next stage that data is valid - * ready_i: input from next stage indicating that it can accept data - * data_o : an output - added by the user of this class - """ - def __init__(self, stage_ctl=False): - self.stage_ctl = stage_ctl - self.valid_o = Signal(name="n_valid_o") # self out>> next - self.ready_i = Signal(name="n_ready_i") # self < 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] - pipe2 = pipechain[i+1] - eqs += pipe1.connect_to_next(pipe2) - - # connect front of chain to ourselves - front = pipechain[0] - self.p.data_i = _spec(front.stage.ispec, "chainin") - eqs += front._connect_in(self) - - # connect end of chain to ourselves - end = pipechain[-1] - self.n.data_o = _spec(end.stage.ospec, "chainout") - eqs += end._connect_out(self) + 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 _postprocess(self, i): # XXX DISABLED - return i # RETURNS INPUT - if hasattr(self.stage, "postprocess"): - return self.stage.postprocess(i) - return i - def set_input(self, i): - """ helper function to set the input data + """ helper function to set the input data (used in unit tests) """ - return eq(self.p.data_i, i) + return nmoperator.eq(self.p.data_i, i) def __iter__(self): - yield from self.p - yield from self.n + yield from self.p # yields ready/valid/data (data also gets yielded) + yield from self.n # ditto def ports(self): return list(self) @@ -633,8 +305,7 @@ class ControlBase(Elaboratable): m.submodules.p = self.p m.submodules.n = self.n - if self.stage is not None and hasattr(self.stage, "setup"): - self.stage.setup(m, self.p.data_i) + self.setup(m, self.p.data_i) if not self.p.stage_ctl: return m @@ -707,24 +378,24 @@ class BufferedHandshake(ControlBase): ] # store result of processing in combinatorial temporary - self.m.d.comb += eq(result, self.stage.process(self.p.data_i)) + 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 += eq(r_data, result) # update buffer + 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) + 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 - eq(self.n.data_o, data_o), # update output + 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) + data_o = self._postprocess(r_data) # XXX TBD, does nothing right now self.m.d.sync += [self.n.valid_o.eq(1), # reg empty - eq(self.n.data_o, data_o), # flush buffer + 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) @@ -790,18 +461,18 @@ class SimpleHandshake(ControlBase): ] # store result of processing in combinatorial temporary - m.d.comb += eq(result, self.stage.process(self.p.data_i)) + 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) + data_o = self._postprocess(result) # XXX TBD, does nothing right now m.d.sync += [r_busy.eq(1), # output valid - eq(self.n.data_o, data_o), # update output + 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) - m.d.sync += [eq(self.n.data_o, data_o)] + 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 @@ -901,9 +572,9 @@ class UnbufferedPipeline(ControlBase): m.d.sync += data_valid.eq(p_valid_i | buf_full) with m.If(pv): - m.d.sync += eq(r_data, self.stage.process(self.p.data_i)) - data_o = self._postprocess(r_data) - m.d.comb += eq(self.n.data_o, data_o) + 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 @@ -982,25 +653,14 @@ class UnbufferedPipeline2(ControlBase): 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.stage.process(self.p.data_i)) - data_o = self._postprocess(data_o) - m.d.comb += eq(self.n.data_o, data_o) - m.d.sync += eq(buf, self.n.data_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 PassThroughStage(StageCls): - """ a pass-through stage which has its input data spec equal to its output, - and "passes through" its data from input to output. - """ - def __init__(self, iospecfn): - self.iospecfn = iospecfn - def ispec(self): return self.iospecfn() - def ospec(self): return self.iospecfn() - def process(self, i): return i - - class PassThroughHandshake(ControlBase): """ A control block that delays by one clock cycle. @@ -1048,10 +708,10 @@ class PassThroughHandshake(ControlBase): 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.stage.process(self.p.data_i), r_data) - m.d.sync += eq(r_data, odata) - r_data = self._postprocess(r_data) - m.d.comb += eq(self.n.data_o, r_data) + 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 @@ -1066,26 +726,29 @@ class RegisterPipeline(UnbufferedPipeline): class FIFOControl(ControlBase): - """ FIFO Control. Uses SyncFIFO to store data, coincidentally + """ 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, buffered=False, pipe=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) - * buffered: use buffered FIFO (introduces extra cycle delay) + * :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. - NOTE 1: FPGAs may have trouble with the defaults for SyncFIFO - (fwft=True, buffered=False) + 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 - NOTE 2: data_i *must* have a shape function. it can therefore - be a Signal, or a Record, or a RecordObject. + 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: @@ -1096,12 +759,7 @@ class FIFOControl(ControlBase): this is how the FIFO gets de-catted without needing a de-cat function """ - - assert not (fwft and buffered), "buffered cannot do fwft" - if buffered: - depth += 1 self.fwft = fwft - self.buffered = buffered self.pipe = pipe self.fdepth = depth ControlBase.__init__(self, stage, in_multi, stage_ctl) @@ -1110,35 +768,34 @@ class FIFOControl(ControlBase): self.m = m = ControlBase.elaborate(self, platform) # make a FIFO with a signal of equal width to the data_o. - (fwidth, _) = shape(self.n.data_o) - if self.buffered: - fifo = SyncFIFOBuffered(fwidth, self.fdepth) - else: - fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe) + (fwidth, _) = nmoperator.shape(self.n.data_o) + fifo = Queue(fwidth, self.fdepth, fwft=self.fwft, pipe=self.pipe) m.submodules.fifo = fifo - # store result of processing in combinatorial temporary - result = _spec(self.stage.ospec, "r_temp") - m.d.comb += eq(result, self.stage.process(self.p.data_i)) - - # connect previous rdy/valid/data - do cat on data_i - # NOTE: cannot do the PrevControl-looking trick because - # of need to process the data. shaaaame.... - m.d.comb += [fifo.we.eq(self.p.valid_i_test), - self.p.ready_o.eq(fifo.writable), - eq(fifo.din, cat(result)), - ] - - # connect next rdy/valid/data - do cat on data_o - connections = [self.n.valid_o.eq(fifo.readable), - fifo.re.eq(self.n.ready_i_test), - ] - if self.fwft or self.buffered: - m.d.comb += connections + 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 # unbuffered fwft mode needs sync - data_o = cat(self.n.data_o).eq(fifo.dout) - data_o = self._postprocess(data_o) + 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