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.

More information

For more information about the background and the design decisions regarding MyHDL decorators, see mep-100.

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.

More info

For more information about the background and the design decisions regarding user-defined Verilog code, see mep-101.

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:

  • a delay object
  • a Signal object
  • a tuple of Signal objects
  • a posedge or negedge object
  • a tuple of posedge and/or negedge objects

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.