MEP | 114 |
Author | Jan Decaluwe |
Status | Draft |
Created | 12-Feb-2016 |
MyHDL-version | 1.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 usetoVerilog(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 toself.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.