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

Table of Contents
1. 2. Introduction to Vera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 System File Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Running the Tutorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Arbiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arbiter Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vera Testbench Key Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verifying the Arbiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Memory Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Memory Controller Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verifying the Memory Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using the Vera Debugger with the cntrlr Example . . . . . . . . . . . . . . . . . . . . . . 13 13 15 19 25 25 28 37

3.

4.

5.

Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Memory System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Verifying the Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

Table of Contents

Tutorial

Tutorial

Chapter 1. Introduction to Vera

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.

Chapter 1. Introduction to Vera

Tutorial

Tutorial

Chapter 2. System Overview

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. S R A M S R A M S R A M S R A M ce0_N

rdWr_N ce1_N reset

MEMORY CONTROLLER ROUND-ROBIN ARBITER


ce2_N

request[0] CPU0 grant[0]

System Bus ce3_N address data


Figure 2-1 Memory System Schematic

grant[1] CPU1 request[1]

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. 2. sub-modules are individually verified, 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 submodules 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

2.2

System File Setup


The tutorials directory structure is shown below.

new_memsys

sram rtl rtl

arb test rtl

cntrlr test rtl

memsys test

sram.v sram.vhd

arb.v arb.vhd arb_top.vhd

cntrlr.v cntrlr.vhd cntrlr_top.vhd

memsys.v memsys.vhd memsys_top.vhd

README

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 setup

include

source

vera_out

run_scr

interface.vri port_binds.vri

testbench.vr

empty before simulation

makefile

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 RedHat 7.2

X X

X X

X 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 arbiters 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 grant xx xx 00 00 01 01 00 00 10 10 00 11 00

clk

reset request grant 11 00 01 10 00 10 00 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 Output Signals

Device Under Test

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 1 time unit after driving clock edge grant driven 1 time unit after next driving clock edge

request

grant

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 output output input } clk CLOCK ; reset PHOLD #1 ; [1:0] [1:0] request PHOLD #1 ; 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 1 time unit after driving clock edge grant driven 1 time unit after next driving clock edge

request

grant

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 = 2b00; // de-assert request on next positive clock edge @1 arb.grant==2b00; // 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; Vera: finish encountered at time total mismatch: 0 vca_error: 0 fail(expected): 0 drive: 3 expect: 1 sample: 0 sync: 0 250 cycle Aug 6 17:18 2003 3

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==2b01;

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; EXPECT MISMATCH TIME: 250 CYCLE: 3 Signal: arb.grant.0 Exp Value: 1 : 01 Actual Value: 0 : 00 VERIFICATION ERROR: Expect mismatch arb_test (arb.vr, line 14, cycle 3) $stop at time 250 Scope: arb_test_top.vshell File: arb.vshell Line: 50 Location: WAIT_ON_EXPECT in program Aug 6 17:21 2003

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 echo echo "WORK > DEFAULT" > "TIMEBASE = ns" .synopsys_vss.setup

"DEFAULT : work" >> .synopsys_vss.setup >> .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" echo "vera_continue_on_error ON" > ./vera.ini

>> ./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 "# echo "# " "> ./vera_out/sc.do >> ./vera_out/sc.do >> ./vera_out/sc.do >> ./vera_out/sc.do

echo "#Type run to start simulation" echo "run "

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 = 2b01; // assert bit 0 of request @2 arb.grant == 2b01; // check that bit 0 of grant is asserted @0 arb.request = 2b00; // de-assert bit 0 of request @2 arb.grant == 2b00; // 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 = 2b10; // assert bit 1 of request @2 arb.grant == 2b10; // check that bit 1 of grant is asserted @0 arb.request = 2b00; // de-assert bit 0 of request @2 arb.grant == 2b00; // 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 = 2b11; // assert both request signals @2 arb.grant == 2b01; // check for first grant @0 arb.request = 2b10; // de-assert corresponding request @1 arb.request = 2b11; // assert both request signals @1 arb.grant == 2b00; // check that grant de-asserts for 1 cycle @1 arb.grant == 2b10; // check that other grant is asserted @1 arb.request = 2b01; // de-assert corresponding request @2 arb.grant ==2b00; // check that grant de-asserts for 1 cycle @1 arb.grant == 2b01; // check for first grant @1 arb.request = 2b00; // de-assert both request signals @2 arb.grant == 2b00; // 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 Veras 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 ramAddr ce_N ramData SRAM Side


Figure 4-1

MEMORY CONTROLLER

adxStrb busAddr busRdWr_N busData System Bus side

Vera/Memory Controller Interaction

Tutorial

Chapter 4. Memory Controller

27

Figure 4-2 and Figure 4-3 show the timing diagrams for the memory controllers read and write operations respectively (note the signal names as you will be using them in the verification process)

clk reset

adxStrb busAddr busData busRdWr_ cex_ valid valid

ramData ramAddr rdWr_


Figure 4-2

valid valid

Memory Controller Read Operation Timing Diagram

28

Chapter 4. Memory Controller

Tutorial

clk reset

adxStrb busAddr busData busRdWr_ cex_ valid valid

ramData ramAddr rdWr_

valid valid

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_ = 1b1; cntrlr.adxStrb = 1b1; @1 cntrlr.adxStrb = 1b0; }

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_ = 1b0; cntrlr.adxStrb = 1b1; @1 cntrlr.busRdWr_ = 1b1; cntrlr.busData = 8bzzzzzzzz; cntrlr.adxStrb = 1b0; }

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

Veras 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 engineers 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 Veras 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 8bit 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 == 8bzzzzzzzz; }

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] = 4b1010 async; data[2:0] = main_bus.data[2:0] async; main_bus.data[7:4] == 4b0101 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_

ce_ is driven just after rising edge

data should be driven here data VALID

data is driven here because of sampling skews

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 <= 8bzzzzzzzz; }

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 = 1b1; cntrlr.adxStrb = 1b0; @1,100 cntrlr.ce0_N == 1b1; cntrlr.ce1_N == 1b1; cntrlr.ce2_N == 1b1; cntrlr.ce3_N == 1b1; @1 cntrlr.reset = 1b0;

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 (8h01, 8h5A); checkSramWrite (device0, 6b000001, 8h5A);

This code drives the bus and then checks the write operation using the specified virtual ports signals (device0). When checking other devices, remember that each device has a range of valid addresses: Device 0 1 2 3 Valid Address Range 0-63 64-127 128-191 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 (8h03); checkSramRead (device0, 6b000011, 8h95); @1 cntrlr.busData == 8h95;

36

Chapter 4. Memory Controller

Tutorial

This code drives the bus and then checks the read operation using the specified virtual ports 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 devices 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, 8h5A); case (index[7:6]) { 2b00: dev = device0; 2b01: dev = device1; 2b10: dev = device2; 2b11: dev = device3; } checkSramWrite (dev, index[5:0], 8h5A); readOp(index); checkSramRead (dev, index[5:0], 8h5A); @1 cntrlr.busData == 8h5A; }

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]) { 2b00: vca(OFF, cntrlr.ce0_N); 2b01: vca(OFF, cntrlr.ce1_N); 2b10: vca(OFF, cntrlr.ce2_N); 2b11: vca(OFF, cntrlr.ce3_N); } checkSramRead (dev, index[5:0], 8h5A); case (index[7:6]) { 2b00: vca(ON, cntrlr.ce0_N); 2b01: vca(ON, cntrlr.ce1_N); 2b10: vca(ON, cntrlr.ce2_N); 2b11: 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.

S R A M S R A M S R A M S R A M

ce0_N

rdWr_N ce1_N reset

MEMORY CONTROLLER ROUND-ROBIN ARBITER


ce2_N

request[0] CPU0 grant[0]

System Bus ce3_N address data


Figure 5-1 Memory System Schematic

grant[1] CPU1 request[1]

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 - Veras 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 1b1; memsys.adxStrb = 1b0; memsys.request = 2b00; @1,3 memsys.grant == 2b00; memsys.reset = 1b0;

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_ = 1b0; memsys.adxStrb = 1b1; @1 memsys.busRdWr_ = 1b1; memsys.busData = bbzzzzzzzz; memsys.adxStrb = 1b0; }

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_ = 1b1; memsys.adxStrb = 1b1; @1 memsys.adxStrb = 1b0; @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); 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] = 1b1; // request the bus @2,20 memsys.grant == 2b01; // check for grant writeOp(address0, data0); // issue write operation @1 memsys.request[0] = 1b0; // release request @2,20 memsys.grant == 2b00; // check for release @1 memsys.request[0] = 1b1; // request again @2,20 memsys.grant == 2b01; // check for grant readOp(address0, data0); // issue read operation @1 memsys.request[0] = 1b0; // release request @2,20 memsys.grant == 2b00; // 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] = 1b1; // request the bus @2,20 memsys.grant == 2b10; // check for grant writeOp(address1, data1); // issue write operation @1 memsys.request[1] = 1b0; // release request @2,20 memsys.grant == 2b00; // check for release @1 memsys.request[1] = 1b1; // request again @2,20 memsys.grant == 2b10; // check for grant readOp(address1, data1); // issue read operation @1 memsys.request[1] = 1b0; // release request @2,20 memsys.grant == 2b00; // check for grant } } join // call random with seed

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 Xs or Zs. 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 Xs or Zs. 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); 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] = 1b1; // request the bus @2,20 memsys.grant == 2b01; // check for grant writeOp(address0, data0); // issue write operation @1 memsys.request[0] = 1b0; // release request // call random with seed

46

Chapter 5. Memory System

Tutorial

@2,20 memsys.grant == 2b00; // check for release @1 memsys.request[0] = 1b1; // request again @2,20 memsys.grant == 2b01; // check for grant readOp(address0, data0); // issue read operation @1 memsys.request[0] = 1b0; // release request @2,20 memsys.grant == 2b00; // 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] = 1b1; // request the bus @2,20 memsys.grant == 2b10; // check for grant writeOp(address1, data1); // issue write operation @1 memsys.request[1] = 1b0; // release request @2,20 memsys.grant == 2b00; // check for release @1 memsys.request[1] = 1b1; // request again @2,20 memsys.grant == 2b10; // check for grant readOp(address1, data1); // issue read operation @1 memsys.request[1] = 1b0; // release request @2,20 memsys.grant == 2b00; // 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 classs data is referred to as properties, and a classs 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 Veras 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_ = 1b1; memsys.adxStrb = 1b1; @1 memsys.adxStrb = 1b0; @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_ = 1b0; memsys.adxStrb = 1b1; @1 memsys.busRdWr_ = 1b1; memsys.busData = 8bzzzzzzzz; memsys.adxStrb = 1b0; 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 = 1b1; // request the bus @2,20 localarb.$grant == 1b1; // 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 = 1b0; // release the bus @2,20 localarb.$grant == 1b0; // 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

Veras 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 Veras 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 attributes 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_groups 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# make text# HTML report, opens with Netscape 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, Veras 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