+++ /dev/null
-""" 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)
+++ /dev/null
-""" 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)
-
-
+++ /dev/null
-""" 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 "<OP %s>" % 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)
-
+++ /dev/null
-""" 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 <<out stage n.ready_i <<in stage+1
- stage-1 p.data_i >>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 <<out stage n.ready_i <<in stage+1
- stage-1 p.data_i >>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 <<out stage n.ready_i <<in stage+1
- stage-1 p.data_i >>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 <<out stage n.ready_i <<in stage+1
- stage-1 p.data_i >>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)
-"""
--- /dev/null
+""" 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)
--- /dev/null
+""" 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)
+
+
--- /dev/null
+""" 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 "<OP %s>" % 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)
+
--- /dev/null
+""" 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 <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>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 <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>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 <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>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 <<out stage n.ready_i <<in stage+1
+ stage-1 p.data_i >>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)
+"""