MYHDL(1)

๐Ÿ“ฆ version 0.11, in ubuntu jammy, ubuntu noble

๐Ÿ’– NAME ๐Ÿ’–

okay bestie, welcome to MYHDL(1) โ€” it's the MyHDL manual, and we're gonna get you fluent, no cap. ๐Ÿซถ

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. periodt.

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 ๐ŸŽ€

okay so this is the lore section, bestie. ๐Ÿซถ

๐Ÿ“š 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 ๐Ÿ’ญ

ok so before anything else, here's the footnote receipts from the chapter we just wrapped:

  • [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 ๐Ÿงธ

Bestie, we're starting with a classic Hello Worldโ€“style example, because of course we are. All the example code lives in the distribution directory under example/manual/. In there you'll find a MyHDL simulation script called hello1.py โ€” here are its contents:

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

Okay so let's break it down, because the way this works is lowkey elegant.

The first line of the script imports a bunch of objects from the myhdl package. โœจ In Python you can literally only use identifiers that are literally defined in the source file โ€” that's just the rules, no cap.

Then we define a function called HelloWorld. In MyHDL, a hardware module gets modeled by a function that's decorated with the block decorator. The name block was chosen to avoid confusion with the Python concept of a module โ€” we'll be using this terminology further on, so keep it in your pocket.

The parameter list of the HelloWorld function is what defines the interface of the hardware block. In this first example, the interface is empty. ๐Ÿ’€ iconic minimalism.

Inside the top level function we declare a local function called say_hello that defines the desired behavior. That function is decorated with an always decorator that takes 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 for defining the contents of a hardware block โ€” we'll get to the general case further on.

In MyHDL, we create an instance of a hardware block by calling the corresponding function. The block decorator makes sure the return value is actually an instance of a block class, with a useful API. In the example, the 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. easy as that. ๐Ÿ“


Signals and concurrency ๐ŸŒ€

An actual hardware design is typically massively concurrent โ€” meaning a large amount of functional units are running in parallel, all at once, no waiting their turn. MyHDL supports this behavior by allowing an arbitrary number of concurrently running generators. slay.

But 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're gonna 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. It 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. iykyk.

The say_hello function is modified from the first example. It's 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 their actions are coordinated by the clock signal. it's giving choreography honestly. ๐Ÿ’ƒ

When we run the simulation, we get:


Parameters, ports and hierarchy ๐Ÿช†

We've seen that MyHDL uses functions to model hardware blocks. So far those functions didn't have parameters. But bestie โ€” to create general, reusable blocks, we 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, clk, is the clock signal. A signal parameter is MyHDL's way to model a port.
  • The second parameter is the clock period, with a default value of 20.

Now โ€” because the low time of the clock may differ from the high time in case of an odd period, we can't 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's decorated with the instance decorator. You can tell drive_clk is a generator function because it contains yield statements. ๐Ÿ’ซ

When a generator function is called, it returns a generator object. That's basically what the instance decorator does. It's 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.

Heads up: to make sure 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. so true.

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's important to make these differences explicit. buckle up, bestie. ๐ŸŽ€

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 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 a 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 of 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's 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's 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's an overview of what we've learned in this chapter, bestie:

  • 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's 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. periodt.

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, bestie. Like class int, it gives you access to the underlying two's complement representation for bitwise operations. But here's the plot twist: unlike int, it's a mutable type. Which means its value can be changed after object creation, through methods and operators such as slice assignment. Character development!! ๐Ÿ’…

intbv supports the same operators as int for arithmetic. On top of that it brings a whole extra set of features so it's actually suitable for hardware design:

  1. 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.
  2. 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 provided 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. No cap, that's just the rule. ๐Ÿ’–

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. ๐Ÿ˜ญ iconic behaviour honestly.

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 exists 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:]

okay so now picture this: a complex number is hitchhiking on that same data bus. ๐Ÿ’ซ The upper 4 bits are carrying the real value, the lower 4 bits are carrying the imaginary value. And bc real and imaginary values both have a positive and negative range, we can just slice them off the bus and convert them like this:

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

โœจ Structural modeling โœจ

๐ŸŒธ Introduction ๐ŸŒธ

Hardware descriptions need to support module instantiation and hierarchy, no way around it. In MyHDL, an instance is recursively defined as being either a sequence of instances, or a generator. ๐Ÿงš Hierarchy gets modeled by defining instances inside a higher-level function and then returning them. Here's the schematic of the basic case, bestie:

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

And the way MyHDL uses conventional procedural techniques for modeling structure? It's giving effortless. It makes modeling the more complex cases genuinely straightforward. ๐Ÿ’…

๐Ÿฆ‹ Conditional instantiation ๐Ÿฆ‹

To model conditional instantiation, we just select the returned instance under parameter control. Like, parameter says jump, the instance says how high:

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 so easy to create, and we can absolutely use them to model lists of instances. Let's say we have a top module that instantiates a single channel submodule, like so:

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

Now if we wanted to support an arbitrary number of channels, we just use lists of signals plus a list of instances. Periodt:

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 like VHDL and Verilog, MyHDL signals are lowkey less flexible for structural modeling. Like, slicing a signal returns a slice of the current value. For behavioral code? Totally fine, no notes. โœจ But it does mean you cannot use such a slice in structural descriptions. In other words: a signal slice cannot be used as a signal. That's just how it is.

In MyHDL you can handle these cases with something 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 a list of signals. For example, a list of request signals can be constructed like this:

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

Suppose an arbiter module is available that's instantiated like this:

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're easy to process in RTL code. In MyHDL, a bit vector is modeled using the intbv type. ๐Ÿง 

So we need a way to "connect" the list of signals to the bit vector and vice versa. Yeah, sure, we could do it with explicit code, but shadow signals can do it 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 โ€” and that's on purpose! The index range of lists is the inverse of the range of intbv bit vectors, so by reversing, the indices correspond to the same bit. ๐ŸŽฏ

The inverse problem exists 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)]

Not me pointing out the round brackets used for this type of slicing โ€” yes, round, not square. Also, it may not even be necessary to construct this list explicitly. You can simply use grant_vector(i) in an instantiation. ๐Ÿ™ˆ

To decide when to use normal vs shadow signals, look at the data flow: use normal signals to connect to outputs, and use shadow signals to transform those signals so they can be used as inputs. ๐Ÿ’…

๐Ÿ’œ Inferring the list of instances ๐Ÿ’œ

In MyHDL, instances have to be returned explicitly by a top level function. But it can be convenient to assemble the list of instances automatically โ€” and for that, MyHDL provides the function instances. Using the first example in this section, it's used like this:

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 get assembled in a list, and that list is returned. ๐Ÿช„ Slay.


๐Ÿ”ฅ RTL modeling ๐Ÿ”ฅ

๐ŸŒŸ Introduction ๐ŸŒŸ

RTL (Register Transfer Level) is a modeling abstraction level that's 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 is all about how MyHDL supports it. ๐Ÿ’ช

โญ Combinatorial logic โญ

๐ŸŽ€ Template ๐ŸŽ€

Combinatorial logic is described with a code pattern like this:

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 is a nod to a similar construct in SystemVerilog, iykyk. The decorated function is a local function that specifies what happens when one of the input signals of the logic changes. And 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'll simulate the logic with some random patterns. The random module in Python's standard library is genuinely so handy for this. The function randrange(n) returns a random natural integer smaller than n, and it's used in the test bench code to produce random input values. ๐ŸŽฒ

It's often useful to keep the random values reproducible. That 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. On top of that, they may also 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's an asynchronous (True) or a synchronous (False) reset. And if no reset is needed at all? Just assign None to the reset parameter in the always_seq parameter. Easy. ๐Ÿ’…

๐ŸŒˆ Example ๐ŸŒˆ

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

For the test bench, we'll 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 as follows

The simulation produces the following output

๐Ÿงš Alternative template ๐Ÿงš

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

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. No autopilot this time. ๐Ÿซถ

๐Ÿ‘‘ Finite State Machine modeling ๐Ÿ‘‘

Finite State Machine (FSM) modeling is very common in RTL design, so it genuinely deserves special attention. โœจ

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

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

However, bestie, 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. Iconic.

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. Periodt.

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. No cap.

โš ๏ธ 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. It's giving range.

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. Lowkey obsessed.

This chapter explores some of the options for high level modeling with MyHDL. Let's get into it. ๐Ÿ’–

๐Ÿงš 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. Main character energy fr.

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. Iykyk.

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. Slay.

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. Big brain.

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

...so remember how the original generator just resumes the second one of its forked generators comes back? ๐Ÿ’ซ In the cases we just looked at, that's lowkey perfect, because the transmitter and receiver generators run in lockstep anyway. But sometimes you actually want the caller to wait until all the forked generators have finished. ๐Ÿ‘‘

Say we wanna stress-test how robust the transmitter/receiver design is when the bit durations don't match up. We can tweak our test bench 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)

Simulate that and you'll see the new byte's transmission starting before the previous one even got received โ€” which can create extra transmission errors, no cap:

-- 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

Realistically though, bestie, we probably wanna characterize the design byte by byte and line the two generators up before each byte goes out. In MyHDL that's literally what the join function is for. ๐Ÿ’… When you join clauses together in a yield statement, you create a brand new clause that only triggers once all of its clause arguments have triggered. Like this:

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))

And now? Transmission of a new byte only kicks off once the previous one has been received. Slay:

-- 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's built-in data types are so powerful, and they're genuinely useful for modeling hardware memories. Honestly it can be as simple as wrapping an interface around some data type operations. ๐ŸŽ€

For example, a dictionary is giving sparse memory structure. (In other languages this type is called an associative array, or a hash table.) A sparse memory is one where only a small part of the address space actually gets used in a given application or simulation. Instead of statically allocating the whole address space โ€” which can be huge โ€” it's way better to dynamically allocate only the storage you actually need. And that is exactly what a dictionary does for you. โœจ Here's 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

Notice how we use the val attribute of the din signal โ€” because we do not wanna store the signal object itself, we want its current value, periodt. Same energy with the val attribute of addr as the dictionary key. ๐Ÿ—๏ธ

In lots of cases, MyHDL code just uses a signal's current value automatically whenever there's no ambiguity โ€” like when a signal shows up in an expression. But other times, like right here, you have to refer to the value explicitly: for instance when the Signal is used as a dictionary key, or when it's not used in an expression at all. One option is the val attribute, like we just did. Another option is to typecast the Signal with the int() or bool() functions to get an integer or a boolean value. Those functions are also handy with intbv objects. ๐Ÿฆ‹

Second example incoming โ€” watch how we 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 literally just a MyHDL interface wrapped around some list operations โ€” insert to insert entries, pop to retrieve them, and len to get the size of a Python object. ๐Ÿ“


๐Ÿ˜ญ Modeling errors using exceptions ๐Ÿšจ

So in the last section we used Python data types for modeling. And if you use one of those types inappropriately, Python's run-time error system is gonna step in and read you. ๐Ÿ™ˆ For example, if you access an address in the sparseMemory model that was never initialized, you'll get a traceback kinda like this (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)

And similarly, if the fifo is empty and you try to read from it, you get:

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

But instead of these low-level errors, it's often way better to define errors at the functional level. ๐Ÿ’œ In Python that's typically done by defining a custom Error exception, subclassing the standard Exception class. Then you raise that exception explicitly whenever an error condition shows up.

Like, we can change the sparseMemory function like this (doc string 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

The trick is catching the low-level data type exception and raising the custom exception with a nice error message instead. So if the sparseMemory function lives in a module with the same name, an access error now reports like this:

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

Same deal โ€” the fifo function can be adapted like this 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

Here, the underflow error gets detected the same way as before โ€” by catching a low-level exception on the list data type. The overflow error though? That one's 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 โ€” but they still had a conventional RTL-style interface. Communication with a module like that happens through signals attached to it during instantiation.

A more advanced approach is modeling hardware blocks as objects. Communication with objects happens through method calls. A method encapsulates every detail of a certain task the object performs. And since an object has a method interface instead of an RTL-style hardware interface, this is a much higher-level approach. ๐Ÿ’…

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

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, plus a sync signal to synchronize the operation between the methods. Whenever put puts an item in the queue, that signal gets triggered. ๐ŸŽ€ And when the get method sees the list is empty, it waits on the trigger first. get is a generator method because it may consume time. Since the yield statement is used in MyHDL for timing control, the method can't "yield" the item itself. Instead, it makes the item available in the item instance variable. ๐Ÿซถ

To test the queue operation, we're gonna model a producer and a consumer right inside the test bench. ๐Ÿงš

Quick vibe check though: a waiting consumer should not block the entire system, so it needs to run in a concurrent "thread". And as always in MyHDL, concurrency is modeled by Python generators โ€” no cap. ๐Ÿ’… So Producer and Consumer run independently, and we keep an eye on them 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()

Now bestie, real talk about one line in there: the generator method get is called inside a yield statement in the Consumer function. ๐Ÿ‘€ The new generator takes over from Consumer, and it stays in charge until it's done. Running this test bench gives you this 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

So many things about the design flow of modern digital hardware design can be viewed as a special kind of software development. ๐ŸŒธ And once you're looking at it like that, the natural question pops up: can the glow-ups we've had in software design techniques also be applied to hardware design?

One software design approach that genuinely deserves your attention is Extreme Programming (XP). It's a fascinating little collection of techniques and guidelines that often seems to go completely against the conventional wisdom. Other times, XP is just out here emphasizing plain common sense โ€” which, lowkey, doesn't always line up with common practice. ๐Ÿ˜ญ For example, XP stresses how important normal workweeks are, because you need a fresh mind if you want good software development.

It is not my intention nor qualification to present a tutorial on Extreme Programming. Nope, not me. ๐Ÿ™ˆ Instead, in this section I'm going to highlight one XP concept that I think is super 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, like collective ownership of code and continuous refinement, are only possible by having unit tests. And XP goes further: it 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 these concepts apply directly to hardware design. On top of that, 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. Which is why 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'll do it with 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 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. That would comply with the requirement, but it 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. ๐Ÿฆ‹

Okay so we denote the original Gray code of order n as Ln ๐Ÿ’–. Some examples, bestie:

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

And you can also specify these codes by a recursive algorithm, like this:

  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 literally famous for its elegant algorithmic descriptions โœจ and this is such a good example. We can write the algorithm in Python as follows:

The code ['0' + codeword for ...] is called a list comprehension ๐ŸŽ€. It's 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 MyHDL is 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? Verification is the most complex and time-consuming task of the design process, periodt. 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 MyHDL side 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 MyHDL simulator. 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 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 ๐Ÿ’–.

Okay so it's a whole different vibe when you wanna use a signal that the HDL drives (and its MyHDL twin) as a clock, bestie. Communication triggered by that kind of clock is not race free ๐Ÿ˜ญ. The fix? Treat that interface like a chip interface instead of an RTL interface. Like, when data is triggered at positive clock edges, you can safely sample it at negative clock edges. OR โ€” you can declare the MyHDL data signals with a delay value, so they're guaranteed to change after the clock edge. ๐Ÿ’…


๐Ÿง  Implementation notes ๐Ÿง 

Heads up bestie: 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" โœจ. But โ€” and this is the annoying part โ€” different simulators implement different versions and portions of that standard. Worse yet, the behavior of certain PLI callbacks is not defined on some essential points. So basically, plan to write โ€” or at least customize โ€” a specific PLI module for any simulator you touch. 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, plus some reflections on future implementations. ๐Ÿฆ‹


๐ŸŒˆ Icarus Verilog ๐ŸŒˆ

๐Ÿ’ซ Delta cycle implementation ๐Ÿ’ซ

To make co-simulation work, you need a very specific type of PLI callback. It should run when all pending events have been processed, while still 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. But in others โ€” including Icarus โ€” it does not. The callback's behavior isn't fully standardized; some simulators run the callback before non-blocking assignment events have been processed. ๐Ÿ’€

So 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 โ€” slay โ€” BUT it doesn't permit you 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. And thankfully 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. So 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, and the limit is checked at run-time โฐ. The factor 1000 also makes it super 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's 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's 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 is much less popular than vpi. Also, to our knowledge there is only one credible open source VHDL simulator (GHDL) and it's 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's 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 โœจ

Okay so Python does not have a case statement. It's just not a thing she does ๐Ÿ’…. BUT โ€” the converter is smart, bestie, and it recognizes if-then-else structures where a variable gets compared to items of an enumeration type, one after the other, sequentially. When it sees that vibe, it maps the whole thing to a Verilog or VHDL case statement with all the appropriate synthesis attributes. Chef's kiss ๐Ÿ‘‘


๐ŸŒˆ choice of encoding schemes for enumeration types ๐ŸŒˆ

The enum function in MyHDL returns an enumeration type โ€” iconic. And it takes an additional parameter, encoding, which specifies the encoding you want in the implementation: binary, one hot, or one cold. Then the converter just... generates the appropriate code for whatever encoding you picked. Slay โœจ


๐Ÿงš RAM memory ๐Ÿงš

Certain synthesis tools can map Verilog memories or VHDL arrays to RAM structures. To support this very interesting feature, the converter maps lists of signals to Verilog memories or VHDL arrays. Fr fr, that's the whole thing ๐Ÿซถ


๐ŸŽ€ ROM memory ๐ŸŽ€

Some synthesis tools can infer a ROM from a case statement. And the converter? It does the expansion into a case statement automatically, based on a higher level description. The ROM access itself is described in a single line โ€” by indexing into a tuple of integers. One line!! So clean, so demure ๐ŸŒธ


๐Ÿ”ฅ signed arithmetic ๐Ÿ”ฅ

In MyHDL, working with negative numbers is literally trivial: you just use an intbv object with an appropriate constraint on its values. Done. No drama.

In contrast โ€” and bestie, sit down for this โ€” both Verilog and VHDL make a difference between an unsigned and a signed representation. So to work with negative values, the user has to declare a signed variable explicitly. And 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. Periodt. Moreover, the converter automates all 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. Not me having a whole converter do my homework for me ๐Ÿ˜Œ


๐Ÿ’œ user-defined code ๐Ÿ’œ

If desired, the user can bypass the conversion process and describe user-defined code to be inserted instead. Your rules, your code, your moment ๐Ÿ‘‘


โœจ the convertible subset โœจ

๐ŸŒŸ introduction ๐ŸŒŸ

Unsurprisingly, not all MyHDL code can be converted. The restrictions are significant, yes โ€” but 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. But that's not even the best part โ€” it's also possible to write convertible code for non-synthesizable models or test benches. Versatile queen ๐ŸŒˆ

The converter attempts to issue clear error messages when it encounters a construct that cannot be converted. It's trying its best to tell you what went wrong, okay ๐Ÿ˜ญ

Recall that any restrictions only apply to the design after elaboration. In practice, this means they apply only to the code of the generators โ€” 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
  • always_comb

Use the decorators, bestie. It's giving correct ๐Ÿ’…


โญ 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 a slice returns a new intbv object, with minimum value 0, and maximum value 2**N. Math girlies rise up ๐ŸŽ€

In addition to the scalar types we just talked about, bestie, the converter also handles a whole bunch of tuple and list based types. Here's the mapping from MyHDL types, and yes it's giving very much "chart" energy:

  • int โ†’ VHDL integer, Verilog integer
  • bool โ†’ VHDL std_logic (see note 1), Verilog reg
  • intbv with min >= 0 โ†’ VHDL unsigned (note 2), Verilog reg
  • intbv with min < 0 โ†’ VHDL signed (note 2), Verilog reg signed
  • enum โ†’ VHDL dedicated enumeration type, Verilog reg
  • tuple of int โ†’ VHDL mapped to case statement (note 3), Verilog mapped to case statement (note 3)
  • list of bool โ†’ VHDL array of std_logic, Verilog reg (note 5)
  • list of intbv with min >= 0 โ†’ VHDL array of unsigned (note 4), Verilog reg (notes 4 and 5)
  • list of intbv with min < 0 โ†’ VHDL array of signed (note 4), Verilog reg signed (notes 4 and 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.

Okay so, that whole table applies to MyHDL variables, got it? But the converter also supports MyHDL signals that use bool, intbv or enum objects as their underlying type. For VHDL, these get mapped to VHDL signals with an underlying type as specified in that table above โœจ. Verilog doesn't have the signal concept, periodt. 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. ๐Ÿ‘‘

And ooh, the converter also 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. But โ€” and this is important โ€” 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 โœจ

Here's the list of statements that are supported by the Verilog converter, possibly qualified with restrictions or usage notes. Not me being thorough or anything ๐Ÿ’…

  • assert โ€” An assert statement in Python looks like this:
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 โœจ

These are the built-in functions that the converter is obsessed with (in a supportive way):

  • 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, bestie. 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

okay bestie, sit down for this one because it's the glow-up moment. Name assignment in Python is a whole different concept than in so many other languages. This point is so important for effective modeling in Python, and even more so for synthesizable MyHDL code. So bestie, we're discussing it explicitly, no skipping. ๐Ÿ’…

Consider these name assignments:

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

In lots of languages, you'd read that as: an existing variable a gets a bunch of different values, one after another. But in Python? That concept of a variable does not exist. Instead, assignment merely creates a new binding of a name to a certain object, and that replaces any previous binding. So in the example, the name a is bound to a bunch of different objects in sequence. That's it, that's the tea. ๐Ÿต

The converter has to investigate name assignment and usage in MyHDL code, and map names to Verilog or VHDL objects. To do that, it tries to infer the type and possibly the bit width of each expression that's 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 across those assignments. This works for boolean expressions, numeric expressions, and enumeration type literals. slay. โœจ

In other cases, you want a single assignment when an object is created. After that, value changes are achieved by modification of an existing object. This technique is the one you use for Signal and intbv objects.

Signal assignment

Signal assignment in MyHDL is implemented using attribute assignment to the 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 lowkey the workhorse for synthesizable modeling in MyHDL. An intbv instance behaves like a (mutable) integer whose individual bits can be accessed and modified. Also, you can constrain its set of values. On top of error checking, this makes it possible to infer a bit width, which is required for implementation. iconic behavior honestly. ๐Ÿ‘‘

As noted before, it is not possible to modify the value of an intbv object using name assignment. So here's how you do it instead. Consider:

a = intbv(0)[8:]

That's 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

Same result if you leave the bit width unspecified โ€” which has the meaning of changing "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 like this:

a += 1

Excluding code from conversion

For some tasks, like 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 an 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's 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 specify 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's a difference between VHDL and Verilog in the way sensitivity to signal edges is specified. In Verilog, edge specifiers can be used directly in the sensitivity list. In VHDL, that's not possible: only signals can be used in the sensitivity list. To check for an edge, you use 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'll 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'll present a number of non-synthesizable features 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, sometimes hierarchy is literally the vibe. Like, if you're converting a test bench, you might want to keep its code separate from the design under test. Translation: conversion should stop at the design-under-test instance and insert an instantiation instead. ๐ŸŽ€

There's a workaround for this, and it only takes a lil extra effort. You define user-defined code that consists of an instantiation of the design under test. Like we talked about in User-defined code, when the converter spots the hook it stops converting and drops the instantiation in instead. And obvi you'll want to convert the design under test too โ€” so use a flag that controls whether the hook is defined or not, and set it according to the conversion you want. ๐Ÿ’…


๐Ÿฉน Known issues (the tea, spilled) โ˜•

Verilog and VHDL integers are 32 bit wide

So, usually Verilog and VHDL integers are 32 bit wide. Meanwhile Python is moving toward integers with undefined width. Python int and long variables get 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. But the Icarus simulator doesn't support them for case statements (we're talking the parallel_case and full_case pragmas here). So the old, 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. So the inferred sensitivity list gets put at the top of the corresponding always block or process. This may cause inconsistent behavior at the start of the simulation. The workaround? Create events at time 0. โญ


๐Ÿ”ฅ Conversion examples ๐Ÿ”ฅ

Introduction

In this chapter we're gonna demonstrate the conversion process with a bunch of examples. For the concepts of MyHDL conversion, go 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. Step one is to elaborate it, exactly like 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. no cap. ๐Ÿงš

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, since the bin2gray module is combinatorial, the output bits may have transient glitches, which may not be desirable. To fix 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're gonna 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 happens 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, the 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;

okay and that's the whole FSM, bestie โ€” she's serving state machine realness and the VHDL just ate ๐Ÿ’…โœจ


๐Ÿง  RAM inference

ngl this one is lowkey iconic. Certain synthesis tools can infer RAM structures โ€” like, they'll literally just build you a memory if you ask nicely ๐Ÿซถ. To support that whole situation, the converter maps lists of signals in MyHDL to Verilog memories and VHDL arrays. periodt.

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, which is honestly so main character of them. 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 โ€” it's giving "look but don't touch" ๐Ÿ’….

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

okay so this is the part where you get to hand-write your own output ๐Ÿ˜ญ๐Ÿ”ฅ. 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. slay.

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. no cap, it protects your old data ๐Ÿซถ.

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 attribute 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() โ€” okay so bestie, this decorator is literally the glow-up that gives you a method-based API. It's more consistent, simplifies implementation, and reduces the size of the myhdl namespace. periodt.

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. no cap.
  • <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 method is currently required because only a single simulation can be active. it's giving exclusive ๐Ÿ’….
  • <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. vibe check.
  • <block_instance>.analyze_convert() โ€” Analyze conversion output by compilation with target HDL compiler. fr fr.

๐ŸŒธ 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. iykyk.

๐ŸŒˆ 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. iconic.

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 cases 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. lowkey.

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. bestie, remember left is non-inclusive but right is inclusive โ€” that's the vibe.

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. iconic.

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. drama. ๐Ÿ’€

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 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 ๐Ÿงšโœจ

okay so MyHDL gives you a bunch of decorator functions, and they make creating generators from local generator functions so much easier. no cap. ๐Ÿ’…

instance() ๐ŸŽ€

The instance decorator is the most general one. 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 be 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 a 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:

  • bv[i] โ€” item i of bv (note (1))
  • bv[i] = x โ€” item i of bv is replaced by x (note (1))
  • bv[i:j] โ€” slice of bv from i downto j (notes (2)(3))
  • bv[i:j] = t โ€” slice of bv from i downto j is replaced by t (notes (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 one is modeled after the standard range function, but it works in the downward direction. ๐Ÿ’ซ The interval it hands back is half-open, which means the high index is not included. low is optional and defaults to zero. This one is especially useful when you're paired up with the intbv class, because that one works with downward indexing too. Obsessed with the teamwork honestly. ๐Ÿซถ


๐Ÿฆ‹ instances ๐Ÿฆ‹

instances()

Looks up all MyHDL instances hanging out in the local name space and returns them in a list. That's it, that's the whole vibe. ๐Ÿ’…

Return type: list


๐ŸŽ€ Co-simulation ๐ŸŽ€

MyHDL

class Cosimulation(exe, **kwargs)

A class to construct a brand-new Cosimulation object. โœจ

The exe argument is the command that executes an HDL simulation, and it can be either a string containing the entire command line, or a list of strings. If you go the list route, the first element is the executable and the subsequent elements are program arguments. Handing it a list of arguments is the move because it lets Python correctly handle spaces or other characters in program arguments. ๐Ÿซถ

The kwargs keyword arguments give you 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. No cap.

๐Ÿ’ป Verilog ๐Ÿ’ป

$to_myhdl(arg, [, arg ...])

A task that defines which signals (regs & nets) should be read by the MyHDL simulator. Call it at the start of the simulation, okay bestie? โญ

$from_myhdl(arg, [, arg ...])

A task that defines which signals should be driven by the MyHDL simulator. Heads up: in Verilog, only regs can be specified. ๐Ÿ’€ This task should be called at the start of the simulation too.


๐Ÿ”ฅ Conversion to Verilog and VHDL ๐Ÿ”ฅ

Conversion

toVerilog(func [, *args] [, **kwargs])

Converts a MyHDL design instance into 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 info about the restrictions on convertible MyHDL code, see section conv-subset in Chapter conv.

toVerilog has the following attributes:

  • 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 into 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. ๐Ÿ‘€

Real talk though: these function attributes cannot be used with generator functions or decorated local functions, because those aren't elaborated before simulation or conversion. In other words, they can only be used with functions that define structure. No exceptions, sorry bestie. ๐Ÿ˜ญ

๐ŸŒธ 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 too. Generous of it, honestly. ๐Ÿ’–

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 an HDL simulator to verify conversions, it needs to be registered first. This is needed once per simulator. Just once, we promise. ๐Ÿซถ

A number of HDL simulators are preregistered in the MyHDL distribution, as follows:

  • "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. We're rooting for you. ๐Ÿ’œ


โœจ 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. That's it, that's the glow-up. ๐Ÿ’…


๐Ÿ 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. Go on, be the main character. ๐Ÿ‘‘

โœจ WHAT'S NEW IN MYHDL 0.10 โœจ

๐Ÿงฑ The block decorator

Rationale

Okay so bestie, the old way MyHDL did hierarchy extraction? It was giving problematic, ngl. The result: complex code, a bunch of non-intuitive API concepts, and honestly just a whole lot of difficulty for anybody trying to develop on it in the future. ๐Ÿ’€

So in this release, a new block decorator got introduced to fix all of that. ๐ŸŽ€

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, and yes we're listing every single one because that's the vibe ๐Ÿ’…:

  • <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, where:
    • backend: defaults to 'myhdl
    • trace: enable waveform tracing, default False.
  • <block_instance>.quit_sim() โ€” quit an active simulation. This 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, bestie, here we go:
      • 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 the target HDL simulation log with the MyHDL simulation log. ๐Ÿ”
  • <block_instance>.analyze_convert() โ€” analyze conversion output by compilation with the target HDL compiler. ๐Ÿ› ๏ธ

Backwards compatibility issues

In the 0.10 release, the old API is still available right next to the new API based on the block decorator. So nothing's broken yet. ๐Ÿ˜Œ

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. Don't get caught slipping, fr fr. ๐Ÿ˜ญ

๐ŸŒˆ WHAT'S NEW IN MYHDL 0.9 ๐ŸŒˆ

๐Ÿ Python 3 support

Experimental Python 3 support got added to MyHDL 0.9. This was a major effort to modernize the code, and the glow-up is real โ€” 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 so many signals getting passed down to different levels of hierarchy, and typically a bunch of those signals logically belong together. You can model that with 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. Iconic behavior honestly. ๐Ÿ‘‘

Here's 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))

So previous versions supported interfaces for modeling but they were not convertible. ๐Ÿ˜ญ MyHDL 0.9 now supports conversion of designs that use interfaces. Slay.

And here's 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. Basically, the converter replaces 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. Main character energy tbh. ๐Ÿ’…

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 got modernized by moving to git and github for version control. In addition, travis has been 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 it. 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

So in hardware modeling, there's 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, you currently have 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 just 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 exists for a left shift as follows:

shifter <<= 4

These operations can only be 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 also want to give it "bit-vector" support, like intbv. Therefore, the 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. So don't come for the converter, bestie, it's doing its best. ๐Ÿ˜Œ

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 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>

So the always_seq decorator takes two arguments, bestie: a clock edge and a reset signal. It literally inspects your code to sniff out the registers, then uses their initial values to build the reset behavior for you. Iconic behavior honestly. ๐Ÿ’…

The second construct is a specialized signal subclass called ResetSignal. It's used like this:

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

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

The proposed solution has some very desirable features, no cap.

Explicit reset behavior coding is no longer necessary. Code reviewers are thus no longer distracted by non-functional code. It's sufficient to check the initial values to verify whether the reset value is correctly specified. Moreover, one indentation level is saved for functional coding. (the way that saves you a whole indent levelโ€ฆ we love to see it ๐ŸŽ€)

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. Like, it just works. ๐Ÿ”ฅ

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 a single location in the ResetSignal constructor. Changes are trivial. For example, to change to an active high synchronous reset you merely have to change the constructor as follows:

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

Occasionally, it's 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 be 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's 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's 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. slay.

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 a 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 a 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 constructor 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.

  • driver() โ€” hands u a brand new driver for the tristate signal, initialized to None ๐Ÿ’ซ. A driver object is an instance of a special SignalType subclass, and here's the cute part: its next attribute can be used to assign a new value to it. So the driver gets the next privilege that the tristate signal itself doesn't have. Iconic behavior, honestly.

Example

A super typical glow-up for shadow signals is converting a whole list of signals into bit vectors and back again. It's giving "why didn't I think of that" ๐Ÿ’…

So picture this: we have a system with N requesters that need arbitration. Every requester has a request output and a grant input. To hook them up in the system, we can use a list of signals. Like, a list of request signals gets constructed like this:

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

Now say an arbiter module is available and gets instantiated like this:

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

The request_vector input is a bit vector where any of its bits can be asserted. The grant_vector is an output bit vector with just a single bit asserted, or none at all. A module like this is typically based on bit vectors because bit vectors are easy to process in RTL code. In MyHDL, a bit vector is modeled using the intbv type. โœจ

So we need a way to "connect" the list of signals to the bit vector and vice versa. Obviously we could do it with explicit code, but bestie, shadow signals do it 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 exists for the grant_vector. It would be defined like this:

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)]

Notice the round brackets used for this type of slicing ๐ŸŽ€. Also, it may not even be necessary to construct this list explicitly. You can literally just use grant_vector(i) in an instantiation.

So how do you decide when to use normal signals vs shadow signals? Consider the data flow, babes. Use normal signals to connect to outputs. Use shadow signals to transform these signals so that they can be used as inputs. That's the whole vibe check right there. ๐ŸŒˆ


Using Signal and intbv objects as indices

Back in the day ๐Ÿ˜ญ it was necessary to convert Signal and intbv objects explicitly to int when using them as indices for indexing and slicing. That 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. And this feature is fully supported by the VHDL/Verilog converter. Slay. ๐Ÿ‘‘

New configuration attributes for conversion file headers

Fresh configuration attributes just dropped 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 like this:

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 should not. It's giving intentionality. ๐Ÿฆ‹

New method to specify user-defined code

The current way to specify user-defined code for conversion is through the __vhdl__ and __verilog__ hooks. And ngl, 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. Not the move.

A second problem: 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. Cryptic errors? We don't claim them. ๐Ÿ’€

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. Heads up, bestie. ๐Ÿšจ

More powerful mapping to case statements

The converter has become more powerful at mapping 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. Level up. ๐Ÿ”ฅ

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. Periodt.

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. Bye bye, bad vibes. ๐Ÿ’…

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 ๐Ÿ’–

okay so bestie, the converter is out here doing the MOST โ€” it handles a whole bunch of stuff that's genuinely painful to write in VHDL by hand. Like, when you're mixing unsigned and signed types it can be straight up hard to describe the behavior you actually want without messing it up. But a MyHDL designer? She just uses the high-level intbv type and lets the converter deal with all the type conversions and resizings. iconic behavior honestly. ๐Ÿ‘‘

๐ŸŒˆ MyHDL as an IP development platform ๐ŸŒˆ

and here's the part that's giving main character energy: you can convert the same MyHDL source to equivalent Verilog and VHDL. That unlocks a whole novel application โ€” using MyHDL as an IP development platform. IP developers can serve customers for both target languages from a single MyHDL code base. no cap. plus MyHDL's flexibility and parametrizability make it ideally suited to this. slay. โœจ


๐ŸŒธ 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. fr fr, same energy.

Specifically, the MyHDL code analyzer inside 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. And bestie, this has been achieved โ€” except for a few minor issues that come from limitations of the target languages themselves. (not MyHDL's fault, the languages are just like that sometimes ๐Ÿ’€)

๐Ÿฆ‹ User interface ๐Ÿฆ‹

Conversion to VHDL is implemented by the following function in the myhdl package:

toVHDL(func[, *args][, **kwargs])
  • 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. So 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:

  • 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 โญ

okay so there's a real difference between VHDL and Verilog in the way sensitivity to signal edges is specified. In Verilog, edge specifiers can be used directly in the sensitivity list. In VHDL? nope, not possible โ€” only signals can go in the sensitivity list. To check for an edge, you use the rising_edge() or falling_edge() functions in the code instead.

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. like, all of them. ๐Ÿ˜ญ

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. super handy honestly.

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. decisions decisions ๐Ÿ’ญ

okay so if it's possible, plain signal declarations are preferred โ€” because Verilog memories and arrays have some restrictions in usage and tool support. And you can do the plain thing if the list syntax is strictly used outside generator code, for example when lists of signals are used to describe structure. โœจ

Conversely โ€” flip it โ€” when list syntax shows up in some generator, then a Verilog memory or VHDL array gets declared. The typical example, the moment everyone thinks of: describing RAM memories. ๐Ÿง 

Now the tea ๐Ÿต: the converter in the previous MyHDL release had a severe restriction on that latter case โ€” it didn't allow that, for a certain signal, list syntax was used in some generator and plain signal syntax in another. That restriction, together with its rather obscure error message, has caused regular user complaints. In this release, this restriction has been lifted. periodt ๐Ÿ’…

Conversion of test benches

Background

After conversion, we obviously want to verify that the VHDL or Verilog code works correctly, right? 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. ๐Ÿซถ

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. But 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

So the question is: do the conversion restrictions permit you to develop sufficiently complex test benches? Let's talk about it.

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 ๐ŸŒŸ

Okay so intbv just got a whole new method and it's called signed, and what it does is sign extension. The extended bit is literally the msb bit of the bit representation of the object. That's it, that's the whole thing. ๐Ÿ‘‘

And like, obviously, this method only has an effect for intbv objects whose valid values are a finite range of positive integers. Not a vibe that applies everywhere, iykyk. ๐Ÿ™ˆ

Bestie, the good news: this method can be converted to VHDL and Verilog. Slay. ๐Ÿ’…

๐Ÿ’– always_comb and list of signals ๐Ÿ’–

So in the previous MyHDL release, you 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. And honestly? Fair. ๐Ÿ˜ญ

In the present release though, lists of signals are considered, and the corresponding signals are added to the sensitivity list. Periodt. 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. That's the whole foundation, no cap. ๐Ÿฆ‹

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. No stress, it's giving "easy fix". ๐Ÿ’…

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. Main character energy, fr fr. โœจ

๐Ÿฆ‹ 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. That's the rule now, bestie. ๐Ÿ‘‘

๐ŸŒธ 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. So yeah. That's the situation. ๐Ÿ’€


๐Ÿ‘‘ 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. It's giving "magic but legal". โœจ

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. Periodt. ๐Ÿ’–

๐ŸŽ€ The @instance decorator ๐ŸŽ€

The @instance decorator is the most general decorator in MyHDL. The headliner, honestly. ๐Ÿ‘‘

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. Obsessed, actually. ๐Ÿ’…

๐Ÿ’œ 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. Lowkey iconic. โœจ

๐ŸŒˆ 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. It just knows. ๐Ÿ’…

๐Ÿ”ฅ More information ๐Ÿ”ฅ

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


๐ŸŽ€ 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. Just do it, bestie. ๐Ÿ’–

๐Ÿฆ‹ 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 โœจ

okay bestie, so when it can, 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.

That was done for the exact same reasons as the similar case with toVerilog, which we already spilled the tea about earlier. ๐Ÿ’…

Wanna name your output file yourself? Say less, bestie:

   traceSignals.name = "my_name"
   inst = traceSignals(func, ....)
Warning: when you re-run converting code that already existed, the vcd output filename is gonna come out different than it was in 0.4. Not a drill, plan accordingly. ๐Ÿ˜ญ

โœจ WHAT'S NEW IN MYHDL 0.4: CONVERSION TO VERILOG โœจ

Author Jan Decaluwe

๐Ÿฆ‹ Introduction

MyHDL 0.4 says: you know what? Automatic conversion of a subset of MyHDL code to synthesizable Verilog code. That's the vibe. And why does this matter? Because it gives you a direct path from Python straight to an FPGA or ASIC implementation. Slay. ๐Ÿ‘‘

MyHDL's whole dream is to be a complete design language โ€” high level modeling, verification, but also implementation. The catch? Before 0.4, you had to translate MyHDL code by hand into Verilog or VHDL. Which, no cap, was inconvenient. With MyHDL 0.4, that manual step is gone. Not me being emotional about it. ๐Ÿฅน

๐ŸŒธ Solution description

So here's how it works, step by step:

  • You model your hardware description in MyHDL style, and it has to satisfy certain constraints โ€” the kind that are typical for implementation-oriented hardware modeling.
  • Then that design gets converted into an equivalent model in the Verilog language, using the function toVerilog from the MyHDLlibrary.
  • Finally, a third-party synthesis tool takes the Verilog design and converts it into a gate implementation for an ASIC or FPGA.

There are a whole bunch of Verilog synthesis tools out there, and they vary in price, capabilities, and target implementation technology. Pick your fighter. โœจ

Here's the juicy part: the conversion does not start from source files. It starts from a design that has already been elaborated by the Python interpreter. The converter uses the Python profiler to track what the interpreter's doing and to infer the design structure and name spaces. Then it selectively compiles pieces of source code for additional analysis and for conversion. All of that is done using the Python compiler package. Iconic. ๐Ÿ”ฅ

๐ŸŽ€ Features

๐Ÿ’œ The design is converted after elaboration

So "elaboration" means the initial processing of a hardware description to get a representation of a design instance that's ready for simulation or synthesis. Specifically, structural parameters and constructs get processed in this step. In MyHDL, the Python interpreter itself is what's used for elaboration. A Simulation object gets constructed with elaborated design instances as arguments. And same energy for Verilog conversion โ€” it works on an elaborated design instance. So the Python interpreter is used as much as possible. Reuse queen. ๐Ÿซถ

๐ŸŒˆ The structural description can be arbitrarily complex and hierarchical

Because conversion works on an elaborated design instance, any modeling constraints only apply to the leaf elements of the design structure โ€” aka the co-operating generators. In other words? There are zero restrictions on how you describe the design structure. Python's full power is yours for that. And the design hierarchy can be arbitrarily deep. Go off. ๐Ÿ•ณ๏ธโœจ

โญ Generators are mapped to Verilog always or initial blocks

The converter analyzes the code of each generator and maps it to a Verilog always block if it can, and to an initial block otherwise. The converted Verilog design is gonna be a flat "net list of blocks". Clean. ๐Ÿ’…

๐Ÿฆ‹ The Verilog module interface is inferred from signal usage

In MyHDL, you don't explicitly declare whether an interface signal is input or output. Nope. The converter investigates how signals are used in the design hierarchy and infers whether a signal is used as an input, an output, or as an internal signal. Internal signals get a hierarchical name in the Verilog output. She reads you for filth (affectionately) and figures it out herself. ๐Ÿ‘‘

๐Ÿ“ 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. And here's the thing: Python functions are so much more powerful than Verilog subprograms. They're inherently generic, and they can be called with named association. To support all that power in Verilog, a unique Verilog function or task is generated per Python function call. One per call, no sharing, no cutting corners. โœจ

๐ŸŒธ If-then-else structures may be mapped to Verilog case statements

Python doesn't give you a case statement. Sad. But the converter recognizes if-then-else structures where a variable is sequentially compared to items of an enumeration type, and maps that structure to a Verilog case statement with the appropriate synthesis attributes. Manifesting a case statement for you. ๐Ÿช„

๐Ÿ’– 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 encoding you want in the implementation: binary, one hot, or one cold. The Verilog converter then generates the appropriate code. Pick your aesthetic, bestie. ๐ŸŽ€

๐Ÿ’€ The convertible subset

๐Ÿงš Introduction

Unsurprisingly, not all MyHDL code can be converted to Verilog. Fr fr, there are some very important restrictions. But because the whole point of conversion is implementation, this shouldn't be a big deal: anyone who's familiar with synthesis is already used to similar restrictions in the synthesizable subset of Verilog and VHDL. When the converter runs into a construct it can't convert, it tries to give you clear error messages. No cryptic nonsense. ๐Ÿ™ˆ

In practice, "the synthesizable subset" usually means RTL synthesis, which is by far the most popular type of synthesis today. There are industry standards defining the RTL synthesis subset โ€” but those were not used as the model for the MyHDL converter's restrictions. They were used as a minimal starting point. And on top of that, whenever it was judged easy or useful to support an extra feature, it got supported. For example: it's actually easier to convert while loops than for loops, even though they're not RTL-synthesizable. Another example: print is supported because it's so useful for debugging, even though it's not synthesizable. So in summary? The convertible subset is a superset of the standard RTL synthesis subset, and it supports synthesis tools with more advanced capabilities, like behavioral synthesis. Overachiever behavior. ๐Ÿ’…

And remember: any restrictions only apply to the design post elaboration. In practice that means they apply only to the code of the generators โ€” the leaf functional blocks in a MyHDL design. ๐ŸŒฟ

๐ŸŒˆ Coding style

A natural restriction on convertible code: it should be written in MyHDL style. That means cooperating generators, communicating through signals, with yield statements specifying wait points and resume conditions. The supported resume conditions are a signal edge, a signal change, or a tuple of such conditions. Know the assignment, do the assignment. โœจ

๐Ÿ‘‘ Supported types

The most important restriction is about object types. Verilog is an almost typeless language, while Python is strongly (albeit dynamically) typed. So the converter has to infer the types of the names used in the code, and map those names to Verilog variables. The detective work is real. ๐Ÿ”

Only a limited amount of types can be converted. Python int and long objects get mapped to Verilog integers. Every other supported type gets mapped to Verilog regs (or wires), which means it needs to have a defined bit width. The supported types are:

  • the Python bool type
  • the MyHDL intbv type
  • MyHDL enumeration types returned by function enum

Those last ones can also be used as the base object of a Signal. ๐ŸŽ€

intbv objects must be constructed so that a bit width can be inferred. Two ways to do that. Option one โ€” specify minimum and maximum values, like this:

   index = intbv(0, min=0, max=2**N)

Option two โ€” take a slice from an intbv object, like this:

   index = intbv(0)[N:]

That slice returns a new intbv object with minimum value 0 and maximum value 2**N. Look at her, doing math. ๐Ÿงฎโœจ

๐Ÿฆ‹ Supported statements

Here's the list of statements the Verilog converter supports โ€” possibly with restrictions or usage notes attached, so read the fine print:

  • 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. Dramatic exit, honestly valid. ๐Ÿ’€
  • 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 even try 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, you should run it, preferably using unit testing to verify each feature. Test it, don't assume it. ๐Ÿงช

The same philosophy applies when converting a MyHDL description to Verilog: make sure the simulation runs fine first. The converter checks a lot of things and tries to give clear error messages, but there's no guarantee it does a meaningful job unless the simulation runs fine. Periodt. ๐Ÿ’…

๐ŸŽ€ Conversion output verification

It's always prudent to verify the converted Verilog output. To make that 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 lets you verify the Verilog code with the exact same test bench you used for the MyHDL code. And fun fact: this is also how the Verilog converter development itself gets verified. She practices what she preaches. โœจ

โญ 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. So let's talk about it explicitly, no skipping. ๐Ÿ‘€

Consider these 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. But in Python, that concept of a variable doesn't exist. Instead, assignment merely creates a new binding of a name to a certain object, which replaces any previous binding. So in that example, the name a is bound to a number of different objects in sequence. That's it, that's the whole thing. ๐Ÿคฏ

okay bestie, so the Verilog converter is like the nosy friend who reads all ur receipts ๐Ÿ’… โ€” it has to investigate name assignment and usage in MyHDL code, and then 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. No cap.

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. slay.

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. periodt.


๐Ÿ’– 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. it's giving object mutation realness.

๐Ÿฆ‹ 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. iconic behavior.

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

โœจ Converter usage โœจ

We will demonstrate the conversion process by showing some examples. bestie, let's get into it.

๐ŸŒธ 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. ate and left no crumbs.

๐Ÿ’œ 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. fr fr, it can go 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. the way it flattens everything? obsessed.

๐Ÿ‘‘ Optimizations for finite state machines ๐Ÿ‘‘

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

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, bestie โ€” same ritual, no cap:

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)

And the Verilog output it spits out? It's giving very much hardware, periodt:

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 ๐Ÿซฃ

Okay so this is the part where I have to be honest with you, bestie. These are the little gremlins in the closet. Not my fault, not your fault, just vibes that need a future version to fix them.

  • Negative values of intbv instances are not supported. โ€” The intbv class is fully capable of representing negative values, okay, it's not the class's fault. But 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. Lowkey a "we'll deal with it next semester" situation.
  • 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. Watch out. ๐Ÿ‘€
  • 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. Deprecated = the ick, but it works.
  • 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. Fr fr, unresolved. ๐Ÿ’€

โœจ WHAT'S NEW IN MYHDL 0.3 โœจ

Author Jan Decaluwe


๐ŸŽ€ VCD output for waveform viewing ๐ŸŽ€

๐Ÿ–ผ๏ธ (there were two waveform screenshots right here in the original, bestie โ€” very pretty, very squiggly)

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. Obsessed, honestly. Seeing your signals wiggle is the moment it all clicks. ๐Ÿ’–

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. So it's flexible like that. Slay.

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. Don't come crying to me when your other viewer is confused, bestie. ๐Ÿ˜ญ

๐ŸŒธ 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. It's giving "we deserve better than raw ints." ๐Ÿ’…

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)

Main character state machine energy, literally. ๐Ÿ‘‘


๐Ÿฆ‹ 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. No more forgetting a signal and debugging for three hours, bestie. We love to see it. ๐Ÿ™ˆ

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. Iconic fix, no notes. โœจ

๐Ÿฆ‹ Inferring the list of instances ๐Ÿฆ‹

Okay so, in MyHDL, the instances you define inside a top level function? You gotta return them explicitly, bestie. No sneaking around. Here's the schematic vibe:

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

Now, assembling that list by hand may be convenient... except when it's literally not, especially if there are many instances. So MyHDL 0.3 said "not on my watch" and handed us the instances function ๐Ÿ’…. You use it like this:

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. Every variable that complies with the definition of an instance gets assembled into a list, and that list is what gets returned. Slay. โœจ

๐ŸŒธ Inferring the list of processes ๐ŸŒธ

Besides instances, a top level function may also define local generators functions โ€” which I'm calling processes, because of the analogy with VHDL, periodt. Just like instances, processes need to be returned explicitly, with the qualification that they have to be called first to turn them into generators. Schematic example:

def top(...):
    ...
    def process_1():
        ...
    def process_2():
        ...
    ...
    def process_n():
        ...
    ...
    return process_1(), process_2(), ..., process_n()

Same energy as instances, though: it may be more convenient to assemble that list of processes automatically ๐Ÿซถ.

One option: 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: use the processes function 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. Like so:

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. Glow-up era.

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 MyHDL release, the new features will become required and only Python 2.3 (and higher) will be supported.

  • genindex
  • search

๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

Jan Decaluwe

2019, Jan Decaluwe


And that's a wrap on the MYHDL(1) page, bestie ๐Ÿซถ this was the version dated October 18, 2019, straight from the MYHDL(1) manual itself. Go forth and instantiate responsibly ๐Ÿ’…โœจ

๐Ÿ“ฆ where this page comes from