What’s new in MyHDL 0.5¶
Author: | Jan Decaluwe |
---|
Modeling¶
Creating generators with decorators¶
Introduction¶
Python 2.4 introduced a new feature called decorators. A decorator consists of special syntax in front of a function declaration. It refers to a decorator function. The decorator function automatically transforms the declared function into some other callable object.
MyHDL 0.5 defines decorators that can be used to create ready-to-run generators from local functions. The use of decorators results in clearer, more explicit code.
The @instance
decorator¶
The @instance
decorator is the most general decorator in MyHDL.
In earlier versions of MyHDL, local generator functions are typically used as follows:
def top(...):
...
def gen_func():
<generator body>
...
inst = gen_func()
...
return inst, ...
Note that the generator function gen_func
is intended to be called
exactly once, and that its name is not necessary anymore afterwards. In MyHDL
0.5, this can be rewritten as follows, using the @instance
decorator:
def top(...):
...
@instance
def inst():
<generator body>
...
return inst, ...
Behind the curtains, the @instance
decorator automatically creates a
generator by calling the generator function, and by reusing its name. Note that
it is placed immediately in front of the corresponding generator function,
resulting in clearer code.
The @always
decorator¶
The @always
decorator is a specialized decorator that targets a very
popular coding pattern. It is used as follows:
def top(...):
...
@always(event1, event2, ...)
def inst()
<body>
...
return inst, ...
The meaning of this code is that the decorated function is executed whenever one of the events occurs. The argument list of the decorator corresponds to the sensitivity list. Appropriate events are edge specifiers, signals, and delay objects. The decorated function is a classic function instead of a generator function.
Behind the curtains, the always
decorator creates an enclosing while
True
loop automatically, and inserts a yield
statement with the
sensitivity list.
The @always_comb
decorator¶
The @always_comb
decorator is used to describe combinatorial logic. It is
nothing else than the always_comb
function from earlier MyHDL versions
used as a decorator:
def top(...):
...
@always_comb
def comb_inst():
<combinatorial body>
...
return comb_inst, ...
The @always_comb
decorator infers the inputs of the combinatorial logic and
the corresponding sensitivity list automatically.
Recommended style changes¶
Decorator usage¶
The use of decorators has clear advantages in terms of code clarity. Therefore, it is recommended that all local generators be created using decorators.
Edge specifiers¶
Signal edges are typically specified using the posedge
and
negedge
functions in MyHDL. However, these functions are simply
wrappers around attributes with the same name. The design decision to use
functions have been reviewed and found questionable. In fact, using the
attributes directly instead has significant advantages, listed in order of
increasing significance:
- one character less to type
- more object-oriented style
- less symbols in the
myhdl
namespace- no brackets, which is better for clarity
- no function call overhead
From MyHDL 0.5 on, it is therefore recommended to use the edge specifier attributes. For example:
clk.posedge # instead of posedge(clk)
rst.negedge # instead of negedge(clk)
Deprecated features¶
Edge specifier functions¶
Functions posedge
and negedge
are deprecated. As discussed
before, it is recommended to use the signal attributes with the same name
instead.
In MyHDL 0.5, the functions will be removed from all documentation and examples. They will be removed from MyHDL in a future version.
processes() function¶
Function processes
is deprecated. It looks up local generator functions and
calls them to create generators. When MyHDL 0.5 decorators are used as
recommended, this functionality becomes superfluous as it is part of the
decorator functionality.
On the other hand, the companion function instances
continues to be
relevant and useful. It merely looks up instances in a local namespace. Having
a single lookup function will also improve usability.
In MyHDL 0.5, the processes
function will be removed from all documentation
and examples. It will be removed from MyHDL in a future version.
Backwards incompatible changes¶
Default initial value of an intbv instance¶
It has always been possible to construct an intbv
instance without
explicit initial value:
m = intbv()
Prior to MyHDL 0.4, the default initial value was 0
. In MyHDL 0.5, this has
been changed to None
. This is a first step towards support for X
and
Z
functionality as found in other HDLs. This may be occasionally useful :-)
For example, it may be meaningful to initialize memory locations to None
to
make sure that they will not be read before they have been initialized. If
None
is supported, it seems also logical to make it the default initial
value, to be interpreted as “No value”.
Warning: if you have calls like the above in your code, it will probably fail with MyHDL 0.5, as many integer-like operations are not supported with None values.
Workaround: change your existing code by using 0
as an explicit initial
value, like so:
m = intbv(0)
Python version¶
Because of the usage of new features such as decorators, MyHDL 0.5 requires upgrading to Python 2.4 or higher.
Verilog conversion¶
Decorator support¶
The Verilog converter was enhanced to support the proposed decorators.
Mapping a list of signals to a RAM memory¶
Certain synthesis tools can map Verilog memories to memory structures. For example, this is supported by the Xilinx toolset. To support this interesting feature, the Verilog converter now maps lists of signals in MyHDL to Verilog memories.
The following MyHDL example is a ram model that uses a list of signals to model the internal memory.
def RAM(dout, din, addr, we, clk, depth=128):
""" Ram model """
mem = [Signal(intbv(0)[8:]) for i in range(depth)]
@always(clk.posedge)
def write():
if we:
mem[int(addr)].next = din
@always_comb
def read():
dout.next = mem[int(addr)]
return write, read
With the appropriate signal definitions for the interface ports, it is mapped
by toVerilog
to the following Verilog code. Note how the list of signals
mem
is mapped to a Verilog memory.
module RAM (
dout,
din,
addr,
we,
clk
);
output [7:0] dout;
wire [7:0] dout;
input [7:0] din;
input [6:0] addr;
input we;
input clk;
reg [7:0] mem [0:128-1];
always @(posedge clk) begin: _RAM_write
if (we) begin
mem[addr] <= din;
end
end
assign dout = mem[addr];
endmodule
Lists of signals can also be used in MyHDL to elegantly describe iterative
hierarchical structures. (See the MyHDL manual.) However, there is an important
difference: such signals will have a name at some level of the hierarchy, while
in the case described above, the individual signals are anonymous. The
toVerilog
converter detects which case we are in. In the first case,
the individual signals will still be declared in the Verilog output, using the
highest-level hierarchical name. It is only in the second case that the list of
signals is declared as a Verilog memory.
Mapping combinatorial logic to assign statements¶
When possible, combinatorial logic is now converted to Verilog assign
statements. There are two conditions for this to happen. First, the logic has
to be explicitly described as a combinatorial function using the
@always_comb
decorator. Secondly, the function has to be simple enough so
that a mapping to assign statements is possible: only signal assignments are
permitted. Otherwise, a Verilog always block is used as previously.
See the RAM model of the previous section for an example.
This was done because certain synthesis tools require assign statements to recognize code templates.
Mapping a tuple of integers to a ROM memory¶
Some synthesis tools, such as the Xilinx tool, can infer a ROM memory from a
case statement. toVerilog
has been enhanced to do the expansion into a
case statement automatically, based on a higher level description. The rom
access is described in a single line, by indexing into a tuple of integers. The
tuple can be described manually, but also by programmatical means. Note that a
tuple is used instead of a list to stress the read-only character of the
memory.
The following example illustrates this functionality.
def rom(dout, addr, CONTENT):
@always_comb
def read():
dout.next = CONTENT[int(addr)]
return read
dout = Signal(intbv(0)[8:])
addr = Signal(intbv(0)[4:])
CONTENT = (17, 134, 52, 9)
toVerilog(rom, dout, addr, CONTENT)
The output Verilog code is as follows:
module rom (
dout,
addr
);
output [7:0] dout;
reg [7:0] dout;
input [3:0] addr;
always @(addr) begin: _rom_read
// synthesis parallel_case full_case
case (addr)
0: dout <= 17;
1: dout <= 134;
2: dout <= 52;
default: dout <= 9;
endcase
end
endmodule
Support for signed arithmetic¶
Getting signed representations right in Verilog is tricky. One issue is that a signed representation is treated as a special case, and unsigned as the rule. For example, whenever one of the operands in an expression is unsigned, all others are also treated like unsigned. While this is understandable from a historical perspective (for backwards compatibility reasons) it is the opposite from what one expects from a high-level point of view, when working with negative numbers. The basic problem is that a Verilog user has to deal with representation explicitly in all cases, even for abstract integer operations. It would be much better to leave representational issues to a tool.
MyHDL doesn’t make the distinction between signed and unsigned. The
intbv
class can handle any kind of integer, including negative ones.
If required, you can access the 2’s complement representation of an
intbv
object, but for integer operations such a counting, there is no
need to worry about this.
Of course, the Verilog converter has to deal with the representation carefully.
MyHDL 0.4 avoided the issue by simply prohibiting intbv
objects with
negative values. MyHDL 0.5 adds support for negative values and uses the signed
Verilog representation to accomplish this.
The problematic cases are those when signed and unsigned representations are
mixed in Verilog expressions. The converter avoids this by making sure that
signed arithmetic is used whenever one of the operands is signed. Note that
this is exactly the opposite of the Verilog default. More specifically, the
converter may convert an unsigned operand by adding a sign bit and casting to a
signed interpretation, using the Verilog $signed
function. Operands that
are treated like this are positive intbv
objects, slices and
subscripts of intbv
objects, and bool
objects.
Integer constants are treated as a special case. Unsized integer numbers were always treated as signed numbers in Verilog. However, as their representation is minimally 32 bits wide, they usually don’t give problems when mixed with unsigned numbers. Therefore, integer constants don’t cause signed casting of other operands in the same expression: users would actually find it surprizing if they did.
Support for user-defined Verilog code¶
Introduction¶
In order to provide a path to implementation, MyHDL code can be converted to Verilog. However, in some cases the conversion may fail or the result may not be acceptable. For example:
- conversion will fail if the MyHDL code doesn’t follow the rules of the convertible subset
- a user may want to explicitly instantiate an existing Verilog module, instead of converting the corresponding MyHDL code
- it may be necessary to include technology-dependent modules in the Verilog output
As a conclusion, MyHDL users need a method to include user-defined Verilog code during the conversion process.
Solution¶
MyHDL 0.5 defines a hook that is understood by toVerilog
but ignored by the
MyHDL simulator. The hook is called __verilog__
. Its operation can be
understood as a special return value. When a MyHDL function defines
__verilog__
, the Verilog converter will use its value instead of the
regular return value.
The value of __verilog__
should be a format string that uses keys in its
format specifiers. The keys refer to the variable names in the context of the
string.
Example:
def inc_comb(nextCount, count, n):
@always_comb
def logic():
nextCount.next = (count + 1) % n
__verilog__ = \
"""
assign %(nextCount)s = (%(count)s + 1) %% %(n)s;
"""
nextCount.driven = "wire"
return logic
In this example, conversion of the inc_comb
function is bypassed and the
user-defined Verilog code is inserted instead. Note that the user-defined code
refers to signals and parameters in the MyHDL context by using format
specifiers. During conversion, the appropriate hierarchical names and parameter
values will be filled in. Note also that the format specifier indicator %
needs to be escaped (by doubling it) if it is required in the user-defined
code.
There is one more issue that needs user attention. Normally, the Verilog
converter infers inputs, internal signals, and outputs. It also detects
undriven and multiple driven signals. To do this, it assumes that signals are
not driven by default. It then processes the code to find out which signals are
driven from where. However, it cannot do this for user-defined code. Without
additional help, this will result in warnings or errors during the inference
process, or in compilation errors from invalid Verilog code. The user should
solve this by setting the driven
attribute for signals that are driven from
the user-defined code. In the example code above, note the following
assignment:
nextCount.driven = "wire"
This specifies that the nextCount
signal is driven as a Verilog wire from
this module. The allowed values of the driven
attribute are "wire"
and
"reg"
. The value specifies how the user-defined Verilog code drives the
signal in Verilog. To decide which value to use, consider how the signal should
be declared in Verilog after the user-defined code is inserted.
Limitations¶
It is not possible to use the __verilog__
hook in a generator function -
it should be in a classic function. This is because in MyHDL those functions
are completely run (elaborated) before the conversion starts, while generator
functions are not.
Backwards incompatible changes¶
Verilog conversion output filename¶
A Verilog conversion is performed with a call that looks as follows:
instance_name = toVerilog(func, ...)
In MyHDL 0.4, the Verilog output filename was called instance_name.v
. In
MyHDL 0.5, the default output filename is func_name.v
, where func_name
is the name of the function, available as the func.func_name
attribute.
This was done for the following reasons. The MyHDL 0.4 was overly clever and therefore complicated. It involves frame handling and parsing the source file for the assignment pattern. Besides being too clever, it also had awkward limitations. For example, it was not possible to construct a dynamic name for the instance, which is very un-Pythonic behavior.
Both the implementation complexity and the limitations are gone with the new behavior: the name of the top-level function argument is simply used. In addition, it is now possible to specify a user-defined name for the instance as follows:
toVerilog.name = "my_name"
toVerilog(func, ....)
To support this feature, it was necessary to make toVerilog an instance of a class with a call interface.
Warning: When existing converting code is re-run, the Verilog output filename will be different than in 0.4.
Simulation¶
Performance optimizations¶
To improve the simulation performance of MyHDL, we mainly put our trust in Python development itself. There are good reasons to be optimistic.
What MyHDL itself can do is to minimize the overhead of the simulation loop. In MyHDL 0.5, a first step was taken in this respect.
MyHDL supports a number of “trigger objects”. These are the objects that can
occur in yield
statements, for example delay
, posedge
,
Signal
, and generator objects. Each of these are handled differently
and so the simulation loop has to account for the object type. Prior to MyHDL
0.5, this type check was explicitly done for each occurrence of a yield
statement during simulation. As many generators will loop endlessly, it is
clear that the same things will be checked over and over again, resulting in an
important overhead.
In MyHDL 0.5, all generators are predigested. Certain trigger object patterns
that tend to occur often are given specialized simulation handlers, so that
continuously performing the same type check is avoided. More specifically, they
consist of generators that only contain yield
statements with a specific
argument. Currently, 5 different kinds of generators are recognized and
accelerated, corresponding to the following yield
statement arguments:
Backwards incompatible changes¶
Waveform tracing output filename¶
Waveform tracing is initiated by a call that looks as follows:
instance_name = traceSignals(func, ...)
In MyHDL 0.4, the output filename was called instance_name.vcd. In MyHDL 0.5, the default output filename is func_name.vcd, where func_name is the name of the function, available as the func.func_name attribute.
This was done for the same reasons as in the similar case for toVerilog, as described earlier.
A user-defined name for the output file can be specified as follows:
traceSignals.name = "my_name"
inst = traceSignals(func, ....)
Warning: When existing converting code is re-run, the vcd output filename will be different than in 0.4.