SVTB Tutorial
SVTB Tutorial
SVTB Tutorial
VCS /VCSi
SystemVerilog
Testbench Tutorial
Version X-2005.06
August 2005
Comments?
E-mail your comments about Synopsys
documentation to doc@synopsys.com
ii
Copyright Notice and Proprietary Information
Copyright 2003 Synopsys, Inc. All rights reserved. This software and documentation are owned by Synopsys, Inc., and
furnished under a license agreement. The software and documentation may be used or copied only in accordance with the terms of
the license agreement. No part of the software and documentation may be reproduced, transmitted, or translated, in any form or by
any means, electronic, mechanical, manual, optical, or otherwise, without prior written permission of Synopsys, Inc., or as expressly
provided by the license agreement.
Right to Copy Documentation
The license agreement with Synopsys permits licensee to make copies of the documentation for its internal use only.
Each copy shall include all copyrights, trademarks, service marks, and proprietary rights notices, if any. Licensee must
assign sequential numbers to all copies. These copies shall contain the following legend on the cover page:
“This document is duplicated with the permission of Synopsys, Inc. for the exclusive
use of __________________________________________ and its employees. This
is copy number __________.”
Trademarks (™)
Active Parasitics, AFGen, Apollo, Apollo II, Apollo-DPII, Apollo-GA, ApolloGAII, ASTRO, Astro-Rail, Astro-Xtalk,
ATRANS, Aurora, AvanTestchip, AvanWaves, CALAVARAS, ChipPlanner, Circuit Analysis, Columbia, Columbia-CE,
Comet 3D, Cosmos, Cosmos SE, CosmosLE, Cosmos-Scope, Cyclelink, Davinci, DFM-Workbench, Dynamic-
Macromodeling, Dynamic Model Switcher, EDAnavigator, Encore, Encore PQ, Evaccess, FASTMAST, Formal Model
Checker, FRAMEWAY, GATRAN, Hercules, Hercules-Explorer, Hercules-II, Hierarchical Optimization Technology, High
Performance Option, HotPlace, HSPICE-LINK, iQBus, Jupiter, Jupiter-DP, JupiterXT, JupiterXT-ASIC, Libra-Passport,
Libra-Visa, LRC, Mars, Mars-Rail, Mars-Xtalk, Medici, Metacapture, Metacircuit, Metamanager, Metamixsim, Milkyway,
Nova Product Family, Nova-ExploreRTL, Nova-Trans, Nova-VeriLint, Nova-VHDLlint, Optimum Silicon, Orion_ec,
Parasitic View, Passport, Planet, Planet-PL, Planet-RTL, Polaris, Polaris-CBS, Polaris-MT, Progen, Prospector, Proteus
OPC, PSMGen, Raphael-NES, Saber Co-Simulation, Saber for IC Design, SaberDesigner, SaberGuide, SaberRT,
SaberScope, SaberSketch, Saturn, ScanBand, Silicon Blueprint, Silicon Early Access, SinglePass-SoC, Smart
Extraction, SOFTWIRE, Star, Star-DC, Star-Hspice, Star-HspiceLink, Star-MS, Star-MTB, Star-Power, Star-Rail,
Star-RC, Star-RCXT, Star-Sim, Star-Sim XT, Star-Time, Star-XP, Taurus, Taurus-Device, Taurus-Layout,
Taurus-Lithography, Taurus-OPC, Taurus-Process, Taurus-Topography, Taurus-Visual, Taurus-Workbench, The Power
in Semiconductors, THEHDL, TimeSlice, TopoPlace, TopoRoute, True-Hspice, TSUPREM-4, Venus, VERIFICATION
PORTAL, VERIVIEW, VFORMAL
SystemC™ is a trademark of the Open SystemC Initiative and is used under license.
All other product or company names may be trademarks of their respective owners.
iii
iv
Contents
2. Design Overview
Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-1
Tutorial-design Directory Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-3
Location of Files for Tutorial. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-4
3. Arbiter
Arbiter Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-1
Testbench Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-3
iii
How to Get Going . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-4
Verifying the Arbiter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-4
Reset Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-4
Simple Request Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-7
Sequenced Request Verification . . . . . . . . . . . . . . . . . . . . . . . . 3-7
Doing Things in an Organized Manner with Tasks. . . . . . . . . . . 3-8
4. Memory Controller
Memory Controller Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-1
Verifying the Memory Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-5
Controller Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-6
Controller Reset Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-8
Driving the System Bus For Read and Write Operations. . . . . . 4-10
Implementing Virtual Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 4-11
Verifying Read and Write Operations. . . . . . . . . . . . . . . . . . . . . 4-14
5. Memory System
Memory System Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5-2
Verifying the Memory System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5-4
General Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5-4
Object-Oriented Programming . . . . . . . . . . . . . . . . . . . . . . . . . . 5-8
Interprocess Communication and Synchronization . . . . . . . . . . 5-20
Functional Coverage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5-24
iv
1
Introducing SystemVerilog for Testbench 1
For quite some time now, design and verification engineers, alike, have felt the
need for a single unified design and verification language that allows them to
both simulate their HDL designs and verify them with high-level testbench
constructs. To this end, Synopsys has implemented SystemVerilog, including
SystemVerilog for design, assertions and testbench in its Verilog simulator, VCS.
This unified language essentially enables engineers to write testbenches and
simulate them in VCS along with their design in an efficient, high-performance
environment.
• High-Level Verification
• SystemVerilog for Testbench
Synopsys has become the leader in providing a single, unified design and
verification environment based on VCS that not only enables completing
functional validation of designs with the desired degree of confidence, but also
helps achieving this goal in the most intelligent and efficient way and in the
shortest time possible.
From our experience with customers, the number one objective on the way to
reaching this verification goal is to be able to find and fix all bugs in a design
before tape-out. The cost of fixing bugs grows exponentially with time as designs
evolve. Not catching a few bugs early on in the design process inevitably leads
to the dangerous scenario of their proliferation into more bugs, leading to very
costly time-delays and design re-spins. In this regard, Synopsys’ unified design
and verification environment of VCS is uniquely geared towards catching such
hard-to-find bugs efficiently and early in the design cycle. Additionally, the tight
integration of the Synopsys Discovery platform makes this environment
unmatched in its efficiency and speed.
Concurrency basically allows you to spawn off multiple parallel processes from
a parent process. It brings power and flexibility to your verification environment,
which inherently requires the execution of many processes in parallel, both for
efficiency and smarter inter-process interaction. A typical example would be to
stimulate a design and check the outcome in parallel. This allows your testbench
to react decisively to the outcome in real time by enabling it to modify the stimulus
(even before the simulation ends).
Concurrency is enabled via the fork-join construct, which spawns off multiple
parallel processes. The type of join-back to the parent process depends on
whether all, any, or none of the parallel processes have completed.
Named events in processes are triggered using -> operator. These triggered
events are received by other processes with the help of event control constructs
in order to synchronize with the triggering processes.
Clocking Blocks
Clocking blocks encapsulate synchronous timing details. You can have as many
clocking blocks as needed as long as each is associated with a specific clock.
Input signals are driven and output signals are sampled only at clock edges.
Clocking blocks provide separation and clarity between clock domains in a multi-
clock design environment.
Functional Coverage
Because of the complexity of chips and broad scope of functions that need tests
for any given chip or system, the completeness of verification is a key issue.
Functional coverage complements the traditional code coverage by making sure
all the functionalities of the chip or system are fully verified. SystemVerilog
provides built-in functional coverage capability to address this need.
• Memory System
• Tutorial-design Directory Setup
The files for this tutorial can be accessed in the following directory: $VCS_HOME/
doc/examples/nativetestbench/systemverilog/tutorial.
Memory System
The design 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
schematic of this system.
Design Overview
2-1
ce0_N
S
R
A
M rdWr_N
ce1_N reset
S
R
A
M request[0]
MEMORY
CONTROLLER CPU0
S ROUND-ROBIN
R ARBITER grant[0]
A
M ce2_N
S System Bus
R grant[1]
A ce3_N
M CPU1
address
request[1]
data
The memory system consists of the SRAMs, the Memory Controller, and the
Arbiter. These are all defined in the HDL files of each sub-module. The approach
used to verify the memsys system is similar to most project verification flows:
Design Overview
2-2
First, in Chapter 3 of the tutorial, the arbiter sub-module is verified. To do this,
the surrounding blocks are modeled in the testbench.
Then, in Chapter 4, the memory controller sub-module is verified. For this, both
the CPU and the memory are designed in the testbench. This gives you a chance
to use some of the advanced features of SystemVerilog for Testbench that could
be used to verify protocol based designs.
Finally, Chapter 5 of the tutorial shows you how to verify the complete system by
integrating the arbiter and controller sub-modules as shown in the diagram above,
with the SystemVerilog models of the CPUs instantiated in the testbench. Several
different features of SystemVerilog for Testbench will be used in different
approaches. We will also introduce object-oriented testbench design in this last
chapter.
tutorial
• 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 solution directory
Design Overview
2-3
• cntrlr — contains the submodule RTL and solution directory
• memsys — contains the top-level RTL netlist that integrates the entire
memsys design and the solution directory
Also note the following:
Design Overview
2-4
3
Arbiter 3
This chapter focuses on the arbiter’s roll in the design. It briefly describes what
the arbiter does, including a short timing and logic discussion. The chapter then
describes the SystemVerilog for Testbench technology as it pertains to verifying
the arbiter portion of the system. Specifically, this chapter explains how, using
this technology, a testbench written in SystemVerilog interacts with a
SystemVerilog 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 the following sections:
• Arbiter Overview
• Testbench Overview
• Verifying the Arbiter
Arbiter Overview
Arbiter
3-1
• The solution top-level Verilog is in the following:
tutorial/arb/solution/arb.test_top.v
• The details of running the solution testbench are in the following file:
tutorial/arb/solution/README
• The solution compile and run scripts are in the following file:
tutorial/arb/solution/run.csh
The timing diagram that describes its IO behavior is now examined. Figure 3-1
shows the timing diagram.
clk
reset
request[1:0] zz 00 01 00 10 00 11 10 00
grant[1:0] xx 00 01 00 10 00 01 00 10 00
Arbiter
3-2
Testbench Overview
clock generator
Device SystemVerilog
SystemVerilog
Under Test
arb.test_top.v file
Arbiter
3-3
How to Get Going
arb.test_top.v
The arb.test_top.v file contains the top-level SystemVerilog code. The top-level
code contains the signals that connects the testbench to the DUT. It also
instantiates the testbench and the DUT, and contains the clock-generator for the
system clock (clk) that is passed to the both DUT and testbench.
arb_test.v
The arb_test.v file shall contain the testbench program.
program arb_test(
input clk,
input [1:0] grant_p,
output [1:0] request_p,
output reset_p);
endprogram
First, verify the arbiter’s reset. Then, verify that the arbiter handles simple
requests appropriately and can grant access to one of the CPUs. Finally, check
for the arbiter’s proper handling of request sequences.
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.
To advance the simulation to the next value change of a specified signal, use the
synchronize construct:
Arbiter
3-4
@(clock_edge signal_name); //clock_edge:posedge/negedge/no
edge specified
This advances the simulation to the specified edge of the signal. If the clock edge
is omitted, it advances the simulation to the next signal-value change, which
represents a sampling edge.
To assert and de-assert signals, use the SystemVerilog for Testbench drive
construct:
signal_name <=value;
The specified signal is driven to the appropriate value after the current time.
Note: Non-blocking Assignment (NBA) operator "<=" must be used for drives.
Where, clock_sensitivity is the name of the clock and edge that should be used
during the checking, and property_spec is the temporal expression. For
example,
If the signal value is the same as the specified value, the simulation continues.
If there is a mismatch, a user defined error message in the action block in the
else branch will be printed or a default error message without using action block,
but the simulation still continues. User can stop or finish the simulation in the
action block if needed.
Arbiter
3-5
request_p <= 2'b00;
expect(@(posedge clk) grant_p == 2'b00);
Note that the request and grant signals are 2-bit signals. Each bit of the
signals must be de-asserted.
At this point, you should be in the tutorial/arb/test directory. With the code added
to your arbiter testbench (arb_test.v), run the simulation and test the results.
Compile the testbench use the following VCS command line and fix any syntax
errors during compilation.
Run the simulation by simply invoking the VCS executable simv in the following
way:
% simv -l sim.log
Any verification errors found during the simulation will be automatically reported.
For instance, change the line (expect) that checks the de-assertion of grant in
the arb_test.v file to the following:
Recompile and run the simulation again. The testbench now expects the grant
signal to be asserted while the SystemVerilog model continues to de-assert the
signal as before. This results in an expect mismatch and a user defined error as
described on the previous page. The simulation will, however, proceed.
Note: Remember to edit the testbench file arb_test.v to correct this error before
moving ahead with the tutorial.
Arbiter
3-6
Simple Request Verification
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.
Arbiter
3-7
• Assert both request signals and check that the correct grant is asserted
(depends on which grant was previously asserted)
• Release the granted request and check that both grants are released
• Then check that the other grant is asserted
• Finally release the other request and check that both grants are released
Given this verification methodology, the code to check the arbiter behavior is the
following:
The following code shows you how to write a task for verifying the reset:
task reset_test;
$write("Task reset_test: asserting and checking reset\n");
reset_p <= 1;
repeat (2) @(posedge clk);
reset_p <= 0;
request_p <= 2'b00;
expect(@(posedge clk) grant_p == 2'b00) else
$display($time, " Failed");
Endtask
Arbiter
3-8
You can write a similar task called request_grant_test for verifying the request
and grant sequence and then call both the tasks from the main program of the
testbench in the following manner:
repeat (2000)
begin
reset_test();
request_grant_test();
end
Arbiter
3-9
Arbiter
3-10
4
Memory Controller 4
This chapter discusses the memory controller in your design. It gives you an
overview of how the memory controller operates. It discusses some of the major
features of SystemVerilog for Testbench used to verify the controller, including a
description of polymorphism and virtual task/function as well as synchronous
events. These concepts are presented within the verification framework so that
you can learn how to adequately validate the memory controller. This chapter
includes the following sections:
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. You will be working inside the tutorial/cntrlr/test directory.
Memory Controller
4-1
• The Verilog controller RTL source code is in the following file:
tutorial/cntrlr/rtl/cntrlr.v
• The details of running the solution testbench are in the following file:
tutorial/cntrlr/solution/README
• The solution compile and run scripts are in the following file:
tutorial/cntrlr/solution/run.csh
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 the testbench works with both the system bus and SRAM device signals.
Memory Controller
4-2
Figure 4-1 SystemVerilog Testbench/Memory Controller Interaction
Testbench
reset
rdWr_N
adxStrb
ramAddr[5:0]
busAddr[7:0] ce0_N
MEMORY ce1_N
CONTROLLER
busRdWr_N ce2_N
ce3_N
busData[7:0]
ramData[7:0]
Figure 4-2 and Figure 4-3 show the timing diagrams for the memory controller’s
read and write operations respectively (note the signal names as you will be using
them in the verification process).
Memory Controller
4-3
Figure 4-2 Memory Controller Read Operation Timing Diagram
clk
reset
adxStr
busAdd vali
busDat vali
busRdWr
busRdWr_N
cex_
cex_N
cex_N
ramDat vali
ramAdd vali
rdWr_N
rdWr
Memory Controller
4-4
Figure 4-3 Memory Controller Write Operation Timing Diagram
clk
reset
adxStr
busAdd vali
busDat vali
busRdWr
cex_N
cex_
cex_N
ramDat vali
ramAdd vali
rdWr
This chapter focuses on checking 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 four different
memory devices in the testbench.
Memory Controller
4-5
To start the verification process, first create the following files:
Controller Interface
Let’s create an interface and call it “cntrlr_intf”. This interface will be instantiated
in the cntrlr.test_top.v file and passed to the program block and the dut. The
interface will encapsulate the interface signal in a single structure.
endinterface
cntrlr_intf intf(clk);
Memory Controller
4-6
SystemVerilog provides clocking block construct to specify the sampling and
driving skews. Note that each clocking block has a clock associated with it. All
signal operations occur on the corresponding clocking block clock-edge. For
example, given an clocking block with drives and samples occurring on the
positive clock edges and an input skew of #1step and an output skew of #0
respectively, the timing diagram is given by the following:
clk
request driven
request at the driving clock edge
We can pass the interface directly to the program block, but we must pass each
element of the interface to the dut:
cntrlr_test testbench(intf);
cntrlr dut(
.clk ( clk ),
.reset ( intf.reset ),
.busAddr ( intf.busAddr ),
.busData ( intf.busData ),
.busRdWr_N ( intf.busRdWr_N ),
.adxStrb ( intf.adxStrb ),
.rdWr_N ( intf.rdWr_N ),
.ce0_N ( intf.ce0_N ),
.ce1_N ( intf.ce1_N ),
Memory Controller
4-7
.ce2_N ( intf.ce2_N ),
.ce3_N ( intf.ce3_N ),
.ramAddr ( intf.ramAddr ),
.ramData ( intf.ramData )
);
task resetSequence();
vintf.CBcntrlr.reset <= 1'b1;
vintf.CBcntrlr.ramData <= 8’bzzzzzzzz;
repeat (2) @CBcntrlr;
vintf.CBcntrlr.reset <= 1'b0;
endtask
Memory Controller
4-8
Invoking the Tasks
Having created the two tasks as described above, write calls to them in the main
program with a clocking block in the cntrlr_test.v file as follows:
$finish;
end
//reset sequence
task resetSequence ();
$write("Task resetSequence entered\n");
vintf.CBcntrlr.reset <= 1’b1;
vintf.CBcntrlr.ramData <= 8'bzzzzzzzz;
repeat (2) @vintf.CBcntrlr;
vintf.CBcntrlr.reset <= 1'b0;
endtask
endtask
endprogram
Memory Controller
4-9
Compiling and running the Simulation with VCS
At this point, you should be in the tutorial/cntrlr/test directory. With the code added
to your controller testbench (cntrlr_test.v), run the simulation and check the
results.
Compile the testbench using the following VCS command line and fix any syntax
errors that you may have:
Run the simulation by simply invoking the VCS executable simv in the following
way:
% simv -l sim.log
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 the following:
This task is passed the argument adx. It then drives the busAddr signal to that
value. Finally, it drives the busRdWr_N and adxStrb signals such that they
match the timing diagram for the read operation of the controller.
Note: Since this is a read operation, do not drive the data onto the bus and check
for the expected data here.
Memory Controller
4-10
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_N is de-asserted). Given these
requirements, the write operation task is the following:
This task is passed the argument adx. It then drives the busAddr signal to that
value. Finally, it drives the busData, busRdWr_N, and adxStrb signals such
that they match the timing diagram for the write operation of the controller.
Memory Controller
4-11
Implementing 4 devices in the Memory Controller
Let us define a device base class for the SRAM parts (ramAddr, ramData,
rdWr_N, and ce_N) which has methods to drive and sample these signals
synchronously as follows:
endclass
After defining the device base class, let’s extend it to create 4 device classes,
device0, device1, device2 and device3 as follows:
Memory Controller
4-12
task waitCe_N();
@vintf.CBcntrlr.ce0_N;
endtask
endclass
task waitCe_N();
@vintf.CBcntrlr.ce1_N;
endtask
endclass
task waitCe_N();
@vintf.CBcntrlr.ce2_N;
endtask
endclass
task waitCe_N();
@vintf.CBcntrlr.ce3_N;
endtask
endclass
In the extended class, the two methods getCe_N and waitCe_N is connected
to its device-specific signal. These classes definitions will be included within the
program block and instantiated as follows:
‘include "device.v"
device0 d0 = new;
Memory Controller
4-13
device1 d1 = new;
device2 d2 = new;
device3 d3 = new;
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.
Memory Controller
4-14
endtask
The task first checks whether the address (ramAddr) is valid in a timing window
that begins after the first rising edge and lasts for the next five rising clock edges.
As soon as the address is found to be valid, the task checks whether the data
(ramData) to be written is valid in a timing window that begins immediately and
lasts for the next two rising clock edges. At the rising edge after the data is found
to be valid, the task checks whether the write (rdWr_N) and enable (ce_N) signals
are asserted. At the same time, the address and data are checked again to see
if they are still valid. Then, after another rising clock edge, the task checks whether
the write and enable signals are de-asserted while checking the address and
data for the third time to make sure they were valid.
Notice how the task accesses each SRAM instance by passing "device" object
to the task argument device_id. The SRAM instance that is accessed depends
on the port variable passed to the task through the argument device_id. For
example, if the port variable passed is d0, the fourth statement in the task would
be interpreted as vintf.CBcntrlr.Ce0_N==1’b0.
device_id.driveRamData(data);
device_id.waitCe_N();
assert(device_id.getCe_N() == 0);
assert(device_id.getRdWr_N() == 1);
assert(device_id.getRamAddr() == adx);
$write("Task checkSramRead: Address %0h data %0h\n", adx,
data);
printCycle();
expect (@(vintf.CBcntrlr) ##[0:2] device_id.getBusData()
=== data);
Memory Controller
4-15
printCycle();
device_id.driveRamData (8'bzzzzzzzz);
endtask
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 the
following:
readOp (8’h03);
checkSramRead (d0, 6’b000011, 8’h95);
@vintf.CBcntrlr assert(vintf.CBcntrlr.busData ==
8’h95);
Memory Controller
4-16
This code drives the bus and then checks the read operation using the specified
device signals associated with d0 (similar code can be written for the other
devices). 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. This
is achieved with the help of a for loop, as shown in the following code:
bit[7:0] index;
...
for (int i=0;i<=255;i++) begin
index = i;
writeOp(index, 8’h5A);
case (index[7:6])
2’b00: device_id = d0;
2’b01: device_id = d1;
2’b10: device_id = d2;
2’b11: device_id = d3;
endcase
Memory Controller
4-17
Memory Controller
4-18
5
Memory System 5
Now that you’ve become familiar with the separate functionality of the arbiter and
memory controller, you can examine the way these components interact in a
complete system. This chapter briefly discusses the overview of the system,
which includes the arbiter, controller, and SRAM devices.
Also discussed are some of the higher level verification techniques used by
SystemVerilog Testbench. These include concurrency control mechanisms, such
as semaphores, events, mailboxes, object-oriented methodology by way of
classes, and random stimulus generation. Finally, this chapter will show you how
to use these features to validate the memory system. This chapter includes the
following sections:
Memory System
5-1
Memory System Overview
You will be working inside the tutorial/memsys/test directory, which includes the
following files:
• The Verilog memory system RTL source code is in the following file:
tutorial/memsys/rtl/memsys.v
• Links to all the RTL for sram, arb, cntrlr and memsys are in the following file:
tutorial/memsys/solution/memsys.f
• The testbench code that mimics the CPUs is in the following file:
tutorial/memsys/solution/cpu.v
• The two functional coverage groups for memory system is in the following file:
tutorial/memsys/solution/memsys_coverage.v
• The details of running the solution testbench are in the following file:
tutorial/memsys/solution/README
• The solution compile and run script are in the following file:
tutorial/memsys/solution/run.csh
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 provided in Figure 5-1.
Memory System
5-2
Figure 5-1 Memory System Schematic
ce0_N
S
R
A
M rdWr_N
ce1_N reset
S
R
A
M request[0]
MEMORY
CONTROLLER CPU0
S ROUND-ROBIN
R ARBITER grant[0]
A
M ce2_N
S System Bus
R grant[1]
A ce3_N
M CPU1
address
request[1]
data
Memory System
5-3
Verifying the Memory System
The methodology used to verify the entire memory system is broken down by
the following tasks and concepts:
General Verification
To start the verification process, first create the following files:
The general verification tasks include initializing ports, checking the reset
procedure and modifying the read and write operations previously developed for
the memory controller. You can do this with the help of tasks. Finally, you can
develop a testbench that checks both CPUs being run concurrently using multiple
processes.
Memory System
5-4
Setting Up the Top Level
In the file “memsys.test_top.v” create an interface with a single clocking block as
we did in the previous section.
task init_ports () ;
$write("Task init_ports\n");
@vintf.CBmemsys vintf.CBmemsys.request <= 2'b00;
vintf.CBmemsys.busRdWr_N <= 1'b1;
vintf.CBmemsys.adxStrb <= 1'b0;
vintf.CBmemsys.reset <= 1'b0;
endtask
task reset_sequence () ;
$write("Task reset_sequence\n");
vintf.CBmemsys.reset <= 0;
@vintf.CBmemsys vintf.CBmemsys.reset <= 1;
repeat (10) @vintf.CBmemsys;
vintf.CBmemsys.reset <= 0;
@vintf.CBmemsys assert(vintf.CBmemsys.grant == 2'b00); /
/ check if grants are
// 0's
Endtask
Memory System
5-5
vintf.CBmemsys.adxStrb <= 1'b0;
$write("WRITE address = 0%H, data = 0%H \n", address, data);
endtask
During the read operation, the read operation task must also check for correct
return data. The new readOp() task with this data checking is the following:
Concurrent Processes
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/join_none/join_any
statementN
Can be any valid SystemVerilog statement.
join — When this is used, the code after the fork/join block executes only after
all of the concurrent processes have completed.
join_any — When this is used, the code after the fork/join_any block executes
right after any concurrent process within the fork/join_any completes.
join_none — When this is used, the code after the fork/join_none block
executes immediately without waiting for any of the fork/join_none processes
to complete.
Memory System
5-6
With the read and write operations defined, you want to set up your testbench
such that each CPU issues a series of read and write requests to the memory
system with random addresses and data. The two CPUs should operate
concurrently or in parallel. Each CPU should use the random() system function
to generate random addresses within the valid address space and an 8-bit data
type. The CPUs should then request and access the bus, write the data to the
bus, and release the bus (check for the release of the grant signal upon bus
release). This sequence should be repeated 256 times using the repeat() flow
control statement. Given these criteria, the testbench code is the following:
repeat(256)
begin
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
Memory System
5-7
@vintf.CBmemsys vintf.CBmemsys.request[1] <= 1'b1;
// request the bus
expect(@(vintf.CBmemsys) ##[2:20] (memsys.grant ==
2'b10));
writeOp(address1, data1); // issue write operation
@vintf.CBmemsys vintf.CBmemsys.request[1] <= 1'b0;
// release request
expect(@(vintf.CBmemsys) ##[2:20] (memsys.grant ==
2'b00));
@vintf.CBmemsys vintf.CBmemsys.request[1] <= 1'b1;
// request again
expect (@(vintf.CBmemsys) (memsys.grant == 2'b10));
readOp(address1, data1); // issue read operation
@vintf.CBmemsys vintf.CBmemsys.request[1] <= 1'b0;
// release request
expect( @(vintf.CBmemsys) ##[2:20](memsys.grant ==
2'b00));
end
join
This test works well in exhaustively checking the read and write operations for
each CPU. However, because the CPUs are operating concurrently, problems
can arise when each CPU accesses the same address space with different data.
For instance, if CPU0 first writes to an address space and then CPU1 writes to
the same address space, the data that CPU0 reads will be different from what it
expects (it reads the data that CPU1 wrote). This will result in simulation failure
because of the discrepancy between data read and data expected. A solution to
this issue is to use basic concurrency control and this is discussed in the section
following object-oriented programming and random stimulus generation.
Object-Oriented Programming
Object-oriented programming allows you to develop programs that are easier to
debug and reuse by encapsulating related code (methods) and data (properties)
together into what is called a class. In this section, you will examine how classes
can be implemented in our memory system using polymorphism to associate
specific signals with each object (instance) of a class, how classes are
constructed, how concurrency-control is achieved, how concurrent processes
communicate and finally how automatic constrained randomization of stimulus
is achieved.
Memory System
5-8
Encapsulation
A class is a collection of data and a set of methods that act on that data. A class’s
data is referred to as properties, and a class’s methods are referred to as
methods. An instance of a class is called an object, and an object is comprised
of the class’s properties and methods.
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:
Implementing a Class
In the memsys example, since you have two CPUs (CPU0 and CPU1), you first
declare a class called cpu, so that each CPU can be represented by an object
of this class. This is done in the following manner:
Memory System
5-9
class cpu;
//properties
arb localarb;
local integer cpu_id;
bit [7:0] address;
bit [7:0] data;
integer delay;
string name;
//methods
function new(arb arb, integer id, string n);
...
endfunction
task readOp();
...
endtask
task writeOp();
...
endtask
task request_bus();
...
endtask
task release_bus();
...
endtask
task delay_cycle();
...
endtask
endclass
Currently, all methods declaration and definitions have to be kept within the class.
This limitation will be removed in the future releases.
Memory System
5-10
Synchronous Signal Assignment
When implementing object-oriented concepts in your system, you should make
specific Synchronous signal assignments to each instance or object of a class
using virtual ports. Since each of the two CPUs is represented by an object of
the class cpu and accesses the system bus through the common arbiter, we
declare a base class arb and then, using it, declare two extended classes, arb0
and arb1, one for use with each of the two class cpu objects. Thus, we ensure
each CPU connects to the arbiter using its specific device signals. The code for
declaring a base class for arbiter as following
//object of this base class can not be created directly
virtual class arb;
extern virtual task driveRequest(input bit s);
extern virtual function logic getGrant();
endclass
Using this class declaration, we declare two extended classes, one for each CPU,
as follows:
Memory System
5-11
Class Methods
In your class cpu, you must create the initialization method that is executed when
the class is constructed. You 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 object of type arb (as declared
above) and assign it to a local property. The code for the initialization method
new is the following:
task readOp () ;
$write("CPU %0d readOp: address %h data %h\n", cpu_id,
address, data);
@vintf.CBmemsys vintf.CBmemsys.busAddr <= address;
vintf.CBmemsys.busRdWr_N <= 1'b1;
vintf.CBmemsys.adxStrb <= 1'b1;
@vintf.CBmemsys vintf.CBmemsys.adxStrb <= 1'b0;
expect (@(vintf.CBmemsys) ##[2:5] vintf.CBmemsys.busData
== data);
$write("READ address = 0%H, data = 0%H \n", address, data);
Endtask
The write operation writeOp must behave as before. Depending on the object
of the class cpu that is invoked, the writeOp class method only applies to the
CPU associated with that object. The conditional statement inside the method
evaluates the cpu_id was passed in the initialization process and thus the method
is able to print which CPU is writing. The code for the writeOp class method is
the following:
Memory System
5-12
task writeOp() ;
$write("CPU %0d writeOp: address %h data %h\n", cpu_id,
address, data);
@vintf.CBmemsys vintf.CBmemsys.busAddr <= address;
vintf.CBmemsys.busData <= data;
vintf.CBmemsys.busRdWr_N <= 1'b0;
vintf.CBmemsys.adxStrb <= 1'b1;
@vintf.CBmemsys vintf.CBmemsys.busRdWr_N <= 1'b1;
vintf.CBmemsys.busData <= 8'bzzzzzzzz;
vintf.CBmemsys.adxStrb <= 1'b0;
$write("CPU%0d is writing \n", cpu_id);
$write("WRITE address = 0%H, data = 0%H \n", address, data);
Endtask
Our request_bus method must assert the corresponding CPU's request line and
then check for the appropriate grant line. This is done with the help of the
associated bind, which was passed to the local property localarb. The code for
the request_bus class method is the following:
Finally, you write the method delay_cycle() to introduce the delay between CPU
accesses. The code for the delay_cycle class method is the following:
Memory System
5-13
task delay_cycle();
$write("CPU%0d Delay cycle value: %d\n", cpu_id, delay);
repeat(delay) @vintf.CBmemsys;
$write("delay = %d \n",delay);
Endtask
You can declare class properties as random using the rand declaration:
Using random declarations, we declare our class properties address, data and
delay as random as follows:
Memory System
5-14
Each time an instance is randomized, the address, data and delay values for
that instance are randomized. In particular, address has to be declared cyclic
random (randc), for we want it to cycle through all the 256 memory addresses
without repetition.
Note that there are no restrictions on the value that delay can assume because
it is declared as a random integer. We can implement constraints on the values
that random variables can assume using the constraint construct:
constraint del_lt10
{
delay < 10;
delay >=0;
}
class cpu;
//properties
arb localarb;
integer cpu_id;
randc bit [7:0] address;
rand bit [7:0] data;
rand integer delay;
constraint del_lt10
{
delay < 10;
delay >= 0;
}
Memory System
5-15
//methods
...
endclass
Memory System
5-16
With your class cpu defined with the properties and methods described above,
the same concurrent execution sequences for the two CPUs created earlier using
fork/join can now be written as follows:
fork
//fork CPU 0
repeat(256)
begin
cpu0.randomize();
cpu0.request_bus();
cpu0.writeOp();
cpu0.release_bus();
cpu0.request_bus();
cpu0.readOp();
cpu0.release_bus();
cpu0.delay_cycle();
end
//fork CPU 1
repeat(256)
begin
cpu1.randomize();
cpu1.request_bus();
cpu1.writeOp();
cpu1.release_bus();
cpu1.request_bus();
cpu1.readOp();
cpu1.release_bus();
cpu1.delay_cycle();
end
join
Note how the class property address is passed (using the instance name). Also,
note the ease of reuse through invoking the class methods with the appropriate
instance name.
Memory System
5-17
Basic Concurrency Control
The two objects of the class cpu that were invoked concurrently in the previous
subsection have the potential to access the same location in the SRAM memory
if the random addresses generated for them happen to be the same. To prevent
a potential conflict between the two cpu objects from leading to a data or resource
hazard, concurrency control can be achieved using semaphores.
Semaphores
Conceptually, a semaphore is like a bucket containing a number of keys. No
process can execute without first obtaining a key. Therefore only as many
processes as there are keys can execute at any time. All other processes must
wait until the keys are returned.
new (number_of_keys)
It specifies the initial number of keys in the semaphore.
put(number_of_keys)
Increments the number of keys in the semaphore.
get(number_of_keys)
Decrements the number of keys in the semaphore. If there aren’t the specified
number of keys in the semaphore, VCS halts simulation of the process (initial
block, task, etc.) until there the put method in another process increments the
number of keys to the sufficient number.
try_get (number_of_keys)
Decrements the number of keys in the semaphore. If there aren’t the specified
number of keys in the semaphore, this method returns a 0. If the semaphore has
the specified number of keys, this method returns 1. After returning the value,
VCS executes the next statement.
Memory System
5-18
Implementing Semaphores
The fork/join code that you wrote earlier should now be modified to include
semaphores as follows:
task check_all () ;
bit[7:0] mem_add0[*], mem_add1[*];
semaphore semaphoreId = new(1);
$write("Task check_all:\n");
fork
// fork process for CPU 0
repeat(256) begin
if(cpu0.randomize() == 0) begin
$display("Fatal Error Randomization Failed.
Exiting\n");
$finish;
semaphoreId.get(1);
cpu0.request_bus();
cpu0.writeOp();
cpu0.release_bus();
cpu0.request_bus();
cpu0.readOp();
cpu0.release_bus();
semaphoreId.put(1);
cpu0.delay_cycle();
end
Memory System
5-19
cpu1.delay_cycle();
end
join
endtask
However, you must ensure that CPU0 writes before CPU1 reads and that the
address generated by CPU0 is passed on to CPU1. Also, the data generated by
CPU0 has to be passed on to CPU1 so that it can check for memory corruption
by comparing the data read from the memory with that passed to it by CPU0.
This interprocess communication in which CPU0 passes the address and data
to the waiting CPU1 can be achieved with mailboxes. Further, you must also
ensure that CPU1 finishes with its read operation and checks completely before
CPU0 starts its next write cycle. This final synchronization can be achieved with
events.
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, SystemVerilog for Testbench’s mailboxes allow you to transfer
and retrieve data in a very controlled manner
Memory System
5-20
put(expression)
Puts a message in the mailbox.
get(variable)
Assigns the value of the first message to the variable. VCS removes the first
message so that the next message becomes the first method. If the mailbox is
empty, VCS suspends simulation of the process (initial block, task, etc.) until a
put method put a message in the mailbox.
try_get(variable)
Assigns the value of the first message to the variable. If the mailbox is empty,
this method returns the 0 value. If the variable is available, this method returns
the 1 value. After returning the value, VCS executes the next statement.
peek(variable)
Assigns the value of the first message to the variable without removing the
message. If the mailbox is empty, VCS suspends simulation of the process (initial
block, task, etc.) until a put method put a message in the mailbox.
try_peek(variable)
Assigns the value of the first message to the variable without removing the
message. If the mailbox is empty, this method returns the 0 value. If the variable
is available, this method returns the 1 value. After returning the value, VCS
executes the next statement.
Event variable serve as the link between triggering and synchronizing processes.
They are bi-directional as they can be used both to pass and receive triggers.
Triggers are used to pass or send events. A trigger is initiated by using event
triggering operator as follows:
->event_name;
Memory System
5-21
Waits are used to receive events and thereby synchronize the receiving
processes with the triggering processes. The triggered event property is used in
the context of a wait construct:
wait (hierarchical_event_identifier.triggered)
Using this mechanism, an event trigger shall unblock the waiting process whether
the wait executes before or at the same simulation time as the trigger operation.
The triggered event property, thus, helps eliminate a common race condition that
occurs when both the trigger and the wait happen at the same time. A process
that blocks waiting for an event might or might not unblock, depending on the
execution order of the waiting and triggering processes. However, a process that
waits on the triggered state always unblocks, regardless of the order of execution
of the wait and trigger operations.
integer randflag;
event CPU1done;
mailbox mboxId = new;
$write("Task check_all:\n");
fork
// fork process for CPU 0
repeat(256) begin
$write("\nthe start of block 1 at %t\n\n", $time);
randflag = cpu0.randomize();
cpu0.request_bus();
cpu0.writeOp();
cpu0.release_bus();
mboxId.put(cpu0.address);
Memory System
5-22
mboxId.put(cpu0.data);
mboxId.put(cpu0.delay);
wait(CPU1done.triggered);
CPU1done = 0;
cpu0.delay_cycle();
end
Finally, you can structurally make your code more elegant by encasing the code
for the fork/join in a task called check_all() and then calling this and other tasks
from inside the program memsys_test in the memsys_test.v file as follows:
initial begin
init_ports();
reset_sequence();
check_all();
end
Memory System
5-23
Functional Coverage
In this tutorial, SystemVerilog functional coverage capabilities are built within the
object-oriented framework. You specify what you want VCS to monitor for
coverage with the covergroup construct. The definition inside covergroup
includes valid and invalid states and transitions, a measure of coverage, the
coverage goal, and coverage options. The official description of the covergroup
construct begins on page 306 of the SystemVerilog 3.1a LRM.
We also want to monitor the state machine for the memory controller if all the
valid state transitions have been covered:
We must instantiate these two coverage groups within our main program to create
our coverage objects:
Memory System
5-24
range cov1 = new;
cntrlr_cov cov2 = new;
The command line to generate the coverage ASCII text file after the coverage
database is created is as follows:
Memory System
5-25
Memory System
5-26