We will introduce MyHDL with a classic Hello World style example. All example code can be found in the distribution directory under example/manual/. Here are the contents of a MyHDL simulation script called hello1.py:
from myhdl import Signal, delay, always, now, Simulation
def HelloWorld():
interval = delay(10)
@always(interval)
def sayHello():
print "%s Hello World!" % now()
return sayHello
inst = HelloWorld()
sim = Simulation(inst)
sim.run(30)
When we run this simulation, we get the following output:
% python hello1.py
10 Hello World!
20 Hello World!
30 Hello World!
_SuspendSimulation: Simulated 30 timesteps
The first line of the script imports a number of objects from the myhdl package. In Python we can only use identifiers that are literally defined in the source file [1].
Then, we define a function called HelloWorld(). In MyHDL, hardware modules can be modeled using classic functions. In particular, the parameter list is then used to define the interface. In this first example, the interface is empty.
Inside the top level function we declare a local function called sayHello() that defines the desired behavior. This function is decorated with an always() decorator that has a delay object as its parameter. The meaning is that the function will be executed whenever the specified delay interval has expired.
Behind the curtains, the always() decorator creates a Python generator and reuses the name of the decorated function for it. Generators are the fundamental objects in MyHDL, and we will say much more about them further on.
Finally, the top level function returns the local generator. This is the simplest case of the basic MyHDL code pattern to define the contents of a hardware module. We will describe the general case further on.
In MyHDL, we create an instance of a hardware module by calling the corresponding function. In the example, variable inst refers to an instance of HelloWorld(). To simulate the instance, we pass it as an argument to a Simulation object constructor. We then run the simulation for the desired amount of timesteps.
In the previous section, we simulated a design with a single generator and no concurrency. Real hardware descriptions are typically massively concurrent. MyHDL supports this by allowing an arbitrary number of concurrently running generators.
With concurrency comes the problem of deterministic communication. Hardware languages use special objects to support deterministic communication between concurrent code. In particular, MyHDL has a Signal object which is roughly modeled after VHDL signals.
We will demonstrate signals and concurrency by extending and modifying our first example. We define two hardware modules, one that drives a clock signal, and one that is sensitive to a positive edge on a clock signal:
from myhdl import Signal, delay, always, now, Simulation
def ClkDriver(clk):
halfPeriod = delay(10)
@always(halfPeriod)
def driveClk():
clk.next = not clk
return driveClk
def HelloWorld(clk):
@always(clk.posedge)
def sayHello():
print "%s Hello World!" % now()
return sayHello
clk = Signal(0)
clkdriver_inst = ClkDriver(clk)
hello_inst = HelloWorld(clk)
sim = Simulation(clkdriver_inst, hello_inst)
sim.run(50)
The clock driver function ClkDriver() has a clock signal as its parameter. This is how a port is modeled in MyHDL. The function defines a generator that continuously toggles a clock signal after a certain delay. A new value of a signal is specified by assigning to its next attribute. This is the MyHDL equivalent of the VHDL signal assignment and the Verilog non-blocking assignment.
The HelloWorld() function is modified from the first example. It now also takes a clock signal as parameter. Its generator is made sensitive to a rising edge of the clock signal. This is 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. When creating an instance of each hardware module, the same clock signal is passed as the argument. The result is that the instances are now connected through the clock signal. The Simulation object is constructed with the two instances.
When we run the simulation, we get:
% python hello2.py
10 Hello World!
30 Hello World!
50 Hello World!
_SuspendSimulation: Simulated 50 timesteps
We have seen that MyHDL uses functions to model hardware modules. We have also seen that ports are modeled by using signals as parameters. To make designs reusable we will also want to use other objects as parameters. For example, we can change the clock generator function to make it more general and reusable, by making the clock period parametrizable, as follows:
from myhdl import Signal, delay, instance, always, now, Simulation
def ClkDriver(clk, period=20):
lowTime = int(period/2)
highTime = period - lowTime
@instance
def driveClk():
while True:
yield delay(lowTime)
clk.next = 1
yield delay(highTime)
clk.next = 0
return driveClk
In addition to the clock signal, the clock period is a parameter, with a default value of 20.
As the low time of the clock may differ from the high time in case of an odd period, we cannot use the always() decorator with a single delay value anymore. Instead, the driveClk() function is now a generator function with an explicit definition of the desired behavior. It is decorated with the instance() decorator. You can see that driveClk() is a generator function because it contains yield statements.
When a generator function is called, it returns a generator object. This is basically what the instance() decorator does. It is less sophisticated than the always() decorator, but it can be used to create a generator from any local generator function.
The yield statement is a general Python construct, but MyHDL uses it in a dedicated way. In MyHDL, it has a similar meaning as the wait statement in VHDL: the statement suspends execution of a generator, and its clauses specify the conditions on which the generator should wait before resuming. In this case, the generator waits for a certain delay.
Note that to make sure that the generator runs “forever”, we wrap its behavior in a while True loop.
Similarly, we can define a general Hello() function as follows:
def Hello(clk, to="World!"):
@always(clk.posedge)
def sayHello():
print "%s Hello %s" % (now(), to)
return sayHello
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:
def greetings():
clk1 = Signal(0)
clk2 = Signal(0)
clkdriver_1 = ClkDriver(clk1) # positional and default association
clkdriver_2 = ClkDriver(clk=clk2, period=19) # named association
hello_1 = Hello(clk=clk1) # named and default association
hello_2 = Hello(to="MyHDL", clk=clk2) # named association
return clkdriver_1, clkdriver_2, hello_1, hello_2
inst = greetings()
sim = Simulation(inst)
sim.run(50)
As in standard Python, positional or named parameter association can be used in instantiations, or a mix of both [2]. All these styles are demonstrated in the example above. Named association can be very useful if there are a lot of parameters, as the argument order in the call does not matter in that case.
The simulation produces the following output:
% python greetings.py
9 Hello MyHDL
10 Hello World!
28 Hello MyHDL
30 Hello World!
47 Hello MyHDL
50 Hello World!
_SuspendSimulation: Simulated 50 timesteps
Warning
Some commonly used terminology has different meanings in Python versus hardware design. Rather than artificially changing terminology, I think it’s best to keep it and explicitly describing the differences.
A module in Python refers to all source code in a particular file. A module can be reused by other modules by importing. In hardware design, a module is a reusable block of hardware with a well defined interface. It can be reused in another module 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 module.
Normally, the meaning should be clear from the context. Occasionally, I may qualify terms with the words “hardware” or “MyHDL” to avoid ambiguity.
To conclude this introductory chapter, it is useful to stress that MyHDL is not a language in itself. The underlying language is Python, and MyHDL is implemented as a Python package called myhdl. Moreover, it is a design goal to keep the myhdl package as minimalistic as possible, so that MyHDL descriptions are very much “pure Python”.
To have Python as the underlying language is significant in several ways:
Here is an overview of what we have learned in this chapter:
These concepts are sufficient to start modeling and simulating with MyHDL.
However, there is much more to MyHDL. Here is an overview of what can be learned from the following chapters:
Footnotes
[1] | The exception is the from module import * syntax, that imports all the symbols from a module. Although this is generally considered bad practice, it can be tolerated for large modules that export a lot of symbols. One may argue that myhdl falls into that category. |
[2] | All positional parameters have to go before any named parameter. |