MEP | 105 |
Author | Jan Decaluwe |
Status | Final |
Created | 19-Jun-2009 |
MyHDL-version | 0.7 |
Introduction
Compared to mainstream HDLs, MyHDL signals are less flexible. For example, slicing a signal returns a slice of the current value. For behavioral code, this is just fine. However, it implies that you cannot use such as slice in structural descriptions. In other words, a signal slice cannot be used as a signal. Obviously, this behavior is restrictive and confusing to users of mainstream HDLs.
In this MEP, we will propose a solution for most use cases of signal slicing and similar modelling problems. The basis of the solution is new kind of signal subclass, called the shadow signal.
Analysis
Consider signal slicing. In MyHDL, you take a slice as follows:
sl = sig[m:n]
Under the hood, the slicing operation is delegated to the signal's current value, just like for any other operator. Within behavioral code - the code you write within a generator - this works just like expected. However, it is clear that such a slice is useless when writing structural code. Structure requires signals as composing elements, not plain values.
This behavior is restrictive and confusing because in HDLs such as Verilog and VHDL, signal slices can indeed be used as signals. To understand this better, let's define two forms of slicing:
- Behavioral slicing
- Slicing is delegated to the current signal value. This is how MyHDL signals behave, as described above.
- Structural slicing
- The slice behaves as another signal.
It is clear that behavioral slicing is easy and cheap. On the other hand, structural slicing in its most general form is complex and expensive. Structural slices should behave as some kind of proxy objects of the original signal, reflecting and propagating changes to the original signal. At the same time, they should be behave as signals themselves.
In Verilog and VHDL, structural and behavioral slicing are supported by the same slicing syntax. If you think about it, this is remarkable, because the two forms of slicing are so different in behavior and complexity. In Verilog and VHDL, this is possible because they are compiled languages. From the context, the compiler can find out whether behavioral or structural slicing should be used. Obviously, it will avoid the complexities of structural slicing within code that merely needs a slice of the current signal value.
In MyHDL, we don't have this luxury as there is no compiler. Slicing therefore behaves the same in all contexts. As the primary focus of MyHDL is behavioral descriptions, signal slicing is implemented in the easy way, as behavioral slicing. To support structural slicing, we will need new concepts and new syntax.
To find a solution for structural slicing, let's consider the current workaround. If you want to use a signal slice as a signal elsewhere, you can define a new signal and a generator as follows:
sl = Signal(intbv(0)[m-n:0]) @always_comb def slice_shadower(): sl.next = sig[m:n]
While this may feel redundant and inconvenient, it works just fine functionally. It is also conceptually simple. However, note that this workaround is not fully general. It works perfectly for reading signal slices elsewhere in the code. However, it doesn't work for driving signal slices. Nor is there an obvious similar solution for that functionality. In short, we have a good and simple workaround for reading but not for driving. Let's reflect on this to see whether it is a problem or not.
Reading and driving a signal are not symmetrical. Reading may occur on any place in the design without any problem. However, for the most common case of unresolved signals, driving should normally happen from a single generator only. In Verilog and VHDL, it is possible to write to exclusive parts of such a signal from different processes or always blocks. But even then, I wouldn't call that good practice. I don't think many users would miss that functionality if it were not available. My conclusion is that it is not a problem if we don't have a solution to drive signal slices structurally. Reading signal slices should cover most, if not all, of the practical use cases.
To summarize, my conclusion for structural slicing is as follows:
- Read-only is OK. A solution that only covers reading slices is just fine.
- Delta cycles are OK. In Verilog and VHDL, a structural slice directly refers to the original signal. In the MyHDL workaround as describe above, we simply introduce a new signal that follows the original slice after a delta cycle. But this is just fine: delta cycles are intended to maintain illusion of zero time delays while preserving proper event ordering.
We have come up with a solution that basically implements the workaround automatically. However, structural slicing is not the only modeling issue. Obviously, indexing is a similar problem. Moreover, other modeling problems can be interpreted and solved in a similar way. Therefore, the first step is an abstract signal subclass that implements the general ideas outlined above: the shadow signal.
Introducing shadow signals
A shadow signal is related to another signal or signals as a shadow to its parent object. It follows any change to its parent signal or signals directly. However, it is not the same as the original: in particular, the user cannot assign to a shadow signal. Also, there may be a delta cycle delay between a change in the original and the corresponding change in the shadow signal. Finally, to be useful, the shadow signal performs some kind of transformation of the values of its parent signal or signals.
A shadow signal is convenient because it is directly constructed from its parent signals. The constructor infers everything that's needed: the type info, the initial value, and the transformation of the parent signal values. For simulation, the transformation is defined by a generator which is automatically created and added to the list of generators to be simulated. For conversion, the constructor defines a piece of dedicated Verilog and VHDL code which is automatically added to the convertor output.
Concrete shadow signal subclasses
_SliceSignal
The original inspiration for shadow signals was to a have solution for
structural slicing. This is the purpose of the _SliceSignal
subclass.
class _SliceSignal
(sig, left[, right=None])
This class implements read-only structural slicing and indexing. It creates a
new signal that shadows the slice or index of the parent signal sig
. If the
right
parameter is ommitted, you get indexing instead of slicing. Parameters
left
and right
have the usual meaning for slice indices: in particular,
left
is non-inclusive but right
is inclusive. sig
should be appropriate
for slicing and indexing, which means it should be based on intbv
in
practice.
The class constructors is not intended to be used explicitly. Instead, regular signals now have a call interface that returns a _SliceSignal:
__call__
(left[, right=None])
Therefore, instead of:
sl = _SliceSignal(sig, left, right)
you can do:
sl = sig(left, right)
Obviously, the call interface was intended to be similar to a slicing
interface. Of course, it is not exacly the same which may seem inconvenient. On
the other hand, there are Python functions with a similar slicing functionality
and a similar interface, such as the range
function. Moreover, the call
interface conveys the notion that something is being constructed, which is what
really happens.
ConcatSignal
_SliceSignal
creates a shadow signal on a part of another signal. The
opposite is also useful: a signal that shadows a composition of other signals.
This is the purpose of the ConcatSignal
subclass.
class ConcatSignal
(*args)
This class creates a new signal that shadows the concatenation of its parent signal values. You can pass an arbitrary number of signals to the constructor. The signal arguments should be bit-oriented with a defined number of bits.
TristateSignal
As often is the case, the idea of shadow signals had some useful side effects.
In particular, I realized that the TristateSignal
proposed in Tristate and bidirectional signals
could be interpreted as a shadow signal of its drivers. With the machinery of
the shadow signal in place, it became easier to support this for simulation and
conversion.
class TristateSignal
(val)
A shadow signal that supports tristate values. For more info, see Tristate and bidirectional signals.
Methodology notes
Shadow signals are intended to be constructed at elaboration time, that is, before the simulation or conversion starts. If required, it may be possible to lift this restriction for modeling (not for conversion), but I don't think this will be necessary.
The implication is that shadow signals should be constructed outside generator code (like other signals normally.) A happy side effect is that you can use any Python code in the book to construct them, without compromising convertibility. Therefore, they should be ideal to describe complex structures and convert them to Verilog and VHDL.
Be careful though. Shadow signals, being signals, are expensive and slow compared to plain variables. Using them to describe complex structures elegantly is fine. For other purposes, prefer behavioral code and variables within generators.
Examples
The examples are taken from design problems described here.
A permutation circuit
Consider a circuit whose output should be a permutation of the inputs bits. In
a particular instance, the structure is fixed, but we want to make the design
parametrizable by defining the permutation by a mapping
parameter. Using
shadow signals, the design can be described as follows:
def permute(x, a, mapping): """ Permute input bits. x: output port a: input port mapping: tuple that maps input bit indices to output bit positions """ p = [a(m) for m in mapping] # index signals q = ConcatSignal(*p) @always_comb def assign(): x.next = q return assign
Shadow signals are convertible to Verilog and VHDL. For example, consider the following parameter definitions:
x = Signal(intbv(0)[3:]) a = Signal(intbv(0)[3:]) mapping = (0, 2, 1)
The output Verilog code for this example is as follows:
module permute ( x, a ); output [2:0] x; wire [2:0] x; input [2:0] a; wire [2:0] q; assign q[2] = a[0]; assign q[1] = a[2]; assign q[0] = a[1]; assign x = q; endmodule
An error bit adapter circuit
A more complicated example shows the capabilities of inline scripting with
shadow signals. The circuit maps input error bits i_err
to output error bits
o_err
, according to a spec. The output spec o_spec
is a tuple of strings.
The input spec i_spec
is a mapping of strings to bit indices. For example:
o_spec = ('c', 'a', 'other', 'nomatch') i_spec = { 'a' : 1, 'b' : 2, 'c' : 0, 'd' : 3, 'e' : 4, 'f' : 5, }
Strings 'other' and 'nomatch' are special. 'nomatch' specifies that the output error bit on that position should be assigned to zero. 'other' specifies a bit that is the or-ing of all input error bits whose string key is not present in the output spec. For other output spec strings, the corresponding input bit index is looked up in the input spec.
For the design description, the idea is to assemble the individual bits for the
other
and o_err
vectors in a list, and then use ConcatSignal
to make new
signals out of them:
def adapter(o_err, i_err, o_spec, i_spec): nomatch = Signal(bool(0)) other = Signal(bool(0)) o_err_bits = [] for s in o_spec: if s == 'other': o_err_bits.append(other) elif s == 'nomatch': o_err_bits.append(nomatch) else: bit = i_err(i_spec[s]) # index signal o_err_bits.append(bit) o_err_vec = ConcatSignal(*o_err_bits) other_bits = [] for s, i in i_spec.items(): if s in o_spec: continue bit = i_err(i) # index signal other_bits.append(bit) other_vec = ConcatSignal(*other_bits) @always_comb def assign(): nomatch.next = 0 other.next = (other_vec != 0) o_err.next = o_err_vec return assign
For the example spec above, the convertor generates the following Verilog code for the design instance:
module adapter ( o_err, i_err ); output [3:0] o_err; wire [3:0] o_err; input [5:0] i_err; wire nomatch; wire [3:0] o_err_vec; wire other; wire [3:0] other_vec; assign o_err_vec[3] = i_err[0]; assign o_err_vec[2] = i_err[1]; assign o_err_vec[1] = other; assign o_err_vec[0] = nomatch; assign other_vec[3] = i_err[2]; assign other_vec[2] = i_err[4]; assign other_vec[1] = i_err[3]; assign other_vec[0] = i_err[5]; assign nomatch = 0; assign other = (other_vec != 0); assign o_err = o_err_vec; endmodule
Status and conclusion
A initial version of shadow signal support has been pushed to the mercurial repository under development version 0.7dev. There are some unit tests that check basic functionality, but there are no appropriate usage, type or value checks. The purpose is to get feedback from the community to see if shadow signals solve the problems they are intended to solve.