mirror of
https://github.com/myhdl/myhdl.git
synced 2024-12-14 07:44:38 +08:00
1198 lines
32 KiB
TeX
1198 lines
32 KiB
TeX
\chapter{Modeling techniques \label{model}}
|
|
|
|
\section{Structural modeling \label{model-structure}}
|
|
\index{modeling!structural}
|
|
|
|
Hardware descriptions need to support the concepts of module
|
|
instantiation and hierarchy. In \myhdl{}, an instance is recursively
|
|
defined as being either a sequence of instances, or a generator.
|
|
Hierarchy is modeled by defining instances in a higher-level
|
|
function, and returning them. The following is a schematic example
|
|
of the basic case.
|
|
|
|
\begin{verbatim}
|
|
def top(...):
|
|
...
|
|
instance_1 = module_1(...)
|
|
instance_2 = module_2(...)
|
|
...
|
|
instance_n = module_n(...)
|
|
...
|
|
return instance_1, instance_2, ... , instance_n
|
|
\end{verbatim}
|
|
|
|
Note that \myhdl\ uses conventional procedural techniques
|
|
for modeling structure. This makes it straightforward
|
|
to model more complex cases.
|
|
|
|
\subsection{Conditional instantiation \label{model-conf}}
|
|
\index{conditional instantiation}
|
|
|
|
To model conditional instantiation, we can
|
|
select the returned instance under parameter control.
|
|
For example:
|
|
|
|
\begin{verbatim}
|
|
SLOW, MEDIUM, FAST = range(3)
|
|
|
|
def top(..., speed=SLOW):
|
|
...
|
|
def slowAndSmall():
|
|
...
|
|
...
|
|
def fastAndLarge():
|
|
...
|
|
if speed == SLOW:
|
|
return slowAndSmall()
|
|
elif speed == FAST:
|
|
return fastAndLarge()
|
|
else:
|
|
raise NotImplementedError
|
|
\end{verbatim}
|
|
|
|
|
|
\subsection{Arrays of instances \label{model-instarray}}
|
|
\index{arrays of instances}
|
|
|
|
Python lists are easy to create. We can use them
|
|
to model arrays of instances.
|
|
|
|
Suppose we have a top module that instantiates a
|
|
single \code{channel} submodule, as follows:
|
|
|
|
\begin{verbatim}
|
|
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
|
|
\end{verbatim}
|
|
|
|
If we wanted to support an arbitrary number of channels,
|
|
we can use lists of signals and a list of instances,
|
|
as follows:
|
|
|
|
\begin{verbatim}
|
|
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
|
|
\end{verbatim}
|
|
|
|
\subsection{Inferring the list of instances \label{model-infer-instlist}}
|
|
|
|
In \myhdl{}, instances have to be returned explicitly by
|
|
a top level function. It may be convenient to assemble
|
|
the list of instances automatically. For this purpose,
|
|
\myhdl provides the function \function{instances()}.
|
|
Using the first example in this section, it is used as follows:
|
|
|
|
\begin{verbatim}
|
|
from myhdl import instances
|
|
|
|
def top(...):
|
|
...
|
|
instance_1 = module_1(...)
|
|
instance_2 = module_2(...)
|
|
...
|
|
instance_n = module_n(...)
|
|
...
|
|
return instances()
|
|
\end{verbatim}
|
|
|
|
Function \function{instances()} uses introspection to inspect the type
|
|
of the local variables defined by the calling function. All variables
|
|
that comply with the definition of an instance are assembled in
|
|
a list, and that list is returned.
|
|
|
|
\section{RTL modeling \label{model-rtl}}
|
|
\index{modeling!RTL style}
|
|
|
|
The present section describes how \myhdl\ supports RTL style modeling
|
|
as is typically used for synthesizable models.
|
|
|
|
\subsection{Combinatorial logic \label{model-comb}}
|
|
\index{combinatorial logic}
|
|
|
|
\subsubsection{Template \label{model-comb-templ}}
|
|
|
|
Combinatorial logic is described with a code pattern as
|
|
follows:
|
|
|
|
\begin{verbatim}
|
|
def top(<parameters>):
|
|
...
|
|
@always_comb
|
|
def combLogic():
|
|
<functional code>
|
|
...
|
|
return combLogic, ...
|
|
\end{verbatim}
|
|
|
|
The \function{always_comb} decorator describes
|
|
combinatorial logic.
|
|
\footnote{The name \function{always_comb} refers to a construct with
|
|
similar semantics in SystemVerilog.}.
|
|
The decorated function is a local function that specifies what
|
|
happens when one of the input signals of the logic changes. The
|
|
\function{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.
|
|
|
|
|
|
\subsubsection{Example \label{model-comb-ex}}
|
|
|
|
The following is an example of a combinatorial multiplexer:
|
|
|
|
\begin{verbatim}
|
|
from myhdl import Signal, Simulation, delay, always_comb
|
|
|
|
def Mux(z, a, b, sel):
|
|
|
|
""" Multiplexer.
|
|
|
|
z -- mux output
|
|
a, b -- data inputs
|
|
sel -- control input: select a if asserted, otherwise b
|
|
|
|
"""
|
|
|
|
@always_comb
|
|
def muxLogic():
|
|
if sel == 1:
|
|
z.next = a
|
|
else:
|
|
z.next = b
|
|
|
|
return muxLogic
|
|
\end{verbatim}
|
|
|
|
|
|
To verify it, we will simulate the logic with some random patterns. The
|
|
\code{random} module in Python's standard library comes in handy for
|
|
such purposes. The function \code{randrange(\var{n})} returns a random
|
|
natural integer smaller than \var{n}. It is used in the test bench
|
|
code to produce random input values:
|
|
|
|
\begin{verbatim}
|
|
from random import randrange
|
|
|
|
z, a, b, sel = [Signal(0) for i in range(4)]
|
|
|
|
mux_1 = Mux(z, a, b, sel)
|
|
|
|
def test():
|
|
print "z a b sel"
|
|
for i in range(8):
|
|
a.next, b.next, sel.next = randrange(8), randrange(8), randrange(2)
|
|
yield delay(10)
|
|
print "%s %s %s %s" % (z, a, b, sel)
|
|
|
|
test_1 = test()
|
|
|
|
sim = Simulation(mux_1, test_1)
|
|
sim.run()
|
|
\end{verbatim}
|
|
|
|
Because of the randomness, the simulation output varies between runs
|
|
\footnote{It also possible to have a reproducible random output, by
|
|
explicitly providing a seed value. See the documentation of the
|
|
\code{random} module.}. One particular run produced the following
|
|
output:
|
|
|
|
\begin{verbatim}
|
|
% python mux.py
|
|
z a b sel
|
|
6 6 1 1
|
|
7 7 1 1
|
|
7 3 7 0
|
|
1 2 1 0
|
|
7 7 5 1
|
|
4 7 4 0
|
|
4 0 4 0
|
|
3 3 5 1
|
|
StopSimulation: No more events
|
|
\end{verbatim}
|
|
|
|
|
|
\subsection{Sequential logic \label{model-seq}}
|
|
\index{sequential logic}
|
|
|
|
\subsubsection{Template \label{model-seq-templ}}
|
|
Sequential RTL models are sensitive to a clock edge. In addition, they
|
|
may be sensitive to a reset signal. We will describe one of the most
|
|
common patterns: a template with a rising clock edge and an
|
|
asynchronous reset signal. Other templates are similar.
|
|
|
|
\begin{verbatim}
|
|
def top(<parameters>, clock, ..., reset, ...):
|
|
...
|
|
@always(clock.posedge, reset.negedge)
|
|
def seqLogic():
|
|
if reset == <active level>:
|
|
<reset code>
|
|
else:
|
|
<functional code>
|
|
...
|
|
return seqLogic, ...
|
|
\end{verbatim}
|
|
|
|
|
|
\subsubsection{Example \label{model-seq-ex}}
|
|
The following code is a description of an incrementer with enable, and
|
|
an asynchronous reset.
|
|
|
|
\begin{verbatim}
|
|
from random import randrange
|
|
from myhdl import *
|
|
|
|
ACTIVE_LOW, INACTIVE_HIGH = 0, 1
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
@always(clock.posedge, reset.negedge)
|
|
def incLogic():
|
|
if reset == ACTIVE_LOW:
|
|
count.next = 0
|
|
else:
|
|
if enable:
|
|
count.next = (count + 1) % n
|
|
|
|
return incLogic
|
|
\end{verbatim}
|
|
|
|
For the test bench, we will use an independent clock generator, stimulus
|
|
generator, and monitor. After applying enough stimulus patterns, we
|
|
can raise the \code{StopSimulation} exception to stop the
|
|
simulation run. The test bench for a small incrementer and a small
|
|
number of patterns is a follows:
|
|
|
|
\begin{verbatim}
|
|
def testbench():
|
|
count, enable, clock, reset = [Signal(intbv(0)) for i in range(4)]
|
|
|
|
inc_1 = Inc(count, enable, clock, reset, n=4)
|
|
|
|
HALF_PERIOD = delay(10)
|
|
|
|
@always(HALF_PERIOD)
|
|
def clockGen():
|
|
clock.next = not clock
|
|
|
|
@instance
|
|
def stimulus():
|
|
reset.next = ACTIVE_LOW
|
|
yield clock.negedge
|
|
reset.next = INACTIVE_HIGH
|
|
for i in range(12):
|
|
enable.next = min(1, randrange(3))
|
|
yield clock.negedge
|
|
raise StopSimulation
|
|
|
|
@instance
|
|
def monitor():
|
|
print "enable count"
|
|
yield reset.posedge
|
|
while 1:
|
|
yield clock.posedge
|
|
yield delay(1)
|
|
print " %s %s" % (enable, count)
|
|
|
|
return clockGen, stimulus, inc_1, monitor
|
|
|
|
|
|
tb = testbench()
|
|
|
|
def main():
|
|
Simulation(tb).run()
|
|
\end{verbatim}
|
|
|
|
The simulation produces the following output:
|
|
\begin{verbatim}
|
|
% python inc.py
|
|
enable count
|
|
0 0
|
|
1 1
|
|
0 1
|
|
1 2
|
|
1 3
|
|
1 0
|
|
0 0
|
|
1 1
|
|
0 1
|
|
0 1
|
|
0 1
|
|
1 2
|
|
StopSimulation
|
|
\end{verbatim}
|
|
|
|
|
|
\subsection{Finite State Machine modeling \label{model-fsm}}
|
|
\index{modeling!Finite State Machine}
|
|
|
|
Finite State Machine (FSM) modeling is very common in RTL
|
|
design and therefore deserves special attention.
|
|
|
|
For code clarity, the state values are typically represented by a set
|
|
of identifiers. A standard Python idiom for this purpose is to assign
|
|
a range of integers to a tuple of identifiers, like so:
|
|
|
|
\begin{verbatim}
|
|
>>> SEARCH, CONFIRM, SYNC = range(3)
|
|
>>> CONFIRM
|
|
1
|
|
\end{verbatim}
|
|
|
|
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
|
|
\emph{enumeration type}.
|
|
|
|
\myhdl\ supports enumeration types by providing a function
|
|
\function{enum()}. The arguments to \function{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:
|
|
|
|
\begin{verbatim}
|
|
>>> from myhdl import enum
|
|
>>> t_State = enum('SEARCH', 'CONFIRM', 'SYNC')
|
|
>>> t_State
|
|
<Enum: SEARCH, CONFIRM, SYNC>
|
|
>>> t_State.CONFIRM
|
|
CONFIRM
|
|
\end{verbatim}
|
|
|
|
We can use this type to construct a state signal as follows:
|
|
|
|
\begin{verbatim}
|
|
state = Signal(t_State.SEARCH)
|
|
\end{verbatim}
|
|
|
|
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 \code{syncFlag} signal. When
|
|
found, the FSM moves from the initial \code{SEARCH} state to the
|
|
\code{CONFIRM} state. When the \code{syncFlag}
|
|
is confirmed on the expected position, the FSM declares \code{SYNC},
|
|
otherwise it falls back to the \code{SEARCH} state. This FSM can be
|
|
coded as follows:
|
|
|
|
\begin{verbatim}
|
|
from myhdl import *
|
|
|
|
ACTIVE_LOW = 0
|
|
FRAME_SIZE = 8
|
|
t_State = enum('SEARCH', 'CONFIRM', 'SYNC')
|
|
|
|
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(0) # 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
|
|
\end{verbatim}
|
|
|
|
At this point, we will use the example to demonstrate
|
|
the \myhdl\ support for
|
|
\index{waveform viewing}%
|
|
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 \program{gtkwave}.
|
|
|
|
The user interface of this feature consists of a single function,
|
|
\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:
|
|
|
|
\begin{verbatim}
|
|
tb_fsm = testbench()
|
|
\end{verbatim}
|
|
|
|
To enable VCD tracing, the instance should be created as follows
|
|
instead:
|
|
|
|
\begin{verbatim}
|
|
tb_fsm = traceSignals(testbench)
|
|
\end{verbatim}
|
|
|
|
Note that the first argument of
|
|
\function{traceSignals()} consists of the uncalled function. By
|
|
calling the function under its control, \function{traceSignals()}
|
|
gathers information about the hierarchy and the signals to be traced.
|
|
In addition to a function argument, \function{traceSignals()} accepts
|
|
an arbitrary number of non-keyword and keyword arguments that will be
|
|
passed to the function call.
|
|
|
|
A small test bench for our framing controller example,
|
|
with signal tracing enabled, is shown below:
|
|
|
|
\begin{verbatim}
|
|
def testbench():
|
|
|
|
SOF = Signal(bool(0))
|
|
syncFlag = Signal(bool(0))
|
|
clk = Signal(bool(0))
|
|
reset_n = Signal(bool(1))
|
|
state = Signal(t_State.SEARCH)
|
|
|
|
framectrl = FramerCtrl(SOF, state, syncFlag, clk, reset_n)
|
|
|
|
@always(delay(10))
|
|
def clkgen():
|
|
clk.next = not clk
|
|
|
|
@instance
|
|
def stimulus():
|
|
for i in range(3):
|
|
yield clk.posedge
|
|
for n in (12, 8, 8, 4):
|
|
syncFlag.next = 1
|
|
yield clk.posedge
|
|
syncFlag.next = 0
|
|
for i in range(n-1):
|
|
yield clk.posedge
|
|
raise StopSimulation
|
|
|
|
return framectrl, clkgen, stimulus
|
|
|
|
|
|
tb_fsm = traceSignals(testbench)
|
|
sim = Simulation(tb_fsm)
|
|
sim.run()
|
|
\end{verbatim}
|
|
|
|
When we run the test bench, it generates a VCD file
|
|
called \file{testbench.vcd}. When we load this file into
|
|
\program{gtkwave}, we can view the waveforms:
|
|
|
|
\ifpdf
|
|
\includegraphics{tbfsm.png}
|
|
\fi
|
|
|
|
Signals are dumped in a suitable format. This format is inferred at
|
|
the \class{Signal} construction time, from the type of the initial
|
|
value. In particular, \class{bool} signals are dumped as single
|
|
bits. (This only works starting with Python~2.3, when \class{bool} has
|
|
become a separate type). Likewise, \class{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 \function{str()} function.
|
|
|
|
\begin{notice}[warning]
|
|
Support for literal string representations is not part of the VCD
|
|
standard. It is specific to \program{gtkwave}. To generate a
|
|
standard VCD file, you need to use signals with a defined bit width
|
|
only.
|
|
\end{notice}
|
|
|
|
|
|
\section{High level modeling \label{model-hl}}
|
|
\index{modeling!high level}
|
|
|
|
\subsection{Modeling with bus-functional procedures \label{model-bfm}}
|
|
|
|
\index{bus-functional procedure}%
|
|
A \dfn{bus-functional procedure} is a reusable encapsulation of the
|
|
low-level operations needed to implement some abstract transaction on
|
|
a physical interface. Bus-functional procedures are typically used in
|
|
flexible verification environments.
|
|
|
|
Once again, \myhdl\ uses generator functions to support
|
|
bus-functional procedures. In \myhdl\, the difference between
|
|
instances and bus-functional procedure calls comes from the way in
|
|
which a generator function is used.
|
|
|
|
As an example, we will design a bus-functional procedure of a
|
|
simplified UART transmitter. We assume 8 data bits, no parity bit, and
|
|
a single stop bit, and we add print statements to follow the
|
|
simulation behavior:
|
|
|
|
\begin{verbatim}
|
|
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)
|
|
\end{verbatim}
|
|
|
|
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:
|
|
|
|
|
|
\begin{verbatim}
|
|
testvals = (0xc5, 0x3a, 0x4b)
|
|
|
|
def stimulus():
|
|
tx = Signal(1)
|
|
for val in testvals:
|
|
txData = intbv(val)
|
|
yield rs232_tx(tx, txData)
|
|
\end{verbatim}
|
|
|
|
We use the bus-functional procedure call as a clause in a
|
|
\code{yield} statement. This introduces a fourth form of the
|
|
\code{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
|
|
\index{wait!for the completion of a generator}%
|
|
wait for the completion of a generator.
|
|
In this case, the original generator resumes when the
|
|
\code{rs232_tx(tx, txData)} generator returns.
|
|
|
|
When simulating this, we get:
|
|
|
|
\begin{verbatim}
|
|
-- 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
|
|
...
|
|
\end{verbatim}
|
|
|
|
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 \code{yield}
|
|
statement.
|
|
|
|
Until now, the \code{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
|
|
\index{sensitivity list}%
|
|
sensitivity lists in Verilog and VHDL.
|
|
|
|
For example, suppose we want to design an UART receive procedure with
|
|
a timeout. We can specify the timeout condition while waiting for the
|
|
start bit, as in the following generator function:
|
|
|
|
\begin{verbatim}
|
|
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)
|
|
\end{verbatim}
|
|
|
|
If the timeout condition is triggered, the receive bit \code{rx}
|
|
will still be \code{1}. In that case, we raise an exception to stop
|
|
the simulation. The \code{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 \code{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:
|
|
|
|
\begin{verbatim}
|
|
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)
|
|
\end{verbatim}
|
|
|
|
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:
|
|
|
|
\begin{verbatim}
|
|
-- 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
|
|
...
|
|
\end{verbatim}
|
|
|
|
For completeness, we will verify the timeout behavior with a test
|
|
bench that disconnects the \code{rx} from the \code{tx} signal, and we
|
|
specify a small timeout for the receive procedure:
|
|
|
|
\begin{verbatim}
|
|
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)
|
|
\end{verbatim}
|
|
|
|
The simulation now stops with a timeout exception after a few
|
|
transmit cycles:
|
|
|
|
\begin{verbatim}
|
|
-- Transmitting 0xc5 --
|
|
TX: start bit
|
|
TX: 1
|
|
TX: 0
|
|
TX: 1
|
|
StopSimulation: RX time out error
|
|
\end{verbatim}
|
|
|
|
Recall that the original generator resumes as soon as one of the
|
|
forked generators returns. In the previous cases, this is just fine,
|
|
as the transmitter and receiver generators run in lockstep. However,
|
|
it may be desirable to resume the caller only when \emph{all} of the
|
|
forked generators have finished. For example, suppose that we want to
|
|
characterize the robustness of the transmitter and receiver design to
|
|
bit duration differences. We can adapt our test bench as follows, to
|
|
run the transmitter at a faster rate:
|
|
|
|
\begin{verbatim}
|
|
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)
|
|
\end{verbatim}
|
|
|
|
Simulating this shows how the transmission of the new byte starts
|
|
before the previous one is received, potentially creating additional
|
|
transmission errors:
|
|
|
|
\begin{verbatim}
|
|
-- 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
|
|
\end{verbatim}
|
|
|
|
It is more likely that we want to characterize the design on a byte
|
|
by byte basis, and align the two generators before transmitting each
|
|
byte. In \myhdl{}, this is done with the \function{join} function. By
|
|
joining clauses together in a \code{yield} statement, we create a new
|
|
clause that triggers only when all of its clause arguments have
|
|
triggered. For example, we can adapt the test bench as follows:
|
|
|
|
\begin{verbatim}
|
|
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))
|
|
\end{verbatim}
|
|
|
|
Now, transmission of a new byte only starts when the previous one is received:
|
|
|
|
\begin{verbatim}
|
|
-- 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
|
|
\end{verbatim}
|
|
|
|
|
|
|
|
\subsection{Modeling memories with built-in types \label{model-mem}}
|
|
\index{modeling!memories}
|
|
|
|
Python has powerful built-in data types that can be useful to model
|
|
hardware memories. This can be merely a matter of putting an interface
|
|
around some data type operations.
|
|
|
|
For example, a \dfn{dictionary} comes in handy to model sparse memory
|
|
structures. (In other languages, this data type is called
|
|
\dfn{associative array}, or \dfn{hash table}.) A sparse memory is one in
|
|
which only a small part of the addresses is used in a particular
|
|
application or simulation. Instead of statically allocating the full
|
|
address space, which can be large, it is better to dynamically
|
|
allocate the needed storage space. This is exactly what a dictionary
|
|
provides. The following is an example of a sparse memory model:
|
|
|
|
\begin{verbatim}
|
|
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
|
|
\end{verbatim}
|
|
|
|
Note how we use the \code{val} attribute of the \code{din} signal, as
|
|
we don't want to store the signal object itself, but its current
|
|
value. Similarly, we use the \code{val} attribute of the \code{addr}
|
|
signal as the dictionary key.
|
|
|
|
In many cases, \myhdl\ code uses a signal's current value
|
|
automatically when there is no ambiguity: for example, when a signal
|
|
is used in an expression. However, in other cases such as in this
|
|
example you have to refer to the value explicitly: for example, when
|
|
the Signal is used as an index, or when it is not used in an
|
|
expression. One option is to use the \code{val} attribute, as in this
|
|
example. Another possibility is to use the \code{int()} or
|
|
\code{bool()} functions to typecast the Signal to an integer or a
|
|
boolean value. These functions are also useful with \class{intbv}
|
|
objects.
|
|
|
|
As a second example, we will demonstrate how to use a list to model a
|
|
synchronous fifo:
|
|
|
|
\begin{verbatim}
|
|
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
|
|
\end{verbatim}
|
|
|
|
Again, the model is merely a \myhdl\ interface around some operations
|
|
on a list: \function{insert()} to insert entries, \function{pop()} to
|
|
retrieve them, and \function{len()} to get the size of a Python
|
|
object.
|
|
|
|
\subsection{Modeling errors using exceptions \label{model-err}}
|
|
|
|
In the previous section, we used Python data types for modeling. If
|
|
such a type is used inappropriately, Python's run time error system
|
|
will come into play. For example, if we access an address in the
|
|
\function{sparseMemory} model that was not initialized before, we will
|
|
get a traceback similar to the following (some lines omitted for
|
|
clarity):
|
|
|
|
\begin{verbatim}
|
|
Traceback (most recent call last):
|
|
...
|
|
File "sparseMemory.py", line 31, in access
|
|
dout.next = memory[addr.val]
|
|
KeyError: Signal(51)
|
|
\end{verbatim}
|
|
|
|
Similarly, if the \code{fifo} is empty, and we attempt to read from
|
|
it, we get:
|
|
|
|
\begin{verbatim}
|
|
Traceback (most recent call last):
|
|
...
|
|
File "fifo.py", line 34, in fifo
|
|
dout.next = memory.pop()
|
|
IndexError: pop from empty list
|
|
\end{verbatim}
|
|
|
|
Instead of these low level errors, it may be preferable to define
|
|
errors at the functional level. In Python, this is typically done by
|
|
defining a custom \code{Error} exception, by subclassing the standard
|
|
\code{Exception} class. This exception is then raised explicitly when
|
|
an error condition occurs.
|
|
|
|
For example, we can change the \function{sparseMemory} function as
|
|
follows (with the doc string is omitted for brevity):
|
|
|
|
\begin{verbatim}
|
|
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
|
|
|
|
\end{verbatim}
|
|
|
|
This works by catching the low level data type exception, and raising
|
|
the custom exception with an appropriate error message instead. If
|
|
the \function{sparseMemory} function is defined in a module with the
|
|
same name, an access error is now reported as follows:
|
|
|
|
\begin{verbatim}
|
|
Traceback (most recent call last):
|
|
...
|
|
File "sparseMemory.py", line 61, in access
|
|
raise Error, "Uninitialized address %s" % hex(addr)
|
|
Error: Uninitialized address 0x33
|
|
|
|
\end{verbatim}
|
|
|
|
Likewise, the \function{fifo} function can be adapted as follows, to
|
|
report underflow and overflow errors:
|
|
|
|
\begin{verbatim}
|
|
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
|
|
\end{verbatim}
|
|
|
|
In this case, the underflow error is detected as before, by catching a
|
|
low level exception on the list data type. On the other hand, the
|
|
overflow error is detected by a regular check on the length of the
|
|
list.
|
|
|
|
|
|
\subsection{Object oriented modeling \label{model-obj}}
|
|
\index{modeling!object oriented}
|
|
|
|
The models in the previous sections used high-level built-in data
|
|
types internally. However, they had a conventional RTL-style
|
|
interface. Communication with such a module is done through signals
|
|
that are attached to it during instantiation.
|
|
|
|
A more advanced approach is to model hardware blocks as
|
|
objects. Communication with objects is done through method calls.
|
|
A method encapsulates all details of a certain task performed
|
|
by the object. As an object has a method interface instead
|
|
of an RTL-style hardware interface, this is a much
|
|
higher level approach.
|
|
|
|
As an example, we will design a synchronized queue object.
|
|
Such an object can be filled by producer, and independently
|
|
read by a consumer. When the queue is empty, the consumer
|
|
should wait until an item is available. The queue can be modeled
|
|
as an object with a \method{put(item)} and a \method{get()}
|
|
method, as follows:
|
|
|
|
\begin{verbatim}
|
|
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)
|
|
\end{verbatim}
|
|
|
|
The \class{queue} object constructor initializes an internal list to
|
|
hold items, and a \var{sync} signal to synchronize the operation
|
|
between the methods. Whenever \method{put()} puts an item in the
|
|
queue, the signal is triggered. When the \method{get()} method sees
|
|
that the list is empty, it waits on the trigger first.
|
|
\method{get()} is a generator method because
|
|
it may consume time. As the \code{yield} statement is used in \myhdl\
|
|
for timing control, the method cannot ``yield'' the item. Instead, it
|
|
makes it available in the \var{item} instance variable.
|
|
|
|
To test the queue operation, we will model a producer and a consumer
|
|
in the test bench. As a waiting consumer should not block a whole
|
|
system, it should run in a concurrent ``thread''. As always in
|
|
\myhdl{}, concurrency is modeled by Python generators. Producer
|
|
and consumer will thus run independently, and we will monitor
|
|
their operation through some print statements:
|
|
|
|
\begin{verbatim}
|
|
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()
|
|
\end{verbatim}
|
|
|
|
Note that the generator method \method{get()} is called in a
|
|
\code{yield} statement in the \function{Consumer} function. The new
|
|
generator will take over from \function{Consumer}, until it is done.
|
|
Running this test bench produces the following output:
|
|
|
|
\begin{verbatim}
|
|
% 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
|
|
\end{verbatim}
|