MEP114
AuthorJan Decaluwe
StatusDraft
Created12-Feb-2016
MyHDL-version1.0

Background

The design approach behind MyHDL is minimalistic: existing Python features are reused as much as possible, and new features are only introduced when deemed strictly necessary. However, the design decisions are not always straightforward, especially because some degree of metaprogramming and introspection is unavoidable in this project.

For example, early on it was considered mandatory to have support for the automatic inference of combinatorial behavior. As a result, a function always_comb() was introduced that converts a function into a generator that is sensitive to its input signals.

Sometimes, new Python concepts greatly facilitate the implementation of certain features. For MyHDL, the most significant example is the introduction of decorators in Python 2.4, as a structured approach for metaprogramming. For example, it was immediately clear that the usage pattern of the always_comb() function matched the decorator pattern exactly. It was therefore a no-brainer to use always_comb as a decorator instead. Subsequently, it made sense to introduce a number of additional decorators.

The feature of concern in this MEP is hierarchy extraction. Here, the historical design decision was to try to make it transparent to the user. In particular, instead of introducing specific data structures to facilitate the task, the Python profiler was reused in the background for the purpose. Over time, it has become clear that the approach suffers from a number of significant disadvantages. In this MEP, the problems are discussed and a solution is proposed.

The problem with hierarchy extraction

Using the Python profiler for hierarchy extraction originally seemed like an elegant hack. As MyHDL uses functions to describe hardware building blocks, and as the profiler tracks function calls, the match seemed obvious. However, the approach has several disadvantages:

  • Non-intuitive API As the hardware data structure must be built under profiler control, the API functions involved need the elements to build it instead of the data structure itself. For example, instead of: toVerilog(top(*args, **kwargs)) you have to use toVerilog(top, *args, **kwargs).

  • Cumbersome error checking A meaningful hardware hierarchy implies certain restrictions on the data structure. However, when using the profiler, data structure building and extraction occur at the same time. In practice, this makes it hard to detect certain data structure issues, resulting in cumbersome errors.

  • Normal profiler tasks disabled As the profiler is reused for hierarchy extraction, it cannot be used for its normal purposes, such as debugging, during this process. Understandably, this is unexpected and surprising.

  • Hard-to-maintain code Last but not least, the hierarchy extraction code has proven to be complex and hard to maintain. It is a part of MyHDL that hampers progress.

As a result, hierarchy extraction is the part where MyHDL feels most "brittle", as reported by users. Therefore, there is a need for a more explicit solution that overcomes these issues and improves robustness.

The block decorator

Python decorators have proven their value as a solution for metaprogramming, in general and in MyHDL in particular. The proposal is therefore to introduce a new block decorator, to decorate functions that describe hardware blocks.

Like before, a hardware description function should return instantiations of subblocks and/or local generators. However, the block decorator can check the return values, and encapsulate them in a dedicated object.

The advantages of this approach are as follows:

  • Issues will be flagged upfront, so that the resulting hardware data structure will be well-formed.

  • Hardware extraction becomes a separate, subsequent task that is guaranteed to succeed. It can be done using a classical data structure traversal, so that the profiler is no longer necessary. It is clear that the hierarchy extraction code will be simplified significantly.

  • The API can be simplified by directly passing the hardware data structure to the functions involved.

  • Functions intended for hardware description will clearly stand out, resulting in clearer code.

Implementation

Under the hood, the functionality is implemented using two classes: _Block and _BlockInstance. The block decorator creates a _Block object with the decorated function as its parameter. It has a call interface that creates _BlockInstance objects.

A _BlockInstance object calls the original function with the actual parameters upon construction. That call returns the subinstances of the object, which can then be verified. A _BlockInstance object also maintains the namespaces for signals and lists of signals.

Backwards compatibility issues

The proposed solution has a significant disadvantage: it introduces some backwards incompatible changes and a new method-based API for simulation and conversion.

Let us first make clear what does not change: For simulation, old code will continue to work as before. All the simulator needs is a structure of communicating generators, regardless of how it is built. That having said, the new method-based API also contains simulation methods. Although the Simulation() class will still be available, it is probably easier to use the method-based API in most cases.

The block decorator will become mandatory when hardware extraction is required, more specifically for waveform tracing and conversion. The required user code change is minimal:

       old code               ->            new code
------------------------------------------------------------------
                                        @block
def myblock(<ports>):                   def myblock(<ports>):
    ...                       ->            ...
    return <instances>                      return <instances>

However, the block decorator will be "viral": when used on a function, it must be used on the functions that describe subblocks too, or else an error will occur.

New API

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

This work is inspired by the related work on uhdl.

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

The API looks as follows:

inst.run_sim(duration=None)

Run a simulation "forever" (default) or for a specified duration.

inst.config_sim(backend='myhdl', trace=False)

Optional simulation configuration.

  • backend: Defaults to 'myhdl'.
  • trace: Enable waveform tracing, default False.
inst.quit_sim()

Quit an active simulation. This is method is currently required because only a single simulation can be active.

inst.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.
inst.verify_convert()

Verify conversion output, by comparing target HDL simulation log with MyHDL simulation log.

inst.analyze_convert()

Analyze conversion output by compilation with target HDL compiler.

Instance naming

A visible change in the output will be the naming of instances. Previously, their names were based on local variable names in the instantiating function. However, because instantiation no longer occurs under fine-grained profiler control, those local variables names can no longer be used.

Instead, the basename of an instance will be synthesized automatically from the name of the corresponding function and an instantiation counter as follows:

instance_name = '{function_name}_{instance_count}'

An advantage of this approach is that it is no longer necessary to give instances a local name: anonymous instantiation will work too. A disadvantage is of course that the local variable name, which may be clearer and shorter than the synthesized name, is not present in the output. To accommodate this disadvantage, explicit naming control is possible as follows:

inst = myblock(<signals>)
inst.name = "myinst"

Note that explicit naming is always optional: it is purely a convenience to have clearer output.

Also note that in the previous discussion "name" refers to the basename of the instance. The eventual name in the conversion output will contain a hierarchical prefix.

Introduction path

The changes are significant but are expected to result in important benefits, including facilitating new development considerably. The proposal is therefore to move carefully but swiftly. In particular, there should be one release that supports both the old and the new API. When the old API is used, a deprecation warning will be issued to encourage users to switch to the new API.

Why is it called block?

The name block was chosen because the original choice module is the name of a first-class Python object with a completely different meaning. It was felt that this would cause too much confusion and ambiguity.

Other names were considered by the community. A name referring to hardware was rejected because it was felt that this would be too restrictive with regard to MyHDL's capabilities. There was a clear preference for a more generic name without a strongly different meaning in other HDLs, like unit or block. Eventually block was chosen because it is sometimes already used as a synonym to (hardware) module, like in subblocks or top block. Also, the commonly used term block diagram basically refers to the same concept.

For consistency, an attempt will be made to prefer using block instead of module in MyHDL terminology, in particular in the documentation.

Status

The functionality described in this MEP is under development in a feature branch mep-114. Interested users are encouraged to review, test and provide feedback.