You are on page 1of 64

Vera Tutorial

Last updated: 20 Aug 2003

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.

Vera Tutorial 6.0 (2003)

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.

If you have any questions or problems, do not hesitate to contact us by email at


vera-support@synopsys.com.
6 Chapter 1. Introduction to Vera Tutorial
Tutorial Chapter 2. System Overview 7

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

2.1 Memory System


The system used in this tutorial is a simple memory system for a two CPU machine. It consists
of a system bus, a centralized round-robin arbiter, and a memory controller that controls four
static SRAM devices. Figure 2-1 shows the system block diagram.
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

Figure 2-1 Memory System Schematic


8 Chapter 2. System Overview Tutorial

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:

1. sub-modules are individually verified,

2. sub-modules are integrated into the final design.

This “full chip” functionality is verified in the system simulation.

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

2.2 System File Setup


The tutorial’s directory structure is shown below.

new_memsys

sram arb cntrlr memsys

rtl test rtl test rtl test


rtl
cntrlr.v
memsys.v README
arb.v cntrlr.vhd
memsys.vhd
sram.v arb.vhd cntrlr_top.vhd
sram.vhd arb_top.vhd memsys_top.vhd

README: short description and file/directory index and listing of tools and versions used

sram: contains the memory RTL

arb: contains the submodule RTL and test directory

cntrlr: contains the submodule RTL and test directory

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

vera_out: compilation and runtime generated files go here

run_scr: cshell and make run scripts for various simulators are stored here
10 Chapter 2. System Overview Tutorial

current test directory

Makefile include source vera_out run_scr


setup

interface.vri testbench.vr empty before makefile


port_binds.vri simulation

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.

2.3 Running the Tutorial


Before running the tutorial you should customize the setup script found in the current test
directory. The test directory can be found in new_memsys/PROJECT_DIR/test. Where
PROJECT_DIR is either arb, cntrlr, or memsys. Within the setup script you will be setting tool
specific settings for Vera and your simulator.

After you have finished editing, source the setup file:

>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:

Table 1: Supported Simulators


Platform VCS VCS-MX MTI VHDL MTI Verilog NC Verilog

Solaris 5.7, 5.8 X X X X X


RedHat 7.2 X X X X X
Tutorial Chapter 2. System Overview 11

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

Vera Tutorial Makefile

Note – You must edit and source setup to customize your environment

General Options:

make help : displays this message


make clean :cleans all files created during compilation and runtime
make cleanall :cleans all files created during compilation and runtime

Synopsys Simulation

make vcs : run with VCS (Verilog)


make vcs-mx : run with VCS-MX (VHDL)

MTI Simulation

make mti_vlog : run with MTI (Verilog)


make mti_vhdl : run with MTI (VHDL)

NC Simulation

make nc-vlog : run with NC (Verilog)

Coverage Report Options

make html : generates coverage report and opens under Netscape.


make text : generates coverage report and opens under more.

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

To run the solution for MTI VHDL:

>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

3.1 Arbiter Overview


One reason Vera is flexible is because the same Vera testbench works with devices described
using Verilog or VHDL. Once Vera is hooked up to the simulator, everything in Vera stays the
same even though the simulator is changed.

In this section, you will be working inside the arb/test directory.


• The tutorial arbiter Vera source file solution is in this file:
new_memsys/arb/test/source/arb.vr

• The VHDL arbiter RTL source code is in the file:


new_memsys/arb/rtl/arb.vhd

• The Verilog arbiter RTL source code is in this file:


new_memsys/arb/rtl/arb.v

• The Tutorial solution run scripts for Verilog/VHDL simulators are in the
following directory:
new_memsys/arb/test/run_scr

• The Tutorial solution Vera interface file is in the following file:


new_memsys/arb/test/include/arb.if.vri

• Tutorial solution Vera compile output files are written to the following
directory:
new_memsys/arb/test/vera_out
14 Chapter 3. Arbiter Tutorial

• Tutorial Makefile and setup script is located in:


new_memsys/arb/test

Make sure you edit setup for your installation and then source the file.

Figure 3-1 shows the arbiter timing diagram.

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

Figure 3-1 Arbiter Timing Diagram

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

3.2 Vera Testbench Key Components


A Vera testbench suite is comprised of several key components:
• Vera Testbench Module - Vera testbench
• Interface Specification - Defines the Vera signals for the testbench module.
Typically the interface specification is contained in a file with a name of this
format: filename.if.vrh or filename.vri. The vrh stands for Vera compiler
generated interface file or header file, and the .vri suffix denotes user
generated interface or header file.
• Vera Shell File (filename.vshell or filename_shell.vhd) - Verilog and VHDL,
respectively, file that acts as a wrapper or gasket around the testbench
module. The Vera testbench drives the DUT through this shell file. The shell
file also contains all the PLI calls in Verilog and the simulator interface calls
in VHDL required to run the testbench.
• Test-top File (filename.test_top.v or filename_top.vhd) - Top level netlist file that
encapsulates the DUT and the Vera testbench suite. It instantiates the DUT
and the shell file, handles the clock generation, and handles file dumping in
Verilog.

Figure 3-2 shows the basic schematic for this configuration.

Clock Generator

Device Under Test Vera Shell File


Input Signals
Device
Under Test
Output Signals

Vera
Testbench
Module

Interface Specification

Test-top file

Figure 3-2 Test-top Configuration Schematic


16 Chapter 3. Arbiter Tutorial

3.2.1 How to Hook Up Verilog


Vera provides a template generator to assist in the setup of this configuration. Within the
arbiter working directory (new_memsys/arb/test), invoke the template generator:

vera -tem -t arb -c clk ../rtl/arb.v

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..

Writing top_file to arb.test_top.v

Writing vera interface file to arb.if.vrh

Writing vera template file to arb.vr.tmp

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.

Rename the arb.vr.tmp to arb.vr.

3.2.2 How to Hook Up VHDL (VCS-MX)


This description shows how to hook up Vera to VCS-MX. Other VHDL simulators are similar.
See the README file in the Vera installation directory:

$VERA_HOME/doc/README.simulators.

Also, see $VERA_HOME/doc/install/install.pdf for the installation directions.

Vera provides a toplevel testbench template generator to assist in the setup of connecting Vera
to the DUT.

Within the arbiter working directory (new_memsys/arb/test) create an interface declaration.


For this tutorial call the file arb.if.vri and use it to define the interfaces of all the signals Vera
will be connecting to in the device. Then create an “empty” arb.vr file that simply includes the
arb.if.vri file and the vera_defines.vrh. The content of the arb.vr file should look like:

#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.

Your arb.if.vri file should look like:

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.

To create the VHDL code, type:

vera -cmp -sro -top arb.vr

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

Invoking the Vera template generator creates these files:


• arb_top.vhd
• arb_shell.vhd
• arb.vro

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

vera -cmp arb.vr

however, this is not necessary at this point as you have not added anything new to the arb.vr
file.

3.3 Verifying the Arbiter


Identify the required tests. First, verify the arbiter reset. Second, verify the arbiter handles
simple requests appropriately and can grant access to one of the CPUs. Finally, check for
proper Arbiter handling of request sequences.

3.3.1 Reset Verification


Verify resets are working correctly. First assert the reset signal. With the reset signal asserted,
hold the request signals inactive for each CPU (drive them to 0) and check that the grant
signals are at their inactive states (0) after the reset.
20 Chapter 3. Arbiter Tutorial

Referencing Vera Signals


To reference a Vera signal, specify the interface name and the Vera signal name. Using our arb
interface, the reset, request, and grant signals are referenced as:

arb.reset
arb.request
arb.grant

Basic Signal Operation


All signal operations occur on the clock edge specified in the interface. If an output signal is
marked PHOLD, all drives occur on the positive edge of the interface clock. Similarly, input
signals marked PSAMPLE are sampled on the positive edge of the interface clock.

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:

#define INPUT_SKEW #-1

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

Verifying the Reset


Using the basic signal operations described earlier, add this code to the arb.vr.tmp file to verify
the resets:

arb.reset = 1; //assert reset


@1 arb.reset = 0; // de-assert reset after 1 clock cycle
@0 arb.request = 2’b00; // de-assert request on next positive clock edge
@1 arb.grant==2’b00; // check that grant is de-assert after 1 clock cycle

Note that the request and grant signals are 2-bit signals. Each bit of the signals must be
de-asserted.

Running the Simulation with VCS


At this point you should be in the arb/test directory. See the “run_scr” directory for different
simulator run scripts and the makefiles for both Verilog and VHDL. If you want to use any of
these to run the tutorial solution, invoke the script from the test directory. Also see the
README file description containing the simulation output.

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:

vera -cmp arb.vr

Compiling the testbench generates the Vera shell file (arb.vshell) and the Vera testbench binary
object file (arb.vro).

Run the simulation:

vcs arb.test_top.v ../rtl/arb.v arb.vshell -vera

simv +vera_load=arb.vro

Your test should run to completion without any errors and the output should be as follows:

Compiler version 7.0.1; Runtime version 7.0.1; Aug 6 17:18 2003

Vera: finish encountered at time 250 cycle 3


total mismatch: 0
vca_error: 0
fail(expected): 0
drive: 3
expect: 1
sample: 0
sync: 0
22 Chapter 3. Arbiter Tutorial

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.

You will observe the following output:

Compiler version 7.0.1; Runtime version 7.0.1; Aug 6 17:21 2003

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

Running the Simulation with VCS-MX


At this point you should be in the arb/test directory. See the “run_scr” directory for different
simulator run scripts and the makefiles for both Verilog and VHDL. If you want to use any of
these to run the tutorial solution, invoke the script from the test directory. Also see the
README file description containing the simulation output.

With the code added to the arbiter testbench (arb.vr), run the simulation and test the results.
Compile the Vera testbench:

vera -cmp arb.vr

Compiling the testbench generates the Vera testbench binary object file (arb.vro). These files
need to be included when the simulation is run.

1) Create .synopsys_vss.setup file

echo "WORK > DEFAULT" > .synopsys_vss.setup


echo "DEFAULT : work" >> .synopsys_vss.setup
echo "TIMEBASE = ns" >> .synopsys_vss.setup
Tutorial Chapter 3. Arbiter 23

2) Create vera.ini file with inclusion of arbiter object code

echo "vera_load=./vera_out/arb.vro" > ./vera.ini


echo "vera_continue_on_error ON" >> ./vera.ini

3) Analyze vhdl source code

vhdlan -nc -event ../rtl/arb.vhd


vhdlan -nc -event ./vera_out/arb_shell.vhd
vhdlan -nc -event ./vera_out/arb_top.vhd

4) Create scsim (compile)

scs -nc DUT_BENCH_CFG

5) Create run script (optional)

echo "# "> ./vera_out/sc.do


echo "#Type ’run’ to start simulation" >> ./vera_out/sc.do
echo "# " >> ./vera_out/sc.do
echo "run " >> ./vera_out/sc.do

6) Run simulation

scsim -nc -include ./vera_out/sc.do

3.3.2 Simple Request Verification (Verilog and VHDL)


To check if the arbiter is handling simple requests correctly, monitor the request signals, check
that the grant signal is set appropriately, and then check that the grant signal is de-asserted
after the request is released.

Test For Simple Request by CPU0


To test that simple requests are handled correctly for CPU0, drive bit 0 of the request signal
and then monitor bit 0 of the grant signal. Finally, de-assert both bits of the request signal and
check that both signals of the grant signal are properly de-asserted.

@0 arb.request = 2’b01; // assert bit 0 of request


@2 arb.grant == 2’b01; // check that bit 0 of grant is asserted
@0 arb.request = 2’b00; // de-assert bit 0 of request
@2 arb.grant == 2’b00; // check that both bits of grant are de-asserted

Test For Simple Request by CPU1


To test that simple requests are handled correctly for CPU1, drive bit 1 of the request signal
and then monitor bit 1 of the grant signal. Finally, de-assert both bits of the request signal and
check that both signals of the grant signal are properly de-asserted.
24 Chapter 3. Arbiter Tutorial

@0 arb.request = 2’b10; // assert bit 1 of request


@2 arb.grant == 2’b10; // check that bit 1 of grant is asserted
@0 arb.request = 2’b00; // de-assert bit 0 of request
@2 arb.grant == 2’b00; // check that both bits of grant are de-asserted

3.3.3 Sequenced Request Verification


Verify sequences of requests are handled properly by checking a series of conditions:
• Assert both request signals and check for correct grant assertion
• Release the granted request and check for grant release
• Assert both request signals and check for correct grant assertion
• Release the newly granted request and check for grant release
• Check for new grant assertion
• Release last request and check that both grants are released

Given this verification methodology, the code to check arbiter behavior is:

@0 arb.request = 2’b11; // assert both request signals


@2 arb.grant == 2’b01; // check for first grant
@0 arb.request = 2’b10; // de-assert corresponding request
@1 arb.request = 2’b11; // assert both request signals
@1 arb.grant == 2’b00; // check that grant de-asserts for 1 cycle
@1 arb.grant == 2’b10; // check that other grant is asserted
@1 arb.request = 2’b01; // de-assert corresponding request
@2 arb.grant ==2’b00; // check that grant de-asserts for 1 cycle
@1 arb.grant == 2’b01; // check for first grant
@1 arb.request = 2’b00; // de-assert both request signals
@2 arb.grant == 2’b00; // check that both grant signals are de-asserted

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:

input [1:0] grant INPUT_EDGE INPUT_SKEW vca r0;

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

4.1 Memory Controller Overview


In our system, the CPU accesses the bus through the arbiter. Once the CPU has access, it puts
its request on the system bus. The memory controller acts on this request by reading data from
the SRAM devices and returning data when necessary. All edits will be performed inside the
new_memsys/cntrlr/test directory.
• The tutorial controller Vera source file solution is in this file:
new_memsys/cntrlr/source/cntrlr.vr

• The VHDL controller RTL source code is in the file:


new_memsys/cntrlr/rtl/cntrlr.vhd

• The Verilog controller RTL source code is in this file:


new_memsys/cntrlr/rtl/cntrlr.v

• The project Makefile is located in :


new_memsys/cntrlr/test

type ‘make help’ for details

• The Tutorial solution Vera interface declaration is in the following program


file:
new_memsys/cntrlr/test/source/cntrlr.vr

• 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

• Tutorial Makefile and setup script is located in:


new_memsys/cntrlr/test

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

rdWr_N MEMORY adxStrb


ramAddr CONTROLLER busAddr
ce_N busRdWr_N
ramData busData

SRAM Side System Bus side


Figure 4-1 Vera/Memory Controller Interaction
Tutorial Chapter 4. Memory Controller 27

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_

Figure 4-2 Memory Controller Read Operation Timing Diagram


28 Chapter 4. Memory Controller Tutorial

clk

reset

adxStrb

busAddr valid

busData valid

busRdWr_

cex_

ramData valid

ramAddr valid

rdWr_

Figure 4-3 Memory Controller Write Operation Timing Diagram

4.2 Verifying the Memory Controller


To completely check the functionality of the memory controller, perform a series of tests. First,
check the read and write capabilities of the controller. To do this, create Vera tasks that drive
the bus for read and write operations. Then check the integrity of the read and write
operations. Finally, exhaustively check the address map (all 256 addresses) for the read and
write functions.

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:

vera -tem -t cntrlr -c clk ../rtl/cntrlr.v

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:

task readOp (bit[7:0] adx)


{
cntrlr.busAddr = adx;
cntrlr.busRdWr_ = 1’b1;
cntrlr.adxStrb = 1’b1;
@1 cntrlr.adxStrb = 1’b0;
}

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:

task writeOp (bit[7:0] adx, bit[7:0] data)


{
@1 cntrlr.busAddr = adx;
cntrlr.busData = data;
cntrlr.busRdWr_ = 1’b0;
cntrlr.adxStrb = 1’b1;
@1 cntrlr.busRdWr_ = 1’b1;
cntrlr.busData = 8’bzzzzzzzz;
cntrlr.adxStrb = 1’b0;
}
30 Chapter 4. Memory Controller Tutorial

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.

4.2.2 Implementing Virtual Ports


Vera’s virtual ports allows the grouping of Vera interface signals into logical bundles. These
signals can be passed to tasks that you want to act on specific sets of signals. This is done by
defining a virtual (or generic) port, which is a set of generic port signal names that act as
placeholders for the actual interface signals they are bound to. The virtual ports are then
bound to specific interface signals as needed. This feature allows a task to be written once,
then re-used many times at different interfaces to the design under test. The port variable
allows task and function reuse by giving the verification engineer the ability to pass task or
function specific interface connections to both tasks and functions. So in essence, Vera turns
interface connections into parameters that can be passed around the testbench as needed. The
only limit is the limit of the verification engineer’s imagination.

Defining Virtual Ports


Vera virtual ports are defined outside the main program block using this construct:

port port_name {port_signal_member1; ...; port_signal_memberN;}

port_name - The port_name must be a valid identifier.

port_signal_memberN - port_signal_memberN must be a valid identifier. Multiple port signal names


are separated by semi-colons (;).

Binding Virtual Port Signal Members to Interface Signals


The bind construct (For a complete discussion see “bind Construct for Static Connection,” in the
Vera User Guide) not only associates port-signal-members with interface signals, but also
involves declaring a port variable as well. Outside the main program block, use the bind
construct:

bind port_name port_variable


{
port_signal_memberN interface_name.signal_name;
}

port_name - The port_name is the user defined virtual port whose signal member names you want
associated with interface signals.

port_variable - The port_variable is the name of the variable being declared.

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].

Referencing Ports and Binds


To reference or pass a port to a subroutine, use port variables. Port variables store virtual
port/bind pairs. Each virtual port definition becomes a new data type (much like enumerated
types) that can be used to declare new port variables. The syntax to declare a port variable,
when not using the bind construct, is:

port_name port_variable = initial_value;

port_name - The port_name is the name of the port data type.

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.

To reference individual port signals within a subroutine, use this construct:

$signal_name

This references the specified port signal in the bind passed to the subroutine.

Implementing Ports and Binds in the Memory Controller


Given the port/bind methodology presented here, define a device port for the SRAM parts
(ramAddr, ramData, rdWr_, and ce_):

port device
{
ramAddr;
ramData;
rdWr_;
ce_;
}

After defining the virtual port, connect the port signals to actual interface signals using the
bind construct:

bind device device0


{
ramAddr cntrlr.ramAddr;
ramData cntrlr.ramData;
32 Chapter 4. Memory Controller Tutorial

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.

4.2.3 Verifying Read and Write Operations


The memory controller issues read and write operations to each of the four SRAM devices as
shown in the earlier timing diagram. Create read and write tasks in our testbench that check
these operations. Earlier, we modeled the timing diagram exactly, cycle by cycle. Our
approach now is to make use of Vera’s timing windows, which allow you to specify ranges of
time and event sequences.

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:

@window signal_name == value;

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.

Verifying the Write Operation


To verify the write operation, create a Vera task that checks the SRAM write operation against
the timing diagram provided. The task should have an argument of port variable type device
so that we can pass in the signals we want it to act on. The task also has 6-bit address and 8-
bit data busses as inputs. It must check that the SRAM signals are driven correctly, check that
the address is the right address, and drive the data onto the ramData bus at the appropriate
time. Given these requirements, the code is:

task checkSramWrite (device d, bit[5:0] adx, bit[7:0] data)


{
@1,5 d.$ramAddr == adx;
@,2 d.$ramData == data;
@1 d.$rdWr_ == 0;
d.$ce_ == 0;
d.$ramData == data;
Tutorial Chapter 4. Memory Controller 33

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.

Synchronous and Asynchronous Timing


By default, all Vera signal operations are synchronous. That is they occur on the clock edges
specified in the interface specification. However, all Vera signal operations can be used
asynchronously by adding the async keyword after the operation:

@(edge signal_name async); //advance to next edge of signal


signal_name = value async; // drive new value immediately
signal_name == value async;// execute expect expression immediately

Note that the delays for the drive and expect operations are not used since they occur
immediately.

These are examples of async statements:


@(posedge main_bus.request async);
memsys.data[3:0] = 4’b1010 async;
data[2:0] = main_bus.data[2:0] async;
main_bus.data[7:4] == 4’b0101 async;

Verifying the Read Operation


To verify the read operation, check that the control signals are asserted, the correct address is
driven by the memory controller, and the input data is driven as return data. However, an
interesting timing issue arises in this case. The SRAM device drives data after the
corresponding ce_ signal is asserted. This must happen in the same clock cycle for the device
34 Chapter 4. Memory Controller Tutorial

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

data should be data is driven here


driven here because of sampling skews
data VALID

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:

task checkSramRead (device d, bit[5:0] adx, bit[7:0] data)


{
@1,5 d.$ramAddr == adx;
@(d.$ce_ async);
d.$ce_ == 0 async;
d.$rdWr_ == 1 async;
d.$ramAddr == adx async;
d.$ramData = data async;
@1 d.$ramData <= 8’bzzzzzzzz;
}

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

Running the Simulation


Before running the simulation, set up a reset check to ensure that the controller is resetting
correctly. To check the controller reset, assert the reset signal and de-assert adxStrb. Next check
that all the chip enables are de-asserted (cex_). Finally, de-assert the reset signal. These
requirements are met with this code:

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:

writeOp (8’h01, 8’h5A);


checkSramWrite (device0, 6’b000001, 8’h5A);

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:

Device Valid Address Range


0 0-63
1 64-127
2 128-191
3 192-255

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.

4.3 Using the Vera Debugger with the cntrlr Example


Refer to the Debugger tutorial by invoking “vera-doc” and selecting the debugger tutorial
under application notes.

Using either or both of the following runtime options will bring up the debugger:

+vera_debug_on_start brings up the debugger immediatley before running the simulation.

+vera_debug_on_error causes the debugger to come up in the event of a verification error.

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

5.1 Memory System Overview


You will be working inside the new_memsys/memsys/test directory.
• The tutorial memsys Vera source file solution is in this file:
new_memsys/memsys/source/memsys.vr

• The VHDL memsys RTL is in the file:


new_memsys/memsys/rtl/memsys.vhd

• The VHDL memsys RTL toplevel netlist is in the file:


new_memsys/memsys/rtl/memsys3_oop_top.vhd

• The Verilog memsys RTL netlist is in this file:


new_memsys/memsys/rtl/memsys.v

• Tutorial solution run scripts for Verilog/VHDL simulators are in the


following directory:
new_memsys/memsys/test/run_scr

• The Tutorial solution Vera interface file is in the following file:


new_memsys/memsys/test/include/memsys.if.vri

• 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

• Tutorial Makefile and setup script is located in:


new_memsys/memsys/test

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

Figure 5-1 Memory System Schematic

5.2 Verifying the Memory System


The methodology used to verify the entire memory system is broken down by tasks and
concepts:

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 41

• General Verification - Reset verification and read/write operations.


• Basic Concurrency Control - Using regions, checks that each address is
unique before the bus is requested.
• Object Oriented Programming (OOP) - OOP allows us to simplify our
testbench and provide re-usable code blocks.
• Functional Coverage - Vera’s coverage objects help ensure that our address
space is tested sufficiently.
• Interprocess Communication - Using triggers, we advance or simulation in
lock-step fashion to make sure that data is read only after it is written to the
bus. Mailboxes allow us to use random addresses while checking that each
address is used only once.

5.2.1 General Verification


The general verification tasks include checking the reset procedure and modifying the read
and write operations previously developed for the memory controller. Finally, we develop a
testbench that checks both CPUs running concurrently using multiple threads.

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;

Read and Write Operations


The bus in the system is very similar to the bus used in the memory controller. We can use the
write operation task by simply modifying the interface names in the signal operations
(memsys.signal). The writeOp task is:

task writeOp (bit[7:0] adx, bit[7:0] data)


{
@1 memsys.busAddr = adx;
memsys.busData = data;
memsys.busRdWr_ = 1’b0;
memsys.adxStrb = 1’b1;
@1 memsys.busRdWr_ = 1’b1;
memsys.busData = b’bzzzzzzzz;
memsys.adxStrb = 1’b0;
}
42 Chapter 5. Memory System Tutorial

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:

task readOp (bit[7:0] adx, bit[7:0] data)


{
@1 memsys.busAddr = adx;
memsys.busRdWr_ = 1’b1;
memsys.adxStrb = 1’b1;
@1 memsys.adxStrb = 1’b0;
@2,5 memsys.busData == data;
}

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

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 43

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:

random(12933); // call random with seed


fork
{ // CPU0
repeat(256)
{
randVar0 = random(); // get 32 bit random variable
address0 = randVar0[13:6]; // get random 8-bit address
data0 = randVar0[29:22]; // get random 8-bit data
@1 memsys.request[0] = 1’b1; // request the bus
@2,20 memsys.grant == 2’b01; // check for grant
writeOp(address0, data0); // issue write operation
@1 memsys.request[0] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for release
@1 memsys.request[0] = 1’b1; // request again
@2,20 memsys.grant == 2’b01; // check for grant
readOp(address0, data0); // issue read operation
@1 memsys.request[0] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for grant
}
}
{ // CPU1
repeat(256)
{
randVar1 = random(); // get 32 bit random variable
address1 = randVar1[13:6]; // get random 8-bit address
data1 = randVar1[29:22]; // get random 8-bit data
@1 memsys.request[1] = 1’b1; // request the bus
@2,20 memsys.grant == 2’b10; // check for grant
writeOp(address1, data1); // issue write operation
@1 memsys.request[1] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for release
@1 memsys.request[1] = 1’b1; // request again
@2,20 memsys.grant == 2’b10; // check for grant
readOp(address1, data1); // issue read operation
@1 memsys.request[1] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for grant
}
}
join
44 Chapter 5. Memory System Tutorial

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.

5.2.2 Basic Concurrency Control


In our system, we must check that the address is unique before the bus is requested to avoid
conflicts. To do this, we use regions.

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.

To allocate a region, you must use the alloc() system function:

function int alloc(REGION, int region_id, int region_count);

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.

The region_enter() system function checks to see if a particular region is in use:

function int region_enter(keyword wait_option, int region_id,


bit|int value1, value2, ..., valueN);

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.

region_id - The region_id specifies which region is being entered.

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.

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 45

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:

task region_exit(int region_id, bit|int value1, value2, ..., valueN);

region_id - The region_id specifies which region that is being exited.

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:

regId = alloc(REGION, 0, 1);


trace(ON, REGION, regId);

random(12933); // call random with seed


fork
{ // CPU0
repeat(256)
{
randVar0 = random(); // get 32 bit random variable
address0 = randVar0[13:6]; // get random 8-bit address
data0 = randVar0[29:22]; // get random 8-bit data
region_enter(WAIT, regId, address0); // check if address is free
@1 memsys.request[0] = 1’b1; // request the bus
@2,20 memsys.grant == 2’b01; // check for grant
writeOp(address0, data0); // issue write operation
@1 memsys.request[0] = 1’b0; // release request
46 Chapter 5. Memory System Tutorial

@2,20 memsys.grant == 2’b00; // check for release


@1 memsys.request[0] = 1’b1; // request again
@2,20 memsys.grant == 2’b01; // check for grant
readOp(address0, data0); // issue read operation
@1 memsys.request[0] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for grant
region_exit(regId, address0); // exit region
repeat (randVar0[20:17]) @(posedge memsys.clk);
}
}
{ // CPU1
repeat(256)
{
randVar1 = random(); // get 32 bit random variable
address1 = randVar1[13:6]; // get random 8-bit address
data1 = randVar1[29:22]; // get random 8-bit data
region_enter(WAIT, regId, address1); // check if address is free
@1 memsys.request[1] = 1’b1; // request the bus
@2,20 memsys.grant == 2’b10; // check for grant
writeOp(address1, data1); // issue write operation
@1 memsys.request[1] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for release
@1 memsys.request[1] = 1’b1; // request again
@2,20 memsys.grant == 2’b10; // check for grant
readOp(address1, data1); // issue read operation
@1 memsys.request[1] = 1’b0; // release request
@2,20 memsys.grant == 2’b00; // check for grant
region_exit(regId, address1); // exit region
repeat (randVar1[20:17]) @(posedge memsys.clk);
}
}
join

5.2.3 Object Oriented Programming (OOP)


OOP allows you to develop programs that are easier to debug and easier to reuse by
encapsulating related code and data together and making access to the class formal and
rigorous. Vera then uses many of its features within this object-oriented framework. In this
section, we examine how classes can be implemented into our memory system using modified
port declarations, how classes are constructed, and how random stimuli are built into the Vera
objects.

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 47

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:

class_name instance_name = new();

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:

class_name instance_name = new(argument1, argument2, ... argumentN);

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:

bind bus_arb arb0


{
request memsys.request[0];
grant memsys.grant[0];
}

bind bus_arb arb1


{
request memsys.request[1];
grant memsys.grant[1];
}

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:

task new (bus_arb arb)


{
printf(“Constructing new CPU.\n”);
localarb = arb;
}

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.

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 49

The readOp method is:

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:

rand data_type variable = initial_value;

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:

function int object_name.randomize();

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:

rand bit[7:0] address, data;

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:

rand integer delay;


...
task delay_cycle()
{
repeat(delay) @(posedge memsys.clk);
printf(“delay = %d/n”,delay);
}

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 51

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 constraint_name { contraint_expressions }

constraint_name - The constraint_name is the name of the constraint block.

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 operator expression;

random_variable - The random_variable parameter specifies the variable to which the constraint is
applied.

operator - The valid operators for constraints are: <, <=, ==, >=, >, !=, ===, !==, =?=, and !?=.

expression - The constraint expression where:

• Constraints can be any OpenVera expression with variables and


constants of type bit, integer, or enumerated type.

• Constraint expressions follow Verilog syntax and semantics,


including precedence, associativity, sign extension, truncation,
and wrap-around.

• 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:

CPU cpu0 = new(arb0);


CPU cpu1 = new(arb1);

With our class CPU defined with the properties and methods described above, the same
execution sequence created using fork/join can be written as:

regId = alloc(REGION, 0, 1);


trace(ON, REGION, regId);

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.

5.2.4 Functional Coverage


Vera’s functional coverage capabilities are available in both an external coverage definition as
well as within the OOP framework. First, you specify a coverage definition through the
coverage_group directive. The definition includes valid and invalid states and transitions that
are monitored through out the simulation. Additionally coverage goals and the sample event
can be defined. As an introduction to Vera’s functional coverage, a subset of the coverage
features is discussed here: state and transition declarations, instantiation and sampling, and
coverage reports.

Note – For the purposes of the tutorial we will be working with


coverage group definitions external to a class definition. Please see
the User Guide for defining embedded coverage groups.

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 53

Coverage Definition
The basic syntax for defining a coverage_group is:

coverage_group definition_name [(argument_list)]

{
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:

sample variable_name [, variable_name];

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:

cross cross_name (sampled_variable_list);

where sampled_variable_list is a comma separated list of the sampled variables of the


coverage_group. We will not be using cross coverage in the tutorial
54 Chapter 5. Memory System Tutorial

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;

The event_expression can be an expression such as @([specified_edge] interface_signal),


sync(), or wait_var().

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.

coverage_group MyCov(sample bit[3:0] paramVar){


sample_event = @(posedge CLOCK); // Sample event
sample paramVar; // Passed-in sample parameter

Sampled Variable State and Transition Bins


If you do not define any state or transition bins for a sampled variable, Vera automatically
creates state bins for you. This provides an easy-to-use mechanism for binning different values
of a sampled variable.

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 55

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.

The syntax for a state declaration is:

state state_bin_name (state_specification);

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:

m_state state_bin_name (exp1:exp2);

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:

bad_state error_bin_name (state_specification);

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:

bad_state error_bin_name (not state);

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:

trans trans_bin_name (state_transitions) conditional;

Declaring a sequence of transitions between states specifies state transitions. The general
format is:

trans trans_bin_name (state_set_1 -> state_set_2 -> ... ->state_set_N);

Illegal transition declarations associate an illegal transition with a monitor bin. The syntax is:

bad_trans trans_bin_name (state_transitions);

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.

bad_trans trans_bin_name (not trans);

The counter associated with the specified bin will be incremented every time a transition
occurs that is not explicitly defined in the transition declaration.

Defining Sample Events


You must specify a sampling event expression in the coverage group definition. This will be
used for all instantiations of a coverage definition. The sampling event expression allows you
to control when the object takes a sample. Coverage objects can be triggered on clock edges,
signal edges, variable changes, sync events and OVA events. For the purposes of the tutorial
we will be sampling on Clock and signal edges. For a complete description of other sample
events please refer to the Vera User Guide.

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:

sample_event = @([specified_edge] interface_signal | CLOCK);

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 57

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;
}

Implementing Coverage Groups


We have added a memory controller probe that acts as a state machine for the memory
controller. The signal that it monitors is memcntlr_probe.cntlr_state. Because we are monitoring
a new signal, we must add a new interface:

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:

coverage_group cntlr_cov(sample bit[1:0] cntlr_state)


{
sample_event = @(posedge CLOCK);
sample cntlr_state {
state IDLE(0);
state START(1);
state WRITE0(2);
state WRITE1(3);
bad_state (not state);
trans t0 ("IDLE" -> "IDLE");
trans t1 ("IDLE" -> "START");
trans t2 ("START" -> "IDLE");
trans t3 ("START" -> "WRITE0");
trans t4 ("WRITE0" -> "WRITE1");
trans t5 ("WRITE1" -> "IDLE");
bad_trans (not trans);
}
}
58 Chapter 5. Memory System Tutorial

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:

coverage_group range(sample bit[7:0] peek)

{
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:

cov1 = new (memcntlr_probe.cntlr_state);


cov2 = new(memsys.busAddr);

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:

inout adxStrb OUTPUT_EDGE INPUT_EDGE OUTPUT_SKEW;

Analysis of Coverage Results


Following the simulation run, Vera will generate a coverage database:

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:

vera –cov_report memsys_test.db


netscape memsys_test.index.html&

Text report:

vera –cov_text_report memsys_test.db


more memsys_test.txt

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 59

Note –
The Makefile for this tutorial has two options for generating reports.
Please select either of the following two options:

make html# HTML report, opens with Netscape


make text# text report, opens in more

Figure 5-2 shows a typical example of an HTML coverage report.

Figure 5-2 Coverage Rerport


60 Chapter 5. Memory System Tutorial

5.2.5 Interprocess Communication


In addition to regions, we can use triggers and mailboxes to control our concurrent processes.
Triggers allow us to advance the simulation in lockstep such that one CPU writes data to the
bus and the other reads that data before the next write. This allows us to verify that data is
correctly written to memory.

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:

task sync(keyword sync_type, event event1, event2, ... eventN);

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.

Triggers send events. The syntax to call a trigger is:

function int trigger([keyword trigger_type,] event event_name);

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.

event_name - The event_name is the event being triggered.

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):

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 61

event go_ahead, done;


integer index0, index1;
...

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:

function int alloc(MAILBOX, int mailbox_id, int mailbox_count);

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:

task mailbox_put(int mailbox_id, scalar data);

mailbox_id - The mailbox_id specifies which mailbox receives the data.

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:

function scalar mailbox_get(keyword wait_option, int mailbox_id


[, scalar dest_var [, keyword check_option]]);

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.

dest_var - The dest_var is the destination variable of the mailbox data.

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:

Confidential and Proprietary Synopsys Inc.


Tutorial Chapter 5. Memory System 63

mboxId = alloc(MAILBOX, 0, 1);

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

Confidential and Proprietary Synopsys Inc.

You might also like