X-Git-Url: https://git.libre-soc.org/?a=blobdiff_plain;f=src%2Fadd%2Fsinglepipe.py;h=68b62e432d4fc6022b99361c3358d01638414fce;hb=6bff1a997f3846872cf489c24b5c01426c4dc97c;hp=5a8f6c272c791b0412440e9f6b79f95ac2be90ec;hpb=0b9b71982476dc72badee17487e4f031dcf83a57;p=ieee754fpu.git diff --git a/src/add/singlepipe.py b/src/add/singlepipe.py index 5a8f6c27..68b62e43 100644 --- a/src/add/singlepipe.py +++ b/src/add/singlepipe.py @@ -1,11 +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 - Important: see Stage API (iocontrol.py) in combination with below + Important: see Stage API (stageapi.py) in combination with below RecordBasedStage: ---------------- @@ -27,6 +26,18 @@ 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: ------------------ @@ -117,22 +128,16 @@ 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 iocontrol import (PrevControl, NextControl, Object, RecordObject) +from stageapi import (_spec, StageCls, Stage, StageChain, StageHelper) import nmoperator -from iocontrol import (Object, RecordObject, _spec, - PrevControl, NextControl, StageCls, Stage, - ControlBase, StageChain) class RecordBasedStage(Stage): @@ -151,6 +156,170 @@ class RecordBasedStage(Stage): 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 @@ -209,7 +378,7 @@ class BufferedHandshake(ControlBase): ] # store result of processing in combinatorial temporary - self.m.d.comb += nmoperator.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 @@ -292,7 +461,7 @@ class SimpleHandshake(ControlBase): ] # store result of processing in combinatorial temporary - m.d.comb += nmoperator.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): @@ -403,7 +572,7 @@ class UnbufferedPipeline(ControlBase): m.d.sync += data_valid.eq(p_valid_i | buf_full) with m.If(pv): - m.d.sync += nmoperator.eq(r_data, self.stage.process(self.p.data_i)) + 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) @@ -484,7 +653,7 @@ 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 = 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) @@ -492,21 +661,6 @@ class UnbufferedPipeline2(ControlBase): return self.m -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() - def process(self, i): return i - - class PassThroughHandshake(ControlBase): """ A control block that delays by one clock cycle. @@ -554,7 +708,7 @@ 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) + 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) @@ -572,23 +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). XXX TODO: fix this by - using Queue in all cases instead. + 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: @@ -599,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) @@ -614,33 +769,32 @@ class FIFOControl(ControlBase): # make a FIFO with a signal of equal width to the data_o. (fwidth, _) = nmoperator.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) + 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 += nmoperator.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), - nmoperator.eq(fifo.din, nmoperator.cat(result)), - ] - - # connect next rdy/valid/data - do cat on data_o (further below) - 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 # combinatorial on next ready/valid + 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 = nmoperator.cat(self.n.data_o).eq(fifo.dout) + 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