What’s new in MyHDL 0.5¶
Creating generators with decorators¶
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.
@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
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.
@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
True loop automatically, and inserts a
yield statement with 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, ...
@always_comb decorator infers the inputs of the combinatorial logic and
the corresponding sensitivity list automatically.
Recommended style changes¶
The use of decorators has clear advantages in terms of code clarity. Therefore, it is recommended that all local generators be created using decorators.
Signal edges are typically specified using the
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
- one character less to type
- more object-oriented style
- less symbols in the
- 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)
Edge specifier functions¶
negedge() are deprecated. As discussed
before, it is recommended to use the signal attributes with the same name
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() 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
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
Z functionality as found in other HDLs. This may be occasionally useful :-)
For example, it may be meaningful to initialize memory locations 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)
Because of the usage of new features such as decorators, MyHDL 0.5 requires upgrading to Python 2.4 or higher.
The Verilog convertor 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 convertor 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
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() convertor 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
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
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 convertor 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 convertor 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
convertor 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
intbv objects, and
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¶
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.
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
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
There is one more issue that needs user attention. Normally, the Verilog
convertor 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
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
"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.
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
MyHDL 0.5, the default output filename is
is the name of the function, available as the
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.
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
yield statements, for example
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 occurence of a
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
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.