-""" 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:
----------------
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:
------------------
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):
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
]
# 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
]
# 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):
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)
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)
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.
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)
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:
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)
# 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