Professional Documents
Culture Documents
Synopsys Inc.
700 East Middlefield Road
Mountain View, California 94043
Tel (650) 584-5612 • Fax (650) 584-5620
http://www.synopsys.com • vera-info@synopsys.com
VERA-VS, VERA-SV, VERA-VL, VERA-HVL, VERA Verification System, Verity, Verity
ToolKit, ISDB, ISDB-cycle, and PowerFault are trademarks of Synopsys Inc. Magellan,
PowerSim, SimWave, and VERA are registered trademarks of Synopsys Inc. All other trademarks
are the property of their respective owners.
This software and the concepts embodied in it are proprietary and confidential in nature, and are
not to be used, duplicated in whole or in part, reverse-engineered, modified, or disclosed in any
manner, for any purpose whatsoever, without prior written permission from Synopsys Inc.
Synopsys Inc. assumes no liability for any use of this software, and provides no warranty of any
kind for the software, its documentation, or the correctness of the results. Receipt of this material
shall be considered acceptance of the conditions specified herein.
Copyright © 1996, 1997, 1998, 1999, 2000, 2002, 2003 by Synopsys, Inc.
All rights reserved.
PATENTS PENDING.
Aug, 2003
Tutorial Table of Contents 3
Table of Contents
1. Introduction to Vera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2. System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
System File Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Running the Tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3. Arbiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Arbiter Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Vera Testbench Key Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Verifying the Arbiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4. Memory Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Memory Controller Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Verifying the Memory Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Using the Vera Debugger with the cntrlr Example . . . . . . . . . . . . . . . . . . . . . . 37
5. Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Memory System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Verifying the Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4 Table of Contents Tutorial
Tutorial Chapter 1. Introduction to Vera 5
1. Introduction to Vera
Vera is a robust and thorough verification tool for design and verification engineers. Vera is
not only simple to use, it is also powerful and a lot of fun.
Vera has been recognized as the leading testbench automation tool by numerous customer
evaluations and reviews. One reason we believe our product is successful is that we put great
importance on the satisfaction of our customers. We are actively increasing the functionality
and usability of our tool, and we listen carefully to what our customers have to say.
2. System Overview
This chapter introduces the system used for the remainder of the tutorial. It discusses briefly
the components of the system and describes how they interact to complete the system. It also
details the basic structure of the files used for this tutorial. This chapter includes these
sections:
• Memory System
• System File Setup
ce1_N reset
S
R
A
M request[0]
MEMORY
CONTROLLER CPU0
S ROUND-ROBIN
R ARBITER grant[0]
A
M ce2_N
S System Bus
R grant[1]
A ce3_N
M CPU1
address
request[1]
data
Notice that the blocks labeled CPU0 and CPU1 are shaded. This is to indicate that these blocks
are not part of the system under test, but rather these blocks will be modeled within our
testbench. The signals shown between the CPUs and the rest of the system are the interface
between the system under test and the “outside world.”
The memory system consists of the SRAMs, the Memory Controller, and the Arbiter. These
files are all described in the HDL files of each sub-module. The approach used to verify the
memsys system is similar to most project verification flows:
First, in Chapter 3 of the tutorial, the arbiter sub-module is verified. To do this, the
surrounding blocks in the Vera testbench are modeled.
Second, in Chapter 4, the memory controller sub-module is verified. For this module level
verification, both the CPU interface and the memory interfaces are designed with Vera. This
gives us a chance to show some of the advanced features in Vera that are used to verify
protocol based designs.
Finally, Chapter 5 verifies the complete system by integrating the arbiter and controller sub-
modules as shown in Figure 2-1 with a Vera model of the CPUs instantiated in the testbench.
Several different features of Vera are used in different approaches. We also introduce Object
Oriented Programming (OOP), Functional Coverage, and Interprocess Communication using
Triggers and Mailboxes.
Tutorial Chapter 2. System Overview 9
new_memsys
README: short description and file/directory index and listing of tools and versions used
memsys: contains the top-level RTL netlist that integrates the entire memsys design and the
test directory
Each “rtl” directory contains both VHDL and Verilog HDL code.
You will be working inside the arb, cntrlr, and memsys test directories where you will be
creating your Vera testbench. Each test directory, contains the solution or testbench for each
module. You can refer to this solution while creating your own testbench.
The diagram below, shows the general “test” directory structure for each module.
include: contains Vera interface and ports and binds files for arb and memsys
source: contains Vera testbench running both Verilog and VHDL code
run_scr: cshell and make run scripts for various simulators are stored here
10 Chapter 2. System Overview Tutorial
You use the test directory for creating testbench, run scripts, compilation, and simulation
generated files and directories. The files contained in the include, source, and “run_scr”
directories are tutorial solutions.
>source setup
The tutorial contains a combination of makefiles and run scripts for execution of the tutorial
solutions that run on the supported simulators shown in Table 2. The options for invoking the
simulation are best handled from the Makefile included in new_memsys/PROJECT_DIR/test.
This Makefile abstracts the commands for all supported simulators:
The Makefile is compatible with both make and gmake. To see the list of options invoke the
Makefile with either make/gmake or make –help / gmake –help. For example from within
new_memsys/memsys/test:
make -help
Note – You must edit and source setup to customize your environment
General Options:
Synopsys Simulation
MTI Simulation
NC Simulation
Note – The Makefile will run the solution files not your custom file.
Examples of Usage:
To run the solution for VCS:
>gmake cleanall
>gmake vcs
>make cleanall
>make mti_build
>make mti_vhdl
12 Chapter 2. System Overview Tutorial
Tutorial Chapter 3. Arbiter 13
3. Arbiter
This chapter focuses on the arbiter’s roll in the design. It briefly describes what the arbiter
does, including a short timing and logic discussion. The chapter then describes the Vera
methodology and functionality used to verify the arbiter section of the system. This chapter
explains how Vera interacts with both a Verilog and a VHDL design to drive signals, how the
connections between the testbench and DUT are made, and how some of the basic signal
operations behave. This chapter is divided into these sections:
• Arbiter Overview
• Vera Testbench Key Components
• Verifying the Arbiter
• The Tutorial solution run scripts for Verilog/VHDL simulators are in the
following directory:
new_memsys/arb/test/run_scr
• Tutorial solution Vera compile output files are written to the following
directory:
new_memsys/arb/test/vera_out
14 Chapter 3. Arbiter Tutorial
Make sure you edit setup for your installation and then source the file.
clk
reset
request xx 00 01 00 10 00 11
grant xx 00 01 00 10 00
clk
reset
request 11 10 00
grant 00 01 00 10 00
The arbiter implements a round-robin arbitration algorithm between two CPUs. Each CPU can
drive a request input signal (request[0] or request[1]). The arbiter queues the requests and
determines which CPU will gain access to the system bus. The arbiter grants this access by
asserting one of the grant output signals (grant[0] or grant[1]). While the grant signal is
asserted for a given CPU, the CPU continues to assert its request signal so that both the grant
and request signals for the CPU remain high while the CPU accesses the system bus. Once the
CPU is done, it de-asserts its request signal and, on the next subsequent clock cycle, the arbiter
de-asserts its grant signal. With all the signals de-asserted, the cycle can continue with the next
request.
Tutorial Chapter 3. Arbiter 15
Clock Generator
Vera
Testbench
Module
Interface Specification
Test-top file
The -tem compiler option invokes the Vera template generator. The -t switch defines the top
level name of the circuit under test as arb. The -c switch defines the clock signal to be used in
the generated interface. The specified file (../rtl/arb.v) is the RTL source code from which the
template files are generated. Note that the names of the generated files are derived from the
top-level RTL filename.
Invoking the Vera template generator command will create the following output:
Parsing ../rtl/arb.v..
Done.
As an alternative, you could use the interface wizard to create the interface defintion. See the
Vera user guide for more information about the wizard.
arb.test_top.v
The generated arb.test_top.v file is the Verilog test-top file. It contains the signal and wire
declarations that connect the Vera testbench to the DUT. The declarations are made using the
top level RTL (arb.v). The test-top file also instantiates the Vera shell file (vera_shell). Finally,
the test-top file defines a clock generator (SystemClock) that is passed to the Vera interface as
the clk signal. This could be hand generated: the instantiations and interconnections between
the DUT and the Vera shell, clock generators, and any needed infrastructure being written in
plain Verilog.
arb.if.vrh
The generated arb.if.vrh is the Vera interface file. It contains the Vera signal declarations made
within the arb interface. The signal names are taken from the top level RTL (arb.v). Signals
declared as outputs in the RTL are declared as inputs in the Vera interface (and vice versa).
Bidirectional signals remain bidirectional. Input signals are given the default skew of -1 and
output signals are given the defaut skew of +1. Signals are driven or sampled on the positive
edge of the interface clock (clk in this example). You can customize the interface by editing this
file if you want to.
Tutorial Chapter 3. Arbiter 17
Note that each interface has a clock associated with it by which all timing takes place. All
signal operations occur on the corresponding interface clock edge. For example, given an
interface with drives occurring on positive clock edges and a skew of 1, the timing diagram is
given by:
clk
request driven
request 1 time unit after
driving clock edge
grant driven 1
grant time unit after next
driving clock edge
The Vera shell file connections to the HDL simulation are generated from the interface
declarations when the Vera program file is successfully compiled.
arb.vr.tmp
The generated arb.vr.tmp is the Vera template testbench file. It contains preprocessor directives
that include the vera_defines.vrh header file as well as the arb.if.vrh interface file.
$VERA_HOME/doc/README.simulators.
Vera provides a toplevel testbench template generator to assist in the setup of connecting Vera
to the DUT.
#include <vera_defines.vrh>
#include “arb.if.vrh”
The arb.if.vri file contains the Vera signal declarations made within the arb interface. Signals
declared as outputs in the RTL are declared as inputs in the Vera interface (and vice versa).
Bidirectional signals remain bidirectional. Input signals are given the default skew of -1 and
18 Chapter 3. Arbiter Tutorial
output signals are given the defaut skew of +1. Signals are driven or sampled on the positive
edge of the interface clock (clk in this example). You can customize the interface by editing this
file if you want to. These signals correspond to signals in the DUT.It is recommended that the
customized interface file name use a “.vri” suffix to indicate a user-edited file as opposed to
compiler generated. Also, is also recommended that non-zero hold and setup delays are
defined. This pulls these delays away from the clock edges and can more realistically model
the back-annotated delays of the actual device.
interface arb {
input clk CLOCK ;
output reset PHOLD #1 ;
output [1:0] request PHOLD #1 ;
input [1:0] grant PSAMPLE #-1 ;
} // end of interface arb
Note that each interface has a clock associated with it by which all timing takes place. All
signal operations occur on the corresponding interface clock edge. For example, given an
inter-face with drives occurring on positive clock edges and a skew of 1, the timing diagram is
given by:
clk
request driven
request 1 time unit after
driving clock edge
grant driven 1
grant time unit after next
driving clock edge
After you have created the interface arb.if.vri and the program template file arb.vr, you need
to create the VHDL code to hook up Vera to the VHDL simulator. The -sro switch is used with
VCS-MX for VHDL only. Type vera -help for other VHDL simulation options including mixed
language support.
The –top compiler option invokes the Vera template generator. The -sro switch creates the
appropriate top level file as well as the shell.vhd file for the VCS-MX simulator.
Tutorial Chapter 3. Arbiter 19
arb_top.vhd
The arb_top.vhd file is the VHDL test-top file. It contains the signal and wire declarations that
connect the Vera testbench to the DUT. The declarations are made using the top level RTL
(arb.vhd). The test-top file also instantiates the Vera shell file (vera_shell). Finally, the test-top
file defines a clock generator (SystemClock) that is passed to the Vera interface as the clk
signal.
Now edit the arb_top.vhd to give it the name of the entity and details of how the wires hook up
from the Vera entity to the VHDL entity. Follow the directions listed in the comments at the
top of arb_top.vhd. You may also refer to ../rtl/arb.vhd for more details on the VHDL DUT.
arb_shell.vhd
The arb_shell.vhd is the interface from the Vera testbench to the device. Be sure to call it first in
the command line of the VHDL compiler before arb_top.vhd, otherwise the VHDL simulator
could get confused about the missing Vera entity.
arb.vro
The arb.vro is the compiled Vera testbench contained in arb.vr. The Vera .vr file instructions are
used by the Vera simulator to test the DUT. New code can be complied into the arb.vro file
with the command
however, this is not necessary at this point as you have not added anything new to the arb.vr
file.
arb.reset
arb.request
arb.grant
To advance the simulation to the next change of a specified signal, use the synchronize
construct:
@(clock_edge signal_name);
This advances the simulation to the next specified edge of the signal. If the clock edge is
omitted, it advances the simulation to the next sampling edge that indicates a signal change.
To assert and de-assert the signals, use the Vera drive construct:
@n signal_name = value;
The specified signal is driven to the appropriate value after n clock cycles pass. If the delay is
omitted, the drive occurs on the next driving edge as defined in the interface (positive clock
edge in our example).
To check that a signal has a specific value at a specified time, use the Vera expect construct:
@n signal_name == value;
The specified signal is compared to the given value after n clock cycles pass. If the signal value
is the same as the specified value, the simulation continues. If there is a mismatch, a
verification error occurs, the simulation terminates, and an error message is displayed (note
that the error mode can be set so that errors do not terminate the simulation using the soft
keyword). The soft keyword should be used in conjunction with the flag() method to
determine if the expect was satisfied.
Generally, it is best to sample signals slightly before the rising edge of the clock to avoid race
conditions. For this purpose, define an input skew of -1 unit inside the arb.vr.tmp file.Below
the already included #define statements add:
This define should be set in arb .vr following the other defines generated by the template
generator or by the user.
Tutorial Chapter 3. Arbiter 21
Note that the request and grant signals are 2-bit signals. Each bit of the signals must be
de-asserted.
NOTE: The prebuilt scripts will run the solution, not your custom code. To compile and run
your code you must follow the steps outlined below:
With the code added to the arbiter testbench (arb.vr.tmp), run the simulation and test the
results. First, verfiy that you have renamed arb.vr.tmp to arb.vr. Compile the Vera testbench:
Compiling the testbench generates the Vera shell file (arb.vshell) and the Vera testbench binary
object file (arb.vro).
simv +vera_load=arb.vro
Your test should run to completion without any errors and the output should be as follows:
If there are verification errors when the simulation is run, the simulation terminates and an
error message is reported. For instance, change the grant de-assertion line within the arb.vr file
so that it is incorrect:
@1 arb.grant==2’b01;
Recompile the Vera code and run the simulation again. (The HDL does not need to be
recompiled when only the Vera code is changed.) In this case, the testbench expects that the
grant signal is asserted while the Verilog model continues to de-assert the signal as before.
This results in an expect mismatch and a verification error as shown below.
Note – Remember to edit the testbench file to correct this error before
continuing.
EXPECT MISMATCH
TIME: 250 CYCLE: 3
Signal: arb.grant.0
Exp Value: 1 : 01
Actual Value: 0 : 00
VERIFICATION ERROR: Expect mismatch Location: WAIT_ON_EXPECT in program
arb_test (arb.vr, line 14, cycle 3)
$stop at time 250 Scope: arb_test_top.vshell File: arb.vshell Line: 50
With the code added to the arbiter testbench (arb.vr), run the simulation and test the results.
Compile the Vera testbench:
Compiling the testbench generates the Vera testbench binary object file (arb.vro). These files
need to be included when the simulation is run.
6) Run simulation
Given this verification methodology, the code to check arbiter behavior is:
Given this testing configuration, there is no way to ensure that grant does not change
unpredictably (it is only checked using the expects). To check for unexpected changes, use
Vera’s Value Change Alert (VCA). The VCA generates a verification error when unexpected
changes occur. To enable the VCA, the signal declaration in the interface file for the signal
being monitored must include the vca keyword as shown. This has already been included in
the ./include/arb.if.vri:
This signal declaration enables the VCA for the grant signal, assigning a default quiescent
value of 0 to the signal. To use the VCA, turn it on from within the testbench (before the test
sequence begins):
vca(ON, arb.grant);
When the VCA is turned on, any change in signal grant that is not expected (by an expect
statement) or explicitly driven generates a verification error. Comment out one of the expect
statements and run the simulation, the now unexpected signal change generates an error.
Tutorial Chapter 4. Memory Controller 25
4. Memory Controller
This chapter discusses the memory controller portion of the design. It gives an overview of
how the memory controller functions. It discusses some of the major features of Vera that are
used to verify the controller, including a description of virtual ports and binds as well as
synchronous and asynchronous events. These concepts are presented within the verification
framework so that you can learn how to adequately validate our memory controller. This
chapter includes these sections:
• Memory Controller Overview
• Verifying the Memory Controller
• Using the Vera Debugger with the cntrlr Example
• The Tutorial solution Vera compile output files are written to the following
directory:
new_memsys/cntrlr/test/vera_out
26 Chapter 4. Memory Controller Tutorial
Make sure you edit setup for your installation and then source the file.
The memory controller reads requests from the system bus and generates control signals for
the SRAM devices attached to it. For read requests, the controller reads data and transfers it
back to the bus and the CPU making the request. The address bus is 8 bits wide, which creates
an address space of 256 bytes. The controller supports up to 4 devices, allocating a maximum
of 64 bytes of memory to each. The controller decodes the address and generates the chip
enable for the corresponding device during a transaction. Figure 4-1 shows a diagram of how
Vera works with both the system bus and SRAM device signals.
Vera
Figure 4-2 and Figure 4-3 show the timing diagrams for the memory controller’s read and
write operations respectively (note the signal names as you will be using them in the
verification process)
clk
reset
adxStrb
busAddr valid
busData valid
busRdWr_
cex_
ramData valid
ramAddr valid
rdWr_
clk
reset
adxStrb
busAddr valid
busData valid
busRdWr_
cex_
ramData valid
ramAddr valid
rdWr_
Note that this chapter checks the memory controller by emulating both the system bus and the
memory bus behavior.Rather than connecting the rtl models of the memory to the controller,
model the behavior of the 4 different memory devices in Vera.
To start the verification for Verilog designs, create the template files using the -tem switch as
described with the arbiter verification:
To start the verification with VHDL designs, follow the steps in the previous chapter for arb,
but now working with cntrlr.vhd. You are provided an example of the Vera interface definition
inside the controller testbench program file ./cntrlr/source/cntrlr.vr.
Tutorial Chapter 4. Memory Controller 29
4.2.1 Driving the System Bus For Read and Write Operations
In testing the read and write capabilities of the controller, create two Vera tasks that drive the
bus for read and write operations.
Read Operation
Create a task that drives the read operation onto the system bus as specified in the timing
diagram for the controller. The task should use an 8-bit bus address as an input. Given this
requirement, the read operation task is:
This task is passed the argument adx. It then drives the busAddr signal to that value. Finally, it
drives the busRdWr_ and adxStrb signals such that they match the timing diagram for the read
operation of the controller.
Note: do not drive the data onto the bus and check for the expected data here. Before checking
for the expected data, check that the read operation displays the correct waveform at the
SRAM interface. When checking the entire system in Chapter 5 ”Memory System”, this check
is made using multiple threads.
Write Operation
Create a task that drives the write operation onto the system bus as specified in the timing
diagram for the controller. The task should use 8-bit address and data busses as inputs.
Finally, the task should leave the bus in an idle state (defined when busData is in high z and
busRdWr_ is de-asserted). Given these requirements, the write operation task is:
This task is passed the argument adx. It then drives the busAddr signal to that value. Finally, it
drives the busData, busRdWr_, and adxStrb signals such that they match the timing diagram for
the write operation of the controller.
port_name - The port_name is the user defined virtual port whose signal member names you want
associated with interface signals.
port_signal_member - The port_signal_memberN is the name of the generic signal names you are
including in the bind. Generally, all of the signals in the port are bound. However, you can bind
selected signals if you want, and leave others unbound.
Tutorial Chapter 4. Memory Controller 31
interface_name - The interface_name is the name of the interface to which you are binding the port
signal members.
signal_name - The signal_name is the name of the signal you are binding to a particular port signal
member. You can specify signal subfields using signal_name[x:y].
port_variable - The port_variable is the name of the port variable you are declaring.
initial_value - The initial_value can be any existing port of the same type as the port variable. If it is
not set, the port_variable has a NULL value until it is assigned a port.
$signal_name
This references the specified port signal in the bind passed to the subroutine.
port device
{
ramAddr;
ramData;
rdWr_;
ce_;
}
After defining the virtual port, connect the port signals to actual interface signals using the
bind construct:
rdWr_ cntrlr.rdWr_N;
ce_ cntrlr.ce0_N;
}
This bind construct results in the port variable device0 of port type device. It connects the
port signals to their corresponding interface signals. Note that the ce_ signal is connected to its
device-specific signal. Similar binds for each device (device1, device2, and device3)
should be constructed.
Because of complex timing issues with the read operation, examine the write operation first. A
discussion of the timing issues and the read operation follows.
Timing Windows
Vera provides timing windows for its expect signal operation. The syntax is:
The window of time for which the check is made must be in the form x,y. The check begins x cycles
after the call is made and continues for y cycles after the call is made. If the x is omitted (,y), the check
is made immediately and lasts y cycles after the call. The signal value must match the expected value
for the duration of the check. This mechanism provides a means to evaluate a signal over a specified
period of time. For more details, see the Vera User Guide.
d.$ramAddr == adx;
@1 d.$rdWr_ == 1;
d.$ce_ == 1;
d.$ramData == data;
d.$ramAddr == adx;
@1 d.$ramData == 8’bzzzzzzzz;
}
This task checks that the address (ramAddr) is valid over the timing window 1-5 cycles after
the call is made. The the write data (ramData) is checked for two cycles from that point. After
checking these signals, check that rdWr_ and ce_ are asserted simultaneously for exactly one
cycle, and check that the address and write data remain valid. Next check that rdWr_ and ce_
are de-asserted, and check that the address and write data are still valid. After the checks,
make sure ramData returns to tri-state.
Note that the delays for the drive and expect operations are not used since they occur
immediately.
to work. However, because of the sampling skew, ce_ is sampled just after the rising clock
edge. This means that the data is driven on the next rising clock edge, which is invalid. This
timing diagram shows this behavior:
clk
ce_ is driven
just after rising
ce_
edge
With this in mind, create a read task that checks the read operation against the timing diagram
provided. The task must have an argument of type device to pass in the virtual port. It also has
6-bit address and 8-bit data busses as inputs. This is the code:
This task first checks that the address is valid over the specified window of time. Next
advance the simulation to the exact change of the chip enable signal (ce_) using the
synchronize construct. Use the async form because we want this change to happen
immediately without waiting for the next sampling edge. Next, immediately check that ce_ is
0, rdWr_ is de-asserted, and ramAddr has the appropriate value. After these checks, drive the
data (ramData) immediately. Use the async construct here so that the drive is done
immediately after the checks and not on the next rising clock edge. Finally, drive the data back
to tri-state at the next rising clock edge (note the use of the <= drive operator, which indicates
a non-blocking drive so that execution continues immediately).
Tutorial Chapter 4. Memory Controller 35
cntrlr.reset = 1’b1;
cntrlr.adxStrb = 1’b0;
@1,100 cntrlr.ce0_N == 1’b1;
cntrlr.ce1_N == 1’b1;
cntrlr.ce2_N == 1’b1;
cntrlr.ce3_N == 1’b1;
@1 cntrlr.reset = 1’b0;
With the reset check completed, write code to check the write operation of one of the devices.
The code to drive the bus for the write operation is in the writeOp task, and the code to check
that write operation is in the checkSramWrite task. To check the operation, use these two
functions:
This code drives the bus and then checks the write operation using the specified virtual port’s
signals (device0). When checking other devices, remember that each device has a range of valid
addresses:
Because the address busses are device specific, if you change the address parameter to a value
that is not valid for the device you are checking, the check fails. This fails because of the
address dependence in activating the chip enable signals within the RTL. To test this behavior,
remember to recompile after making the changes.
Now add in the code to check the write operations for the other devices. The same tasks can
be used with different virtual ports and different address parameters.
Similarly, use the generic tasks to drive the bus for read operations and check the device read
operations. Remember to check that the returned data matches the return data specified in the
timing diagram. The code for these checks is:
readOp (8’h03);
checkSramRead (device0, 6’b000011, 8’h95);
@1 cntrlr.busData == 8’h95;
36 Chapter 4. Memory Controller Tutorial
This code drives the bus and then checks the read operation using the specified virtual port’s
signals (device0). Finally, the return data is checked to see that it matches the correct value.
These tests only cover a subset of the valid addresses. To exhaustively test the entire range
using these calls, each task must be called with every address. To simplify this task, use virtual
ports and for-loops. First, define a port variable that will have each device’s port signals
assigned to it through the loop:
device dev;
This defines a variable of port type device is used to pass in the ports to each subroutine call in
our loop. Now create a for loop, using a case statement to switch device ports and calling our
subroutines to drive the bus and check the SRAM operations:
bit[7:0] index;
integer i;
...
for (i=0;i<=255;i++)
{
index = i;
writeOp(index, 8’h5A);
case (index[7:6])
{
2’b00: dev = device0;
2’b01: dev = device1;
2’b10: dev = device2;
2’b11: dev = device3;
}
checkSramWrite (dev, index[5:0], 8’h5A);
readOp(index);
checkSramRead (dev, index[5:0], 8’h5A);
@1 cntrlr.busData == 8’h5A;
}
Each iteration of this for loop acts on a different address. It drives the bus operation and then
checks the SRAM operation using the subroutines defined previously. The case statement
changes the virtual port on which the subroutines act so that they use the correct signal
bundles for each device. The bus data is checked at the end of each iteration to monitor the
return values.
We have exhaustively tested the address space, but must also make sure that the chip enables
(cex_) do not change unexpectedly through the test. Add Value Change Alert (VCA) checks to
the interface specification to enable the VCA for each chip enable signal. Remember the VCA
must be enabled for each chip enable signal by adding the vca r1 keywords to each signal
declaration in the interface.
Tutorial Chapter 4. Memory Controller 37
Now turn on the VCAs before the reset check using this code:
vca(ON, cntrlr.ce0_N);
vca(ON, cntrlr.ce1_N);
vca(ON, cntrlr.ce2_N);
vca(ON, cntrlr.ce3_N);
Note that running the simulation with the VCAs enabled like this fails. This is because of our
asynchronous sampling of cex_ in the checkSramRead task. So, disable the VCAs when the
checkSramRead task is executed and enable them once it is completed. Do this by adding in
case statements to the above block:
readOp(index);
case (index[7:6])
{
2’b00: vca(OFF, cntrlr.ce0_N);
2’b01: vca(OFF, cntrlr.ce1_N);
2’b10: vca(OFF, cntrlr.ce2_N);
2’b11: vca(OFF, cntrlr.ce3_N);
}
checkSramRead (dev, index[5:0], 8’h5A);
case (index[7:6])
{
2’b00: vca(ON, cntrlr.ce0_N);
2’b01: vca(ON, cntrlr.ce1_N);
2’b10: vca(ON, cntrlr.ce2_N);
2’b11: vca(ON, cntrlr.ce3_N);
}
After driving the bus with the readOp task, disable the VCA for the device we are checking.
After the check is made, the VCA is immediately enabled.
Using either or both of the following runtime options will bring up the debugger:
The Vera “breakpoint” command can be used inside the code to start the debugger.
The cntrlr module test code contained in “source/cntrlr.vr” contains a commented breakpoint
command. Uncomment it and recompile and run the simulation to bring up the debugger.
38 Chapter 4. Memory Controller Tutorial
Tutorial Chapter 5. Memory System 39
5. Memory System
After discussing the arbiter and memory controller separately, we now examine the way the
components act in a complete system. This chapter briefly overviews the system, which
includes the arbiter, controller, and SRAM devices. It also discusses some of the higher level
verification techniques used in Vera. These include concurrency control mechanisms such as
regions, triggers, and mailboxes, object-oriented programming, runtime signal mapping,
functional coverage, and random stimulus generation. Finally, this chapter uses these features
to validate our memory system. This chapter includes these sections:
• Memory System Overview
• Verifying the Memory System
• The Tutorial solution Vera ports and binds file is in the following file:
new_memsys/memsys/test/include/memsys.ports_binds.vri
40 Chapter 5. Memory System Tutorial
• The Tutorial solution Vera compile output files are written to the following
directory:
new_memsys/memsys/test/vera_out
Make sure you edit setup for your installation and then source the file.
The memory system acts as a wrapper that instantiates the arbiter, memory controller, and
four SRAM devices. In our system, the system bus is driven by two separate CPUs, with
access granted through the arbiter. The memory controller handles the reading and writing of
data to and from the system bus. A schematic of the complete system is given in Figure 5-1.
ce0_N
S
R
A
M rdWr_N
ce1_N reset
S
R
A
M request[0]
MEMORY
CONTROLLER CPU0
S ROUND-ROBIN
R ARBITER grant[0]
A
M ce2_N
S System Bus
R grant[1]
A ce3_N
M CPU1
address
request[1]
data
Reset Verification
To check that the system is resetting correctly, we must assert the reset signal, release the
adxStrb signal, deassert the request signal, check that the grant signal releases properly, and
then deassert the reset signal. The code to check the reset is:
memsys.reset 1’b1;
memsys.adxStrb = 1’b0;
memsys.request = 2’b00;
@1,3 memsys.grant == 2’b00;
memsys.reset = 1’b0;
In addition to driving the read operation onto the system bus, our read operation task must
check for the correct return data. The new readOp task with the data checking included is:
Multiple Threads
Fork/join blocks are the primary mechanism for creating concurrent processes. The syntax to declare a
fork/join block is:
fork
{statement1;}
{statement2;}
{...}
{statementN;}
join wait_option
statementN - The statements can be any valid Vera statement or sequence of statements.
wait_option - The wait_option specifies when the code after the fork/join block executes. The fork/join
block can be either blocking or non-blocking. If it blocks, the code below the fork/join block will not
execute until the code inside the fork/join thread returns. The wait_option must be one of the following:
all
any
none
The all option is the default. Code after the fork/join block executes after all of the concurrent
processes have completed.
When the any option is used, code after the fork/join block executes after any single concurrent process
within the fork/join is completed.
When the none option is used, code after the fork/join block executes immediately, without waiting for
any of the fork/join processes to start. Threads within the fork/join block are scheduled but not executed
until the code following the fork/join block hits a blocking statement.
With the read and write operations defined, we want to set up our testbench so that each CPU
issues a series of reads and write requests to the memory system with random addresses and
data. Each CPU should use the random() system function to generate random addresses
within the valid address space and an 8-bit data type. The CPUs should then request and
access the bus, write the data to the bus, and release the bus (check for the release of the grant
signal upon bus release). This sequence should be repeated 256 times using the repeat() flow
control statement. Given these criteria, the code is:
This test works well in exhaustively checking the read and write operations for each CPU.
However, because both CPUs are accessing a single bus, problems arise when each CPU
accesses the same address space with different data. For instance, if CPU0 writes to an address
space, and CPU1 then writes to the same address space, the data that CPU0 reads is different
than expected (it reads the data that CPU1 wrote). This results in simulation failure because of
the discrepancy between data read and expected data. A solution to this issue is to use basic
concurrency control and is discussed in the next section.
Region Overview
A Vera region is a mutual exclusion mechanism that guarantees that the requested values are
unique in the simulation. Conceptually, regions can be viewed as a set of letters. First you
allocate which letters are included in the set. These letters are the only letters from which
words can be made. If one person uses the letters to spell CAT, no one else can spell TIN
because the T is already in use. Once the T is returned, TIN can be created. Effectively, this
ensures that data sets are unique, and it eliminates concurrent crossover.
region_id - The region_id is the ID number of the particular region being created. It must
be an integer value. You should generally use 0. When you use 0, Vera automatically generates a
region ID.
region_count - The region_count specifies how many regions you want to create. It must
be an integer value.
The alloc() function returns the base region ID if the regions are successfully created. Otherwise, it
returns 0.
wait_option - The wait_option can be either NO_WAIT or WAIT. The NO_WAIT option continues
code execution if the specified region is in use. The WAIT option suspends the process until
the specified region is no longer in use.
valueN - The values are integer or bit vectors up to 64 bits, without X’s or Z’s. These values specify
the unique region values.
The region_enter() system function checks the specified values against all region values for the
specified region. If another process has entered the region with one or more of the values, then those
values are in use, and the current region cannot use them. If none of the values are in use elsewhere, the
function returns a 1, flags the values as in use, and passes control to the next line of code. If one or
more of the values is in use elsewhere, the function suspends the current thread until the values become
available, depending on the wait option.
The region_exit() system task removes the specified values from the in-use state. The syntax is:
valueN - The values are integer or bit vectors up to 64 bits, without X’s or Z’s. These values specify
the unique region values.
When the region_exit() system task is called, the specified values are no longer in use and can be used
in other regions. Any processes that are suspended (waiting for region values) execute when the region
values are made available.
Implementing Regions
To implement regions within the testing framework established in the previous section, we
must allocate the region before the forked process. Then, within each CPU fork, the CPU
enters the region with its address value. Next, the address is removed from the pool of valid
addresses and the region prevents the other CPU from using the same address until the region
is exited and the value returned. Each fork must include a region enter and a region exit to
accomplish this. You can monitor the region to see how the synchronization works using the
trace() system function and checking the verilog.log file after the simulation. Finally, we should
force each CPU to wait a random number of cycles after the sequence is executed before
running the sequence again. These requirements are satisfied using this code:
In our system, each of the methods described below should be included within class CPU:
class CPU
{
property declarations;
constraint definitions;
method definitions;
}
Encapsulation
A class is a collection of data and a set of subroutines that act on that data. A class’s data is
referred to as properties, and a class’s subroutines are referred to as methods. These comprise
the contents of a class instance, or object.
Class properties are instance-specific. Each instance of a class has its own copy of the variables
declared in the class definition.
Because multiple instances of classes can exist, when calling a class method, you must identify
the instance name for which the method is being called. This is because each method only
accesses the properties associated with its object, or instance. So, when calling a method, you
must use this syntax:
instance_name.method_name();
Constructors
Objects, or instances, are created when a class is instantiated using the new statement:
This declaration creates an instance (called instance_name) of class class_name. When this
construction takes place, the new() method within the class is executed (if any exists). By
defining a new task within the class, you can initialize the class upon construction or
instantiation. Further, by passing arguments to the constructor, you can allow for runtime
customizing of the object:
Using this constructor, the specified arguments are passed to the new task within the class. The
conventions for these arguments are the same as for Vera subroutine calls.
Port Assignment
When implementing object-oriented concepts into our system, it is useful to simplify our port
declarations. For ease of use, the interface specification generated using Vera’s template
generator is included in the main memsys.vr file (this is only advisable in small examples
where working with a single file is easy).
48 Chapter 5. Memory System Tutorial
We define a bus arbiter virtual port bus_arb to be used with each CPU. It has a request and a
grant signal:
port bus_arb
{
request;
grant;
}
Using this virtual port declaration, we declare two binds, one for each CPU:
These binds are passed to the class methods to determine which signals are affected by
method calls.
Class Methods
In our class, we must create the initialization method that is executed when the class is
constructed. We must then create the read and write operation methods. It is also helpful to
create methods to request and release the bus.
The initialization method should pass in the bind of type bus_arb (as declared above) and
assign it to a local property. The initialization method new is:
Our read operation readOp must behave as before. However, this time the bind is passed to the
object so that we do not have to account for it in the declaration.
task readOp()
{
@1 memsys.busAddr = address;
memsys.busRdWr_ = 1’b1;
memsys.adxStrb = 1’b1;
@1 memsys.adxStrb = 1’b0;
@2,5 memsys.busData == data;
printf(“READ address = 0%H, data = 0%H \n”, address, data);
}
Our write operation writeOp must behave as before. Again, the bind is passed to the object so
that we do not have to account for it in the declaration. However, note the conditional
statement that evaluates the bind passed to the object and prints which CPU is writing. The
writeOp method is:
task writeOp()
{
@1 memsys.busAddr = address;
memsys.busData = data;
memsys.RdWr_ = 1’b0;
memsys.adxStrb = 1’b1;
@1 memsys.busRdWr_ = 1’b1;
memsys.busData = 8’bzzzzzzzz;
memsys.adxStrb = 1’b0;
if (localarb == arb0)
printf(“CPU0 is writing.\n”);
else if (localarb == arb1)
printf(“CPU1 is writing.\n”);
printf(“WRITE address = 0%H, data = 0%H \n”, address, data);
}
Our request_bus method must assert the corresponding request line and check for the
appropriate grant line:
task request_bus()
{
@1 localarb.$request = 1’b1; // request the bus
@2,20 localarb.$grant == 1’b1; // check for grant
}
50 Chapter 5. Memory System Tutorial
Conversely, our release_bus method must release the corresponding request line and check for
the appropriate grant line:
task release_bus()
{
@1 localarb.$request = 1’b0; // release the bus
@2,20 localarb.$grant == 1’b0; // check for grant
}
Random Variables
You can declare class properties as random using the rand declaration:
Variables declared as random within a class are randomized when the randomize() system
function is called. Because randomize() acts as a class method, you must specify the instance
for which the system function is called:
object_name - The object_name is the name of the object in which the random variables have been
declared.
The randomize() class method generates random values for all random variables within the specified
class instance. The randomize() method returns a 1 if it successfully sets all the random variables and
objects to valid values. If it does not, it returns a 0. If an object has no random variables anywhere in
its inheritance hierarchy (no random variables or sub-objects) or if all of its random variables are
inactive, the randomize() function returns a 1.
Using random declarations, we declare our class properties address and data as random:
Each time an instance is randomized, the address and data values for that instance are
randomized.
Earlier, we generated a random delay using the random() system function. Using random
variables, we implement the same delay as a class method delay_cycle:
Note that there are no restrictions on the value that delay can assume because it is declared as
an integer. We can implement constraints on the values that random variables can assume
using the constraint construct:
constraint_expression - The constraint_expressions are the conditional expressions that limits random
values. It is a series of expressions that are enforced when the class is randomized. Constraint
expressions are of the form:
random_variable - The random_variable parameter specifies the variable to which the constraint is
applied.
operator - The valid operators for constraints are: <, <=, ==, >=, >, !=, ===, !==, =?=, and !?=.
• Constraint expressions are evaluated bidirectionally that is, both sides of the equation are
solved simultaneously.
Implementing OOP
Before we can use our objects, we must instantiate each object and invoke our initialization
routines, which specify the binds to pass to the objects:
With our class CPU defined with the properties and methods described above, the same
execution sequence created using fork/join can be written as:
fork
{
repeat(256)
{
errflag = cpu0.randomize();
region_enter(WAIT, regId, cpu0.address); // check if address is free
52 Chapter 5. Memory System Tutorial
cpu0.request_bus();
cpu0.writeOp();
cpu0.release_bus();
cpu0.request_bus();
cpu0.readOp();
cpu0.release_bus();
region_exit(regId, cpu0.address);
cpu0.delay_cycle();
}
}
{
repeat(256)
{
errflag = cpu1.randomize();
region_enter(WAIT, regId, cpu1.address); // check if address is free
cpu1.request_bus();
cpu1.writeOp();
cpu1.release_bus();
cpu1.request_bus();
cpu1.readOp();
cpu1.release_bus();
region_exit(regId, cpu1.address);
cpu1.delay_cycle();
}
}
Note how the class property address is passed (using the instance name). Also note the ease of
reuse through invoking the class methods for the appropriate instance name.
Coverage Definition
The basic syntax for defining a coverage_group is:
{
sample_definitions;
[cross_definitions;]
sample_event_definition;
attribute_definitions;
argument_list:
The arguments are parameters passed at instantiation. They have the same conventions as
subroutine arguments and can have their default values set within the declaration.
sample_definition:
A sample_definition defines the variables and/or DUT signals that are sampled by the
coverage_group. It is declared using the sample construct of a coverage_group. In its simplest
form it can be:
where variable_name is the name of the Vera variable or signal name that is sampled by the
coverage_group. When defining state and or transitions for sampled variables, the sample
construct has the form:
sample variable_name
[state_or_transition_definition];
[attribute_definition];
cross_definition:
You can define cross coverage of variables sampled in a coverage_group using the cross
construct. In its simplest form it can be:
sample_event_definition:
You must specify a sampling event expression in the coverage group definition. The
coverage_group samples all of its sampled variables and updates the appropriate bins when
the sampling event triggers. You define a sampling event for the coverage_group as follows:
sample_event = event_expression;
attribute_definition:
You can use attributes for controlling various aspects of a coverage_group. The User Guide
details the attributes that can be specified at the coverage_group level, and their default
values. You can specify an attribute’s value as follows:
attribute_name = value_expression;
where attribute_name is the name of the attribute, and value_expression is a Vera expression.
Argument list
What if your coverage model cuts across your class abstraction and all of the elements of your
coverage model do not reside in the same class? You can pass arguments to a coverage_group
in order to address this need. The coverage_group construct optionally allows for the
declaration of formal parameters. Actual arguments are passed in to the coverage_group
instance as parameters to the new task call. You can define three kinds of parameters in the
coverage_group’s definition: sampled, passed-by-value, and passed-by-reference. For the
purposes of the tutorial we will be using sampled variables.
Sampled parameters are preceded by the sample keyword in the formal parameter list of the
coverage_group definition. They are treated like a constant “var” argument passed to a task.
In the following example coverage_group MyCov defines a sampled parameter paramVar that
is sampled at every positive edge of the system clock.
You can either let Vera automatically create state bins for a sampled variable or explicitly
define named state and/or transition bins for each of the sampled variables. Each named bin
groups a set of values (state) or a set of value transitions (trans) associated with a sampled
variable.
For the tutorial we will be user-defining the state and transitions bins. If you would like more
information on auto bin creation, refer to the Vera User Guide.
State Declarations
Coverage declarations are used to declare legal states, illegal states, legal transitions, and
illegal transitions. They associate bins with these activities and monitor how many times these
activities occur within a simulation.
In a state declaration, a single state or multiple states are associated with a monitor bin via a
state specification. The state specification is a list of elements (separated by commas) that are
matched against the current value of the state variable. For the current cycle, any matches
increment the bin counter by one.
Each element of the state specification should be an expression. When the state variable
matches the expression, the bin counter is incremented one.
The m_state state declaration is used to declare multiple state bins up to a maximum of 4096
bins. The syntax is:
state_bin_name - The state_bin_name is the base name of the state bins being created.
exp - The exps can be any valid coverage expression. You cannot call functions in the
expression. The expressions can include variables.
When the m_state declaration is used, multiple state bins are created, covering all the values
in the range. The expressions are evaluated when the coverage object is instantiated.
Illegal state declarations associate illegal states with a bin. The syntax is:
Illegal or bad states are those states in the design that, when entered, result in verification
errors.
56 Chapter 5. Memory System Tutorial
The state specification can be any expression or combination of expressions as in the state
declarations. However, it is often useful to define every state that is not in the state
declarations as a bad state. To use that definition of bad states, you can use the not state
specification:
This statement increments the specified bin counter every time the state variable matches a
value not defined in the state declarations.
Transition Declarations
Transition declarations associate state transitions with monitor bins. The syntax for transition
declarations is:
Declaring a sequence of transitions between states specifies state transitions. The general
format is:
Illegal transition declarations associate an illegal transition with a monitor bin. The syntax is:
The state transition can be any state transition set valid for transition declarations. However, it
is often useful to monitor all transitions that have not been defined as legal transitions. For
such instances, Vera uses the not trans argument.
The counter associated with the specified bin will be incremented every time a transition
occurs that is not explicitly defined in the transition declaration.
Coverage objects can be sampled on clock or signal edges as per the synchronization
command. When the specified edge occurs, the object is sampled. The syntax is:
In the following example, all instances of coverage group cov1 will be sampled upon the
posedge of the system clock.
coverage_group cov1
{
sample_event = @(posedge CLOCK);
sample g_var;
}
interface memcntrlr_probe
{
input clk CLK;
input[1:0] cntrlr_state INPUT_EDGE verilog_node “dut.Umem.state”;
}
To implement a coverage object that monitors the states and transitions of our memory
controller, we must first identify the states and transitions we want to check. In our system,
we will monitor the state variable cntrlr_state, which is passed in at the time of instantiation
via a sampled variable. Assuming we have states IDLE (0), START (1), WRITE0 (2), and
WRITE1 (3) and the transitions IDLE to IDLE, IDLE to START, START to IDLE, START to
WRITE0, WRITE0 to WRITE1, and WRITE1 to IDLE, our coverage definition is:
We also want to check that the entire address space is tested. We monitor the state variable
peek to check that it assumes all valid states between 0 and 255:
{
sample_event = @( negedge memsys.adxStrb);
sample peek {
m_state (0:255);
}
}
Before we can instantiate our coverage objects, we must declare the objects within our main
program:
cntlr_cov cov1;
range cov2;
With our objects declared, we must instantiate them within the main program. We want our
objects to monitor the activity in our forked processes, so we instantiate them before the forks:
It is important to note that Vera cannot directly sample output signals. So we must modify the
adxStrb signal declaration within the interface specification such that the signal is defined as
bidirectional (inout) with the proper input and output edges:
memsys_test.db
We have two options for reviewing the data within the database, both involve generating a
coverage report and analyzing the results. The choice is to generate a hyperlinked HTML
report or a text based report.
HTML report:
Text report:
Note –
The Makefile for this tutorial has two options for generating reports.
Please select either of the following two options:
Mailboxes allow us to perform a similar test using random addresses. Without running the
simulation in lockstep, mailboxes allow us to read only addresses that have been written to
previously.
Triggers
Triggers refer to a process that involves events, syncs, and triggers. Events are variables that
synchronize concurrent processes. When a sync is called, a process blocks until another process sends a
trigger to unblock it. Events act as the go-between for triggers and syncs.
Syncs suspend a process until a trigger activates to unblock the process. The sync() system task
synchronizes statement execution to one or more triggers. The syntax to call the sync() task is:
sync_type - To simplify our design, we only use the ALL sync, which blocks until all events are
triggered.
eventN - The event is the event variable name on which the sync is activated. Note that you must
declare your event variables within the scope that the sync/trigger combination is used.
trigger_type - In our system, the only trigger_types we use are ON, which turns on an event, and OFF,
which turns off an event.
When you call a sync, that process is suspended until a trigger sends an event that unblocks
the sync.
Implementing Triggers
In our system, we want to write data to the bus and then read it to check that it was correctly
written to memory. To do this, CPU0 must issue only write requests while CPU1 issues only
read requests. Further, CPU1 must only read data after CPU0 completes a successful write.
This requires that we advance the simulation in lockstep.
To do this, we use triggers within our fork/join block. Note that we must return to the for
loop to ensure lockstep behavior (each iteration of the loop of a process depends on the
trigger/sync call of the other process):
fork
{
for(index0=0; index0 <= 255; index0++)
{
address0 = index0;
cpu0.request_bus();
cpu0.writeOp(address0,address0);
trigger(ON, go_ahead);
cpu0.release_bus();
sync(ALL, done);
trigger(OFF, done);
}
}
{
for(index1=0; index1 <= 255; index1++)
{
address1=index1;
sync(ALL, go_ahead);
trigger(OFF, go_ahead);
cpu1.request_bus();
cpu1.readOp(index1,index1);
cpu1.release_bus();
trigger(ON, done);
}
}
join
Mailboxes
A mailbox is a mechanism to exchange messages between processes. Data can be sent to a mailbox by
one process and retrieved by another. Conceptually, mailboxes behave like real mailboxes. When a
letter is delivered and put into the mailbox, you can retrieve the letter (and any data stored within).
However, if the letter has not been delivered when you check the mailbox, you must choose whether to
wait for the letter or retrieve the letter on subsequent trips to the mailbox. Similarly, Vera’s mailboxes
allow you to transfer and retrieve data in a very controlled manner.
To allocate a mailbox, you must use the alloc() system function. The syntax is:
mailbox_id - The mailbox_id is the ID number of the particular mailbox being created. It must
be an integer value. You should generally use 0. When you use 0, Vera automatically generates a
mailbox ID.
62 Chapter 5. Memory System Tutorial
mailbox_count - The mailbox_count specifies how many mailboxes you want to create. It must
be an integer value.
The alloc() function returns the base mailbox ID if the mailboxes are successfully created. Otherwise, it
returns 0.
The mailbox_put() system task sends data to the mailbox. The syntax is:
data - The data can be any general expression that evaluates to a scalar.
The mailbox_put() system task stores data in a mailbox in a FIFO manner. Note that when passing
objects, only object handles are passed through the mailbox.
The mailbox_get() system function returns data stored in a mailbox. The syntax is:
wait_option - The wait option can either be NO_WAIT or WAIT. The NO_WAIT option continues
code execution if the mailbox is empty. The WAIT option suspends the process until a message
is sent to the mailbox.
mailbox_id - The mailbox_id specifies which mailbox data is being retrieved from.
check_option - The check_option is an optional argument that should be set to CHECK when used. It
specifies whether type checking occurs between the mailbox data and the destination variable.
The mailbox_get() system function assigns any data stored in the mailbox to the destination variable
and returns the number of entries in the mailbox, including the entry just received. If there is a type
mismatch between the data sent to the mailbox and the destination variable, a runtime error occurs
unless the CHECK option is used. If the CHECK option is active, a -1 is returned, and the message is
left in the mailbox and is dequeued on the next mailbox_get() function call. If the mailbox is empty, the
function waits for a message to be sent, depending on the wait option. If the wait option is NO_WAIT,
the function returns a 0. If no destination variable is specified, the function returns the number of
entries in the mailbox, but it does not dequeue an item from the mailbox.
Implementing Mailboxes
Using mailboxes, we do not need to run the simulation in lockstep. Instead, we can have
CPU0 write to random addresses. Each time it writes to an address, that address is sent to the
mailbox and read by CPU1 so that CPU1 knows which addresses are valid to read. In our
implementation, we want to store the address and data after a write. Meanwhile, the other
CPU waits until an address and data are stored before it reads from the bus. The code for this
configuration is:
fork
{
repeat(256)
{
errflag=cpu0.randomize();
cpu0.request_bus();
cpu0.writeOp();
mailbox_put(mboxId, cpu0.address);
mailbox_put(mboxId, cpu0.data);
cpu0.release_bus();
}
}
{
repeat(256)
{
success = mailbox_get(WAIT,mboxId,address,CHECK);
success = mailbox_get(WAIT,mboxId,data,CHECK);
cpu1.request_bus();
cpu1.readOp(address,data);
cpu1.release_bus();
}
}
join
64 Chapter 5. Memory System Tutorial