MYHDL(1)

NAME
myhdl - MyHDL Documentation Back to the main site »

THE MYHDL MANUAL

  Overview

The goal of the MyHDL project is to empower hardware designers with the elegance and simplicity of the Python language.

MyHDL is a free, open-source package for using Python as a hardware description and verification language. Python is a very high level language, and hardware designers can use its full power to model and simulate their designs. Moreover, MyHDL can convert a design to Verilog or VHDL. This provides a path into a traditional design flow.

Modeling

Python's power and clarity make MyHDL an ideal solution for high level modeling. Python is famous for enabling elegant solutions to complex modeling problems. Moreover, Python is outstanding for rapid application development and experimentation.

The key idea behind MyHDL is the use of Python generators to model hardware concurrency. Generators are best described as resumable functions. MyHDL generators are similar to always blocks in Verilog and processes in VHDL.

A hardware module (called a block in MyHDL terminology) is modeled as a function that returns generators. This approach makes it straightforward to support features such as arbitrary hierarchy, named port association, arrays of instances, and conditional instantiation. Furthermore, MyHDL provides classes that implement traditional hardware description concepts. It provides a signal class to support communication between generators, a class to support bit oriented operations, and a class for enumeration types.

Simulation and Verification

The built-in simulator runs on top of the Python interpreter. It supports waveform viewing by tracing signal changes in a VCD file.

With MyHDL, the Python unit test framework can be used on hardware designs. Although unit testing is a popular modern software verification technique, it is still uncommon in the hardware design world.

MyHDL can also be used as hardware verification language for Verilog designs, by co-simulation with traditional HDL simulators.

Conversion to Verilog and VHDL

Subject to some limitations, MyHDL designs can be converted to Verilog or VHDL. This provides a path into a traditional design flow, including synthesis and implementation. The convertible subset is restricted, but much wider than the standard synthesis subset. It includes features that can be used for high level modeling and test benches.

The converter works on an instantiated design that has been fully elaborated. Consequently, the original design structure can be arbitrarily complex. Moreover, the conversion limitations apply only to code inside generators. Outside generators, Python's full power can be used without compromising convertibility.

Finally, the converter automates a number of tasks that are hard in Verilog or VHDL directly. A notable feature is the automated handling of signed arithmetic issues.

  Background information

  Prerequisites

You need a basic understanding of Python to use MyHDL. If you don't know Python, don't worry: it is is one of the easiest programming languages to learn [1]. Learning Python is one of the best time investments that engineering professionals can make [2].

For starters, http://docs.python.org/tutorial is probably the best choice for an on-line tutorial. For alternatives, see http://wiki.python.org/moin/BeginnersGuide.

A working knowledge of a hardware description language such as Verilog or VHDL is helpful.

Code examples in this manual are sometimes shortened for clarity. Complete executable examples can be found in the distribution directory at example/manual/.

  A small tutorial on generators

Generators were introduced in Python 2.2. Because generators are the key concept in MyHDL, a small tutorial is included here.

Consider the following nonsensical function:

        def function():
            for i in range(5):
                return i

You can see why it doesn't make a lot of sense. As soon as the first loop iteration is entered, the function returns:

        >>> function()
        0

Returning is fatal for the function call. Further loop iterations never get a chance, and nothing is left over from the function call when it returns.

To change the function into a generator function, we replace return with yield:

        def generator():
            for i in range(5):
                yield i

Now we get:

        >>> generator()
        <generator object at 0x815d5a8>

When a generator function is called, it returns a generator object. A generator object supports the iterator protocol, which is an expensive way of saying that you can let it generate subsequent values by calling its next method:

        >>> g = generator()
        >>> g.next()
        0
        >>> g.next()
        1
        >>> g.next()
        2
        >>> g.next()
        3
        >>> g.next()
        4
        >>> g.next()
        Traceback (most recent call last):
          File "<stdin>", line 1, in ?
        StopIteration

Now we can generate the subsequent values from the for loop on demand, until they are exhausted. What happens is that the yield statement is like a return, except that it is non-fatal: the generator remembers its state and the point in the code when it yielded. A higher order agent can decide when to get the next value by calling the generator's next method. We say that generators are resumable functions.

If you are familiar with hardware description languages, this may ring a bell. In hardware simulations, there is also a higher order agent, the Simulator, that interacts with such resumable functions; they are called processes in VHDL and always blocks in Verilog. Similarly, Python generators provide an elegant and efficient method to model concurrency, without having to resort to some form of threading.

The use of generators to model concurrency is the first key concept in MyHDL. The second key concept is a related one: in MyHDL, the yielded values are used to specify the conditions on which the generator should wait before resuming. In other words, yield statements work as general sensitivity lists.

  About decorators

Python 2.4 introduced a feature called decorators. MyHDL takes advantage of this feature by defining a number of decorators that facilitate hardware descriptions. However, some users may not yet be familiar with decorators. Therefore, an introduction is included here.

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.

A decorator function deco is used in a decorator statement as follows:

        @deco
        def func(arg1, arg2, ...):
            <body>

This code is equivalent to the following:

        def func(arg1, arg2, ...):
            <body>
        func = deco(func)

Note that the decorator statement goes directly in front of the function declaration, and that the function name func is automatically reused for the final result.

MyHDL uses decorators to create ready-to-simulate generators from local function definitions. Their functionality and usage will be described extensively in this manual.

FOOTNOTES

[1] You must be bored by such claims, but in Python's case it's true.

[2] I am not biased.

  Introduction to MyHDL

  A basic MyHDL simulation

We will introduce MyHDL with a classic Hello World style example. All example code can be found in the distribution directory under example/manual/. Here are the contents of a MyHDL simulation script called hello1.py:

When we run this simulation, we get the following output:

The first line of the script imports a number of objects from the myhdl package. In Python we can only use identifiers that are literally defined in the source file.

Then, we define a function called HelloWorld. In MyHDL, a hardware module is modeled by a function decorated with the block decorator. The name block was chosen to avoid confusion with the Python concept of a module. We will use this terminology further on.

The parameter list of the HelloWorld function is used to define the interface of the hardware block. In this first example, the interface is empty.

Inside the top level function we declare a local function called say_hello that defines the desired behavior. This function is decorated with an always decorator that has a delay object as its parameter. The meaning is that the function will be executed whenever the specified delay interval has expired.

Behind the curtains, the always decorator creates a Python generator and reuses the name of the decorated function for it. Generators are the fundamental objects in MyHDL, and we will say much more about them further on.

Finally, the top level function returns the say_hello generator. This is the simplest case of the basic MyHDL code pattern to define the contents of a hardware block. We will describe the general case further on.

In MyHDL, we create an instance of a hardware block by calling the corresponding function. The block decorator make sure that the return value is actually an instance of a block class, with a useful API. In the example, variable inst refers to a HelloWorld block instance.

To simulate the instance, we use its run_sim method. We can use it to run the simulation for the desired amount of timesteps.

  Signals and concurrency

An actual hardware design is typically massively concurrent, which means that a large amount of functional units are running in parallel. MyHDL supports this behavior by allowing an arbitrary number of concurrently running generators.

With concurrency comes the problem of deterministic communication. Hardware languages use special objects to support deterministic communication between concurrent code. In particular, MyHDL has a Signal object which is roughly modeled after VHDL signals.

We will demonstrate signals and concurrency by extending and modifying our first example. We define a hardware block that contains two generators, one that drives a clock signal, and one that is sensitive to a positive edge on a clock signal:

The clock driver function clk_driver drives the clock signal. If defines a generator that continuously toggles a clock signal after a certain delay. A new value of a signal is specified by assigning to its next attribute. This is the MyHDL equivalent of the VHDL signal assignment and the Verilog non-blocking assignment.

The say_hello function is modified from the first example. It is made sensitive to a rising edge of the clock signal, specified by the posedge attribute of a signal. The edge specifier is the argument of the always decorator. As a result, the decorated function will be executed on every rising clock edge.

The clk signal is constructed with an initial value 0. One generator drives it, the other is sensitive to it. The result of this communication is that the generators run in parallel, but that their actions are coordinated by the clock signal.

When we run the simulation, we get:

  Parameters, ports and hierarchy

We have seen that MyHDL uses functions to model hardware blocks. So far these functions did not have parameters. However, to create general, reusable blocks we will need parameters. For example, we can create a clock driver block as follows:

The block encapsulates a clock driver generator. It has two parameters.

The first parameter is clk is the clock signal. A asignal parameter is MyHDL's way to model a dfn:port:. The second parameter is the clock period, with a default value of 20.

As the low time of the clock may differ from the high time in case of an odd period, we cannot use the always decorator with a single delay value anymore. Instead, the drive_clk function is now a generator function with an explicit definition of the desired behavior. It is decorated with the instance decorator. You can see that drive_clk is a generator function because it contains yield statements.

When a generator function is called, it returns a generator object. This is basically what the instance decorator does. It is less sophisticated than the always decorator, but it can be used to create a generator from any local generator function.

The yield statement is a general Python construct, but MyHDL uses it in a specific way. It has a similar meaning as the wait statement in VHDL: the statement suspends execution of a generator, and its clauses specify the conditions on which the generator should wait before resuming. In this case, the generator waits for a certain delay.

Note that to make sure that the generator runs "forever", we wrap its behavior in a while True loop.

Similarly, we can define a general Hello function as follows:

We can create any number of instances by calling the functions with the appropriate parameters. Hierarchy can be modeled by defining the instances in a higher-level function, and returning them. This pattern can be repeated for an arbitrary number of hierarchical levels. Consequently, the general definition of a MyHDL instance is recursive: an instance is either a sequence of instances, or a generator.

As an example, we will create a higher-level function with four instances of the lower-level functions, and simulate it:

As in standard Python, positional or named parameter association can be used in instantiations, or a mix of both [1]. All these styles are demonstrated in the example above. Named association can be very useful if there are a lot of parameters, as the argument order in the call does not matter in that case.

The simulation produces the following output:

  Terminology review

Some commonly used terminology has different meanings in Python versus hardware design. For a good understanding, it is important to make these differences explicit.

A module in Python refers to all source code in a particular file. A module can be reused by other modules by importing it. In hardware design on the other hand, a module typically refers to a reusable unit of hardware with a well defined interface. Because these meanings are so different, the terminology chosen for a hardware module in MyHDL is block instead, as explained earlier in this chapter.

A hardware block can can be reused in another block by instantiating it.

An instance in Python (and other object-oriented languages) refers to the object created by a class constructor. In hardware design, an instance is a particular incarnation of a hardware block, created by instantiating the block. In MyHDL, such as block instance is actually an instance of a particular class. Therefore, the two meanings are not exactly the same, but they coincide nicely.

Normally, the meaning the words "block" and "instance" should be clear from the context. Sometimes, we qualify them with the words "hardware" or "MyHDL" for clarity.

  Some remarks on MyHDL and Python

To conclude this introductory chapter, it is useful to stress that MyHDL is not a language in itself. The underlying language is Python, and MyHDL is implemented as a Python package called myhdl. Moreover, it is a design goal to keep the myhdl package as minimalistic as possible, so that MyHDL descriptions are very much "pure Python".

To have Python as the underlying language is significant in several ways:

    •   Python is a very powerful high level language. This translates into high productivity and elegant solutions to complex problems.

    •   Python is continuously improved by some very clever minds, supported by a large user base. Python profits fully from the open source development model.

    •   Python comes with an extensive standard library. Some functionality is likely to be of direct interest to MyHDL users: examples include string handling, regular expressions, random number generation, unit test support, operating system interfacing and GUI development. In addition, there are modules for mathematics, database connections, networking programming, internet data handling, and so on.

  Summary and perspective

Here is an overview of what we have learned in this chapter:

    •   Generators are the basic building blocks of MyHDL models. They provide the way to model massive concurrency and sensitivity lists.

    •   MyHDL provides decorators that create useful generators from local functions and a decorator to create hardware blocks.

    •   Hardware structure and hierarchy is described with Python functions, decorated with the block decorator.

    •   Signal objects are used to communicate between concurrent generators.

    •   A block instance provides a method to simulate it.

These concepts are sufficient to start modeling and simulating with MyHDL.

However, there is much more to MyHDL. Here is an overview of what can be learned from the following chapters:

    •   MyHDL supports hardware-oriented types that make it easier to write typical hardware models. These are described in Chapter hwtypes.

    •   MyHDL supports sophisticated and high level modeling techniques. This is described in Chapter model-hl.

    •   MyHDL enables the use of modern software verification techniques, such as unit testing, on hardware designs. This is the topic of Chapter unittest.

    •   It is possible to co-simulate MyHDL models with other HDL languages such as Verilog and VHDL. This is described in Chapter cosim.

    •   Last but not least, MyHDL models can be converted to Verilog or VHDL, providing a path to a silicon implementation. This is the topic of Chapter conv.

FOOTNOTES

[1] All positional parameters have to go before any named parameter.

  Hardware-oriented types

  The intbv class

Hardware design involves dealing with bits and bit-oriented operations. The standard Python type int has most of the desired features, but lacks support for indexing and slicing. For this reason, MyHDL provides the intbv class. The name was chosen to suggest an integer with bit vector flavor.

intbv works transparently with other integer-like types. Like class int, it provides access to the underlying two's complement representation for bitwise operations. However, unlike int, it is a mutable type. This means that its value can be changed after object creation, through methods and operators such as slice assignment.

intbv supports the same operators as int for arithmetic. In addition, it provides a number of features to make it suitable for hardware design. First, the range of allowed values can be constrained. This makes it possible to check the value at run time during simulation. Moreover, back end tools can determine the smallest possible bit width for representing the object. Secondly, it supports bit level operations by providing an indexing and slicing interface.

intbv objects are constructed in general as follows:

        intbv([val=None] [, min=None]  [, max=None])

val is the initial value. min and max can be used to constrain the value. Following the Python conventions, min is inclusive, and max is exclusive. Therefore, the allowed value range is min .. max-1.

Let's look at some examples. An unconstrained intbv object is created as follows:

        >>> a = intbv(24)

After object creation, min and max are available as attributes for inspection. Also, the standard Python function len can be used to determine the bit width. If we inspect the previously created object, we get:

        >>> a
        intbv(24)
        >>> print(a.min)
        None
        >>> print(a.max)
        None
        >>> len(a)
        0

As the instantiation was unconstrained, the min and max attributes are undefined. Likewise, the bit width is undefined, which is indicated by a return value 0.

A constrained intbv object is created as follows:

>>> a = intbv(24, min=0, max=25)

Inspecting the object now gives:

        >>> a
        intbv(24)
        >>> a.min
        0
        >>> a.max
        25
        >>> len(a)
        5

We see that the allowed value range is 0 .. 24, and that 5 bits are required to represent the object.

The min and max bound attributes enable fine-grained control and error checking of the value range. In particular, the bound values do not have to be symmetric or powers of 2. In all cases, the bit width is set appropriately to represent the values in the range. For example:

        >>> a = intbv(6, min=0, max=7)
        >>> len(a)
        3
        >>> a = intbv(6, min=-3, max=7)
        >>> len(a)
        4
        >>> a = intbv(6, min=-13, max=7)
        >>> len(a)
        5

  Bit indexing

A common requirement in hardware design is access to the individual bits. The intbv class implements an indexing interface that provides access to the bits of the underlying two's complement representation. The following illustrates bit index read access:

        >>> from myhdl import bin
        >>> a = intbv(24)
        >>> bin(a)
        '11000'
        >>> int(a[0])
        0
        >>> int(a[3])
        1
        >>> b = intbv(-23)
        >>> bin(b)
        '101001'
        >>> int(b[0])
        1
        >>> int(b[3])
        1
        >>> int(b[4])
        0

We use the bin function provide by MyHDL because it shows the two's complement representation for negative values, unlike Python's builtin with the same name. Note that lower indices correspond to less significant bits. The following code illustrates bit index assignment:

        >>> bin(a)
        '11000'
        >>> a[3] = 0
        >>> bin(a)
        '10000'
        >>> a
        intbv(16)
        >>> b
        intbv(-23)
        >>> bin(b)
        '101001'
        >>> b[3] = 0
        >>> bin(b)
        '100001'
        >>> b
        intbv(-31)

  Bit slicing

The intbv type also supports bit slicing, for both read access assignment. For example:

        >>> a = intbv(24)
        >>> bin(a)
        '11000'
        >>> a[4:1]
        intbv(4)
        >>> bin(a[4:1])
        '100'
        >>> a[4:1] = 0b001
        >>> bin(a)
        '10010'
        >>> a
        intbv(18)

In accordance with the most common hardware convention, and unlike standard Python, slicing ranges are downward. As in standard Python, the slicing range is half-open: the highest index bit is not included. Unlike standard Python however, this index corresponds to the leftmost item.

Both indices can be omitted from the slice. If the rightmost index is omitted, it is 0 by default. If the leftmost index is omitted, the meaning is to access "all" higher order bits. For example:

        >>> bin(a)
        '11000'
        >>> bin(a[4:])
        '1000'
        >>> a[4:] = '0001'
        >>> bin(a)
        '10001'
        >>> a[:] = 0b10101
        >>> bin(a)
        '10101'

The half-openness of a slice may seem awkward at first, but it helps to avoid one-off count issues in practice. For example, the slice a[8:] has exactly 8 bits. Likewise, the slice a[7:2] has 7-2=5 bits. You can think about it as follows: for a slice [i:j], only bits below index i are included, and the bit with index j is the last bit included.

When an intbv object is sliced, a new intbv object is returned. This new intbv object is always positive, and the value bounds are set up in accordance with the bit width specified by the slice. For example:

        >>> a = intbv(6, min=-3, max=7)
        >>> len(a)
        4
        >>> b = a[4:]
        >>> b
        intbv(6L)
        >>> len(b)
        4
        >>> b.min
        0
        >>> b.max
        16

In the example, the original object is sliced with a slice equal to its bit width. The returned object has the same value and bit width, but its value range consists of all positive values that can be represented by the bit width.

The object returned by a slice is positive, even when the original object is negative:

        >>> a = intbv(-3)
        >>> bin(a, width=5)
        '11101'
        >>> b = a[5:]
        >>> b
        intbv(29L)
        >>> bin(b)
        '11101'

In this example, the bit pattern of the two objects is identical within the bit width, but their values have opposite sign.

Sometimes hardware engineers prefer to constrain an object by defining its bit width directly, instead of the range of allowed values. Using the slicing properties of the intbv class one can do that as follows:

        >>> a = intbv(24)[5:]

What actually happens here is that first an unconstrained intbv is created, which is then sliced. Slicing an intbv returns a new intbv with the constraints set up appropriately. Inspecting the object now shows:

        >>> a.min
        0
        >>> a.max
        32
        >>> len(a)
        5

Note that the max attribute is 32, as with 5 bits it is possible to represent the range 0 .. 31. Creating an intbv in this way is convenient but has the disadvantage that only positive value ranges between 0 and a power of 2 can be specified.

  The modbv class

In hardware modeling, there is often a need for the elegant modeling of wrap-around behavior. intbv instances do not support this automatically, as they assert that any assigned value is within the bound constraints. However, wrap-around modeling can be straightforward. For example, the wrap-around condition for a counter is often decoded explicitly, as it is needed for other purposes. Also, the modulo operator provides an elegant one-liner in many scenarios:

        count.next = (count + 1) % 2**8

However, some interesting cases are not supported by the intbv type. For example, we would like to describe a free running counter using a variable and augmented assignment as follows:

        count_var += 1

This is not possible with the intbv type, as we cannot add the modulo behavior to this description. A similar problem exist for an augmented left shift as follows:

        shifter <<= 4

To support these operations directly, MyHDL provides the modbv type. modbv is implemented as a subclass of intbv. The two classes have an identical interface and work together in a straightforward way for arithmetic operations. The only difference is how the bounds are handled: out-of-bound values result in an error with intbv, and in wrap-around with modbv. For example, the modulo counter above can be modeled as follows:

        count = Signal(modbv(0, min=0, max=2**8))
        ...
        count.next = count + 1

The wrap-around behavior is defined in general as follows:

        val = (val - min) % (max - min) + min

In a typical case when min==0, this reduces to:

        val = val % max

  Unsigned and signed representation

intbv is designed to be as high level as possible. The underlying value of an intbv object is a Python int, which is represented as a two's complement number with "indefinite" bit width. The range bounds are only used for error checking, and to calculate the minimum required bit width for representation. As a result, arithmetic can be performed like with normal integers.

In contrast, HDLs such as Verilog and VHDL typically require designers to deal with representational issues, especially for synthesizable code. They provide low-level types like signed and unsigned for arithmetic. The rules for arithmetic with such types are much more complicated than with plain integers.

In some cases it can be useful to interpret intbv objects in terms of "signed" and "unsigned". Basically, it depends on attribute min. if min < 0, then the object is "signed", otherwise it is "unsigned". In particular, the bit width of a "signed" object will account for a sign bit, but that of an "unsigned" will not, because that would be redundant. From earlier sections, we have learned that the return value from a slicing operation is always "unsigned".

In some applications, it is desirable to convert an "unsigned" intbv to a "signed", in other words, to interpret the msb bit as a sign bit. The msb bit is the highest order bit within the object's bit width. For this purpose, intbv provides the intbv.signed method. For example:

        >>> a = intbv(12, min=0, max=16)
        >>> bin(a)
        '1100'
        >>> b = a.signed()
        >>> b
        -4
        >>> bin(b, width=4)
        '1100'

intbv.signed extends the msb bit into the higher-order bits of the underlying object value, and returns the result as an integer. Naturally, for a "signed" the return value will always be identical to the original value, as it has the sign bit already.

As an example let's take a 8 bit wide data bus that would be modeled as follows:

        data_bus = intbv(0)[8:]

Now consider that a complex number is transferred over this data bus. The upper 4 bits of the data bus are used for the real value and the lower 4 bits for the imaginary value. As real and imaginary values have a positive and negative value range, we can slice them off from the data bus and convert them as follows:

        real.next = data_bus[8:4].signed()
        imag.next = data_bus[4:].signed()

  Structural modeling

  Introduction

Hardware descriptions need to support the concepts of module instantiation and hierarchy. In MyHDL, an instance is recursively defined as being either a sequence of instances, or a generator. Hierarchy is modeled by defining instances in a higher-level function, and returning them. The following is a schematic example of the basic case.

        from myhdl import block

        @block
        def top(...):
            ...
            instance_1 = module_1(...)
            instance_2 = module_2(...)
            ...
            instance_n = module_n(...)
            ...
            return instance_1, instance_2, ... , instance_n

Note that MyHDL uses conventional procedural techniques for modeling structure. This makes it straightforward to model more complex cases.

  Conditional instantiation

To model conditional instantiation, we can select the returned instance under parameter control. For example:

        from myhdl import block

        SLOW, MEDIUM, FAST = range(3)

        @block
        def top(..., speed=SLOW):
            ...
            def slowAndSmall():
               ...
            ...
            def fastAndLarge():
               ...
            if speed == SLOW:
                return slowAndSmall()
            elif speed == FAST:
                return fastAndLarge()
            else:
                raise NotImplementedError

  Lists of instances and signals

Python lists are easy to create. We can use them to model lists of instances.

Suppose we have a top module that instantiates a single channel submodule, as follows:

        from myhdl import block, Signal

        @block
        def top(...):

            din = Signal(0)
            dout = Signal(0)
            clk = Signal(bool(0))
            reset = Signal(bool(0))

            channel_inst = channel(dout, din, clk, reset)

            return channel_inst

If we wanted to support an arbitrary number of channels, we can use lists of signals and a list of instances, as follows:

        from myhdl import block, Signal

        @block
        def top(..., n=8):

            din = [Signal(0) for i in range(n)]
            dout = [Signal(0) for in range(n)]
            clk = Signal(bool(0))
            reset = Signal(bool(0))
            channel_inst = [None for i in range(n)]

            for i in range(n):
                channel_inst[i] = channel(dout[i], din[i], clk, reset)

            return channel_inst

  Converting between lists of signals and bit vectors

Compared to HDLs such as VHDL and Verilog, MyHDL signals are less flexible for structural modeling. 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.

In MyHDL, you can address such cases by a concept called shadow signals. A shadow signal is constructed out of other signals and follows their value changes automatically. For example, a _SliceSignal follows the value of an index or a slice from another signal. Likewise, A ConcatSignal follows the values of a number of signals as a concatenation.

As an example, suppose we have a system with N requesters that need arbitration. Each requester has a request output and a grant input. To connect them in the system, we can use list of signals. For example, a list of request signals can be constructed as follows:

        request_list = [Signal(bool()) for i in range(M)]

Suppose that an arbiter module is available that is instantiated as follows:

        arb = arbiter(grant_vector, request_vector, clock, reset)

The request_vector input is a bit vector that can have any of its bits asserted. The grant_vector is an output bit vector with just a single bit asserted, or none. Such a module is typically based on bit vectors because they are easy to process in RTL code. In MyHDL, a bit vector is modeled using the intbv type.

We need a way to "connect" the list of signals to the bit vector and vice versa. Of course, we can do this with explicit code, but shadow signals can do this automatically. For example, we can construct a request_vector as a ConcatSignal object:

        request_vector = ConcatSignal(*reversed(request_list)

Note that we reverse the list first. This is done because the index range of lists is the inverse of the range of intbv bit vectors. By reversing, the indices correspond to the same bit.

The inverse problem exist for the grant_vector. It would be defined as follows:

        grant_vector = Signal(intbv(0)[M:])

To construct a list of signals that are connected automatically to the bit vector, we can use the Signal call interface to construct _SliceSignal objects:

        grant_list = [grant_vector(i) for i in range(M)]

Note the round brackets used for this type of slicing. Also, it may not be necessary to construct this list explicitly. You can simply use grant_vector(i) in an instantiation.

To decide when to use normal or shadow signals, consider the data flow. Use normal signals to connect to outputs. Use shadow signals to transform these signals so that they can be used as inputs.

  Inferring the list of instances

In MyHDL, instances have to be returned explicitly by a top level function. It may be convenient to assemble the list of instances automatically. For this purpose, MyHDL provides the function instances. Using the first example in this section, it is used as follows:

        from myhdl import block, instances

        @block
        def top(...):
            ...
            instance_1 = module_1(...)
            instance_2 = module_2(...)
            ...
            instance_n = module_n(...)
            ...
            return instances()

Function instances uses introspection to inspect the type of the local variables defined by the calling function. All variables that comply with the definition of an instance are assembled in a list, and that list is returned.

  RTL modeling

  Introduction

RTL (Register Transfer Level) is a modeling abstraction level that is typically used to write synthesizable models. Synthesis refers to the process by which an HDL description is automatically compiled into an implementation for an ASIC or FPGA. This chapter describes how MyHDL supports it.

  Combinatorial logic

  Template

Combinatorial logic is described with a code pattern as follows:

        from myhdl import block, always_comb

        @block
        def top(<parameters>):
            ...
            @always_comb
            def comb_logic():
                <functional code>
            ...
            return comb_logic, ...

The always_comb decorator describes combinatorial logic. The name refers to a similar construct in SystemVerilog. The decorated function is a local function that specifies what happens when one of the input signals of the logic changes. The always_comb decorator infers the input signals automatically. It returns a generator that is sensitive to all inputs, and that executes the function whenever an input changes.

  Example

The following is an example of a combinatorial multiplexer

To verify it, we will simulate the logic with some random patterns. The random module in Python's standard library comes in handy for such purposes. The function randrange(n) returns a random natural integer smaller than n. It is used in the test bench code to produce random input values.

It is often useful to keep the random values reproducible. This can be accomplished by providing a seed value as in the code. The run produces the following output:

  Sequential logic

  Template

Sequential RTL models are sensitive to a clock edge. In addition, they may be sensitive to a reset signal. The always_seq decorator supports this model directly:

        from myhdl import block, always_seq

        @block
        def top(<parameters>, clock, ..., reset, ...):
            ...
            @always_seq(clock.posedge, reset=reset)
            def seq_logic():
                <functional code>
            ...
            return seq_logic, ...

The always_seq decorator automatically infers the reset functionality. It detects which signals need to be reset, and uses their initial values as the reset values. The reset signal itself needs to be specified as a ResetSignal object. For example:

        reset = ResetSignal(0, active=0, isasync=True)

The first parameter specifies the initial value. The active parameter specifies the value on which the reset is active, and the isasync parameter specifies whether it is an asychronous (True) or a synchronous (False) reset. If no reset is needed, you can assign None to the reset parameter in the always_seq parameter.

  Example

The following code is a description of an incrementer with enable, and an asynchronous reset.

For the test bench, we will use an independent clock generator, stimulus generator, and monitor. After applying enough stimulus patterns, we can raise the StopSimulation exception to stop the simulation run. The test bench for a small incrementer and a small number of patterns is a follows

The simulation produces the following output

  Alternative template

The template with the always_seq decorator is convenient as it infers the reset functionality automatically. Alternatively, you can use a more explicit template as follows:

        from myhdl import block, always

        @block
        def top(<parameters>, clock, ..., reset, ...):
            ...
            @always(clock.posedge, reset.negedge)
            def seq_logic():
               if not reset:
                   <reset code>
               else:
                   <functional code>

            return seq_logic,...

With this template, the reset values have to be specified explicitly.

  Finite State Machine modeling

Finite State Machine (FSM) modeling is very common in RTL design and therefore deserves special attention.

For code clarity, the state values are typically represented by a set of identifiers. A standard Python idiom for this purpose is to assign a range of integers to a tuple of identifiers, like so

        >>> SEARCH, CONFIRM, SYNC = range(3)
        >>> CONFIRM
        1

However, this technique has some drawbacks. Though it is clearly the intention that the identifiers belong together, this information is lost as soon as they are defined. Also, the identifiers evaluate to integers, whereas a string representation of the identifiers would be preferable. To solve these issues, we need an enumeration type.

MyHDL supports enumeration types by providing a function enum. The arguments to enum are the string representations of the identifiers, and its return value is an enumeration type. The identifiers are available as attributes of the type. For example

        >>> from myhdl import enum
        >>> t_State = enum('SEARCH', 'CONFIRM', 'SYNC')
        >>> t_State
        <Enum: SEARCH, CONFIRM, SYNC>
        >>> t_State.CONFIRM
        CONFIRM

We can use this type to construct a state signal as follows:

        state = Signal(t_State.SEARCH)

As an example, we will use a framing controller FSM. It is an imaginary example, but similar control structures are often found in telecommunication applications. Suppose that we need to find the Start Of Frame (SOF) position of an incoming frame of bytes. A sync pattern detector continuously looks for a framing pattern and indicates it to the FSM with a syncFlag signal. When found, the FSM moves from the initial SEARCH state to the CONFIRM state. When the syncFlag is confirmed on the expected position, the FSM declares SYNC, otherwise it falls back to the SEARCH state. This FSM can be coded as follows

At this point, we will use the example to demonstrate the MyHDL support for waveform viewing. During simulation, signal changes can be written to a VCD output file. The VCD file can then be loaded and viewed in a waveform viewer tool such as gtkwave.

The user interface of this feature consists of a single function, traceSignals. To explain how it works, recall that in MyHDL, an instance is created by assigning the result of a function call to an instance name. For example:

        tb_fsm = testbench()

To enable VCD tracing, the instance should be created as follows instead:

        tb_fsm = traceSignals(testbench)

Note that the first argument of traceSignals consists of the uncalled function. By calling the function under its control, traceSignals gathers information about the hierarchy and the signals to be traced. In addition to a function argument, traceSignals accepts an arbitrary number of non-keyword and keyword arguments that will be passed to the function call.

A small test bench for our framing controller example, with signal tracing enabled, is shown below:

When we run the test bench, it generates a VCD file called testbench.vcd. When we load this file into gtkwave, we can view the waveforms: [image]

Signals are dumped in a suitable format. This format is inferred at the Signal construction time, from the type of the initial value. In particular, bool signals are dumped as single bits. (This only works starting with Python 2.3, when bool has become a separate type). Likewise, intbv signals with a defined bit width are dumped as bit vectors. To support the general case, other types of signals are dumped as a string representation, as returned by the standard str function.

WARNING:
        Support for literal string representations is not part of the VCD standard. It is specific to gtkwave. To generate a standard VCD file, you need to use signals with a defined bit width only.

  High level modeling

  Introduction

To write synthesizable models in MyHDL, you should stick to the RTL templates shown in model-rtl. However, modeling in MyHDL is much more powerful than that. Conceptually, MyHDL is a library for general event-driven modeling and simulation of hardware systems.

There are many reasons why it can be useful to model at a higher abstraction level than RTL. For example, you can use MyHDL to verify architectural features, such as system throughput, latency and buffer sizes. You can also write high level models for specialized technology-dependent cores that are not going through synthesis. Last but not least, you can use MyHDL to write test benches that verify a system model or a synthesizable description.

This chapter explores some of the options for high level modeling with MyHDL.

  Modeling with bus-functional procedures

A bus-functional procedure is a reusable encapsulation of the low-level operations needed to implement some abstract transaction on a physical interface. Bus-functional procedures are typically used in flexible verification environments.

Once again, MyHDL uses generator functions to support bus-functional procedures. In MyHDL, the difference between instances and bus-functional procedure calls comes from the way in which a generator function is used.

As an example, we will design a bus-functional procedure of a simplified UART transmitter. We assume 8 data bits, no parity bit, and a single stop bit, and we add print statements to follow the simulation behavior:

        T_9600 = int(1e9 / 9600)

        def rs232_tx(tx, data, duration=T_9600):

            """ Simple rs232 transmitter procedure.

            tx -- serial output data
            data -- input data byte to be transmitted
            duration -- transmit bit duration

            """

            print "-- Transmitting %s --" % hex(data)
            print "TX: start bit"
            tx.next = 0
            yield delay(duration)

            for i in range(8):
                print "TX: %s" % data[i]
                tx.next = data[i]
                yield delay(duration)

            print "TX: stop bit"
            tx.next = 1
            yield delay(duration)

This looks exactly like the generator functions in previous sections. It becomes a bus-functional procedure when we use it differently. Suppose that in a test bench, we want to generate a number of data bytes to be transmitted. This can be modeled as follows:

        testvals = (0xc5, 0x3a, 0x4b)

        def stimulus():
            tx = Signal(1)
            for val in testvals:
                txData = intbv(val)
                yield rs232_tx(tx, txData)

We use the bus-functional procedure call as a clause in a yield statement. This introduces a fourth form of the yield statement: using a generator as a clause. Although this is a more dynamic usage than in the previous cases, the meaning is actually very similar: at that point, the original generator should wait for the completion of a generator. In this case, the original generator resumes when the rs232_tx(tx, txData) generator returns.

When simulating this, we get:

        -- Transmitting 0xc5 --
        TX: start bit
        TX: 1
        TX: 0
        TX: 1
        TX: 0
        TX: 0
        TX: 0
        TX: 1
        TX: 1
        TX: stop bit
        -- Transmitting 0x3a --
        TX: start bit
        TX: 0
        TX: 1
        TX: 0
        TX: 1
        ...

We will continue with this example by designing the corresponding UART receiver bus-functional procedure. This will allow us to introduce further capabilities of MyHDL and its use of the yield statement.

Until now, the yield statements had a single clause. However, they can have multiple clauses as well. In that case, the generator resumes as soon as the wait condition specified by one of the clauses is satisfied. This corresponds to the functionality of sensitivity lists in Verilog and VHDL.

For example, suppose we want to design an UART receive procedure with a timeout. We can specify the timeout condition while waiting for the start bit, as in the following generator function:

        def rs232_rx(rx, data, duration=T_9600, timeout=MAX_TIMEOUT):

            """ Simple rs232 receiver procedure.

            rx -- serial input data
            data -- data received
            duration -- receive bit duration

            """

            # wait on start bit until timeout
            yield rx.negedge, delay(timeout)
            if rx == 1:
                raise StopSimulation, "RX time out error"

            # sample in the middle of the bit duration
            yield delay(duration // 2)
            print "RX: start bit"

            for i in range(8):
                yield delay(duration)
                print "RX: %s" % rx
                data[i] = rx

            yield delay(duration)
            print "RX: stop bit"
            print "-- Received %s --" % hex(data)

If the timeout condition is triggered, the receive bit rx will still be 1. In that case, we raise an exception to stop the simulation. The StopSimulation exception is predefined in MyHDL for such purposes. In the other case, we proceed by positioning the sample point in the middle of the bit duration, and sampling the received data bits.

When a yield statement has multiple clauses, they can be of any type that is supported as a single clause, including generators. For example, we can verify the transmitter and receiver generator against each other by yielding them together, as follows:

        def test():
            tx = Signal(1)
            rx = tx
            rxData = intbv(0)
            for val in testvals:
                txData = intbv(val)
                yield rs232_rx(rx, rxData), rs232_tx(tx, txData)

Both forked generators will run concurrently, and the original generator will resume as soon as one of them finishes (which will be the transmitter in this case). The simulation output shows how the UART procedures run in lockstep:

        -- Transmitting 0xc5 --
        TX: start bit
        RX: start bit
        TX: 1
        RX: 1
        TX: 0
        RX: 0
        TX: 1
        RX: 1
        TX: 0
        RX: 0
        TX: 0
        RX: 0
        TX: 0
        RX: 0
        TX: 1
        RX: 1
        TX: 1
        RX: 1
        TX: stop bit
        RX: stop bit
        -- Received 0xc5 --
        -- Transmitting 0x3a --
        TX: start bit
        RX: start bit
        TX: 0
        RX: 0
        ...

For completeness, we will verify the timeout behavior with a test bench that disconnects the rx from the tx signal, and we specify a small timeout for the receive procedure:

        def testTimeout():
            tx = Signal(1)
            rx = Signal(1)
            rxData = intbv(0)
            for val in testvals:
                txData = intbv(val)
                yield rs232_rx(rx, rxData, timeout=4*T_9600-1), rs232_tx(tx, txData)

The simulation now stops with a timeout exception after a few transmit cycles:

        -- Transmitting 0xc5 --
        TX: start bit
        TX: 1
        TX: 0
        TX: 1
        StopSimulation: RX time out error

Recall that the original generator resumes as soon as one of the forked generators returns. In the previous cases, this is just fine, as the transmitter and receiver generators run in lockstep. However, it may be desirable to resume the caller only when all of the forked generators have finished. For example, suppose that we want to characterize the robustness of the transmitter and receiver design to bit duration differences. We can adapt our test bench as follows, to run the transmitter at a faster rate:

        T_10200 = int(1e9 / 10200)

        def testNoJoin():
            tx = Signal(1)
            rx = tx
            rxData = intbv(0)
            for val in testvals:
                txData = intbv(val)
                yield rs232_rx(rx, rxData), rs232_tx(tx, txData, duration=T_10200)

Simulating this shows how the transmission of the new byte starts before the previous one is received, potentially creating additional transmission errors:

        -- Transmitting 0xc5 --
        TX: start bit
        RX: start bit
        ...
        TX: 1
        RX: 1
        TX: 1
        TX: stop bit
        RX: 1
        -- Transmitting 0x3a --
        TX: start bit
        RX: stop bit
        -- Received 0xc5 --
        RX: start bit
        TX: 0

It is more likely that we want to characterize the design on a byte by byte basis, and align the two generators before transmitting each byte. In MyHDL, this is done with the join function. By joining clauses together in a yield statement, we create a new clause that triggers only when all of its clause arguments have triggered. For example, we can adapt the test bench as follows:

        def testJoin():
            tx = Signal(1)
            rx = tx
            rxData = intbv(0)
            for val in testvals:
                txData = intbv(val)
                yield join(rs232_rx(rx, rxData), rs232_tx(tx, txData, duration=T_10200))

Now, transmission of a new byte only starts when the previous one is received:

        -- Transmitting 0xc5 --
        TX: start bit
        RX: start bit
        ...
        TX: 1
        RX: 1
        TX: 1
        TX: stop bit
        RX: 1
        RX: stop bit
        -- Received 0xc5 --
        -- Transmitting 0x3a --
        TX: start bit
        RX: start bit
        TX: 0
        RX: 0

  Modeling memories with built-in types

Python has powerful built-in data types that can be useful to model hardware memories. This can be merely a matter of putting an interface around some data type operations.

For example, a dictionary comes in handy to model sparse memory structures. (In other languages, this data type is called associative array, or hash table.) A sparse memory is one in which only a small part of the addresses is used in a particular application or simulation. Instead of statically allocating the full address space, which can be large, it is better to dynamically allocate the needed storage space. This is exactly what a dictionary provides. The following is an example of a sparse memory model:

        def sparseMemory(dout, din, addr, we, en, clk):

            """ Sparse memory model based on a dictionary.

            Ports:
            dout -- data out
            din -- data in
            addr -- address bus
            we -- write enable: write if 1, read otherwise
            en -- interface enable: enabled if 1
            clk -- clock input

            """

            memory = {}

            @always(clk.posedge)
            def access():
                if en:
                    if we:
                        memory[addr.val] = din.val
                    else:
                        dout.next = memory[addr.val]

            return access

Note how we use the val attribute of the din signal, as we don't want to store the signal object itself, but its current value. Similarly, we use the val attribute of the addr signal as the dictionary key.

In many cases, MyHDL code uses a signal's current value automatically when there is no ambiguity: for example, when a signal is used in an expression. However, in other cases, such as in this example, you have to refer to the value explicitly: for example, when the Signal is used as a dictionary key, or when it is not used in an expression. One option is to use the val attribute, as in this example. Another possibility is to use the int() or bool() functions to typecast the Signal to an integer or a boolean value. These functions are also useful with intbv objects.

As a second example, we will demonstrate how to use a list to model a synchronous fifo:

        def fifo(dout, din, re, we, empty, full, clk, maxFilling=sys.maxint):

            """ Synchronous fifo model based on a list.

            Ports:
            dout -- data out
            din -- data in
            re -- read enable
            we -- write enable
            empty -- empty indication flag
            full -- full indication flag
            clk -- clock input

            Optional parameter:
            maxFilling -- maximum fifo filling, "infinite" by default

            """

            memory = []

            @always(clk.posedge)
            def access():
                if we:
                    memory.insert(0, din.val)
                if re:
                    dout.next = memory.pop()
                filling = len(memory)
                empty.next = (filling == 0)
                full.next = (filling == maxFilling)

            return access

Again, the model is merely a MyHDL interface around some operations on a list: insert to insert entries, pop to retrieve them, and len to get the size of a Python object.

  Modeling errors using exceptions

In the previous section, we used Python data types for modeling. If such a type is used inappropriately, Python's run time error system will come into play. For example, if we access an address in the sparseMemory model that was not initialized before, we will get a traceback similar to the following (some lines omitted for clarity):

        Traceback (most recent call last):
        ...
          File "sparseMemory.py", line 31, in access
            dout.next = memory[addr.val]
        KeyError: Signal(51)

Similarly, if the fifo is empty, and we attempt to read from it, we get:

        Traceback (most recent call last):
        ...
          File "fifo.py", line 34, in fifo
            dout.next = memory.pop()
        IndexError: pop from empty list

Instead of these low level errors, it may be preferable to define errors at the functional level. In Python, this is typically done by defining a custom Error exception, by subclassing the standard Exception class. This exception is then raised explicitly when an error condition occurs.

For example, we can change the sparseMemory function as follows (with the doc string is omitted for brevity):

        class Error(Exception):
            pass

        def sparseMemory2(dout, din, addr, we, en, clk):

            memory = {}

            @always(clk.posedge)
            def access():
                if en:
                    if we:
                        memory[addr.val] = din.val
                    else:
                        try:
                            dout.next = memory[addr.val]
                        except KeyError:
                            raise Error, "Uninitialized address %s" % hex(addr)

            return access

This works by catching the low level data type exception, and raising the custom exception with an appropriate error message instead. If the sparseMemory function is defined in a module with the same name, an access error is now reported as follows:

        Traceback (most recent call last):
        ...
          File "sparseMemory.py", line 61, in access
            raise Error, "Uninitialized address %s" % hex(addr)
        Error: Uninitialized address 0x33

Likewise, the fifo function can be adapted as follows, to report underflow and overflow errors:

        class Error(Exception):
            pass

        def fifo2(dout, din, re, we, empty, full, clk, maxFilling=sys.maxint):

            memory = []

            @always(clk.posedge)
            def access():
                if we:
                    memory.insert(0, din.val)
                if re:
                    try:
                        dout.next = memory.pop()
                    except IndexError:
                        raise Error, "Underflow -- Read from empty fifo"
                filling = len(memory)
                empty.next = (filling == 0)
                full.next = (filling == maxFilling)
                if filling > maxFilling:
                    raise Error, "Overflow -- Max filling %s exceeded" % maxFilling

            return access

In this case, the underflow error is detected as before, by catching a low level exception on the list data type. On the other hand, the overflow error is detected by a regular check on the length of the list.

  Object oriented modeling

The models in the previous sections used high-level built-in data types internally. However, they had a conventional RTL-style interface. Communication with such a module is done through signals that are attached to it during instantiation.

A more advanced approach is to model hardware blocks as objects. Communication with objects is done through method calls. A method encapsulates all details of a certain task performed by the object. As an object has a method interface instead of an RTL-style hardware interface, this is a much higher level approach.

As an example, we will design a synchronized queue object. Such an object can be filled by producer, and independently read by a consumer. When the queue is empty, the consumer should wait until an item is available. The queue can be modeled as an object with a put(item) and a get method, as follows:

        from myhdl import *

        def trigger(event):
            event.next = not event

        class queue:
            def __init__(self):
               self.l = []
               self.sync = Signal(0)
               self.item = None
            def put(self,item):
               # non time-consuming method
               self.l.append(item)
               trigger(self.sync)
            def get(self):
               # time-consuming method
               if not self.l:
                  yield self.sync
               self.item = self.l.pop(0)

The queue object constructor initializes an internal list to hold items, and a sync signal to synchronize the operation between the methods. Whenever put puts an item in the queue, the signal is triggered. When the get method sees that the list is empty, it waits on the trigger first. get is a generator method because it may consume time. As the yield statement is used in MyHDL for timing control, the method cannot "yield" the item. Instead, it makes it available in the item instance variable.

To test the queue operation, we will model a producer and a consumer in the test bench. As a waiting consumer should not block a whole system, it should run in a concurrent "thread". As always in MyHDL, concurrency is modeled by Python generators. Producer and consumer will thus run independently, and we will monitor their operation through some print statements:

        q = queue()

        def Producer(q):
            yield delay(120)
            for i in range(5):
                print "%s: PUT item %s" % (now(), i)
                q.put(i)
                yield delay(max(5, 45 - 10*i))

        def Consumer(q):
            yield delay(100)
            while 1:
                print "%s: TRY to get item" % now()
                yield q.get()
                print "%s: GOT item %s" % (now(), q.item)
                yield delay(30)

        def main():
            P = Producer(q)
            C = Consumer(q)
            return P, C

        sim = Simulation(main())
        sim.run()

Note that the generator method get is called in a yield statement in the Consumer function. The new generator will take over from Consumer, until it is done. Running this test bench produces the following output:

        % python queue.py
        100: TRY to get item
        120: PUT item 0
        120: GOT item 0
        150: TRY to get item
        165: PUT item 1
        165: GOT item 1
        195: TRY to get item
        200: PUT item 2
        200: GOT item 2
        225: PUT item 3
        230: TRY to get item
        230: GOT item 3
        240: PUT item 4
        260: TRY to get item
        260: GOT item 4
        290: TRY to get item
        StopSimulation: No more events

  Unit testing

  Introduction

Many aspects in the design flow of modern digital hardware design can be viewed as a special kind of software development. From that viewpoint, it is a natural question whether advances in software design techniques can not also be applied to hardware design.

One software design approach that deserves attention is Extreme Programming (XP). It is a fascinating set of techniques and guidelines that often seems to go against the conventional wisdom. On other occasions, XP just seems to emphasize the common sense, which doesn't always coincide with common practice. For example, XP stresses the importance of normal workweeks, if we are to have the fresh mind needed for good software development.

It is not my intention nor qualification to present a tutorial on Extreme Programming. Instead, in this section I will highlight one XP concept which I think is very relevant to hardware design: the importance and methodology of unit testing.

  The importance of unit tests

Unit testing is one of the corner stones of Extreme Programming. Other XP concepts, such as collective ownership of code and continuous refinement, are only possible by having unit tests. Moreover, XP emphasizes that writing unit tests should be automated, that they should test everything in every class, and that they should run perfectly all the time.

I believe that these concepts apply directly to hardware design. In addition, unit tests are a way to manage simulation time. For example, a state machine that runs very slowly on infrequent events may be impossible to verify at the system level, even on the fastest simulator. On the other hand, it may be easy to verify it exhaustively in a unit test, even on the slowest simulator.

It is clear that unit tests have compelling advantages. On the other hand, if we need to test everything, we have to write lots of unit tests. So it should be easy and pleasant to create, manage and run them. Therefore, XP emphasizes the need for a unit test framework that supports these tasks. In this chapter, we will explore the use of the unittest module from the standard Python library for creating unit tests for hardware designs.

  Unit test development

In this section, we will informally explore the application of unit test techniques to hardware design. We will do so by a (small) example: testing a binary to Gray encoder as introduced in section hwtypes-indexing.

  Defining the requirements

We start by defining the requirements. For a Gray encoder, we want to the output to comply with Gray code characteristics. Let's define a code as a list of codewords, where a codeword is a bit string. A code of order n has 2**n codewords.

A well-known characteristic is the one that Gray codes are all about:

Consecutive codewords in a Gray code should differ in a single bit.

Is this sufficient? Not quite: suppose for example that an implementation returns the lsb of each binary input. This would comply with the requirement, but is obviously not what we want. Also, we don't want the bit width of Gray codewords to exceed the bit width of the binary codewords.

Each codeword in a Gray code of order n must occur exactly once in the binary code of the same order.

With the requirements written down we can proceed.

  Writing the test first

A fascinating guideline in the XP world is to write the unit test first. That is, before implementing something, first write the test that will verify it. This seems to go against our natural inclination, and certainly against common practices. Many engineers like to implement first and think about verification afterwards.

But if you think about it, it makes a lot of sense to deal with verification first. Verification is about the requirements only --- so your thoughts are not yet cluttered with implementation details. The unit tests are an executable description of the requirements, so they will be better understood and it will be very clear what needs to be done. Consequently, the implementation should go smoother. Perhaps most importantly, the test is available when you are done implementing, and can be run anytime by anybody to verify changes.

Python has a standard unittest module that facilitates writing, managing and running unit tests. With unittest, a test case is written by creating a class that inherits from unittest.TestCase. Individual tests are created by methods of that class: all method names that start with test are considered to be tests of the test case.

We will define a test case for the Gray code properties, and then write a test for each of the requirements. The outline of the test case class is as follows:

        import unittest

        class TestGrayCodeProperties(unittest.TestCase):

            def testSingleBitChange(self):
             """Check that only one bit changes in successive codewords."""
             ....

            def testUniqueCodeWords(self):
            """Check that all codewords occur exactly once."""
            ....

Each method will be a small test bench that tests a single requirement. To write the tests, we don't need an implementation of the Gray encoder, but we do need the interface of the design. We can specify this by a dummy implementation, as follows:

For the first requirement, we will test all consecutive input numbers, and compare the current output with the previous one For each input, we check that the difference is exactly a single bit. For the second requirement, we will test all input numbers and put the result in a list. The requirement implies that if we sort the result list, we should get a range of numbers. For both requirements, we will test all Gray codes up to a certain order MAX_WIDTH. The test code looks as follows:

Note how the actual check is performed by a self.assertEqual method, defined by the unittest.TestCase class. Also, we have factored out running the tests for all Gray codes in a separate method runTests.

  Test-driven implementation

With the test written, we begin with the implementation. For illustration purposes, we will intentionally write some incorrect implementations to see how the test behaves.

The easiest way to run tests defined with the unittest framework, is to put a call to its main method at the end of the test module:

        unittest.main()

Let's run the test using the dummy Gray encoder shown earlier:

        % python test_gray_properties.py
        testSingleBitChange (__main__.TestGrayCodeProperties)
        Check that only one bit changes in successive codewords. ... ERROR
        testUniqueCodeWords (__main__.TestGrayCodeProperties)
        Check that all codewords occur exactly once. ... ERROR

As expected, this fails completely. Let us try an incorrect implementation, that puts the lsb of in the input on the output:

Running the test produces:

        python test_gray_properties.py
        testSingleBitChange (__main__.TestGrayCodeProperties)
        Check that only one bit changes in successive codewords. ... ok
        testUniqueCodeWords (__main__.TestGrayCodeProperties)
        Check that all codewords occur exactly once. ... FAIL

        ======================================================================
        FAIL: testUniqueCodeWords (__main__.TestGrayCodeProperties)
        Check that all codewords occur exactly once.
        ----------------------------------------------------------------------
        Traceback (most recent call last):
          File "test_gray_properties.py", line 42, in testUniqueCodeWords
            self.runTests(test)
          File "test_gray_properties.py", line 53, in runTests
            sim.run(quiet=1)
          File "/home/jand/dev/myhdl/myhdl/_Simulation.py", line 154, in run
            waiter.next(waiters, actives, exc)
          File "/home/jand/dev/myhdl/myhdl/_Waiter.py", line 127, in next
            clause = next(self.generator)
          File "test_gray_properties.py", line 40, in test
            self.assertEqual(actual, expected)
        AssertionError: Lists differ: [0, 0, 1, 1] != [0, 1, 2, 3]

        First differing element 1:
        0
        1

        - [0, 0, 1, 1]
        + [0, 1, 2, 3]

        ----------------------------------------------------------------------
        Ran 2 tests in 0.083s

        FAILED (failures=1)

Now the test passes the first requirement, as expected, but fails the second one. After the test feedback, a full traceback is shown that can help to debug the test output.

Finally, we use a correct implementation:

Now the tests pass:

  Additional requirements

In the previous section, we concentrated on the general requirements of a Gray code. It is possible to specify these without specifying the actual code. It is easy to see that there are several codes that satisfy these requirements. In good XP style, we only tested the requirements and nothing more.

It may be that more control is needed. For example, the requirement may be for a particular code, instead of compliance with general properties. As an illustration, we will show how to test for the original Gray code, which is one specific instance that satisfies the requirements of the previous section. In this particular case, this test will actually be easier than the previous one.

We denote the original Gray code of order n as Ln. Some examples:

        L1 = ['0', '1']
        L2 = ['00', '01', '11', '10']
        L3 = ['000', '001', '011', '010', '110', '111', '101', 100']

It is possible to specify these codes by a recursive algorithm, as follows:

    1.  L1 = ['0', '1']

    2.  Ln+1 can be obtained from Ln as follows. Create a new code Ln0 by prefixing all codewords of Ln with '0'. Create another new code Ln1 by prefixing all codewords of Ln with '1', and reversing their order. Ln+1 is the concatenation of Ln0 and Ln1.

Python is well-known for its elegant algorithmic descriptions, and this is a good example. We can write the algorithm in Python as follows:

The code ['0' + codeword for ...] is called a list comprehension. It is a concise way to describe lists built by short computations in a for loop.

The requirement is now that the output code matches the expected code Ln. We use the nextLn function to compute the expected result. The new test case code is as follows:

As it happens, our implementation is apparently an original Gray code:

  Co-simulation with Verilog

  Introduction

One of the most exciting possibilities of MyHDLis to use it as a hardware verification language (HVL). A HVL is a language used to write test benches and verification environments, and to control simulations.

Nowadays, it is generally acknowledged that HVLs should be equipped with modern software techniques, such as object orientation. The reason is that verification it the most complex and time-consuming task of the design process. Consequently, every useful technique is welcome. Moreover, test benches are not required to be implementable. Therefore, unlike with synthesizable code, there are no constraints on creativity.

Technically, verification of a design implemented in another language requires co-simulation. MyHDL is enabled for co-simulation with any HDL simulator that has a procedural language interface (PLI). The MyHDLside is designed to be independent of a particular simulator, On the other hand, for each HDL simulator a specific PLI module will have to be written in C. Currently, the MyHDL release contains a PLI module for two Verilog simulators: Icarus and Cver.

  The HDL side

To introduce co-simulation, we will continue to use the Gray encoder example from the previous chapters. Suppose that we want to synthesize it and write it in Verilog for that purpose. Clearly we would like to reuse our unit test verification environment.

To start, let's recall how the Gray encoder in MyHDL looks like:

To show the co-simulation flow, we don't need the Verilog implementation yet, but only the interface. Our Gray encoder in Verilog would have the following interface:

        module bin2gray(B, G);

           parameter width = 8;
           input [width-1:0]  B;
           output [width-1:0] G;
           ....

To write a test bench, one creates a new module that instantiates the design under test (DUT). The test bench declares nets and regs (or signals in VHDL) that are attached to the DUT, and to stimulus generators and response checkers. In an all-HDL flow, the generators and checkers are written in the HDL itself, but we will want to write them in MyHDL. To make the connection, we need to declare which regs & nets are driven and read by the MyHDLsimulator. For our example, this is done as follows:

        module dut_bin2gray;

           reg [`width-1:0] B;
           wire [`width-1:0] G;

           initial begin
              $from_myhdl(B);
              $to_myhdl(G);
           end

           bin2gray dut (.B(B), .G(G));
           defparam dut.width = `width;

        endmodule

The $from_myhdl task call declares which regs are driven by MyHDL, and the $to_myhdl task call which regs & nets are read by it. These tasks take an arbitrary number of arguments. They are defined in a PLI module written in C and made available in a simulator-dependent manner. In Icarus Verilog, the tasks are defined in a myhdl.vpi module that is compiled from C source code.

  The MyHDL side

MyHDL supports co-simulation by a Cosimulation object. A Cosimulation object must know how to run a HDL simulation. Therefore, the first argument to its constructor is a command string to execute a simulation.

The way to generate and run an simulation executable is simulator dependent. For example, in Icarus Verilog, a simulation executable for our example can be obtained obtained by running the iverilog compiler as follows:

        % iverilog -o bin2gray -Dwidth=4 bin2gray.v dut_bin2gray.v

This generates a bin2gray executable for a parameter width of 4, by compiling the contributing Verilog files.

The simulation itself is run by the vvp command:

        % vvp -m ./myhdl.vpi bin2gray

This runs the bin2gray simulation, and specifies to use the myhdl.vpi PLI module present in the current directory. (This is just a command line usage example; actually simulating with the myhdl.vpi module is only meaningful from a Cosimulation object.)

We can use a Cosimulation object to provide a HDL version of a design to the MyHDL simulator. Instead of a generator function, we write a function that returns a Cosimulation object. For our example and the Icarus Verilog simulator, this is done as follows:

        import os

        from myhdl import Cosimulation

        cmd = "iverilog -o bin2gray.o -Dwidth=%s " + \
              "../../test/verilog/bin2gray.v " + \
              "../../test/verilog/dut_bin2gray.v "

        def bin2gray(B, G):
            width = len(B)
            os.system(cmd % width)
            return Cosimulation("vvp -m ../myhdl.vpi bin2gray.o", B=B, G=G)

After the executable command argument, the Cosimulation constructor takes an arbitrary number of keyword arguments. Those arguments make the link between MyHDL Signals and HDL nets, regs, or signals, by named association. The keyword is the name of an argument in a $to_myhdl or $from_myhdl call; the argument is a MyHDL Signal.

With all this in place, we can now use the existing unit test to verify the Verilog implementation. Note that we kept the same name and parameters for the the bin2gray function: all we need to do is to provide this alternative definition to the existing unit test.

Let's try it on the Verilog design:

        module bin2gray(B, G);

           parameter width = 8;
           input [width-1:0]  B;
           output [width-1:0] G;

           assign G = (B >> 1) ^ B;

        endmodule // bin2gray

When we run our unit tests, we get:

        % python test_gray.py
        testSingleBitChange (test_gray_properties.TestGrayCodeProperties)
        Check that only one bit changes in successive codewords. ... ok
        testUniqueCodeWords (test_gray_properties.TestGrayCodeProperties)
        Check that all codewords occur exactly once. ... ok
        testOriginalGrayCode (test_gray_original.TestOriginalGrayCode)
        Check that the code is an original Gray code. ... ok

        ----------------------------------------------------------------------
        Ran 3 tests in 0.706s

        OK

  Restrictions

In the ideal case, it should be possible to simulate any HDL description seamlessly with MyHDL. Moreover the communicating signals at each side should act transparently as a single one, enabling fully race free operation.

For various reasons, it may not be possible or desirable to achieve full generality. As anyone that has developed applications with the Verilog PLI can testify, the restrictions in a particular simulator, and the differences over various simulators, can be quite frustrating. Moreover, full generality may require a disproportionate amount of development work compared to a slightly less general solution that may be sufficient for the target application.

Consequently, I have tried to achieve a solution which is simple enough so that one can reasonably expect that any PLI-enabled simulator can support it, and that is relatively easy to verify and maintain. At the same time, the solution is sufficiently general to cover the target application space.

The result is a compromise that places certain restrictions on the HDL code. In this section, these restrictions are presented.

  Only passive HDL can be co-simulated

The most important restriction of the MyHDL co-simulation solution is that only "passive" HDL can be co-simulated. This means that the HDL code should not contain any statements with time delays. In other words, the MyHDL simulator should be the master of time; in particular, any clock signal should be generated at the MyHDL side.

At first this may seem like an important restriction, but if one considers the target application for co-simulation, it probably isn't.

MyHDL supports co-simulation so that test benches for HDL designs can be written in Python. Let's consider the nature of the target HDL designs. For high-level, behavioral models that are not intended for implementation, it should come as no surprise that I would recommend to write them in MyHDL directly; that is one of the goals of the MyHDL effort. Likewise, gate level designs with annotated timing are not the target application: static timing analysis is a much better verification method for such designs.

Rather, the targeted HDL designs are naturally models that are intended for implementation, most likely through synthesis. As time delays are meaningless in synthesizable code, the restriction is compatible with the target application.

  Race sensitivity issues

In a typical RTL code, some events cause other events to occur in the same time step. For example, when a clock signal triggers some signals may change in the same time step. For race-free operation, an HDL must differentiate between such events within a time step. This is done by the concept of "delta" cycles. In a fully general, race free co-simulation, the co-simulators would communicate at the level of delta cycles. However, in MyHDL co-simulation, this is not entirely the case.

Delta cycles from the MyHDL simulator toward the HDL co-simulator are preserved. However, in the opposite direction, they are not. The signals changes are only returned to the MyHDL simulator after all delta cycles have been performed in the HDL co-simulator.

What does this mean? Let's start with the good news. As explained in the previous section, the concept behind MyHDL co-simulation implies that clocks are generated at the MyHDL side. When using a MyHDL clock and its corresponding HDL signal directly as a clock, co-simulation is race free. In other words, the case that most closely reflects the MyHDL co-simulation approach, is race free.

The situation is different when you want to use a signal driven by the HDL (and the corresponding MyHDL signal) as a clock. Communication triggered by such a clock is not race free. The solution is to treat such an interface as a chip interface instead of an RTL interface. For example, when data is triggered at positive clock edges, it can safely be sampled at negative clock edges. Alternatively, the MyHDL data signals can be declared with a delay value, so that they are guaranteed to change after the clock edge.

  Implementation notes
        This section requires some knowledge of PLI terminology.

Enabling a simulator for co-simulation requires a PLI module written in C. In Verilog, the PLI is part of the "standard". However, different simulators implement different versions and portions of the standard. Worse yet, the behavior of certain PLI callbacks is not defined on some essential points. As a result, one should plan to write or at least customize a specific PLI module for any simulator. The release contains a PLI module for the open source Icarus and Cver simulators.

This section documents the current approach and status of the PLI module implementation and some reflections on future implementations.

  Icarus Verilog

  Delta cycle implementation

To make co-simulation work, a specific type of PLI callback is needed. The callback should be run when all pending events have been processed, while allowing the creation of new events in the current time step (e.g. by the MyHDL simulator). In some Verilog simulators, the cbReadWriteSync callback does exactly that. However, in others, including Icarus, it does not. The callback's behavior is not fully standardized; some simulators run the callback before non- blocking assignment events have been processed.

Consequently, I had to look for a workaround. One half of the solution is to use the cbReadOnlySync callback. This callback runs after all pending events have been processed. However, it does not permit one to create new events in the current time step. The second half of the solution is to map MyHDL delta cycles onto real Verilog time steps. Note that fortunately I had some freedom here because of the restriction that only passive HDL code can be co-simulated.

I chose to make the time granularity in the Verilog simulator a 1000 times finer than in the MyHDL simulator. For each MyHDL time step, 1000 Verilog time steps are available for MyHDL delta cycles. In practice, only a few delta cycles per time step should be needed. Exceeding this limit almost certainly indicates a design error; the limit is checked at run-time. The factor 1000 also makes it easy to distinguish "real" time from delta cycle time when printing out the Verilog time.

  Passive Verilog check

As explained before, co-simulated Verilog should not contain delay statements. Ideally, there should be a run-time check to flag non-compliant code. However, there is currently no such check in the Icarus module.

The check can be written using the cbNextSimTime VPI callback in Verilog. However, Icarus 0.7 doesn't support this callback. In the meantime, support for it has been added to the Icarus development branch. When Icarus 0.8 is released, a check will be added.

In the mean time, just don't do this. It may appear to "work" but it really won't as events will be missed over the co-simulation interface.

  Cver

MyHDL co-simulation is supported with the open source Verilog simulator Cver. The PLI module is based on the one for Icarus and basically has the same functionality. Only some cosmetic modifications were required.

  Other Verilog simulators

The Icarus module is written with VPI calls, which are provided by the most recent generation of the Verilog PLI. Some simulators may only support TF/ACC calls, requiring a complete redesign of the interface module.

If the simulator supports VPI, the Icarus module should be reusable to a large extent. However, it may be possible to improve on it. The workaround to support delta cycles described in Section Delta cycle implementation may not be necessary. In some simulators, the cbReadWriteSync callback occurs after all events (including non-blocking assignments) have been processed. In that case, the functionality can be supported without a finer time granularity in the Verilog simulator.

There are also Verilog standardization efforts underway to resolve the ambiguity of the cbReadWriteSync callback. The solution will be to introduce new, well defined callbacks. From reading some proposals, I conclude that the cbEndOfSimTime callback would provide the required functionality.

The MyHDL project currently has no access to commercial Verilog simulators, so progress in co-simulation support depends on external interest and participation. Users have reported that they are using MyHDL co-simulation with the simulators from Aldec and Modelsim.

  Interrupted system calls

The PLI module uses read and write system calls to communicate between the co-simulators. The implementation assumes that these calls are restarted automatically by the operating system when interrupted. This is apparently what happens on the Linux box on which MyHDL is developed.

It is known how non-restarted interrupted system calls should be handled, but currently such code cannot be tested on the MyHDL development platform. Also, it is not clear whether this is still a relevant issue with modern operating systems. Therefore, this issue has not been addressed at this moment. However, assertions have been included that should trigger when this situation occurs.

Whenever an assertion fires in the PLI module, please report it. The same holds for Python exceptions that cannot be easily explained.

  What about VHDL?

It would be nice to have an interface to VHDL simulators such as the Modelsim VHDL simulator. Let us summarize the requirements to accomplish that:

    •   We need a procedural interface to the internals of the simulator.

    •   The procedural interface should be a widely used industry standard so that we can reuse the work in several simulators.

    •   MyHDL is an open-source project and therefore there should be also be an open-source simulator that implements the procedural interface.

vpi for Verilog matches these requirements. It is a widely used standard and is supported by the open-source Verilog simulators Icarus and cver.

However, for VHDL the situation is different. While there exists a standard called vhpi, it much less popular than vpi. Also, to our knowledge there is only one credible open source VHDL simulator (GHDL) and it is unclear whether it has vhpi capabilities that are powerful enough for MyHDL's purposes.

Consequently, the development of co-simulation for VHDL is currently on hold. For some applications, there is an alternative: see conv-testbench.

  Conversion to Verilog and VHDL

  Introduction

Subject to some limitations, MyHDL supports the automatic conversion of MyHDL code to Verilog or VHDL code. This feature provides a path from MyHDL into a standard Verilog or VHDL based design environment.

This chapter describes the concepts of conversion. Concrete examples can be found in the companion chapter conv-usage.

  Solution description

To be convertible, the hardware description should satisfy certain restrictions, defined as the convertible subset. This is described in detail in The convertible subset.

A convertible design can be converted to an equivalent model in Verilog or VHDL, using the function toVerilog or toVHDL from the MyHDL library.

When the design is intended for implementation a third-party synthesis tool is used to compile the Verilog or VHDL model into an implementation for an ASIC or FPGA. With this step, there is an automated path from a hardware description in Python to an FPGA or ASIC implementation.

The conversion does not start from source files, but from an instantiated design that has been elaborated by the Python interpreter. The converter uses the Python profiler to track the interpreter's operation and to infer the design structure and name spaces. It then selectively compiles pieces of source code for additional analysis and for conversion.

  Features

    Conversion after elaboration
        Elaboration refers to the initial processing of a hardware description to achieve a representation of a design instance that is ready for simulation or synthesis. In particular, structural parameters and constructs are processed in this step. In MyHDL, the Python interpreter itself is used for elaboration. A Simulation object is constructed with elaborated design instances as arguments. Likewise, conversion works on an elaborated design instance. The Python interpreter is thus used as much as possible.

    Arbitrarily complex structure
        As the conversion works on an elaborated design instance, any modeling constraints only apply to the leaf elements of the design structure, that is, the co-operating generators. In other words, there are no restrictions on the description of the design structure: Python's full power can be used for that purpose. Also, the design hierarchy can be arbitrarily deep.

    Generator are mapped to Verilog or VHDL constructs
        The converter analyzes the code of each generator and maps it to equivalent constructs in the target HDL. For Verilog, it will map generators to always blocks, continuous assignments or initial blocks. For VHDL, it will map them to process statements or concurrent signal assignments.

    The module ports are inferred from signal usage
        In MyHDL, the input or output direction of ports is not explicitly declared. The converter investigates signal usage in the design hierarchy to infer whether a signal is used as input, output, or as an internal signal.

    Interfaces are convertible
        An interface: an object that has a number of Signal objects as its attributes. The converter supports this by name expansion and mangling.

    Function calls are mapped to Verilog or VHDL subprograms
        The converter analyzes function calls and function code. Each function is mapped to an appropriate subprogram in the target HDL: a function or task in Verilog, and a function or procedure in VHDL. In order to support the full power of Python functions, a unique subprogram is generated per Python function call.

    If-then-else structures may be mapped to case statements
        Python does not provide a case statement. However, the converter recognizes if-then-else structures in which a variable is sequentially compared to items of an enumeration type, and maps such a structure to a Verilog or VHDL case statement with the appropriate synthesis attributes.

    Choice of encoding schemes for enumeration types
        The enum function in MyHDL returns an enumeration type. This function takes an additional parameter encoding that specifies the desired encoding in the implementation: binary, one hot, or one cold. The converter generates the appropriate code for the specified encoding.

    RAM memory
        Certain synthesis tools can map Verilog memories or VHDL arrays to RAM structures. To support this interesting feature, the converter maps lists of signals to Verilog memories or VHDL arrays.

    ROM memory
        Some synthesis tools can infer a ROM from a case statement. The converter does 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.

    Signed arithmetic
        In MyHDL, working with negative numbers is trivial: one just uses an intbv object with an appropriate constraint on its values. In contrast, both Verilog and VHDL make a difference between an unsigned and a signed representation. To work with negative values, the user has to declare a signed variable explicitly. But when signed and unsigned operands are mixed in an expression, things may become tricky.

        In Verilog, when signed and unsigned operands are mixed, all operands are interpreted as unsigned. Obviously, this leads to unexpected results. The designer will have to add sign extensions and type casts to solve this.

        In VHDL, mixing signed and unsigned will generally not work. The designer will have to match the operands manually by adding resizings and type casts.

        In MyHDL, these issues don't exist because intbv objects simply work as (constrained) integers. Moreover, the converter automates the cumbersome tasks that are required in Verilog and VHDL. It uses signed or unsigned types based on the value constraints of the intbv objects, and automatically performs the required sign extensions, resizings, and type casts.

    User-defined code
        If desired, the user can bypass the conversion process and describe user-defined code to be inserted instead.

  The convertible subset

  Introduction

Unsurprisingly, not all MyHDL code can be converted. Although the restrictions are significant, the convertible subset is much broader than the RTL synthesis subset which is an industry standard. In other words, MyHDL code written according to the RTL synthesis rules, should always be convertible. However, it is also possible to write convertible code for non-synthesizable models or test benches.

The converter attempts to issue clear error messages when it encounters a construct that cannot be converted.

Recall that any restrictions only apply to the design after elaboration. In practice, this means that they apply only to the code of the generators, that are the leaf functional blocks in a MyHDL design.

  Coding style

A natural restriction on convertible code is that it should be written in MyHDL style: cooperating generators, communicating through signals, and with sensitivity specify resume conditions.

For pure modeling, it doesn't matter how generators are created. However, in convertible code they should be created using one of the MyHDL decorators: instance, always, always_seq, or always_comb.

  Supported types

The most important restriction regards object types. Only a limited amount of types can be converted. Python int and long objects are mapped to Verilog or VHDL integers. All other supported types need to have a defined bit width. The supported types are the Python bool type, the MyHDL intbv type, and MyHDL enumeration types returned by function enum.

intbv objects must be constructed so that a bit width can be inferred. This can be done by specifying minimum and maximum values, e.g. as follows:

        index = intbv(0, min=MIN, max=MAX)

The Verilog converter supports intbv objects that can take negative values.

Alternatively, a slice can be taken from an intbv object as follows:

        index = intbv(0)[N:]

Such as slice returns a new intbv object, with minimum value 0 , and maximum value 2**N.

In addition to the scalar types described above, the converter also supports a number of tuple and list based types. The mapping from MyHDL types is summarized in the following table.
MyHDL type | VHDL type | Notes | Verilog type | Notes
int | integer |  | integer |
bool | std_logic | (1) | reg |
intbv with min >= 0 | unsigned | (2) | reg |
intbv with min < 0 | signed | (2) | reg signed |
enum | dedicated enumeration type |  | reg |
tuple of int | mapped to case statement | (3) | mapped to case statement | (3)
list of bool | array of std_logic |  | reg | (5)
list of intbv with min >= 0 | array of unsigned | (4) | reg | (4)(5)
list of intbv with min < 0 | array of signed | (4) | reg signed | (4)(5)

Notes:

    1.  The VHDL std_logic type is defined in the standard VHDL package IEEE.std_logic_1164.

    2.  The VHDL unsigned and signed types used are those from the standard VHDL packages IEEE.numeric_std.

    3.  A MyHDL tuple of int is used for ROM inference, and can only be used in a very specific way: an indexing operation into the tuple should be the rhs of an assignment.

    4.  All list members should have identical value constraints.

    5.  Lists are mapped to Verilog memories.

The table as presented applies to MyHDL variables. The converter also supports MyHDL signals that use bool, intbv or enum objects as their underlying type. For VHDL, these are mapped to VHDL signals with an underlying type as specified in the table above. Verilog doesn't have the signal concept. For Verilog, a MyHDL signal is mapped to a Verilog reg as in the table above, or to a Verilog wire, depending on the signal usage.

The converter supports MyHDL list of signals provided the underlying signal type is either bool or intbv. They may be mapped to a VHDL signal with a VHDL type as specified in the table, or to a Verilog memory. However, list of signals are not always mapped to a corresponding VHDL or Verilog object. See Conversion of lists of signals for more info.

  Supported statements

The following is a list of the statements that are supported by the Verilog converter, possibly qualified with restrictions or usage notes.

    assert
        An assert statement in Python looks as follow:

            assert test_expression

    It can be converted provided test_expression is convertible.

break

continue

def

    for The only supported iteration scheme is iterating through sequences of integers returned by built-in function range or MyHDL function downrange. The optional else clause is not supported.

    if  if, elif, and else clauses are fully supported.

pass

print
        A print statement with multiple arguments:

                print arg1, arg2, ...

        is supported. However, there are restrictions on the arguments. First, they should be of one of the following forms:

                arg
                formatstring % arg
                formatstring % (arg1, arg2, ...)

        where arg is a bool, int, intbv, enum, or a Signal of these types.

        The formatstring contains ordinary characters and conversion specifiers as in Python. However, the only supported conversion specifiers are %s and %d. Justification and width specification are thus not supported.

        Printing without a newline:

                print arg1 ,

        is not supported.

    raise
        This statement is mapped to statements that end the simulation with an error message.

return

    yield
        A yield expression is used to specify a sensitivity list. The yielded expression can be a Signal, a signal edge as specified by MyHDL functions Signal.posedge or Signal.negedge, or a tuple of signals and edge specifications. It can also be a delay object.

    while
        The optional else clause is not supported.

  Supported built-in functions

The following is a list of the built-in functions that are supported by the converter.

    bool
        This function can be used to typecast an object explicitly to its boolean interpretation.

    len For Signal and intbv objects, function len returns the bit width.

    int This function can be used to typecast an object explicitly to its integer interpretation.

  Docstrings

The converter propagates comments under the form of Python docstrings.

Docstrings are typically used in Python to document certain objects in a standard way. Such "official" docstrings are put into the converted output at appropriate locations. The converter supports official docstrings for the top level module and for generators.

Within generators, "nonofficial" docstrings are propagated also. These are strings (triple quoted by convention) that can occur anywhere between statements.

Regular Python comments are ignored by the Python parser, and they are not present in the parse tree. Therefore, these are not propagated. With docstrings, you have an elegant way to specify which comments should be propagated and which not.

  Conversion of lists of signals

Lists of signals are useful for many purposes. For example, they make it easy to create a repetitive structure. Another application is the description of memory behavior.

The converter output is non-hierarchical. That implies that all signals are declared at the top-level in VHDL or Verilog (as VHDL signals, or Verilog regs and wires.) However, some signals that are a list member at some level in the MyHDL design hierarchy may be used as a plain signal at a lower level. For such signals, a choice has to be made whether to declare a Verilog memory or VHDL array, or a number of plain signal names.

If possible, plain signal declarations are preferred, because Verilog memories and arrays have some restrictions in usage and tool support. This is possible if the list syntax is strictly used outside generator code, for example when lists of signals are used to describe structure.

Conversely, when list syntax is used in some generator, then a Verilog memory or VHDL array will be declared. The typical example is the description of RAM memories.

  Conversion of Interfaces

Complex designs often have many signals that are passed to different levels of hierarchy. Typically, many signals logically belong together. This can be modelled by an interface: an object that has a number of Signal objects as its attributes. Grouping signals into an interface simplifies the code, improves efficiency, and reduces errors.

The converter supports interface using hierarchical name expansion and name mangling.

  Assignment issues

  Name assignment in Python

Name assignment in Python is a different concept than in many other languages. This point is very important for effective modeling in Python, and even more so for synthesizable MyHDL code. Therefore, the issues are discussed here explicitly.

Consider the following name assignments:

        a = 4
        a = ``a string''
        a = False

In many languages, the meaning would be that an existing variable a gets a number of different values. In Python, such a concept of a variable doesn't exist. Instead, assignment merely creates a new binding of a name to a certain object, that replaces any previous binding. So in the example, the name a is bound a number of different objects in sequence.

The converter has to investigate name assignment and usage in MyHDL code, and to map names to Verilog or VHDL objects. To achieve that, it tries to infer the type and possibly the bit width of each expression that is assigned to a name.

Multiple assignments to the same name can be supported if it can be determined that a consistent type and bit width is being used in the assignments. This can be done for boolean expressions, numeric expressions, and enumeration type literals.

In other cases, a single assignment should be used when an object is created. Subsequent value changes are then achieved by modification of an existing object. This technique should be used for Signal and intbv objects.

  Signal assignment

Signal assignment in MyHDL is implemented using attribute assignment to attribute next. Value changes are thus modeled by modification of the existing object. The converter investigates the Signal object to infer the type and bit width of the corresponding Verilog or VHDL object.

  intbv objects

Type intbv is likely to be the workhorse for synthesizable modeling in MyHDL. An intbv instance behaves like a (mutable) integer whose individual bits can be accessed and modified. Also, it is possible to constrain its set of values. In addition to error checking, this makes it possible to infer a bit width, which is required for implementation.

As noted before, it is not possible to modify value of an intbv object using name assignment. In the following, we will show how it can be done instead. Consider:

        a = intbv(0)[8:]

This is an intbv object with initial value 0 and bit width 8. To change its value to 5, we can use slice assignment:

        a[8:] = 5

The same can be achieved by leaving the bit width unspecified, which has the meaning to change "all" bits:

        a[:] = 5

Often the new value will depend on the old one. For example, to increment an intbv with the technique above:

        a[:] = a + 1

Python also provides augmented assignment operators, which can be used to implement in-place operations. These are supported on intbv objects and by the converter, so that the increment can also be done as follows:

        a += 1

  Excluding code from conversion

For some tasks, such as debugging, it may be useful to insert arbitrary Python code that should not be converted.

The converter supports this by ignoring all code that is embedded in a if __debug__ test. The value of the __debug__ variable is not taken into account.

  User-defined code

MyHDL provides a way to include user-defined code during the conversion process. There are special function attributes that are understood by the converter but ignored by the simulator. The attributes are verilog_code for Verilog and vhdl_code for VHDL. They operate like a special return value. When defined in a MyHDL function, the converter will use their value instead of the regular return value. Effectively, it will stop converting at that point.

The value of verilog_code or vhdl_code should be a Python template string. A template string supports $-based substitutions. The $name notation can be used to refer to the variable names in the context of the string. The converter will substitute the appropriate values in the string and then insert it instead of the regular converted output.

There is one more issue with user-defined code. Normally, the 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.

Proper signal usage inference cannot be done with user-defined code. Without user help, this will result in warnings or errors during the inference process, or in compilation errors from invalid code. The user can solve this by setting the driven or read attribute for signals that are driven or read from the user-defined code. These attributes are False by default. The allowed "true" values of the driven attribute are True, 'wire' and 'reg'. The latter two values 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.

For an example of user-defined code, see conv-usage-custom.

  Template transformation

NOTE:
        This section is only relevant for VHDL.

There is a difference between VHDL and Verilog in the way in which sensitivity to signal edges is specified. In Verilog, edge specifiers can be used directly in the sensitivity list. In VHDL, this is not possible: only signals can be used in the sensitivity list. To check for an edge, one uses the rising_edge() or falling_edge() functions in the code.

MyHDL follows the Verilog scheme to specify edges in the sensitivity list. Consequently, when mapping such code to VHDL, it needs to be transformed to equivalent VHDL. This is an important issue because it affects all synthesizable templates that infer sequential logic.

We will illustrate this feature with some examples. This is the MyHDL code for a D flip-flop:

        @always(clk.posedge)
        def logic():
            q.next = d

It is converted to VHDL as follows:

        DFF_LOGIC: process (clk) is
        begin
            if rising_edge(clk) then
                q <= d;
            end if;
        end process DFF_LOGIC;

The converter can handle the more general case. For example, this is MyHDL code for a D flip-flop with asynchronous set, asynchronous reset, and preference of set over reset:

        @always(clk.posedge, set.negedge, rst.negedge)
        def logic():
            if set == 0:
                q.next = 1
            elif rst == 0:
                q.next = 0
            else:
                q.next = d

This is converted to VHDL as follows:

        DFFSR_LOGIC: process (clk, set, rst) is
        begin
            if (set = '0') then
                q <= '1';
            elsif (rst = '0') then
                q <= '0';
            elsif rising_edge(clk) then
                q <= d;
            end if;
        end process DFFSR_LOGIC;

All cases with practical utility can be handled in this way. However, there are other cases that cannot be transformed to equivalent VHDL. The converter will detect those cases and give an error.

  Conversion output verification by co-simulation

NOTE:
        This section is only relevant for Verilog.

To verify the converted Verilog output, co-simulation can be used. To make this task easier, the converter also generates a test bench that makes it possible to simulate the Verilog design using the Verilog co-simulation interface. This permits one to verify the Verilog code with the same test bench used for the MyHDL code.

  Conversion of test benches

After conversion, we obviously want to verify that the VHDL or Verilog code works correctly. For Verilog, we can use co-simulation as discussed earlier. However, for VHDL, co-simulation is currently not supported.

An alternative is to convert the test bench itself, so that both test bench and design can be run in the HDL simulator. Of course, this is not a fully general solution, as there are important constraints on the kind of code that can be converted. Thus, the question is whether the conversion restrictions permit one to develop sufficiently complex test benches. In this section, we present some insights about this.

The most important restrictions regard the types that can be used, as discussed earlier in this chapter. However, the "convertible subset" is wider than the "synthesis subset". We will present a number of non-synthesizable feature that are of interest for test benches.

    the while loop
        while loops can be used for high-level control structures.

    the raise statement
        A raise statement can stop the simulation on an error condition.

    delay objects
        Delay modeling is essential for test benches.

    the print statement
        print statements can be used for simple debugging.

    the assert statement.
        Originally, assert statements were only intended to insert debugging assertions in code. Recently, there is a tendency to use them to write self-checking unit tests, controlled by unit test frameworks such as py.test. In particular, they are a powerful way to write self-checking test benches for MyHDL designs. As assert statements are convertible, a whole unit test suite in MyHDL can be converted to an equivalent test suite in Verilog and VHDL.

Additionally, the same techniques as for synthesizable code can be used to master complexity. In particular, any code outside generators is executed during elaboration, and therefore not considered in the conversion process. This feature can for example be used for complex calculations that set up constants or expected results. Furthermore, a tuple of ints can be used to hold a table of values that will be mapped to a case statement in Verilog and VHDL.

  Methodology notes

  Simulate first

In the Python philosophy, the run-time rules. The Python compiler doesn't attempt to detect a lot of errors beyond syntax errors, which given Python's ultra-dynamic nature would be an almost impossible task anyway. To verify a Python program, one should run it, preferably using unit testing to verify each feature.

The same philosophy should be used when converting a MyHDL description to Verilog: make sure the simulation runs fine first. Although the converter checks many things and attempts to issue clear error messages, there is no guarantee that it does a meaningful job unless the simulation runs fine.

  Handling hierarchy

Recall that conversion occurs after elaboration. A consequence is that the converted output is non-hierarchical. In many cases, this is not an issue. The purpose of conversion is to provide a path into a traditional design flow by using Verilog and VHDL as a "back-end" format. Hierarchy is quite relevant to humans, but much less so to tools.

However, in some cases hierarchy is desirable. For example, if you convert a test bench you may prefer to keep its code separate from the design under test. In other words, conversion should stop at the design under test instance, and insert an instantiation instead.

There is a workaround to accomplish this with a small amount of additional work. The workaround is to define user-defined code consisting of an instantiation of the design under test. As discussed in User-defined code, when the converter sees the hook it will stop converting and insert the instantiation instead. Of course, you will want to convert the design under test itself also. Therefore, you should use a flag that controls whether the hook is defined or not and set it according to the desired conversion.

  Known issues

    Verilog and VHDL integers are 32 bit wide
        Usually, Verilog and VHDL integers are 32 bit wide. In contrast, Python is moving toward integers with undefined width. Python int and long variables are mapped to Verilog integers; so for values wider than 32 bit this mapping is incorrect.

    Synthesis pragmas are specified as Verilog comments.
        The recommended way to specify synthesis pragmas in Verilog is through attribute lists. However, the Icarus simulator doesn't support them for case statements (to specify parallel_case and full_case pragmas). Therefore, the old but deprecated method of synthesis pragmas in Verilog comments is still used.

    Inconsistent place of the sensitivity list inferred from always_comb.
        The semantics of always_comb, both in Verilog and MyHDL, is to have an implicit sensitivity list at the end of the code. However, this may not be synthesizable. Therefore, the inferred sensitivity list is put at the top of the corresponding always block or process. This may cause inconsistent behavior at the start of the simulation. The workaround is to create events at time 0.

  Conversion examples

  Introduction

In this chapter, we will demonstrate the conversion process with a number of examples. For the concepts of MyHDL conversion, read the companion chapter conv.

  A small sequential design

Consider the following MyHDL code for an incrementer block:

This design can be converted to Verilog and VHDL. The first step is to elaborate it, just as we do for simulation. Then we can use the convert method on the elaborated instance.

For flexibility, we wrap the conversion in a convert_inc function. inc_1 is an elaborated design instance that provides the conversion method.

The conversion to Verilog generates an equivalent Verilog module in file inc.v. The Verilog code looks as follows:

The converter infers a proper Verilog module interface and maps the MyHDL generator to a Verilog always block.

Similarly, the conversion to VHDL generates a file inc.vhd with the following content:

The MyHDL generator is mapped to a VHDL process in this case.

Note that the VHDL file refers to a VHDL package called pck_myhdl_<version>. This package contains a number of convenience functions that make the conversion easier.

Note also the use of an inout in the interface. This is not recommended VHDL design practice, but it is required here to have a valid VHDL design that matches the behavior of the MyHDL design. As this is only an issue for ports and as the converter output is non-hierarchical, the issue is not very common and has an easy workaround.

  A small combinatorial design

The second example is a small combinatorial design, more specifically the binary to Gray code converter from previous chapters:

As before, you can create an instance and convert to Verilog and VHDL as follows:

The generated Verilog code looks as follows:

The generated VHDL code looks as follows:

  A hierarchical design

The converter can handle designs with an arbitrarily deep hierarchy.

For example, suppose we want to design an incrementer with Gray code output. Using the designs from previous sections, we can proceed as follows:

According to Gray code properties, only a single bit will change in consecutive values. However, as the bin2gray module is combinatorial, the output bits may have transient glitches, which may not be desirable. To solve this, let's create an additional level of hierarchy and add an output register to the design. (This will create an additional latency of a clock cycle, which may not be acceptable, but we will ignore that here.)

We can convert this hierarchical design as follows:

The Verilog output code looks as follows:

The VHDL output code looks as follows:

Note that the output is a flat "net list of blocks", and that hierarchical signal names are generated as necessary.

  Optimizations for finite state machines

As often in hardware design, finite state machines deserve special attention.

In Verilog and VHDL, finite state machines are typically described using case statements. Python doesn't have a case statement, but the converter recognizes particular if-then-else structures and maps them to case statements. This optimization occurs when a variable whose type is an enumerated type is sequentially tested against enumeration items in an if-then-else structure. Also, the appropriate synthesis pragmas for efficient synthesis are generated in the Verilog code.

As a further optimization, function enum was enhanced to support alternative encoding schemes elegantly, using an additional parameter encoding. For example:

        t_State = enum('SEARCH', 'CONFIRM', 'SYNC', encoding='one_hot')

The default encoding is 'binary'; the other possibilities are 'one_hot' and 'one_cold'. This parameter only affects the conversion output, not the behavior of the type. The generated Verilog code for case statements is optimized for an efficient implementation according to the encoding. Note that in contrast, a Verilog designer has to make nontrivial code changes to implement a different encoding scheme.

As an example, consider the following finite state machine, whose state variable uses the enumeration type defined above:

        ACTIVE_LOW = bool(0)
        FRAME_SIZE = 8
        t_State = enum('SEARCH', 'CONFIRM', 'SYNC', encoding="one_hot")

        def FramerCtrl(SOF, state, syncFlag, clk, reset_n):

            """ Framing control FSM.

            SOF -- start-of-frame output bit
            state -- FramerState output
            syncFlag -- sync pattern found indication input
            clk -- clock input
            reset_n -- active low reset

            """

            index = Signal(intbv(0)[8:]) # position in frame

            @always(clk.posedge, reset_n.negedge)
            def FSM():
                if reset_n == ACTIVE_LOW:
                    SOF.next = 0
                    index.next = 0
                    state.next = t_State.SEARCH
                else:
                    index.next = (index + 1) % FRAME_SIZE
                    SOF.next = 0
                    if state == t_State.SEARCH:
                        index.next = 1
                        if syncFlag:
                            state.next = t_State.CONFIRM
                    elif state == t_State.CONFIRM:
                        if index == 0:
                            if syncFlag:
                                state.next = t_State.SYNC
                            else:
                                state.next = t_State.SEARCH
                    elif state == t_State.SYNC:
                        if index == 0:
                            if not syncFlag:
                                state.next = t_State.SEARCH
                        SOF.next = (index == FRAME_SIZE-1)
                    else:
                        raise ValueError("Undefined state")

            return FSM

The conversion is done as before:

        SOF = Signal(bool(0))
        syncFlag = Signal(bool(0))
        clk = Signal(bool(0))
        reset_n = Signal(bool(1))
        state = Signal(t_State.SEARCH)
        toVerilog(FramerCtrl, SOF, state, syncFlag, clk, reset_n)
        toVHDL(FramerCtrl, SOF, state, syncFlag, clk, reset_n)

The Verilog output looks as follows:

        module FramerCtrl (
            SOF,
            state,
            syncFlag,
            clk,
            reset_n
        );

        output SOF;
        reg SOF;
        output [2:0] state;
        reg [2:0] state;
        input syncFlag;
        input clk;
        input reset_n;

        reg [7:0] index;

        always @(posedge clk, negedge reset_n) begin: FRAMERCTRL_FSM
            if ((reset_n == 0)) begin
                SOF <= 0;
                index <= 0;
                state <= 3'b001;
            end
            else begin
                index <= ((index + 1) % 8);
                SOF <= 0;
                // synthesis parallel_case full_case
                casez (state)
                    3'b??1: begin
                        index <= 1;
                        if (syncFlag) begin
                            state <= 3'b010;
                        end
                    end
                    3'b?1?: begin
                        if ((index == 0)) begin
                            if (syncFlag) begin
                                state <= 3'b100;
                            end
                            else begin
                                state <= 3'b001;
                            end
                        end
                    end
                    3'b1??: begin
                        if ((index == 0)) begin
                            if ((!syncFlag)) begin
                                state <= 3'b001;
                            end
                        end
                        SOF <= (index == (8 - 1));
                    end
                    default: begin
                        $finish;
                    end
                endcase
            end
        end

        endmodule

The VHDL output looks as follows:

        package pck_FramerCtrl is

            type t_enum_t_State_1 is (
            SEARCH,
            CONFIRM,
            SYNC
        );
        attribute enum_encoding of t_enum_t_State_1: type is "001 010 100";

        end package pck_FramerCtrl;

        library IEEE;
        use IEEE.std_logic_1164.all;
        use IEEE.numeric_std.all;
        use std.textio.all;

        use work.pck_myhdl_06.all;

        use work.pck_FramerCtrl.all;

        entity FramerCtrl is
            port (
                SOF: out std_logic;
                state: inout t_enum_t_State_1;
                syncFlag: in std_logic;
                clk: in std_logic;
                reset_n: in std_logic
            );
        end entity FramerCtrl;

        architecture MyHDL of FramerCtrl is

        signal index: unsigned(7 downto 0);

        begin

        FRAMERCTRL_FSM: process (clk, reset_n) is
        begin
            if (reset_n = '0') then
                SOF <= '0';
                index <= "00000000";
                state <= SEARCH;
            elsif rising_edge(clk) then
                index <= ((index + 1) mod 8);
                SOF <= '0';
                case state is
                    when SEARCH =>
                        index <= "00000001";
                        if to_boolean(syncFlag) then
                            state <= CONFIRM;
                        end if;
                    when CONFIRM =>
                        if (index = 0) then
                            if to_boolean(syncFlag) then
                                state <= SYNC;
                            else
                                state <= SEARCH;
                            end if;
                        end if;
                    when SYNC =>
                        if (index = 0) then
                            if (not to_boolean(syncFlag)) then
                                state <= SEARCH;
                            end if;
                        end if;
                        SOF <= to_std_logic(signed(resize(index, 9)) = (8 - 1));
                    when others =>
                        assert False report "End of Simulation" severity Failure;
                end case;
            end if;
        end process FRAMERCTRL_FSM;

        end architecture MyHDL;

  RAM inference

Certain synthesis tools can infer RAM structures. To support this feature, the converter maps lists of signals in MyHDL to Verilog memories and VHDL arrays.

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[addr].next = din

            @always_comb
            def read():
                dout.next = mem[addr]

            return write, read

With the appropriate signal definitions for the interface ports, it is converted 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_1_WRITE
            if (we) begin
                mem[addr] <= din;
            end
        end

        assign dout = mem[addr];

        endmodule

In VHDL, the list of MyHDL signals is modeled as a VHDL array signal:

        library IEEE;
        use IEEE.std_logic_1164.all;
        use IEEE.numeric_std.all;

        use work.pck_myhdl_06.all;

        entity ram is
            port (
                dout: out unsigned(7 downto 0);
                din: in unsigned(7 downto 0);
                addr: in unsigned(6 downto 0);
                we: in std_logic;
                clk: in std_logic
            );
        end entity ram;

        architecture MyHDL of ram is

        type t_array_mem is array(0 to 128-1) of unsigned(7 downto 0);
        signal mem: t_array_mem;

        begin

        RAM_WRITE: process (clk) is
        begin
            if rising_edge(clk) then
                if to_boolean(we) then
                    mem(to_integer(addr)) <= din;
                end if;
            end if;
        end process RAM_WRITE;

        dout <= mem(to_integer(addr));

        end architecture MyHDL;

  ROM inference

Some synthesis tools can infer a ROM memory from a case statement. The Verilog converter can perform 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. ROM access is described as follows:

        def rom(dout, addr, CONTENT):

            @always_comb
            def read():
                dout.next = CONTENT[int(addr)]

            return read

The ROM content is described as a tuple of integers. When the ROM content is defined, the conversion can be performed:

        CONTENT = (17, 134, 52, 9)
        dout = Signal(intbv(0)[8:])
        addr = Signal(intbv(0)[4:])

        toVerilog(rom, dout, addr, CONTENT)
        toVHDL(rom, dout, addr, CONTENT)

The Verilog output 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

The VHDL output code is as follows:

        library IEEE;
        use IEEE.std_logic_1164.all;
        use IEEE.numeric_std.all;
        use std.textio.all;

        use work.pck_myhdl_06.all;

        entity rom is
            port (
                dout: out unsigned(7 downto 0);
                addr: in unsigned(3 downto 0)
            );
        end entity rom;

        architecture MyHDL of rom is

        begin

        ROM_READ: process (addr) is
        begin
            case to_integer(addr) is
                when 0 => dout <= "00010001";
                when 1 => dout <= "10000110";
                when 2 => dout <= "00110100";
                when others => dout <= "00001001";
            end case;
        end process ROM_READ;

        end architecture MyHDL;

  User-defined code

MyHDL provides a way to include user-defined code during the conversion process, using the special function attributes vhdl_code and verilog_code.

For example:

        def inc_comb(nextCount, count, n):

            @always(count)
            def logic():
                # do nothing here
                pass

            nextCount.driven = "wire"

            return logic

        inc_comb.verilog_code =\
        """
        assign $nextCount = ($count + 1) % $n;
        """

        inc_comb.vhdl_code =\
        """
        $nextCount <= ($count + 1) mod $n;
        """

The converted code looks as follows in Verilog:

        module inc_comb (
            nextCount,
            count
        );

        output [7:0] nextCount;
        wire [7:0] nextCount;
        input [7:0] count;

        assign nextCount = (count + 1) % 256;

        endmodule

and as follows in VHDL:

        library IEEE;
        use IEEE.std_logic_1164.all;
        use IEEE.numeric_std.all;

        use work.pck_myhdl_06.all;

        entity inc_comb is
            port (
                nextCount: out unsigned(7 downto 0);
                count: in unsigned(7 downto 0)
            );
        end entity inc_comb;

        architecture MyHDL of inc_comb is

        begin

        nextCount <= (count + 1) mod 256;

        end architecture MyHDL;

In this example, conversion of the inc_comb function is bypassed and the user-defined code is inserted instead. The user-defined code is a Python template string that can refer to signals and parameters in the MyHDL context through $-based substitutions. During conversion, the appropriate hierarchical names and parameter values will be substituted.

The MyHDL code contains the following assignment:

        nextCount.driven = "wire"

This specifies that the nextCount signal is driven as a Verilog wire from this module.

For more info about user-defined code, see conv-custom.

  Reference

MyHDL is implemented as a Python package called myhdl. This chapter describes the objects that are exported by this package.

  Simulation

  The Simulation class

    class Simulation(arg[, arg ...])
        Class to construct a new simulation. Each argument should be a MyHDL instance. In MyHDL, an instance is recursively defined as being either a sequence of instances, or a MyHDL generator, or a Cosimulation object. See section MyHDL generators and trigger objects for the definition of MyHDL generators and their interaction with a Simulation object. See Section Co-simulation for the Cosimulation object. At most one Cosimulation object can be passed to a Simulation constructor.

A Simulation object has the following method:

    Simulation.run([duration])
        Run the simulation forever (by default) or for a specified duration.

    Simulation.quit()
        Quit the simulation after it has run for a specified duration. The method should be called (the simulation instance must be quit) before another simulation instance is created. The method is called by default when the simulation is run forever.

  Simulation support functions

    now()
        Returns the current simulation time.

    exception StopSimulation
        Base exception that is caught by the Simulation.run() method to stop a simulation.

  Waveform tracing

    traceSignals(func [, *args] [, **kwargs])
        Enables signal tracing to a VCD file for waveform viewing. func is a function that returns an instance. traceSignals calls func under its control and passes *args and **kwargs to the call. In this way, it finds the hierarchy and the signals to be traced.

        The return value is the same as would be returned by the call func(*args, **kwargs). The top-level instance name and the basename of the VCD output filename is func.func_name by default. If the VCD file exists already, it will be moved to a backup file by attaching a timestamp to it, before creating the new file.

        The traceSignals callable has the following attribute:

        name
            This attribute is used to overwrite the default top-level instance name and the basename of the VCD output filename.

        directory
            This attribute is used to set the directory to which VCD files are written. By default, the current working directory is used.

        filename
            This attribute is used to set the filename to which VCD files are written. By default, the name attribbute is used.

        timescale
            This attribute is used to set the timescale corresponding to unit steps, according to the VCD format. The assigned value should be a string. The default timescale is "1ns".

  Modeling

  The block decorator

    block()
        The block decorator enables a method-based API which is more consistent, simplifies implementation, and reduces the size of the myhdl namespace.

        The methods work on block instances, created by calling a function decorated with the block decorator:

            @block
            def myblock(<ports>):
            ...
            return <instances>

            inst = myblock(<port-associations>)
            # inst supports the methods of the block instance API

The API on a block instance looks as follows:

    <block_instance>.run_sim(duration=None)
        Run a simulation "forever" (default) or for a specified duration.

    <block_instance>.config_sim(backend='myhdl', trace=False)
        Optional simulation configuration:

        backend: Defaults to 'myhdl

        trace: Enable waveform tracing, default False.

    <block_instance>.quit_sim()
        Quit an active simulation. This is method is currently required because only a single simulation can be active.

    <block_instance>.convert(hdl='Verilog', **kwargs)
        Converts MyHDL code to a target HDL.

        hdl: 'VHDL' or 'Verilog'. Defaults to Verilog.

        Supported keyword arguments:

        path: Destination folder. Defaults to current working dir.

        name: Module and output file name. Defaults to self.mod.__name__.

        trace: Whether the testbench should dump all signal waveforms. Defaults to False.

        testbench: Verilog only. Specifies whether a testbench should be created. Defaults to True.

        timescale: timescale parameter. Defaults to '1ns/10ps'. Verilog only.

    <block_instance>.verify_convert()
        Verify conversion output, by comparing target HDL simulation log with MyHDL simulation log.

    <block_instance>.analyze_convert()
        Analyze conversion output by compilation with target HDL compiler.

  Signals

  The SignalType type

    class SignalType
        This type is the abstract base type of all signals. It is not used to construct signals, but it can be used to check whether an object is a signal.

  Regular signals

    class Signal([val=None] [, delay=0])
        This class is used to construct a new signal and to initialize its value to val. Optionally, a delay can be specified.

        A Signal object has the following attributes:

                posedge
                    Attribute that represents the positive edge of a signal, to be used in sensitivity lists.

                negedge
                    Attribute that represents the negative edge of a signal, to be used in sensitivity lists.

                next
                    Read-write attribute that represents the next value of the signal.

                val Read-only attribute that represents the current value of the signal.

                    This attribute is always available to access the current value; however in many practical case it will not be needed. Whenever there is no ambiguity, the Signal object's current value is used implicitly. In particular, all Python's standard numeric, bit-wise, logical and comparison operators are implemented on a Signal object by delegating to its current value. The exception is augmented assignment. These operators are not implemented as they would break the rule that the current value should be a read-only attribute. In addition, when a Signal object is assigned to the next attribute of another Signal object, its current value is assigned instead.

                min Read-only attribute that is the minimum value (inclusive) of a numeric signal, or None for no minimum.

                max Read-only attribute that is the maximum value (exclusive) of a numeric signal, or None for no maximum.

                driven
                    Writable attribute that can be used to indicate that the signal is supposed to be driven from the MyHDL code, and possibly how it should be declared in Verilog after conversion. The allowed values are 'reg', 'wire', True and False.

                    This attribute is useful when the converter cannot infer automatically whether and how a signal is driven. This occurs when the signal is driven from user-defined code. 'reg' and 'wire' are "true" values that permit finer control for the Verilog case.

                read
                    Writable boolean attribute that can be used to indicate that the signal is read.

                    This attribute is useful when the converter cannot infer automatically whether a signal is read. This occurs when the signal is read from user-defined code.

    A Signal object also has a call interface:

                __call__(left[, right=None])
                    This method returns a _SliceSignal shadow signal.

    class ResetSignal(val, active, isasync)
        This Signal subclass defines reset signals. val, active, and isasync are mandatory arguments. val is a boolean value that specifies the initial value, active is a boolean value that specifies the active level. isasync is a boolean value that specifies the reset style: asynchronous (True) or synchronous (False).

        This class should be used in conjunction with the always_seq decorator.

  Shadow signals

    class _SliceSignal(sig, left[, right=None])
        This class implements read-only structural slicing and indexing. It creates a new shadow signal of the slice or index of the parent signal sig. If the right parameter is omitted, 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 constructor is not intended to be used explicitly. Instead, use the call interface of a regular signal.The following calls are equivalent:

            sl = _SliceSignal(sig, left, right)

            sl = sig(left, right)

    class ConcatSignal(*args)
        This class creates a new shadow signal of the concatenation of its arguments.

        You can pass an arbitrary number of arguments to the constructor. The arguments should be bit-oriented with a defined number of bits. The following argument types are supported: intbv objects with a defined bit width, bool objects, signals of the previous objects, and bit strings.

        The new signal follows the value changes of the signal arguments. The non-signal arguments are used to define constant values in the concatenation.

    class TristateSignal(val)
        This class is used to construct a new tristate signal. The underlying type is specified by the val parameter. It is a Signal subclass and has the usual attributes, with one exception: it doesn't support the next attribute. Consequently, direct signal assignment to a tristate signal is not supported. The initial value is the tristate value None. The current value of a tristate is determined by resolving the values from its drivers. When exactly one driver value is different from None, that is the resolved value; otherwise it is None. When more than one driver value is different from None, a contention warning is issued.

        This class has the following method:

        driver()
            Returns a new driver to the tristate signal. It is initialized to None. A driver object is an instance of a special SignalType subclass. In particular, its next attribute can be used to assign a new value to it.

  MyHDL generators and trigger objects

MyHDL generators are standard Python generators with specialized yield statements. In hardware description languages, the equivalent statements are called sensitivity lists. The general format of yield statements in in MyHDL generators is:

yield clause [, clause ...]

When a generator executes a yield statement, its execution is suspended at that point. At the same time, each clause is a trigger object which defines the condition upon which the generator should be resumed. However, per invocation of a yield statement, the generator resumes exactly once, regardless of the number of clauses. This happens on the first trigger that occurs.

In this section, the trigger objects and their functionality will be described.

Some MyHDL objects that are described elsewhere can directly be used as trigger objects. In particular, a Signal can be used as a trigger object. Whenever a signal changes value, the generator resumes. Likewise, the objects referred to by the signal attributes posedge and negedge are trigger objects. The generator resumes on the occurrence of a positive or a negative edge on the signal, respectively. An edge occurs when there is a change from false to true (positive) or vice versa (negative). For the full description of the Signal class and its attributes, see section Signals.

Furthermore, MyHDL generators can be used as clauses in yield statements. Such a generator is forked, and starts operating immediately, while the original generator waits for it to complete. The original generator resumes when the forked generator returns.

In addition, the following functions return trigger objects:

    delay(t)
        Return a trigger object that specifies that the generator should resume after a delay t.

    join(arg[, arg ...])
        Join a number of trigger objects together and return a joined trigger object. The effect is that the joined trigger object will trigger when all of its arguments have triggered.

Finally, as a special case, the Python None object can be present in a yield statement. It is the do-nothing trigger object. The generator immediately resumes, as if no yield statement were present. This can be useful if the yield statement also has generator clauses: those generators are forked, while the original generator resumes immediately.

  Decorator functions to create generators

MyHDL defines a number of decorator functions, that make it easier to create generators from local generator functions.

    instance()
        The instance decorator is the most general decorator. It automatically creates a generator by calling the decorated generator function.

        It is used as follows:

            def top(...):
                ...
                @instance
                def inst():
                    <generator body>
                ...
                return inst, ...

    This is equivalent to:

            def top(...):
                ...
                def _gen_func():
                    <generator body>
                ...
                inst = _gen_func()
                ...
                return inst, ...

    always(arg[, *args])
        The always decorator is a specialized decorator that targets a widely used coding pattern. It is used as follows:

            def top(...):
                ...
                @always(event1, event2, ...)
                def inst()
                    <body>
                ...
                return inst, ...

    This is equivalent to the following:

            def top(...):
                ...
                def _func():
                    <body>

                def _gen_func()
                    while True:
                        yield event1, event2, ...
                        _func()
                ...
                inst = _gen_func()
                ...
                return inst, ...

    The argument list of the decorator corresponds to the sensitivity list. Only signals, edge specifiers, or delay objects are allowed. The decorated function should be a classic function.

    always_comb()
        The always_comb decorator is used to describe combinatorial logic.

            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. The decorated function should be a classic function.

    always_seq(edge, reset)
        The always_seq decorator is used to describe sequential (clocked) logic.

        The edge parameter should be a clock edge (clock.posedge or clock.negedge). The reset parameter should a ResetSignal object.

  MyHDL data types

MyHDL defines a number of data types that are useful for hardware description.

  The intbv class

    class intbv([val=0] [, min=None] [, max=None])
        This class represents int-like objects with some additional features that make it suitable for hardware design.

        The val argument can be an int, a long, an intbv or a bit string (a string with only '0's or '1's). For a bit string argument, the value is calculated as in int(bitstring, 2). The optional min and max arguments can be used to specify the minimum and maximum value of the intbv object. As in standard Python practice for ranges, the minimum value is inclusive and the maximum value is exclusive.

        The minimum and maximum values of an intbv object are available as attributes:

        min Read-only attribute that is the minimum value (inclusive) of an intbv, or None for no minimum.

        max Read-only attribute that is the maximum value (exclusive) of an intbv, or None for no maximum.

        signed()
            Interprets the msb bit as as sign bit and extends it into the higher-order bits of the underlying object value. The msb bit is the highest-order bit within the object's bit width.

        Return type
            integer

Unlike int objects, intbv objects are mutable; this is also the reason for their existence. Mutability is needed to support assignment to indexes and slices, as is common in hardware design. For the same reason, intbv is not a subclass from int, even though int provides most of the desired functionality. (It is not possible to derive a mutable subtype from an immutable base type.)

An intbv object supports the same comparison, numeric, bitwise, logical, and conversion operations as int objects. See http://www.python.org/doc/current/lib/typesnumeric.html for more information on such operations. In all binary operations, intbv objects can work together with int objects. For mixed-type numeric operations, the result type is an int or a long. For mixed-type bitwise operations, the result type is an intbv.

In addition, intbv supports a number of sequence operators. In particular, the len function returns the object's bit width. Furthermore, intbv objects support indexing and slicing operations:
Operation | Result | Notes
bv[i] | item i of bv | (1)
bv[i] = x | item i of bv is replaced by x | (1)
bv[i:j] | slice of bv from i downto j | (2)(3)
bv[i:j] = t | slice of bv from i downto j is replaced by t | (2)(4)

    1.  Indexing follows the most common hardware design conventions: the lsb bit is the rightmost bit, and it has index 0. This has the following desirable property: if the intbv value is decomposed as a sum of powers of 2, the bit with index i corresponds to the term 2**i.

    2.  In contrast to standard Python sequencing conventions, slicing range are downward. This is a consequence of the indexing convention, combined with the common convention that the most significant digits of a number are the leftmost ones. The Python convention of half-open ranges is followed: the bit with the highest index is not included. However, it is the leftmost bit in this case. As in standard Python, this takes care of one-off issues in many practical cases: in particular, bv[i:] returns i bits; bv[i:j] has i-j bits. When the low index j is omitted, it defaults to 0. When the high index i is omitted, it means "all" higher order bits.

    3.  The object returned from a slicing access operation is always a positive intbv; higher order bits are implicitly assumed to be zero. The bit width is implicitly stored in the return object, so that it can be used in concatenations and as an iterator. In addition, for a bit width w, the min and max attributes are implicitly set to 0 and 2**w, respectively.

    4.  When setting a slice to a value, it is checked whether the slice is wide enough.

In addition, an intbv object supports the iterator protocol. This makes it possible to iterate over all its bits, from the high index to index 0. This is only possible for intbv objects with a defined bit width.

  The modbv class

    class modbv([val=0] [, min=None] [, max=None])
        The modbv class implements modular bit vector types.

        It is implemented as a subclass of intbv and supports the same parameters and operators. The difference is in the handling of the min and max boundaries. Instead of throwing an exception when those constraints are exceeded, the value of modbv objects wraps around according to the following formula:

            val = (val - min) % (max - min) + min

    This formula is a generalization of modulo wrap-around behavior that is often useful when describing hardware system behavior.

  The enum factory function

    enum(arg [, arg ...] [, encoding='binary'])
        Returns an enumeration type.

        The arguments should be string literals that represent the desired names of the enumeration type attributes. The returned type should be assigned to a type name. For example:

            t_EnumType = enum('ATTR_NAME_1', 'ATTR_NAME_2', ...)

    The enumeration type identifiers are available as attributes of the type name, for example: t_EnumType.ATTR_NAME_1

    The optional keyword argument encoding specifies the encoding scheme used in Verilog output. The available encodings are 'binary', 'one_hot', and 'one_cold'.

  Modeling support functions

MyHDL defines a number of additional support functions that are useful for hardware description.

  bin

    bin(num[, width])
        Returns a bit string representation. If the optional width is provided, and if it is larger than the width of the default representation, the bit string is padded with the sign bit.

        This function complements the standard Python conversion functions hex and oct. A binary string representation is often useful in hardware design.

        Return type
            string

  concat

    concat(base[, arg ...])
        Returns an intbv object formed by concatenating the arguments.

        The following argument types are supported: intbv objects with a defined bit width, bool objects, signals of the previous objects, and bit strings. All these objects have a defined bit width.

        The first argument base is special as it does not need to have a defined bit width. In addition to the previously mentioned objects, unsized intbv, int and long objects are supported, as well as signals of such objects.

        Return type
            intbv

  downrange

    downrange(high[, low=0])
        Generates a downward range list of integers.

        This function is modeled after the standard range function, but works in the downward direction. The returned interval is half-open, with the high index not included. low is optional and defaults to zero. This function is especially useful in conjunction with the intbv class, that also works with downward indexing.

  instances

    instances()
        Looks up all MyHDL instances in the local name space and returns them in a list.

        Return type
            list

  Co-simulation

  MyHDL

    class Cosimulation(exe, **kwargs)
        Class to construct a new Cosimulation object.

        The exe argument is the command to execute an HDL simulation, which can be either a string of the entire command line or a list of strings. In the latter case, the first element is the executable, and subsequent elements are program arguments. Providing a list of arguments allows Python to correctly handle spaces or other characters in program arguments.

        The kwargs keyword arguments provide a named association between signals (regs & nets) in the HDL simulator and signals in the MyHDL simulator. Each keyword should be a name listed in a $to_myhdl or $from_myhdl call in the HDL code. Each argument should be a Signal declared in the MyHDL code.

  Verilog

    $to_myhdl(arg, [, arg ...])
        Task that defines which signals (regs & nets) should be read by the MyHDL simulator. This task should be called at the start of the simulation.

    $from_myhdl(arg, [, arg ...])
        Task that defines which signals should be driven by the MyHDL simulator. In Verilog, only regs can be specified. This task should be called at the start of the simulation.

  Conversion to Verilog and VHDL

  Conversion

    toVerilog(func [, *args] [, **kwargs])
            Converts a MyHDL design instance to equivalent Verilog code, and also generates a test bench to verify it. func is a function that returns an instance. toVerilog calls func under its control and passes *args and **kwargs to the call.

            The return value is the same as would be returned by the call func(*args, **kwargs). It should be assigned to an instance name.

            The top-level instance name and the basename of the Verilog output filename is func.func_name by default.

            For more information about the restrictions on convertible MyHDL code, see section conv-subset in Chapter conv.

    toVerilog has the following attribute:

        name
            This attribute is used to overwrite the default top-level instance name and the basename of the Verilog output filename.

        directory
            This attribute is used to set the directory to which converted verilog files are written. By default, the current working directory is used.

        timescale
            This attribute is used to set the timescale in Verilog format. The assigned value should be a string. The default timescale is "1ns/10ps".

    toVHDL(func[, *args][, **kwargs])
        Converts a MyHDL design instance to equivalent VHDL code. func is a function that returns an instance. toVHDL calls func under its control and passes *args and **kwargs to the call.

        The return value is the same as would be returned by the call func(*args, **kwargs). It can be assigned to an instance name. The top-level instance name and the basename of the Verilog output filename is func.func_name by default.

        toVHDL has the following attributes:

        name
            This attribute is used to overwrite the default top-level instance name and the basename of the VHDL output.

        directory
            This attribute is used to set the directory to which converted VHDL files are written. By default, the current working directory is used.

        component_declarations
            This attribute can be used to add component declarations to the VHDL output. When a string is assigned to it, it will be copied to the appropriate place in the output file.

        library
            This attribute can be used to set the library in the VHDL output file. The assigned value should be a string. The default library is work.

        std_logic_ports
            This boolean attribute can be used to have only std_logic type ports on the top-level interface (when True) instead of the default signed/unsigned types (when False, the default).

  User-defined Verilog and VHDL code

User-defined code can be inserted in the Verilog or VHDL output through the use of function attributes. Suppose a function <func> defines a hardware module. User-defined code can be specified for the function with the following function attributes:

    <func>.vhdl_code
        A template string for user-defined code in the VHDL output.

    <func>.verilog_code
        A template string for user-defined code in the Verilog output.

When such a function attribute is defined, the normal conversion process is bypassed and the user-defined code is inserted instead. The template strings should be suitable for the standard string.Template constructor. They can contain interpolation variables (indicated by a $ prefix) for all signals in the context. Note that the function attribute can be defined anywhere where <func> is visible, either outside or inside the function itself.

These function attributes cannot be used with generator functions or decorated local functions, as these are not elaborated before simulation or conversion. In other words, they can only be used with functions that define structure.

  Conversion output verification

MyHDL provides an interface to verify converted designs. This is used extensively in the package itself to verify the conversion functionality. This capability is exported by the package so that users can use it also.

  Verification interface

All functions related to conversion verification are implemented in the myhdl.conversion package.

    verify(func[, *args][, **kwargs])
        Used like toVHDL and toVerilog. It converts MyHDL code, simulates both the MyHDL code and the HDL code and reports any differences. The default HDL simulator is GHDL.

        This function has the following attribute:

        simulator
            Used to set the name of the HDL simulator. "GHDL" is the default.

    analyze(func[, *args][, **kwargs])
        Used like toVHDL and toVerilog. It converts MyHDL code, and analyzes the resulting HDL. Used to verify whether the HDL output is syntactically correct.

        This function has the following attribute:

        simulator
            Used to set the name of the HDL simulator used to analyze the code. "GHDL" is the default.

  HDL simulator registration

To use a HDL simulator to verify conversions, it needs to be registered first. This is needed once per simulator.

A number of HDL simulators are preregistered in the MyHDL distribution, as follows:
Identifier | Simulator
"GHDL" | The GHDL VHDL simulator
"vsim" | The ModelSim VHDL simulator
"icarus" | The Icarus Verilog simulator
"cver" | The cver Verilog simulator
"vlog" | The Modelsim VHDL simulator

Of course, a simulator has to be installed before it can be used.

If another simulator is required, it has to be registered by the user. This is done with the function registerSimulation that lives in the module myhdl.conversion._verify. The same module also has the registrations for the predefined simulators.

The verification functions work by comparing the HDL simulator output with the MyHDL simulator output. Therefore, they have to deal with the specific details of each HDL simulator output, which may be somewhat tricky. This is reflected in the interface of the registerSimulation function. As registration is rarely needed, this interface is not further described here.

Please refer to the source code in myhdl.conversion._verify to learn how registration works. If you need help, please contact the MyHDL community.

WHAT'S NEW IN MYHDL 0.11

  The isasync arguments

The async argument has been replaced with isasync to avoid the Python 3.7 keyword conflict.

PYTHON 3 SUPPORT

MyHDL supports Python 3.4 and above. At the moment, core functions, cosimulation and Verilog conversion work perfectly. However, there are a few unresolved VHDL conversion bugs.

All users are encouraged to try out their existing projects and tests with Python 3 and submit bug reports if anything goes wrong.

WHAT'S NEW IN MYHDL 0.10

  The block decorator

  Rationale

The historical approach for hierarchy extraction in MyHDL suffers from significant issues. This results in complex code, a number of non-intuitive API concepts, and difficulties for future development.

In this release, a new block decorator is introduced to address these issues.

For an in-depth discussion, see mep-114.

  API

    block() :noindex:
        The block decorator enables a method-based API which is more consistent, simplifies implementation, and reduces the size of the myhdl namespace.

        The methods work on block instances, created by calling a function decorated with the block decorator:

            @block
            def myblock(<ports>):
            ...
            return <instances>

            inst = myblock(<port-associations>)
            # inst supports the methods of the block instance API

The API on a block instance looks as follows:

    <block_instance>.run_sim(duration=None)
        Run a simulation "forever" (default) or for a specified duration.

    <block_instance>.config_sim(backend='myhdl', trace=False)
        Optional simulation configuration:

        backend: Defaults to 'myhdl

        trace: Enable waveform tracing, default False.

    <block_instance>.quit_sim()
        Quit an active simulation. This is method is currently required because only a single simulation can be active.

    <block_instance>.convert(hdl='Verilog', **kwargs)
        Converts MyHDL code to a target HDL.

        hdl: 'VHDL' or 'Verilog'. Defaults to Verilog.

        Supported keyword arguments:

        path: Destination folder. Defaults to current working dir.

        name: Module and output file name. Defaults to self.mod.__name__.

        trace: Whether the testbench should dump all signal waveforms. Defaults to False.

        testbench: Verilog only. Specifies whether a testbench should be created. Defaults to True.

        timescale: timescale parameter. Defaults to '1ns/10ps'. Verilog only.

    <block_instance>.verify_convert()
        Verify conversion output, by comparing target HDL simulation log with MyHDL simulation log.

    <block_instance>.analyze_convert()
        Analyze conversion output by compilation with target HDL compiler.

  Backwards compatibility issues

In the 0.10 release, the old API still available next to the new API based on the block decorator.

It is likely that the old deprecated API will be removed in a future release, resulting in backwards incompatibility for legacy code. Therefore, users are encouraged to start using the new API in their development methodology.

WHAT'S NEW IN MYHDL 0.9

  Python 3 support

Experimental Python 3 support has been added to MyHDL 0.9. This was a major effort to modernize the code. As a result, Python 2 and 3 are supported from a single codebase.

See /python3 for more info.

  Interfaces (Conversion of attribute accesses)

  Rationale

Complex designs often have many signals that are passed to different levels of hierarchy. Typically, many signals logically belong together. This can be modelled by an interface: an object that has a number of Signal objects as its attributes. Grouping signals into an interface simplifies the code, improves efficiency, and reduces errors.

The following is an example of an interface definition:

        class Complex:
            def __init__(self, min=-2, max=2):
                self.real = Signal(intbv(0, min=min, max=max))
                self.imag = Signal(intbv(0, min=min, max=max))

Although previous versions supported interfaces for modeling, they were not convertible. MyHDL 0.9 now supports conversion of designs that use interfaces.

The following is an example using the above Complex interface definition:

        a,b = Complex(-8,8), Complex(-8,8)
        c = Complex(-128,128)

        def complex_multiply(clock, reset, a, b, c):

            @always_seq(clock.posedge, reset=reset)
            def cmult():
                c.real.next = (a.real*b.real) - (a.imag*b.imag)
                c.imag.next = (a.real*b.imag) + (a.imag*b.real)

            return cmult

  Solution

The proposed solution is to create unique names for attributes which are used by MyHDL generators. The converter will create a unique name by using the name of the parent and the name of the attribute along with the name of the MyHDL module instance. The converter will essentially replace the "." with an "_" for each interface element. In essence, interfaces are supported using hierarchical name expansion and name mangling.

Note that the MyHDL converter supports interfaces, even though the target HDLs do not. This is another great example where the converter supports a high-level feature that is not available in the target HDLs.

  See also

For additional information see the original proposal mep-107.

  Other noteworthy improvements

  ConcatSignal interface

The interface of ConcatSignal was enhanced. In addition to signals, you can now also use constant values in the concatenation.

  std_logic type ports

toVHDL has a new attribute std_logic_ports. When set, only std_logic type ports are used in the interface of the top-level VHDL module.

  Development flow

The MyHDL development flow has been modernized by moving to git and github for version control. In addition, travis has set up so that all pull requests are tested automatically, enabling continuous intergration.

  Acknowledgments

The Python 3 support effort was coordinated by Keerthan Jaic, who also implemented most of if. Convertible interfaces were championed by Chris Felton, and implemented by Keerthan Jaic.

MyHDL development is a collaborative effort, as can be seen on github. Thanks to all who contributed with suggestions, issues and pull requests.

WHAT'S NEW IN MYHDL 0.8

  Modular bit vector types

  Rationale

In hardware modeling, there is often a need for the elegant modeling of wrap-around behavior. intbv instances don't provide this automatically, because they assert that any assigned value is within the bound constraints. Therefore, one has currently has to use other language features for wrap-around modeling.

Often, this is straightforward. For example, the wrap-around condition for a counter is often decoded explicitly, as it is needed for other purposes. Also, the modulo operator provides an elegant one-liner in most scenarios.

However, in some important cases the current solution is not satisfactory. For example, we would like to describe a free running counter using a variable and augmented assignment as follows:

        count += 1

This is not possible with the intbv type, as we cannot add the modulo behavior to this description. A similar problem exist of for a left shift as follows:

        shifter <<= 4

These operations can only supported directly with a new type. For these reasons, it was felt that this would be a useful addition to MyHDL.

  Solution

The proposed solution is to borrow the idea behind Ada modular types. These are natural integer types with wrap-around behaviour.

The wrap-around behavior of modular types is based on the sound mathematical concept of modulo arithmetic. Therefore, the modulus is not limited to powers of 2.

Ada's modular type is called mod. In MyHDL, we want also want to give it "bit-vector" support, like intbv. Therefore, proposed MyHDL type is called modbv.

  Implementation

modbv is implemented as a subclass of intbv. The two classes have an identical interface and work together in a straightforward way for arithmetic operations.

The only difference is how the bounds are handled: out-of-bound values result in an error with intbv, and in wrap-around with modbv. The Wrap-around behavior would be defined as follows, with val denoting the current value and min/max the bounds:

        val = (val - min) % (max - min) + min

  Interface

    class modbv([val=0] [, min=None] [, max=None])
        The modbv class implements modular bit vector types.

        It is implemented as a subclass of intbv and supports the same parameters and operators. The difference is in the handling of the min and max boundaries. Instead of throwing an exception when those constraints are exceeded, the value of modbv objects wraps around according to the following formula:

            val = (val - min) % (max - min) + min

    This formula is a generalization of modulo wrap-around behavior that is often useful when describing hardware system behavior.

  Conversion

Full-range modbv objects are those where the max bound is a power of 2, and the min bound is 0 or the negative of the max bound. For these objects, conversion worked out-of-the-box because this corresponds to the target types in Verilog and VHDL.

Currently, conversion is restricted to full-range modbv objects. It may be possible to support conversion of the modulo behavior of more general cases, but this requires more sophistication in the converter. This may be considered in the future.

  See also

For a more in-depth discussion, see mep-106.

  always_seq decorator

  Rationale

In classical synthesizable RTL coding, the reset behavior is described explicitly. A typical template is as follows:

        @always(clock.posedge, reset.negedge)
        def seq():
            if reset == 0:
                <reset code>
            else:
                <functional code>

The reset behavior is described using a the top-level if-else structure with a number of assignments under the if. A significant piece of code at a prominent location is therefore dedicated to non-functional behavior.

Reset behavior coding is error-prone. For a proper gate-level implementation, most if not all registers should typically be reset. However, it is easy to forget some reset assignments. Such bugs are not necessarily easily detected during RTL or gate-level simulations.

In the template, the edge that asserts reset is in the sensitivity list. It is easy to forget this, and in that case the reset will not behave asynchronously as intended but synchronously. Note also that it is somewhat strange to specify an edge sensitivity when describing asynchronous behavior.

  Solution

The proposed solution is to infer the reset structure automatically. The main idea is to use the initial values of signals as the specification of reset values. This is possible in MyHDL, because all objects are constructed with an initial value. The assumption is that the initial value also specifies the desired reset value.

The solution is implemented with two new MyHDL constructs. The first one is a new decorator called always_seq. Using this decorator, code with identical behavior as in the previous section can be described as follows:

        @always_seq(clock.posedge, reset=reset)
            <functional code>

The always_seq decorator takes two arguments: a clock edge and a reset signal. It inspects the code to find the registers, and uses the initial values to construct the reset behavior.

The second construct is a specialized signal subclass called ResetSignal. It is used as follows:

        reset = ResetSignal(1, active=0, async=True)

The ResetSignal constructor has three arguments: the initial value as usual, an active argument with the active level, and an async argument that specifies whether the reset style is asynchronous or synchronous.

The proposed solution has some very desirable features.

Explicit reset behavior coding is no longer necessary. Code reviewers are thus no longer distracted by non-functional code. It is sufficient to check the initial values to verify whether the reset value is correctly specified. Moreover, one indentation level is saved for functional coding.

Even more importantly, the reset structure is correct by construction. All registers are automatically included in the reset behavior, and the sensitivity list is automatically correct according to the reset style.

Traditionally, the reset behavior is spread out over all sequential processes. Therefore, it has to be debugged by investigating all those processes. Even worse, if a change in style or active level is required, all processes are affected. In contrast, with the proposed technique all reset features are specified at single location in the ResetSignal constructor. Changes are trivial. For example, to change to an active high synchronous reset one merely has to change the constructor as follows:

        reset = ResetSignal(1, active=1, async=False)

Occasionally, it is useful to have registers without reset at all. The proposed technique is also useful in that case. In particular, the always_seq decorator accepts None as the reset argument:

        @always_seq(clock.posedge, reset=None)

A reviewer will have no doubt what the intention is. In contrast, in the case of a traditional always block, the reviewer may think that the designer has delayed the detailed reset coding for later and then forgotten about it.

  Interface

    always_seq(edge, reset)
        The always_seq decorator is used to describe sequential (clocked) logic.

        The edge parameter should be a clock edge (clock.posedge or clock.negedge). The reset parameter should a ResetSignal object.

    class ResetSignal(val, active, async)
            This Signal subclass defines reset signals. val, active, and async are mandatory arguments. val is a boolean value that specifies the initial value, active is a boolean value that specifies the active level. async is a boolean value that specifies the reset style: asynchronous (True) or asynchronous (False).

  Conversion

As modeling the reset behavior is a typical task in synthesizable RTL coding, the proposed technique is fully convertible to Verilog and VHDL.

  Limitations

  All registers in a process are reset

All registers in a process are automatically included in the reset behavior. If it is the intention that some registers should not be reset, those registers and the corresponding code should be factored out in a separate process.

Actually, this is not really a limitation but a feature. If some registers in a process are reset and others not, a synthesis tool may generate undesirable feedback loops that are active during the reset condition. This is not good practice and probably not the intention.

  Register inferencing from variables is not supported

An important limitation is that the proposed technique is limited to registers inferred from signals. Registers inferred from variables are not supported, because such state variables cannot be described in classic functions (in particular the functions required by MyHDL decorators such as always_seq and always).

In fact, the reason is a Python2 limitation. Currently, to infer registers from variables, one has to use the instance decorator and declare the state variables outside an infinite while True loop.

In Python3, this limitation can be lifted with the introduction of the nonlocal declaration. This will make it possible for functions to modify variables in the enclosing scope. It should be possible to adapt the always_seq and always decorators to support such variables.

  See also

For a more in-depth discussion, see mep-109.

  Other improvements

  Conversion of top-level class methods

Often it is desirable to embed an HDL module description in a class. Previous versions would only convert a class method if it was not the top-level. This release adds the conversion of top-level class methods.

  Example

        class DFilter(object):
            def __init__(self,delay_length=3,fs=1):
                <init code>
            def nulls(self):
                <support method code>
            def m_top(self,clock,reset,x,y):
                <myhdl module code>

        clock = Signal(bool(0))
        reset = ResetSignal(0,active=0,async=True)
        x = Signal(intbv(0, min=-8, max=8))
        y = Signal(intbv(0, min=-64, max=64))

        filt = DFilter()
        toVerilog(filt.m_top,clock,reset,x,y)
        toVHDL(filt.m_top,clock,reset,x,y)

  See also

For a more in-depth discussion, see mep-108.

  Tracing lists of signals

Tracing lists of signals is now supported. Contributed by Frederik Teichtert, http://teichert-ing.de . The following shows how the list of signals are displayed in a waveform viewer

        delay_taps = [Signal(intbv(0,min=-8,max=8)) for ii in range(3)]
[image]

  library attribute for toVHDL

toVHDL now has a library function that can be used to set the library name in the VHDL output. The assigned value should be a string. The default library is "work".

  timescale attribute for traceSignals

traceSignals now has a timescale attribute that can be used to set the timescale in the VCD output. The assigned value should be a string. The default timescale is "1ns".

  Acknowledgments

Several people have contributed to MyHDL 0.8 by giving feedback, making suggestions, fixing bugs and implementing features. In particular, I would like to thank Christopher Felton and Frederik Teichert.

I would also like to thank Easics for the opportunity to use MyHDL in industrial projects.

WHAT'S NEW IN MYHDL 0.7

  Conversion to VHDL/Verilog rewritten with the ast module

The most important code change is a change that hopefully no-one will notice :-). The conversion code is now based on the ast package instead of the compiler package. Since Python 2.6, the compiler package is deprecated and replaced by the new ast package in the standard library. In Python 3, the compiler package is no longer available.

This was a considerable effort, spent on re-implementing existing behavior instead of on new interesting features. This was unfortunate, but it had to be done with priority. Now the conversion code is ready for the future.

  Shadow signals

  Background

Compared to HDLs such as VHDL and Verilog, MyHDL signals are less flexible for structural modeling. 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. To solve these issues, a new concept was introduced: shadow signals.

The whole reasoning behind shadow signals is explained in more detail in mep-105.

  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 converter output.

Currently, three kinds of shadow signals have been implemented. The original inspiration for shadow signals was to have solution for structural slicing. This is the purpose of the _SliceSignal subclass. The opposite is also useful: a signal that shadows a composition of other signals. This is the purpose of the ConcatSignal subclass.

As often is the case, the idea of shadow signals had some useful side effects. In particular, I realized that the TristateSignal proposed in mep-103 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.

  Concrete shadow signal subclasses

    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 omitted, 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:

    Signal.__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 exactly 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.

    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.

    class TristateSignal(val)
            This class is used to construct a new tristate signal. The underlying type is specified by the val parameter. It is a Signal subclass and has the usual attributes, with one exception: it doesn't support the next attribute. Consequently, direct signal assignment to a tristate signal is not supported. The initial value is the tristate value None. The current value of a tristate is determined by resolving the values from its drivers. When exactly one driver value is different from None, that is the resolved value; otherwise it is None. When more than one driver value is different from None, a contention warning is issued.

This class has the following method:

    driver()
            Returns a new driver to the tristate signal. It is initialized to None. A driver object is an instance of a special SignalType subclass. In particular, its next attribute can be used to assign a new value to it.

  Example

A typical application of shadow signals is conversion of list of signals to bit vectors and vice versa.

For example, suppose we have a system with N requesters that need arbitration. Each requester has a request output and a grant input. To connect them in the system, we can use list of signals. For example, a list of request signals can be constructed as follows:

        request_list = [Signal(bool()) for i in range(M)]

Suppose that an arbiter module is available that is instantiated as follows:

        arb = arbiter(grant_vector, request_vector, clock, reset)

The request_vector input is a bit vector that can have any of its bits asserted. The grant_vector is an output bit vector with just a single bit asserted, or none. Such a module is typically based on bit vectors because they are easy to process in RTL code. In MyHDL, a bit vector is modeled using the intbv type.

We need a way to "connect" the list of signals to the bit vector and vice versa. Of course, we can do this with explicit code, but shadow signals can do this automatically. For example, we can construct a request_vector as a ConcatSignal object:

        request_vector = ConcatSignal(*reversed(request_list)

Note that we reverse the list first. This is done because the index range of lists is the inverse of the range of intbv bit vectors. By reversing, the indices correspond to the same bit.

The inverse problem exist for the grant_vector. It would be defined as follows:

        grant_vector = Signal(intbv(0)[M:])

To construct a list of signals that are connected automatically to the bit vector, we can use the Signal call interface to construct _SliceSignal objects:

        grant_list = [grant_vector(i) for i in range(M)]

Note the round brackets used for this type of slicing. Also, it may not be necessary to construct this list explicitly. You can simply use grant_vector(i) in an instantiation.

To decide when to use normal or shadow signals, consider the data flow. Use normal signals to connect to outputs. Use shadow signals to transform these signals so that they can be used as inputs.

  Using Signal and intbv objects as indices

Previously, it was necessary convert Signal and intbv objects explicitly to int when using them as indices for indexing and slicing. This conversion is no longer required; the objects can be used directly. The corresponding classes now have an __index__ method that takes care of the type conversion automatically. This feature is fully supported by the VHDL/Verilog converter.

  New configuration attributes for conversion file headers

New configuration attributes are available to control the file headers of converted output files.

    toVerilog.no_myhdl_header
        Specifies that MyHDL conversion to Verilog should not generate a default header. Default value is False.

    toVHDL.no_myhdl_header
        Specifies that MyHDL conversion to VHDL should not generate a default header. Default value is False.

    toVerilog.header
        Specifies an additional custom header for Verilog output.

    toVHDL.header
        Specifies an additional custom header for VHDL output.

The value for the custom headers should be a string that is suitable for the standard string.Template constructor. A number of variables (indicated by a $ prefix) are available for string interpolation. For example, the standard header is defined as follows:

        myhdl_header = """\
        -- File: $filename
        -- Generated by MyHDL $version
        -- Date: $date
        """

The same interpolation variables are available in custom headers.

  Conversion propagates docstrings

The converter now propagates comments under the form of Python docstrings.

Docstrings are typically used in Python to document certain objects in a standard way. Such "official" docstrings are put into the converted output at appropriate locations. The converter supports official docstrings for the top level module and for generators.

Within generators, "nonofficial" docstrings are propagated also. These are strings (triple quoted by convention) that can occur anywhere between statements.

Regular Python comments are ignored by the Python parser, and they are not present in the parse tree. Therefore, these are not propagated. With docstrings, you have an elegant way to specify which comments should be propagated and which not.

  New method to specify user-defined code

The current way to specify user-defined code for conversion is through the __vhdl__ and __verilog__ hooks. This method has a number of disadvantages.

First, the use of "magic" variables (whose names start and end with double underscores) was a bad choice. According to Python conventions, such variables should be reserved for the Python language itself. Moreover, when new hooks would become desirable, we would have to specify additional magic variables.

A second problem that standard Python strings were used to define the user-defined output. These strings can contain the signal names from the context for interpolation. Typically, these are multiple-line strings that may be quite lengthy. When something goes wrong with the string interpolation, the error messages may be quite cryptic as the line and column information is not present.

For these reasons, a new way to specify user-defined code has been implemented that avoids these problems.

The proper way to specify meta-information of a function is by using function attributes. Suppose a function <func> defines a hardware module. We can now specify user-defined code for it with the following function attributes:

    <func>.vhdl_code
        A template string for user-defined code in the VHDL output.

    <func>.verilog_code
        A template string for user-defined code in the Verilog output.

When such a function attribute is defined, the normal conversion process is bypassed and the user-defined code is inserted instead. The template strings should be suitable for the standard string.Template constructor. They can contain interpolation variables (indicated by a $ prefix) for all signals in the context. Note that the function attribute can be defined anywhere where <func> is visible, either outside or inside the function itself.

The old method for user-defined code is still available but is deprecated and will be unsupported in the future.

  More powerful mapping to case statements

The converter has become more powerful to map if-then-else structures to case statements in VHDL and Verilog. Previously, only if-then-else structures testing enumerated types were considered. Now, integer tests are considered also.

  Small changes

  SignalType as the base class of Signals

Signal has become a function instead of a class. It returns different Signal subtypes depending on parameters. This implies that you cannot use Signal for type checking.

The base type of all Signals is now SignalType. This type can be used to check whether an object is a Signal instance.

  Default value of intbv objects

The default initial value of an intbv object has been changed from None to 0. Though this is backward-incompatible, the None value never has been put to good use, so this is most likely not an issue.

  Combinatorial always blocks use blocking assignments

The converter now uses blocking assignments for combinatorial always blocks in Verilog. This is in line with generally accepted Verilog coding conventions.

  No synthesis pragmas in Verilog output

The converter no longer outputs the synthesis pragmas full_case and parallel_case. These pragmas do more harm than good as they can cause simulation-synthesis mismatches. Synthesis tools should be able to infer the appropriate optimizations from the source code directly.

  Python version

MyHDL 0.7 requires Python 2.6, mainly because of its dependency on the new ast package.

  Acknowledgments

Several people have contributed to MyHDL 0.7 by giving feedback, making suggestions, fixing bugs and implementing features. In particular, I would like to thank Benoit Allard, Günter Dannoritzer, Tom Dillon, Knut Eldhuset, Angel Ezquerra, Christopher Felton, and Jian LUO.

Thanks to Francesco Balau for packaging MyHDL for Ubuntu.

I would also like to thank Easics for the opportunity to use MyHDL in industrial projects.

WHAT'S NEW IN MYHDL 0.6

  Conversion to VHDL

  Rationale

Since the MyHDL to Verilog conversion has been developed, a path to implementation from MyHDL is available. Given the widespread support for Verilog, it could thus be argued that there was no real need for a converter to VHDL.

However, it turns out that VHDL is still very much alive and will remain so for the foreseeable future. This is especially true for the FPGA market, which is especially interesting for MyHDL. It seems much more dynamic than the ASIC market. Moreover, because of the nature of FPGA's, FPGA designers may be more willing to try out new ideas.

To convince designers to use a new tool, it should integrate with their current design flow. That is why the MyHDL to VHDL converter is needed. It should lower the threshold for VHDL designers to start using MyHDL.

  Advantages

MyHDL to VHDL conversion offers the following advantages:

    MyHDL integration in a VHDL-based design flow
        Designers can start using MyHDL and benefit from its power and flexibility, within the context of their proven design flow.

    The converter automates a number of hard tasks
        The converter automates a number of tasks that are hard to do in VHDL directly. For example, when mixing unsigned and signed types it can be difficult to describe the desired behavior correctly. In contrast, a MyHDL designer can use the high-level intbv type, and let the converter deal with type conversions and resizings.

    MyHDL as an IP development platform
        The possibility to convert the same MyHDL source to equivalent Verilog and VHDL creates a novel application: using MyHDL as an IP development platform. IP developers can serve customers for both target languages from a single MyHDL code base. Moreover, MyHDL's flexibility and parametrizability make it ideally suited to this application.

  Solution description

  Approach

The approach followed to convert MyHDL code to VHDL is identical to the one followed for conversion to Verilog in previous MyHDL releases.

In particular, the MyHDL code analyzer in the converter is identical for both target languages. The goal is that all MyHDL code that can be converted to Verilog can be converted to VHDL also, and vice versa. This has been achieved except for a few minor issues due to limitations of the target languages.

  User interface

Conversion to VHDL is implemented by the following function in the myhdl package:

    toVHDL(func[, *args][, **kwargs])
        Converts a MyHDL design instance to equivalent VHDL code. func is a function that returns an instance. toVHDL calls func under its control and passes *args and **kwargs to the call.

        The return value is the same as the one returned by the call func(*args, **kwargs). It can be assigned to an instance name. The top-level instance name and the basename of the Verilog output filename is func.func_name by default.

toVHDL has the following attributes:

    toVHDL.name
        This attribute is used to overwrite the default top-level instance name and the basename of the VHDL output.

    toVHDL.component_declarations
        This attribute can be used to add component declarations to the VHDL output. When a string is assigned to it, it will be copied to the appropriate place in the output file.

  Type mapping

In contrast to Verilog, VHDL is a strongly typed language. The converter has to carefully perform type inferencing, and handle type conversions and resizings appropriately. To do this right, a well-chosen mapping from MyHDL types to VHDL types is crucial.

MyHDL types are mapped to VHDL types according to the following table:
MyHDL type | VHDL type | Notes
int | integer |
bool | std_logic | (1)
intbv with min >= 0 | unsigned | (2)
intbv with min < 0 | signed | (2)
enum | dedicated enumeration type |
tuple of int | mapped to case statement | (3)
list of bool | array of std_logic |
list of intbv with min >= 0 | array of unsigned | (4)
list of intbv with min < 0 | array of signed | (4)

Notes:

    1.  The VHDL std_logic type is defined in the standard VHDL package IEEE.std_logic_1164.

    2.  The VHDL unsigned and signed types used are those from the standard VHDL packages IEEE.numeric_std.

    3.  A MyHDL tuple of int is used for ROM inference, and can only be used in a very specific way: an indexing operation into the tuple should be the rhs of an assignment.

    4.  All list members should have identical value constraints.

The table as presented applies to MyHDL variables. They are mapped to VHDL variables (except for the case of a tuple of int).

The converter also supports MyHDL signals that use bool, intbv or enum objects as their underlying type. These are mapped to VHDL signals with a type as specified in the table above.

The converter supports MyHDL list of signals provided the underlying signal type is either bool or intbv. They may be mapped to a VHDL signal with a VHDL type as specified in the table. However, list of signals are not always mapped to a corresponding VHDL signal. See Conversion of lists of signals for more info.

  Template transformation

There is a difference between VHDL and Verilog in the way in which sensitivity to signal edges is specified. In Verilog, edge specifiers can be used directly in the sensitivity list. In VHDL, this is not possible: only signals can be used in the sensitivity list. To check for an edge, one uses the rising_edge() or falling_edge() functions in the code.

MyHDL follows the Verilog scheme to specify edges in the sensitivity list. Consequently, when mapping such code to VHDL, it needs to be transformed to equivalent VHDL. This is an important issue because it affects all synthesizable templates that infer sequential logic.

We will illustrate this feature with some examples. This is the MyHDL code for a D flip-flop:

        @always(clk.posedge)
        def logic():
            q.next = d

It is converted to VHDL as follows:

        DFF_LOGIC: process (clk) is
        begin
            if rising_edge(clk) then
                q <= d;
            end if;
        end process DFF_LOGIC;

The converter can handle the more general case. For example, this is MyHDL code for a D flip-flop with asynchronous set, asynchronous reset, and preference of set over reset:

        @always(clk.posedge, set.negedge, rst.negedge)
        def logic():
            if set == 0:
                q.next = 1
            elif rst == 0:
                q.next = 0
            else:
                q.next = d

This is converted to VHDL as follows:

        DFFSR_LOGIC: process (clk, set, rst) is
        begin
            if (set = '0') then
                q <= '1';
            elsif (rst = '0') then
                q <= '0';
            elsif rising_edge(clk) then
                q <= d;
            end if;
        end process DFFSR_LOGIC;

All cases with practical utility can be handled in this way. However, there are other cases that cannot be transformed to equivalent VHDL. The converter will detect those cases and give an error.

  Conversion of lists of signals

Lists of signals are useful for many purposes. For example, they make it easy to create a repetitive structure. Another application is the description of memory behavior.

The converter output is non-hierarchical. That implies that all signals are declared at the top-level in VHDL or Verilog (as VHDL signals, or Verilog regs and wires.) However, some signals that are a list member at some level in the design hierarchy may be used as a plain signal at a lower level. For such signals, a choice has to be made whether to declare a Verilog memory or VHDL array, or a number of plain signal names.

If possible, plain signal declarations are preferred, because Verilog memories and arrays have some restrictions in usage and tool support. This is possible if the list syntax is strictly used outside generator code, for example when lists of signals are used to describe structure.

Conversely, when list syntax is used in some generator, then a Verilog memory or VHDL array will be declared. The typical example is the description of RAM memories.

The converter in the previous MyHDL release had a severe restriction on the latter case: it didn't allow that, for a certain signal, list syntax was used in some generator, and plain signal syntax in another. This restriction, together with its rather obscure error message, has caused regular user complaints. In this release, this restriction has been lifted.

  Conversion of test benches

  Background

After conversion, we obviously want to verify that the VHDL or Verilog code works correctly. In previous MyHDL versions, the proposed verification technique was co-simulation: use the same MyHDL test bench to simulate the converted Verilog code and the original MyHDL code. While co-simulation works well, there are a number of issues with it:

    •   Co-simulation requires that the HDL simulator has an interface to its internal workings, such as vpi for Verilog and vhpi for VHDL.

    •   vpi for Verilog is well-established and available for open-source simulators such as Icarus and cver. However, vhpi for VHDL is much less established; it is unclear whether there is an open source solution that is powerful enough for MyHDL's purposes.

    •   Even though vpi is a "standard", there are differences between various simulators. Therefore, some customization is typically required per Verilog simulator.

    •   MyHDL co-simulation uses unix-style interprocess communication that doesn't work on Windows natively. This is an exception to the rest of MyHDL that should run on any Python platform.

The conclusion is that co-simulation is probably not a viable solution for the VHDL case, and it has some disadvantages for Verilog as well.

The proposed alternative is to convert the test bench itself, so that both test bench and design can be run in the HDL simulator. Of course, this is not a fully general solution either, as there are important restrictions on the kind of code that can be converted. However, with the additional features that have been developed, it should be a useful solution for verifying converted code.

  Print statement

In previous MyHDL versions, print statement conversion to Verilog was supported in a quick and dirty way, by merely copying the format string without checks. With the advent of VHDL conversion, this has now been implemented more rigorously. This was necessary because VHDL doesn't work with format strings. Rather, the format string specification has to be converted to a sequence of VHDL write and writeline calls.

A print statement with multiple arguments:

        print arg1, arg2, ...

is supported. However, there are restrictions on the arguments. First, they should be of one of the following forms:

        arg
        formatstring % arg
        formatstring % (arg1, arg2, ...)

where arg is a bool, int, intbv, enum, or a Signal of these types.

The formatstring contains ordinary characters and conversion specifiers as in Python. However, the only supported conversion specifiers are %s and %d. Justification and width specification are thus not supported.

Printing without a newline:

        print arg1 ,

is not supported. This is because the solution is based on std.textio. In VHDL std.textio, subsequent write() calls to a line are only printed upon a writeline() call. As a normal print implies a newline, the correct behavior can be guaranteed, but for a print without newline this is not possible. In the future, other techniques may be used and this restriction may be lifted.

  Assert statement

An assert statement in Python looks as follow:

        assert test_expression

It can be converted provided test_expression is convertible.

  Delay objects

Delay objects are constructed as follows:

        delay(t)

with t an integer. They are used in yield statements and as the argument of always decorators, to specify delays. They can now be converted.

  Methodology notes

The question is whether the conversion restrictions permit one to develop sufficiently complex test benches. In this section, we present some insights about this.

The most important restrictions are the types that can be used. These remain "hardware-oriented" as before.

Even in the previous MyHDL release, the "convertible subset" was much wider than the "synthesis subset". For example, while and raise statement were already convertible.

The support for delay objects is the most important new feature to write high-level models and test benches.

With the print statement, simple debugging can be done.

Of particular interest is the assert statement. Originally, assert statements were only intended to insert debugging assertions in code. Recently, there is a tendency to use them to write self-checking unit tests, controlled by unit test frameworks such as py.test. In particular, they are a powerful way to write self-checking test benches for MyHDL designs. As assert statements are now convertible, a whole test suite in MyHDL can be converted to an equivalent test suite in Verilog and VHDL.

Finally, the same techniques as for synthesizable code can be used to master complexity. In particular, any code outside generators is executed during elaboration, and therefore not considered in the conversion process. This feature can for example be used for complex calculations that set up constants or expected results. Furthermore, a tuple of ints can be used to hold a table of values that will be mapped to a case statement in Verilog and VHDL.

  Conversion output verification

NOTE:
        This functionality is not needed in a typical design flow. It is only relevant to debug the MyHDL converter itself.

  Approach

To verify the converter output, a methodology has been developed and implemented that doesn't rely on co-simulation and works for both Verilog and VHDL.

The solution builds on the features explained in section Conversion of test benches. The idea is basically to convert the test bench as well as the functional code. In particular, print statements in MyHDL are converted to equivalent statements in the HDL. The verification process consists of running both the MyHDL and the HDL simulation, comparing the simulation output, and reporting any differences.

The goal is to make the verification process as easy as possible. The use of print statements to debug a design is a very common and simple technique. The verification process itself is implemented in a single function with an interface that is identical to toVHDL and toVerilog.

As this is a native Python solution, it runs on any platform on which the HDL simulator runs. Moreover, any HDL simulator can be used as no vpi or vhpi capabilities are needed. Of course, per HDL simulator some customization is required to define the details on how it is used. This needs to be done once per HDL simulator and is fully under user control.

  Verification interface

All functions related to conversion verification are implemented in the myhdl.conversion package. (To keep the myhdl namespace clean, they are not available from the myhdl namespace directly.)

    verify(func[, *args][, **kwargs])
        Used like toVHDL. It converts MyHDL code, simulates both the MyHDL code and the HDL code and reports any differences. The default HDL simulator is GHDL.

    analyze(func[, *args][, **kwargs])
        Used like toVHDL. It converts MyHDL code, and analyzes the resulting HDL. Used to verify whether the HDL output is syntactically correct.

The two previous functions have the following attribute:

    analyze.simulator
        Used to set the name of the HDL analyzer. GHDL is the default.

    verify.simulator
        Used to set the name of the HDL simulator. GHDL is the default.

  HDL simulator registration

To be able to use a HDL simulator to verify conversions, it needs to be registered first. This is needed once per simulator (or rather, per set of analysis and simulation commands). Registering is done with the following function:

    registerSimulator(name=None, hdl=None, analyze=None, elaborate=None, simulate=None, offset=0)
        Registers a particular HDL simulator to be used by verify and analyze. name is the name of the simulator. hdl specifies the HDL: "VHDL" or "Verilog". analyze is a command string to analyze the HDL source code. elaborate is a command string to elaborate the HDL code. This command is optional. simulate is a command string to simulate the HDL code. offset is an integer specifying the number of initial lines to be ignored from the HDL simulator output.

        The command strings should be string templates that refer to the topname variable that specifies the design name. The templates can also use the unitname variable which is the lower case version of topname. The command strings can assume that a subdirectory called work is available in the current working directory. Analysis and elaboration results can be put there if desired.

        The analyze function runs the analyze command. The verify function runs the analyze command, then the elaborate command if any, and then the simulate command.

        The GHDL simulator is registered by default, but its registration can be overwritten if required.

  Example: preregistered HDL simulators

A number of open-source HDL simulators are preregistered in the MyHDL distribution. If they are installed in the typical way, they are readily available for conversion verification. We will illustrate the registration process by showing the registrations of these simulators.

GHDL registration:

        registerSimulator(
            name="GHDL",
            hdl="VHDL",
            analyze="ghdl -a --workdir=work pck_myhdl_%(version)s.vhd %(topname)s.vhd",
            elaborate="ghdl -e --workdir=work -o %(unitname)s_ghdl %(topname)s",
            simulate="ghdl -r %(unitname)s_ghdl"
            )

Icarus registration:

        registerSimulator(
            name="icarus",
            hdl="Verilog",
            analyze="iverilog -o %(topname)s.o %(topname)s.v",
            simulate="vvp %(topname)s.o"
            )

cver registration:

        registerSimulator(
            name="cver",
            hdl="Verilog",
            analyze="cver -c -q %(topname)s.v",
            simulate="cver -q %(topname)s.v",
            offset=3
            )

  New modeling features

  New signed() method for intbv

The intbv object has a new method signed that implements sign extension. The extended bit is the msb bit of the bit representation of the object.

Clearly, this method only has an effect for intbv objects whose valid values are a finite range of positive integers.

This method can be converted to VHDL and Verilog.

  always_comb and list of signals

In the previous MyHDL release, one could use lists of signals in an always_comb block, but they were not considered to infer the sensitivity list. To several users, this was unexpected behavior, or even a bug.

In the present release, lists of signals are considered and the corresponding signals are added to the sensitivity list. The converter to Verilog and VHDL is adapted accordingly.

  Backwards incompatible changes

  Decorator usage

The basic building block of a MyHDL design is a specialized Python generator.

In MyHDL 0.5, decorators were introduced to make it easier to create useful MyHDL generators. Moreover, they make the code clearer. As a result, they are now the de facto standard to describe hardware modules in MyHDL.

The implementation of certain tasks, such a signal tracing and conversion, can be simplified significantly if decorators are used to create the generators. These simplifications have now been adopted in the code. This means that decorator usage is assumed. Legacy code written for the mentioned purposes without decorators, can always be easily converted into code with decorators.

For pure modeling, it doesn't matter how generators are created and this will remain so. Therefore, designers can continue to experiment with innovative modeling concepts in the fullest generality.

  instances() function

The instances function can be used to automatically lookup and return the instances that are defined in a MyHDL module. In accordance with the section Decorator usage, its functionality has been changed. Only generators created by decorators are considered when looking up instances.

  Conversion of printing without a newline

Printing without a newline (a print statement followed by a comma) is no longer supported by the converter to Verilog. This is done to be compatible with the converter to VHDL. Currently, the VHDL solution relies on std.textio and this implies that printing without a newline cannot be reliably converted.

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.

  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.

  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.

WHAT'S NEW IN MYHDL 0.4: CONVERSION TO VERILOG

    Author
        Jan Decaluwe

  Introduction

MyHDL 0.4 supports the automatic conversion of a subset of MyHDL code to synthesizable Verilog code. This feature provides a direct path from Python to an FPGA or ASIC implementation.

MyHDL aims to be a complete design language, for tasks such as high level modeling and verification, but also for implementation. However, prior to 0.4 a user had to translate MyHDL code manually to Verilog or VHDL. Needless to say, this was inconvenient. With MyHDL0.4, this manual step is no longer necessary.

  Solution description

The solution works as follows. The hardware description should be modeled in MyHDL style, and satisfy certain constraints that are typical for implementation-oriented hardware modeling. Subsequently, such a design is converted to an equivalent model in the Verilog language, using the function toVerilog from the MyHDLlibrary. Finally, a third-party synthesis tool is used to convert the Verilog design to a gate implementation for an ASIC or FPGA. There are a number of Verilog synthesis tools available, varying in price, capabilities, and target implementation technology.

The conversion does not start from source files, but from a design that has been elaborated by the Python interpreter. The converter uses the Python profiler to track the interpreter’s operation and to infer the design structure and name spaces. It then selectively compiles pieces of source code for additional analysis and for conversion. This is done using the Python compiler package.

  Features

  The design is converted after elaboration

Elaboration refers to the initial processing of a hardware description to achieve a representation of a design instance that is ready for simulation or synthesis. In particular, structural parameters and constructs are processed in this step. In MyHDL, the Python interpreter itself is used for elaboration. A Simulation object is constructed with elaborated design instances as arguments. Likewise, the Verilog conversion works on an elaborated design instance. The Python interpreter is thus used as much as possible.

  The structural description can be arbitrarily complex and hierarchical

As the conversion works on an elaborated design instance, any modeling constraints only apply to the leaf elements of the design structure, that is, the co-operating generators. In other words, there are no restrictions on the description of the design structure: Python’s full power can be used for that purpose. Also, the design hierarchy can be arbitrarily deep.

  Generators are mapped to Verilog always or initial blocks

The converter analyzes the code of each generator and maps it to a Verilog always blocks if possible, and to an initial block otherwise. The converted Verilog design will be a flat “net list of blocks”.

  The Verilog module interface is inferred from signal usage

In MyHDL, the input or output direction of interface signals is not explicitly declared. The converter investigates signal usage in the design hierarchy to infer whether a signal is used as input, output, or as an internal signal. Internal signals are given a hierarchical name in the Verilog output.

  Function calls are mapped to a unique Verilog function or task call

The converter analyzes function calls and function code to see if they should be mapped to Verilog functions or to tasks. Python functions are much more powerful than Verilog subprograms; for example, they are inherently generic, and they can be called with named association. To support this power in Verilog, a unique Verilog function or task is generated per Python function call.

  If-then-else structures may be mapped to Verilog case statements

Python does not provide a case statement. However, the converter recognizes if-then-else structures in which a variable is sequentially compared to items of an enumeration type, and maps such a structure to a Verilog case statement with the appropriate synthesis attributes.

  Choice of encoding schemes for enumeration types

The enum function in MyHDL returns an enumeration type. This function takes an additional parameter encoding that specifies the desired encoding in the implementation: binary, one hot, or one cold. The Verilog converter generates the appropriate code.

  The convertible subset

  Introduction

Unsurprisingly, not all MyHDL code can be converted to Verilog. In fact, there are very important restrictions. As the goal of the conversion functionality is implementation, this should not be a big issue: anyone familiar with synthesis is used to similar restrictions in the synthesizable subset of Verilog and VHDL. The converter attempts to issue clear error messages when it encounters a construct that cannot be converted.

In practice, the synthesizable subset usually refers to RTL synthesis, which is by far the most popular type of synthesis today. There are industry standards that define the RTL synthesis subset. However, those were not used as a model for the restrictions of the MyHDL converter, but as a minimal starting point. On that basis, whenever it was judged easy or useful to support an additional feature, this was done. For example, it is actually easier to convert while loops than for loops even though they are not RTL-synthesizable. As another example, print is supported because it’s so useful for debugging, even though it’s not synthesizable. In summary, the convertible subset is a superset of the standard RTL synthesis subset, and supports synthesis tools with more advanced capabilities, such as behavioral synthesis.

Recall that any restrictions only apply to the design post elaboration. In practice, this means that they apply only to the code of the generators, that are the leaf functional blocks in a MyHDL design.

  Coding style

A natural restriction on convertible code is that it should be written in MyHDL style: cooperating generators, communicating through signals, and with yield statements specifying wait points and resume conditions. Supported resume conditions are a signal edge, a signal change, or a tuple of such conditions.

  Supported types

The most important restriction regards object types. Verilog is an almost typeless language, while Python is strongly (albeit dynamically) typed. The converter has to infer the types of names used in the code, and map those names to Verilog variables.

Only a limited amount of types can be converted. Python int and long objects are mapped to Verilog integers. All other supported types are mapped to Verilog regs (or wires), and therefore need to have a defined bit width. The supported types are the Python bool type, the MyHDL intbv type, and MyHDL enumeration types returned by function enum. The latter objects can also be used as the base object of a Signal.

intbv objects must be constructed so that a bit width can be inferred. This can be done by specifying minimum and maximum values, e.g. as follows:

        index = intbv(0, min=0, max=2**N)

Alternatively, a slice can be taken from an intbv object as follows:

        index = intbv(0)[N:]

Such as slice returns a new intbv object, with minimum value 0 , and maximum value 2**N.

  Supported statements

The following is a list of the statements that are supported by the Verilog converter, possibly qualified with restrictions or usage notes.

The break statement.

The continue statement.

The def statement.

    The for statement.
        The only supported iteration scheme is iterating through sequences of integers returned by built-in function range or MyHDLfunction downrange. The optional else clause is not supported.

    The if statement.
        if, elif, and else clauses are fully supported.

The pass statement.

    The print statement.
        When printing an interpolated string, the format specifiers are copied verbatim to the Verilog output. Printing to a file (with syntax ’>>’) is not supported.

    The raise statement.
        This statement is mapped to Verilog statements that end the simulation with an error message.

The return statement.

    The yield statement.
        The yielded expression can be a signal, a signal edge as specified by MyHDL functions posedge or negedge, or a tuple of signals and edge specifications.

    The while statement.
        The optional else clause is not supported.

  Methodology notes

  Simulation

In the Python philosophy, the run-time rules. The Python compiler doesn’t attempt to detect a lot of errors beyond syntax errors, which given Python’s ultra-dynamic nature would be an almost impossible task anyway. To verify a Python program, one should run it, preferably using unit testing to verify each feature.

The same philosophy should be used when converting a MyHDL description to Verilog: make sure the simulation runs fine first. Although the converter checks many things and attempts to issue clear error messages, there is no guarantee that it does a meaningful job unless the simulation runs fine.

  Conversion output verification

It is always prudent to verify the converted Verilog output. To make this task easier, the converter also generates a test bench that makes it possible to simulate the Verilog design using the Verilog co-simulation interface. This permits one to verify the Verilog code with the same test bench used for the MyHDL code. This is also how the Verilog converter development is being verified.

  Assignment issues

  Name assignment in Python

Name assignment in Python is a different concept than in many other languages. This point is very important for effective modeling in Python, and even more so for synthesizable MyHDL code. Therefore, the issues are discussed here explicitly.

Consider the following name assignments:

        a = 4
        a = ``a string''
        a = False

In many languages, the meaning would be that an existing variable a gets a number of different values. In Python, such a concept of a variable doesn’t exist. Instead, assignment merely creates a new binding of a name to a certain object, that replaces any previous binding. So in the example, the name a is bound a number of different objects in sequence.

The Verilog converter has to investigate name assignment and usage in MyHDL code, and to map names to Verilog variables. To achieve that, it tries to infer the type and possibly the bit width of each expression that is assigned to a name.

Multiple assignments to the same name can be supported if it can be determined that a consistent type and bit width is being used in the assignments. This can be done for boolean expressions, numeric expressions, and enumeration type literals. In Verilog, the corresponding name is mapped to a single bit reg, an integer, or a reg with the appropriate width, respectively.

In other cases, a single assignment should be used when an object is created. Subsequent value changes are then achieved by modification of an existing object. This technique should be used for Signal and intbv objects.

  Signal assignment

Signal assignment in MyHDL is implemented using attribute assignment to attribute next. Value changes are thus modeled by modification of the existing object. The converter investigates the Signal object to infer the type and bit width of the corresponding Verilog variable.

  intbv objects

Type intbv is likely to be the workhorse for synthesizable modeling in MyHDL. An intbv instance behaves like a (mutable) integer whose individual bits can be accessed and modified. Also, it is possible to constrain its set of values. In addition to error checking, this makes it possible to infer a bit width, which is required for implementation.

In Verilog, an intbv instance will be mapped to a reg with an appropriate width. As noted before, it is not possible to modify its value using name assignment. In the following, we will show how it can be done instead. Consider:

        a = intbv(0)[8:]

This is an intbv object with initial value 0 and bit width 8. The change its value to 5, we can use slice assignment:

        a[8:] = 5

The same can be achieved by leaving the bit width unspecified, which has the meaning to change “all” bits:

        a[:] = 5

Often the new value will depend on the old one. For example, to increment an intbv with the technique above:

        a[:] = a + 1

Python also provides augmented assignment operators, which can be used to implement in-place operations. These are supported on intbv objects and by the converter, so that the increment can also be done as follows:

        a += 1

  Converter usage

We will demonstrate the conversion process by showing some examples.

  A small design with a single generator

Consider the following MyHDL code for an incrementer module:

        def inc(count, enable, clock, reset, n):
            """ Incrementer with enable.

            count -- output
            enable -- control input, increment when 1
            clock -- clock input
            reset -- asynchronous reset input
            n -- counter max value
            """
            def incProcess():
                while 1:
                    yield posedge(clock), negedge(reset)
                    if reset == ACTIVE_LOW:
                        count.next = 0
                    else:
                        if enable:
                            count.next = (count + 1) % n
            return incProcess()

In Verilog terminology, function inc corresponds to a module, while generator function incProcess roughly corresponds to an always block.

Normally, to simulate the design, we would “elaborate” an instance as follows:

        m = 8
        n = 2 ** m

        count = Signal(intbv(0)[m:])
        enable = Signal(bool(0))
        clock, reset = [Signal(bool()) for i in range(2)]

        inc_inst = inc(count, enable, clock, reset, n=n)

incinst is an elaborated design instance that can be simulated. To convert it to Verilog, we change the last line as follows:

        inc_inst = toVerilog(inc, count, enable, clock, reset, n=n)

Again, this creates an instance that can be simulated, but as a side effect, it also generates an equivalent Verilog module in file . The Verilog code looks as follows:

        module inc_inst (
            count,
            enable,
            clock,
            reset
        );

        output [7:0] count;
        reg [7:0] count;
        input enable;
        input clock;
        input reset;

        always @(posedge clock or negedge reset) begin: _MYHDL1_BLOCK
            if ((reset == 0)) begin
                count <= 0;
            end
            else begin
                if (enable) begin
                    count <= ((count + 1) % 256);
                end
            end
        end

        endmodule

You can see the module interface and the always block, as expected from the MyHDL design.

  Converting a generator directly

It is also possible to convert a generator directly. For example, consider the following generator function:

        def bin2gray(B, G, width):
            """ Gray encoder.

            B -- input intbv signal, binary encoded
            G -- output intbv signal, gray encoded
            width -- bit width
            """
            Bext = intbv(0)[width+1:]
            while 1:
                yield B
                Bext[:] = B
                for i in range(width):
                    G.next[i] = Bext[i+1] ^ Bext[i]

As before, you can create an instance and convert to Verilog as follows:

        width = 8

        B = Signal(intbv(0)[width:])
        G = Signal(intbv(0)[width:])

        bin2gray_inst = toVerilog(bin2gray, B, G, width)

The generated Verilog code looks as follows:

        module bin2gray_inst (
            B,
            G
        );

        input [7:0] B;
        output [7:0] G;
        reg [7:0] G;

        always @(B) begin: _MYHDL1_BLOCK
            integer i;
            reg [9-1:0] Bext;
            Bext[9-1:0] = B;
            for (i=0; i<8; i=i+1) begin
                G[i] <= (Bext[(i + 1)] ^ Bext[i]);
            end
        end

        endmodule

  A hierarchical design

The hierarchy of convertible designs can be arbitrarily deep.

For example, suppose we want to design an incrementer with Gray code output. Using the designs from previous sections, we can proceed as follows:

        def GrayInc(graycnt, enable, clock, reset, width):

            bincnt = Signal(intbv()[width:])

            INC_1 = inc(bincnt, enable, clock, reset, n=2**width)
            BIN2GRAY_1 = bin2gray(B=bincnt, G=graycnt, width=width)

            return INC_1, BIN2GRAY_1

According to Gray code properties, only a single bit will change in consecutive values. However, as the bin2gray module is combinatorial, the output bits may have transient glitches, which may not be desirable. To solve this, let’s create an additional level of hierarchy and add an output register to the design. (This will create an additional latency of a clock cycle, which may not be acceptable, but we will ignore that here.)

        def GrayIncReg(graycnt, enable, clock, reset, width):

            graycnt_comb = Signal(intbv()[width:])

            GRAY_INC_1 = GrayInc(graycnt_comb, enable, clock, reset, width)

            def reg():
                while 1:
                    yield posedge(clock)
                    graycnt.next = graycnt_comb
            REG_1 = reg()

            return GRAY_INC_1, REG_1

We can convert this hierarchical design as before:

        width = 8
        graycnt = Signal(intbv()[width:])
        enable, clock, reset = [Signal(bool()) for i in range(3)]

        GRAY_INC_REG_1 = toVerilog(GrayIncReg, graycnt, enable, clock, reset, width)

The Verilog output code looks as follows:

        module GRAY_INC_REG_1 (
            graycnt,
            enable,
            clock,
            reset
        );

        output [7:0] graycnt;
        reg [7:0] graycnt;
        input enable;
        input clock;
        input reset;

        reg [7:0] graycnt_comb;
        reg [7:0] _GRAY_INC_1_bincnt;

        always @(posedge clock or negedge reset) begin: _MYHDL1_BLOCK
            if ((reset == 0)) begin
                _GRAY_INC_1_bincnt <= 0;
            end
            else begin
                if (enable) begin
                    _GRAY_INC_1_bincnt <= ((_GRAY_INC_1_bincnt + 1) % 256);
                end
            end
        end

        always @(_GRAY_INC_1_bincnt) begin: _MYHDL4_BLOCK
            integer i;
            reg [9-1:0] Bext;
            Bext[9-1:0] = _GRAY_INC_1_bincnt;
            for (i=0; i<8; i=i+1) begin
                graycnt_comb[i] <= (Bext[(i + 1)] ^ Bext[i]);
            end
        end

        always @(posedge clock) begin: _MYHDL9_BLOCK
            graycnt <= graycnt_comb;
        end

        endmodule

Note that the output is a flat “net list of blocks”, and that hierarchical signal names are generated as necessary.

  Optimizations for finite state machines

As often in hardware design, finite state machines deserve special attention.

In Verilog and VHDL, finite state machines are typically described using case statements. Python doesn’t have a case statement, but the converter recognizes particular if-then-else structures and maps them to case statements. This optimization occurs when a variable whose type is an enumerated type is sequentially tested against enumeration items in an if-then-else structure. Also, the appropriate synthesis pragmas for efficient synthesis are generated in the Verilog code.

As a further optimization, function enum was enhanced to support alternative encoding schemes elegantly, using an additional parameter encoding. For example:

        t_State = enum('SEARCH', 'CONFIRM', 'SYNC', encoding='one_hot')

The default encoding is ’binary’; the other possibilities are ’onehot’ and ’onecold’. This parameter only affects the conversion output, not the behavior of the type. The generated Verilog code for case statements is optimized for an efficient implementation according to the encoding. Note that in contrast, a Verilog designer has to make nontrivial code changes to implement a different encoding scheme.

As an example, consider the following finite state machine, whose state variable uses the enumeration type defined above:

        FRAME_SIZE = 8

        def FramerCtrl(SOF, state, syncFlag, clk, reset_n):

            """ Framing control FSM.

            SOF -- start-of-frame output bit
            state -- FramerState output
            syncFlag -- sync pattern found indication input
            clk -- clock input
            reset_n -- active low reset

            """

            index = intbv(0, min=0, max=8) # position in frame
            while 1:
                yield posedge(clk), negedge(reset_n)
                if reset_n == ACTIVE_LOW:
                    SOF.next = 0
                    index[:] = 0
                    state.next = t_State.SEARCH
                else:
                    SOF.next = 0
                    if state == t_State.SEARCH:
                        index[:] = 0
                        if syncFlag:
                            state.next = t_State.CONFIRM
                    elif state == t_State.CONFIRM:
                        if index == 0:
                            if syncFlag:
                                state.next = t_State.SYNC
                            else:
                                state.next = t_State.SEARCH
                    elif state == t_State.SYNC:
                        if index == 0:
                            if not syncFlag:
                                state.next = t_State.SEARCH
                        SOF.next = (index == FRAME_SIZE-1)
                    else:
                        raise ValueError("Undefined state")
                    index[:]= (index + 1) % FRAME_SIZE

The conversion is done as before:

        SOF = Signal(bool(0))
        syncFlag = Signal(bool(0))
        clk = Signal(bool(0))
        reset_n = Signal(bool(1))
        state = Signal(t_State.SEARCH)
        framerctrl_inst = toVerilog(FramerCtrl, SOF, state, syncFlag, clk, reset_n)

The Verilog output looks as follows:

        module framerctrl_inst (
            SOF,
            state,
            syncFlag,
            clk,
            reset_n
        );
        output SOF;
        reg SOF;
        output [2:0] state;
        reg [2:0] state;
        input syncFlag;
        input clk;
        input reset_n;

        always @(posedge clk or negedge reset_n) begin: _MYHDL1_BLOCK
            reg [3-1:0] index;
            if ((reset_n == 0)) begin
                SOF <= 0;
                index[3-1:0] = 0;
                state <= 3'b001;
            end
            else begin
                SOF <= 0;
                // synthesis parallel_case full_case
                casez (state)
                    3'b??1: begin
                        index[3-1:0] = 0;
                        if (syncFlag) begin
                            state <= 3'b010;
                        end
                    end
                    3'b?1?: begin
                        if ((index == 0)) begin
                            if (syncFlag) begin
                                state <= 3'b100;
                            end
                            else begin
                                state <= 3'b001;
                            end
                        end
                    end
                    3'b1??: begin
                        if ((index == 0)) begin
                            if ((!syncFlag)) begin
                                state <= 3'b001;
                            end
                        end
                        SOF <= (index == (8 - 1));
                    end
                    default: begin
                        $display("Verilog: ValueError(Undefined state)");
                        $finish;
                    end
                endcase
                index[3-1:0] = ((index + 1) % 8);
            end
        end
        endmodule

  Known issues

    Negative values of intbv instances are not supported.
        The intbv class is quite capable of representing negative values. However, the signed type support in Verilog is relatively recent and mapping to it may be tricky. In my judgment, this was not the most urgent requirement, so I decided to leave this for later.

    Verilog integers are 32 bit wide
        Usually, Verilog integers are 32 bit wide. In contrast, Python is moving toward integers with undefined width. Python int and long variables are mapped to Verilog integers; so for values wider than 32 bit this mapping is incorrect.

    Synthesis pragmas are specified as Verilog comments.
        The recommended way to specify synthesis pragmas in Verilog is through attribute lists. However, my Verilog simulator (Icarus) doesn’t support them for case statements (to specify parallelcase and fullcase pragmas). Therefore, I still used the old but deprecated method of synthesis pragmas in Verilog comments.

    Inconsistent place of the sensitivity list inferred from alwayscomb.
        The semantics of alwayscomb, both in Verilog and MyHDL, is to have an implicit sensitivity list at the end of the code. However, this may not be synthesizable. Therefore, the inferred sensitivity list is put at the top of the corresponding always block. This may cause inconsistent behavior at the start of the simulation. The workaround is to create events at time 0.

    Non-blocking assignments to task arguments don’t work.
        I didn’t get non-blocking (signal) assignments to task arguments to work. I don’t know yet whether the issue is my own, a Verilog issue, or an issue with my Verilog simulator Icarus. I’ll need to check this further.

WHAT’S NEW IN MYHDL 0.3

    Author
        Jan Decaluwe

  VCD output for waveform viewing

[image: image] [image]

MyHDL now has support for waveform viewing. During simulation, signal changes can be written to a VCD output file that can be loaded into a waveform viewer tool such as gtkwave.

The user interface of this feature consists of a single function, traceSignals. To explain how it works, recall that in MyHDL, an instance is created by assigning the result of a function call to an instance name. For example:

        tb_fsm = testbench()

To enable VCD tracing, the instance should be created as follows instead:

        tb_fsm = traceSignals(testbench)

All signals in the instance hierarchy will be traced in a VCD file called . Note that first the argument of traceSignals consists of the uncalled function. By calling the function under its control, traceSignals gathers information about the hierarchy and the signals to be traced. In addition to a function argument, traceSignals accepts an arbitrary number of non-keyword and keyword arguments that will be passed to the function call.

Signals are dumped in a suitable format. This format is inferred at the Signal construction time, from the type of the initial value. In particular, bool signals are dumped as single bits. (This only works starting with Python 2.3, when bool has become a separate type). Likewise, intbv signals with a defined bit width are dumped as bit vectors. To support the general case, other types of signals are dumped as a string representation, as returned by the standard str function.

[warning] Support for literal string representations is not part of the VCD standard. It is specific to gtkwave. To generate a standard VCD file, you need to use signals with a defined bit width only.

  Enumeration types

It is often desirable to define a set of identifiers. A standard Python idiom for this purpose is to assign a range of integers to a tuple of identifiers, like so:

        >>> SEARCH, CONFIRM, SYNC = range(3)
        >>> CONFIRM
        1

However, this technique has some drawbacks. Though it is clearly the intention that the identifiers belong together, this information is lost as soon as they are defined. Also, the identifiers evaluate to integers, whereas a string representation of the identifiers would be preferable. To solve these issues, we need an enumeration type.

MyHDL 0.3 supports enumeration types by providing a function enum. The arguments to enum are the string representations of the identifiers, and its return value is an enumeration type. The identifiers are available as attributes of the type. For example:

        >>> from myhdl import enum
        >>> t_State = enum('SEARCH', 'CONFIRM', 'SYNC')
        >>> t_State
        <Enum: SEARCH, CONFIRM, SYNC>
        >>> t_State.CONFIRM
        CONFIRM

Enumeration types are often used for the state variable in a finite state machine. In the waveform in Section 1, you see a Signal called state. Note how the waveforms show the string representation of the enumeration type identifiers The state signal has been constructed with an enumeration type identifier as its initial value, as follows:

        state = Signal(t_State.SEARCH)

  Inferring the sensitivity list for combinatorial logic

In MyHDL, combinatorial logic is described by a generator function with a sensitivity list that contains all inputs signals (the signals that are read inside the function).

It may be easy to forget some input signals, especially it there are a lot of them or if the code is being modified. There are various ways to solve this. One way is to use a sophisticated editor. Another way is direct language support. For example, recent versions of Verilog have the always @* construct, that infers all input signals. The SystemVerilog 3.1 standard improves on this by introducing the always_comb block with slightly enhanced semantics.

MyHDL 0.3 provides a function called always_comb which is named and modeled after the SystemVerilog counterpart. always_comb takes a classic local function as its argument. This function should specify the combinatorial logic behavior. always_comb returns a generator that is sensitive to all inputs, and that will run the function whenever an input changes.

For example, suppose that we have a mux module described as follows:

        def mux(z, a, b, sel):
            """ Multiplexer.

            z -- mux output
            a, b -- data inputs
            sel -- control input

            """
            def logic()
                while 1:
                    yield a, b, sel
                    if sel == 1:
                        z.next = a
                    else:
                        z.next = b
            mux_logic = logic()
            return mux_logic

Using always_comb, we can describe it as follows instead:

        def mux(z, a, b, sel):
            """ Multiplexer.

            z -- mux output
            a, b -- data inputs
            sel -- control input

            """
            def logic()
                if sel == 1:
                    z.next = a
                else:
                    z.next = b
            mux_logic = always_comb(logic)
            return mux_logic

Note that in the first version, the sensitivity list is at the beginning of the generator function code. This is traditionally done in synthesizable RTL style modeling. However, the semantics of this style are not entirely correct: at the start of the simulation, the combinatorial output will not reflect the initial state of the inputs. always_comb solves this by putting the sensitivity list at the end of the code.

  Inferring the list of instances

In MyHDL, the instances defined in a top level function need to be returned explicitly. The following is a schematic example:

        def top(...):
            ...
            instance_1 = module_1(...)
            instance_2 = module_2(...)
            ...
            instance_n = module_n(...)
            ...
            return instance_1, instance_2, ... , instance_n

It may be convenient to assemble the list of instances automatically, especially if there are many instances. For this purpose, MyHDL 0.3 provides the function instances. It is used as follows:

        from myhdl import instances

        def top(...):
            ...
            instance_1 = module_1(...)
            instance_2 = module_2(...)
            ...
            instance_n = module_n(...)
            ...
            return instances()

Function instances uses introspection to inspect the type of the local variables defined by the calling function. All variables that comply with the definition of an instance are assembled in a list, and that list is returned.

  Inferring the list of processes

In addition to instances, a top level function may also define local generators functions, which I will call processes because of the analogy with VHDL. Like instances, processes need to be returned explicitly, with the qualification that they have to be called first to turn them into generators. The following is a schematic example:

        def top(...):
            ...
            def process_1():
                ...
            def process_2():
                ...
            ...
            def process_n():
                ...
            ...
            return process_1(), process_2(), ..., process_n()

As for instances, it may be more convenient to assemble the list of processes automatically. One option is to turn each process into an instance by calling it and assigning the returned generator to a local variable. Those instances will then be found by the instances function described in Section 4.

Another option is to use the function processes provided by MyHDL 0.3. This function uses introspection to find the processes, calls each of them, and assembles the returned generators into a list. It can be used as follows:

        from myhdl import processes

        def top(...):
            ...
            def process_1():
                ...
            def process_2():
                ...
            ...
            def process_n():
                ...
            ...
            return processes()

To conclude, a top level function with both instances and processes can use the following idiomatic code to return all of them:

        return instances(), processes()

  Class intbv enhancements

Class intbv has been enhanced with new features.

It is now possible to leave the left index of a slicing operation unspecified. The meaning is to access “all” higher order bits. For example:

        >>> from myhdl import intbv
        >>> n = intbv()
        >>> hex(n)
        '0x0'
        >>> n[:] = 0xde
        >>> hex(n)
        '0xde'
        >>> n[:8] = 0xfa
        >>> hex(n)
        '0xfade'
        >>> n[8:] = 0xb4
        >>> hex(n)
        '0xfab4'

intbv objects now have min and max attributes that can be specified at construction time. The meaning is that only values within range(min, max) are permitted. The default value for these attributes is None, meaning “infinite”. For example (traceback output shortened for clarity):

        >>> n = intbv(min=-17, max=53)
        >>> n
        intbv(0)
        >>> n.min
        -17
        >>> n.max
        53
        >>> n[:] = 28
        >>> n
        intbv(28)
        >>> n[:] = -18
        Traceback (most recent call last):
            ....
        ValueError: intbv value -18 < minimum -17
        >>> n[:] = 53
        Traceback (most recent call last):
            ....
        ValueError: intbv value 53 >= maximum 53

When a slice is taken from an intbv object, the return value is a new intbv object with a defined bit width. As in Verilog, the value of the new intbv object is always positive, regardless of the sign of the original value. In addition, the min and max attributes are set implicitly:

        >>> v = intbv()[6:]
        >>> v
        intbv(0)
        >>> v.min
        0
        >>> v.max
        64

Lastly, a small change was implemented with regard to binary operations. In previous versions, both numeric and bit-wise operations always returned a new intbv object, even in mixed-mode operations with int objects. This has changed: numeric operations return an int, and bitwise operations return a intbv. In this way, the return value corresponds better to the nature of the operation.

  Function concat

In previous versions, the intbv class provided a method. This method is no longer available. Instead, there is now a concat function that supports a much broader range of objects.

A function is more natural because MyHDL objects of various types can be concatenated: intbv objects with a defined bit width, bool objects, the corresponding signal objects, and bit strings. All these objects have a defined bit width. Moreover, the first argument doesn’t need to have a defined bit width. It can also be an unsized intbv, an int, a long, or a corresponding signal object. Function concat returns an intbv object.

  Python 2.3 support

Python 2.3 was released on July 29, 2003, and as of this writing, it is the latest stable Python release. MyHDL 0.3 works with both Python 2.2 and Python 2.3. In good Python tradition, MyHDL code developed with Python 2.2 should run without changes or problems in Python 2.3.

In general, I am not that keen on early upgrading. However, as it happens, the evolution of Python enables features that are really important or even crucial to MyHDL. Python 2.2 generators are the best example: they are the cornerstone of MyHDL. But Python 2.3 also has significant benefits, which I will summarize below.

First, generators and the yield statement are a default Python 2.3 feature. This means that statements are no longer required.

Second, Python 2.3 has a bool type, which is implemented as a subtype of int. For general Python use, the implications are rather limited - the main difference is that logical result values will print as False and True instead of 0 and 1. However, in MyHDL, I can use the bool type to infer a bit width. If a Signal is constructed with a bool value, it is a single bit Signal. One application is waveform viewing as in Section 1 In the waveform, note how single bit signals are displayed as level changes. With Python 2.2, the waveforms of these signals would only show value changes, which is not as clear for single bits.

Finally, Python 2.3 is significantly faster. MyHDL code runs 25–35% faster in Python 2.3. This is a very nice speedup compared to the small burden of a straightforward upgrade.

Python is a very stable language, so upgrading to Python 2.3 is virtually risk free. Given the additional benefits, I recommend MyHDL users to do so as soon as possible. For the next major MyHDLrelease, the new features will become required and only Python 2.3 (and higher) will be supported.

    •   genindex

    •   search

AUTHOR
Jan Decaluwe

COPYRIGHT
2019, Jan Decaluwe