Book Systemverilog For Verification
Book Systemverilog For Verification
CHRIS SPEAR
Synopsys, Inc.
13
Chris Spear
Synopsys, Inc.
377 Simarano Drive
Marlboro, MA 01752
9 8 7 6 5 4 3 2 1
springer.com
This book is dedicated to my wonderful wife Laura,
whose patience during this project was invaluable,
and my children, Allie and Tyler, who kept me laughing.
Contents
List of Examples xi
List of Figures xxi
List of Tables xxiii
Foreword xxv
Preface xxvii
Acknowledgments xxxiii
1. VERIFICATION GUIDELINES 1
1.1 Introduction 1
1.2 The Verification Process 2
1.3 The Verification Plan 4
1.4 The Verification Methodology Manual 4
1.5 Basic Testbench Functionality 5
1.6 Directed Testing 5
1.7 Methodology Basics 7
1.8 Constrained-Random Stimulus 8
1.9 What Should You Randomize? 10
1.10 Functional Coverage 13
1.11 Testbench Components 15
1.12 Layered Testbench 16
1.13 Building a Layered Testbench 22
1.14 Simulation Environment Phases 23
1.15 Maximum Code Reuse 24
1.16 Testbench Performance 24
1.17 Conclusion 25
2. DATA TYPES 27
2.1 Introduction 27
2.2 Built-in Data Types 27
viii SystemVerilog for Verification
4. BASIC OOP 67
4.1 Introduction 67
4.2 Think of Nouns, not Verbs 67
4.3 Your First Class 68
4.4 Where to Define a Class 69
4.5 OOP Terminology 69
4.6 Creating New Objects 70
4.7 Object Deallocation 74
4.8 Using Objects 76
4.9 Static Variables vs. Global Variables 76
4.10 Class Routines 78
4.11 Defining Routines Outside of the Class 79
4.12 Scoping Rules 81
4.13 Using One Class Inside Another 85
4.14 Understanding Dynamic Objects 87
4.15 Copying Objects 91
4.16 Public vs. Private 95
Contents ix
6. RANDOMIZATION 135
6.1 Introduction 135
6.2 What to Randomize 136
6.3 Randomization in SystemVerilog 138
6.4 Constraint Details 141
6.5 Solution Probabilities 149
6.6 Controlling Multiple Constraint Blocks 154
6.7 Valid Constraints 154
6.8 In-line Constraints 155
6.9 The pre_randomize and post_randomize Functions 156
6.10 Constraints Tips and Techniques 158
6.11 Common Randomization Problems 164
6.12 Iterative and Array Constraints 165
6.13 Atomic Stimulus Generation vs. Scenario Generation 172
6.14 Random Control 175
6.15 Random Generators 177
6.16 Random Device Configuration 180
6.17 Conclusion 182
References 295
Index 297
xi
List of Examples
Example 8-20 Extended transaction class with virtual copy method 234
Example 8-21 Base transaction class with copy_data function 234
Example 8-22 Extended transaction class with copy_data function 235
Example 8-23 Base transaction class with copy_data function 235
Example 8-24 Base callback class 237
Example 8-25 Driver class with callbacks 237
Example 8-26 Test using a callback for error injection 238
Example 8-27 Test using callback for scoreboard 239
Example 9-1 Incomplete D-flip flop model missing a path 244
Example 9-2 Functional coverage of a simple object 249
Example 9-3 Coverage report for a simple object 250
Example 9-4 Coverage report for a simple object, 100% coverage 251
Example 9-5 Functional coverage inside a class 253
Example 9-6 Test using functional coverage callback 254
Example 9-7 Callback for functional coverage 255
Example 9-8 Cover group with a trigger 255
Example 9-9 Module with SystemVerilog Assertion 255
Example 9-10 Triggering a cover group with an SVA 256
Example 9-11 Using auto_bin_max set to 2 257
Example 9-12 Report with auto_bin_max set to 2 258
Example 9-13 Using auto_bin_max for all cover points 258
Example 9-14 Using an expression in a cover point 259
Example 9-15 Defining bins for transaction length 259
Example 9-16 Coverage report for transaction length 260
Example 9-17 Specifying bin names 261
Example 9-18 Report showing bin names 261
Example 9-19 Conditional coverage — disable during reset 262
Example 9-20 Using stop and start functions 262
Example 9-21 Functional coverage for an enumerated type 262
Example 9-22 Report with auto_bin_max set to 2 263
Example 9-23 Specifying transitions for a cover point 263
Example 9-24 Wildcard bins for a cover point 264
Example 9-25 Cover point with ignore_bins 264
Example 9-26 Cover point with auto_bin_max and ignore_bins 264
Example 9-27 Cover point with illegal_bins 265
Example 9-28 Basic cross coverage 266
Example 9-29 Coverage summary report for basic cross coverage 267
Example 9-30 Specifying cross coverage bin names 268
Example 9-31 Cross coverage report with labeled bins 268
xix
When Verilog was first developed in the mid-1980s the mainstream level
of design abstraction was on the move from the widely popular switch and
gate levels up to the synthesizable RTL. By the late 1980s, RTL synthesis and
simulation had revolutionized the front-end of the EDA industry.
The 1990s saw a tremendous expansion in the verification problem space
and a corresponding growth of EDA tools to fill that space. The dominant lan-
guages that grew in this space were proprietary and specific to verification
such as OpenVera and e, although some of the more advanced users did make
the freely available C++ language their solution. Judging which of these lan-
guages was the best is very difficult, but one thing was clear, not only they
were disjointed from Verilog but verification engineers were expected to
learn multiple complex languages. Although some users of Verilog were
using the language for writing testbenches (sometimes going across the PLI
into the C language) it should be no surprise to anybody if I say that using
Verilog for testbenches ran out of steam even before the 1990s started. Unfor-
tunately, during the 1990s, Verilog stagnated as a language in its struggle to
become an industry standard, and so made the problem worse.
Towards the end of the 1990s, a startup company called Co-Design broke
through this stagnation and started the process of designing and implementing
the language we now know as the SystemVerilog industry standard. The
vision of SystemVerilog was to first expand on the abstract capabilities of
synthesizable code, and then to significantly add all the features known to be
necessary for verification, while keeping the new standard a strict superset of
the previous Verilog standards. The benefits of having a single language and a
single coherent run-time environment cannot be expressed enough. For
instance, the user benefits greatly from ease of use, and the vendor can take
xxvi SystemVerilog for Verification
PHIL MOORBY
New England, 2006
Preface
Importance of methodology
There is a difference between learning the syntax of a language and learn-
ing how to use a tool. This book focuses on techniques for verification using
constrained-random tests that use functional coverage to measure progress
and direct the verification. As the chapters unfold, language and methodology
features are shown side by side. For more on methodology, see Bergeron et al.
(2006).
The most valuable benefit of SystemVerilog is that it allows the user to
construct reliable, repeatable verification environments, in a consistent syn-
tax, that can be used across multiple projects.
Comparing SystemVerilog and SystemC for high-level design
Now that SystemVerilog incorporates Object Oriented Programming,
dynamic threads, and interprocess communication, it can be used for system
design. When talking about the applications for SystemVerilog, the IEEE
standard mentions architectural modeling before design, assertions, and test.
SystemC can also be used for architectural modeling. There are several major
differences between SystemC and SystemVerilog:
Final comments
If you would like more information on SystemVerilog and Verification,
you can find many resources at
http://chris.spear.net/systemverilog
This site has the source code for the examples in this book. All of the
examples have been verified with Synopsys’ Chronologic VCS 2005.06 and
2006.06. The SystemVerilog Language Reference Manual covers hundreds of
new features. I have concentrated on constructs useful for verification and
implemented in VCS. It is better to have verified examples than to show all
language features and thus risk having incorrect code. Speaking of mistakes,
if you think you have found a mistake, please check my web site for the Errata
page. If you are the first to find any mistake in a chapter, I will send you a free
book.
CHRIS SPEAR
Synopsys, Inc.
Acknowledgments
Few books are the creation of a single person. I want to thank all the peo-
ple who spent countless hours helping me learn SystemVerilog and reviewing
the book that you now hold in your hand. I especially would like to thank all
the people at Synopsys for their help, including all my patient managers.
Janick Bergeron provided inspiration, innumerable verification tech-
niques, and top-quality reviews. Without his guidance, this book would not
exist. But the mistakes are all mine!
Alex Potapov and the VCS R&D team always showed patience with my
questions and provided valuable insight on SystemVerilog features.
Mike Barnaby, Bob Beckwith, Quinn Canfield, James Chang, Cliff
Cummings, Al Czamara, John Girard, Alex Lorgus, Mike Mintz, Brad
Pierce, Arturo Salz, and Kripa Sundar reviewed some very rough drafts
and inspired many improvements.
Hans van der Schoot gave me the confidence to write that one last chap-
ter on functional coverage, and the detailed feedback to make it useful.
Benjamin Chin, Paul Graykowski, David Lee, and Chris Thompson origi-
nated many of the ideas that evolved into the functional coverage chapter.
Dan McGinley and Sam Starfas patiently helped lift me from the depths
of Word up to the heights of FrameMaker.
Ann K. Farmer — Arrigato gozaimasu! You brought sense to my
scribblings.
Will Sherwood inspired me to become a verification engineer, and taught
me new ways to break things.
xxxiv SystemVerilog for Verification
United Airlines always had a quiet place to work and plenty of snacks.
“Chicken or pasta?”
Lastly, a big thanks to Jay Mcinerney for his brash pronoun usage.
All trademarks and copyrights are the property of their respective owners.
Chapter 1
Verification Guidelines
“Some believed we lacked the programming language to describe your perfect world...”
(The Matrix, 1999)
1.1 Introduction
Imagine that you are given the job of building a house for someone. Where
should you begin? Do you start by choosing doors and windows, picking out
paint and carpet colors, or selecting bathroom fixtures? Of course not! First
you must consider how the owners will use the space, and their budget, so you
can decide what type of house to build. Questions you should consider are; do
they enjoy cooking and want a high-end kitchen, or will they prefer watching
movies in the home theater room and eating takeout pizza? Do they want a
home office or extra bedrooms? Or does their budget limit them to a basic
house?
Before you start to learn details of the SystemVerilog language, you need
to understand how you plan to verify your particular design and how this
influences the testbench structure. Just as all houses have kitchens, bedrooms,
and bathrooms, all testbenches share some common structure of stimulus gen-
eration and response checking. This chapter introduces a set of guidelines and
coding styles for designing and constructing a testbench that meets your par-
ticular needs. These techniques use some of the same concepts as shown in
the Verification Methodology Manual for SystemVerilog (VMM), Bergeron et
al. (2006), but without the base classes.
The most important principle you can learn as a verification engineer is:
“Bugs are good.” Don’t shy away from finding the next bug, do not hesitate to
ring a bell each time you uncover one, and furthermore, always keep track of
each bug found. The entire project team assumes there are bugs in the design,
so each bug found before tape-out is one fewer that ends up in the customer’s
hands. You need to be as devious as possible, twisting and torturing the
design to extract all possible bugs now, while they are still easy to fix. Don’t
let the designers steal all the glory — without your craft and cunning, the
design might never work!
This book assumes you already know the Verilog language and want to
learn the SystemVerilog Hardware Verification Language (HVL). Some of
2 SystemVerilog for Verification
There are many other useful features, but these allow you to create test-
benches at a higher level of abstraction than you are able to achieve with an
HDL or a programming language such as C.
What is the goal of verification? If you answered, “Finding bugs,” you are
only partly correct. The goal of hardware design is to create a device that per-
forms a particular task, such as a DVD player, network router, or radar signal
processor, based on a design specification. Your purpose as a verification
engineer is to make sure the device can accomplish that task successfully —
that is, the design is an accurate representation of the specification. Bugs are
what you get when there is a discrepancy. The behavior of the device when
used outside of its original purpose is not your responsibility although you
want to know where those boundaries lie.
The process of verification parallels the design creation process. A
designer reads the hardware specification for a block, interprets the human
language description, and creates the corresponding logic in a machine-read-
able form, usually RTL code. To do this, he or she needs to understand the
input format, the transformation function, and the format of the output. There
is always ambiguity in this interpretation, perhaps because of ambiguities in
the original document, missing details, or conflicting descriptions. As a verifi-
cation engineer, you must also read the hardware specification, create the
verification plan, and then follow it to build tests showing the RTL code cor-
rectly implements the features.
By having more than one person perform the same interpretation, you
have added redundancy to the design process. As the verification engineer,
your job is to read the same hardware specifications and make an independent
assessment of what they mean. Your tests then exercise the RTL to show that
it matches your interpretation.
Chapter 1: Verification Guidelines 3
What types of bugs are lurking in the design? The easiest ones to detect
are at the block level, in modules created by a single person. Did the ALU
correctly add two numbers? Did every bus transaction successfully complete?
Did all the packets make it through a portion of a network switch? It is almost
trivial to write directed tests to find these bugs as they are contained entirely
within one block of the design.
After the block level, the next place to look for discrepancies is at bound-
aries between blocks. Interesting problems arise when two or more designers
read the same description yet have different interpretations. For a given proto-
col, what signals change and when? The first designer builds a bus driver with
one view of the specification, while a second builds a receiver with a slightly
different view. Your job is to find the disputed areas of logic and maybe even
help reconcile these two different views.
To simulate a single design block, you need to create tests that generate
stimuli from all the surrounding blocks — a difficult chore. The benefit is that
these low-level simulations run very fast. However, you may find bugs in
both the design and testbench as the latter will have a great deal of code to
provide stimuli from the missing blocks. As you start to integrate design
blocks, they can stimulate each other, reducing your workload. These multiple
block simulations may uncover more bugs, but they also run slower.
At the highest level of the DUT, the entire system is tested, but the simula-
tion performance is greatly reduced. Your tests should strive to have all
blocks performing interesting activities concurrently. All I/O ports are active,
processors are crunching data, and caches are being refilled. With all this
action, data alignment and timing bugs are sure to occur.
At this level you are able to run sophisticated tests that have the DUT exe-
cuting multiple operations concurrently so that as many blocks as possible are
active. What happens if an MP3 player is playing music and the user tries to
download new music from the host computer? Then, during the download, the
user presses several of the buttons on the player? You know that when the real
device is being used, someone is going to do all this, so why not try it out
before it is built? This testing makes the difference between a product that is
seen as easy to use and one that locks up over and over.
Once you have verified that the DUT performs its designated functions
correctly, you need to see how it operates when there are errors. Can the
design handle a partial transaction, or one with corrupted data or control
fields? Just trying to enumerate all the possible problems is difficult, not to
mention how the design should recover from them. Error injection and han-
dling can be the most challenging part of verification.
As the design abstraction gets higher, so does the verification challenge.
You can show that individual cells flow through the blocks of an ATM router
4 SystemVerilog for Verification
correctly, but what if there are streams of different priority? Which cell should
be chosen next is not always obvious at the highest level. You may have to
analyze the statistics from thousands of cells to see if the aggregate behavior
is correct.
One last point: you can never prove there are no bugs left, so you need to
constantly come up with new verification tactics.
The verification plan is closely tied to the hardware specification and con-
tains a description of what features need to be exercised and the techniques to
be used. These steps may include directed or random testing, assertions, HW/
SW co-verification, emulation, formal proofs, and use of verification IP. For a
more complete discussion on verification see Bergeron (2006).
This book in your hands draws heavily upon the VMM that has its roots in
a methodology developed by Janick Bergeron and others at Qualis Design.
They started with industry standard practices and refined them based on expe-
rience on many projects. VMM’s techniques were originally developed for
use with the OpenVera language and were extended in 2005 for SystemVer-
ilog. VMM and its predecessor, the Reference Verification Methodology for
Vera, have been used successfully to verify a wide range of hardware designs,
from networking devices to processors. This book uses many of the same
concepts.
So why doesn’t this book teach you VMM? Like any advanced tool,
VMM was designed for use by an expert user, and excels on difficult prob-
lems. Are you in charge of verifying a 10 million gate design with many
communication protocols, complex error handling, and a library of IP? VMM
is the right tool for the job. But if you are working on smaller modules, with a
single protocol, you may not need such a robust methodology. Just remember
that your block is part of a larger system; VMM is still useful to promote
reuse. The cost of verification goes beyond your immediate project.
If you are new to verification, have little experience with Object Oriented
Programming, or are unfamiliar with constrained-random tests, the techniques
in this book might be the right path to choose. Once you are familiar with
them, you will find the VMM to be an easy step up.
The biggest thing missing from this book, when compared with the VMM,
is the set of base classes for data, environment, and utilities for managing log
Chapter 1: Verification Guidelines 5
files and interprocess communication. These are all very useful, but are out-
side the scope of a book on the SystemVerilog language.
Generate stimulus
Apply stimulus to the DUT
Capture the response
Check for correctness
Measure progress against the overall verification goals
100%
Coverage
Time
What if you do not have the necessary time or resources to carry out the
directed testing approach? As you can see, while you may always be making
forward progress, the slope remains the same. When the design complexity
doubles, it takes twice as long to complete or requires twice as many people.
Neither of these situations is desirable. You need a methodology that finds
bugs faster in order to reach the goal of 100% coverage.
Feature
Test Bug
Figure 1-2 shows the total design space and the features that get covered
by directed testcases. In this space are many features, some of which have
bugs. You need to write tests that cover all the features and find the bugs.
Chapter 1: Verification Guidelines 7
Constrained-random stimulus
Functional coverage
Layered testbench using transactors
Common testbench for all tests
Test-specific code kept separate from testbench
All these principles are related. Random stimulus is crucial for exercising
complex designs. A directed test finds the bugs you expect to be in the design,
while a random test can find bugs you never anticipated. When using random
stimulus, you need functional coverage to measure verification progress. Fur-
thermore, once you start using automatically generated stimulus, you need an
automated way to predict the results, generally a scoreboard or reference
model. Building the testbench infrastructure, including self-prediction, takes a
significant amount of work. A layered testbench helps you control the com-
plexity by breaking the problem into manageable pieces. Transactors provide
a useful pattern for building these pieces. With appropriate planning, you can
build a testbench infrastructure that can be shared by all tests and does not
have to be continually modified. You just need to leave “hooks” where the
tests can perform certain actions such as shaping the stimulus and injecting
disturbances. Conversely, code specific to a single test must be kept separate
from the testbench so it does not complicate the infrastructure.
Building this style of testbench takes longer than a traditional directed test-
bench, especially the self-checking portions, causing a delay before the first
test can be run. This gap can cause a manager to panic, so make this effort part
of your schedule. In Figure 1-3, you can see the initial delay before the first
random test runs.
8 SystemVerilog for Verification
100%
Coverage
Random
Test
Directed
Test
Time
While this up-front work may seem daunting, the payback is high. Every
test you create shares this common testbench, as opposed to directed tests
where each is written from scratch. Each random test contains a few dozen
lines of code to constrain the stimulus in a certain direction and cause any
desired exceptions, such as creating a protocol violation. The result is that
your single constrained-random testbench is now finding bugs faster than the
many directed ones.
As the rate of discovery begins to drop off, you can create new random
constraints to explore new areas. The last few bugs may only be found with
directed tests, but the vast majority of bugs will be found with random tests.
While you want the simulator to generate the stimulus, you don’t want
totally random values. You use the SystemVerilog language to describe the
format of the stimulus (“address is 32-bits, opcode is X, Y, or Z, length < 32
bytes”), and the simulator picks values that meet the constraints. Constraining
the random values to become relevant stimuli is covered in Chapter 6. These
values are sent into the design, and also into a high-level model that predicts
what the result should be. The design’s actual output is compared with the
predicted output.
Figure 1-4 shows the coverage for constrained-random tests over the total
design space. First, notice that a random test often covers a wider space than a
directed one. This extra coverage may overlap other tests, or may explore new
areas that you did not anticipate. If these new areas find a bug, you are in
luck! If the new area is not legal, you need to write more constraints to keep
away. Lastly, you may still have to write a few directed tests to find cases not
covered by any other constrained-random tests.
Chapter 1: Verification Guidelines 9
New area
?
Test
overlap
Directed
testcase
? ?
Figure 1-5 shows the paths to achieve complete coverage. Start at the
upper left with basic constrained-random tests. Run them with many different
seeds. When you look at the functional coverage reports, find the holes, where
there are gaps. Now you make minimal code changes, perhaps with new con-
straints, or injecting errors or delays into the DUT. Spend most of your time in
this outer loop, only writing directed tests for the few features that are very
unlikely to be reached by random tests.
Add Directed
Add testcase
constraints Functional
constraints
Coverage
Identify
Minimal code Identify
holes
modifications holes
10 SystemVerilog for Verification
When you think of randomizing the stimulus to a design, the first thing
that you might think of is the data fields. This stimulus is the easiest to create
– just call $random. The problem is that this gives a very low payback in
terms of bugs found. The primary types of bugs found with random data are
data path errors, perhaps with bit-level mistakes. You need to find bugs in the
control logic.
You need to think broadly about all design input, such as the following.
Device configuration
Environment configuration
Input data
Protocol exceptions
Delays
Errors and violations
What is the most common reason why bugs are missed during testing of
the RTL design? Not enough different configurations are tried. Most tests just
use the design as it comes out of reset, or apply a fixed set of initialization
vectors to put it into a known state. This is like testing a PC’s operating sys-
tem right after it has been installed, without any applications installed. Of
course the performance is fine, and there aren’t any crashes.
In a real world environment, the DUT’s configuration becomes more ran-
dom the longer it is in use. For example, I helped a company verify a time-
division multiplexor switch that had 2000 input channels and 12 output chan-
nels. The verification engineer said, “These channels could be mapped to
various configurations on the other side. Each input could be used as a single
channel, or further divided into multiple channels. The tricky part is that
although a few standard ways of breaking it down are used most of the time,
any combination of breakdowns is legal, leaving a huge set of possible cus-
tomer configurations.”
To test this device, the engineer had to write several dozen lines of
directed testbench code to configure each channel. As a result, she was never
able to try configurations with more than a handful of channels. Together, we
wrote a testbench that randomized the parameters for a single channel, and
Chapter 1: Verification Guidelines 11
then put this in a loop to configure all the switch’s channels. Now she had
confidence that her tests would uncover configuration-related bugs that would
have been missed before.
In the real world, your device operates in an environment containing other
components. When you are verifying the DUT, it is connected to a testbench
that mimics this environment. You should randomize the entire environment
configuration, including the length of the simulation, number of devices, and
how they are configured. Of course you need to create constraints to make
sure the configuration is legal.
In another Synopsys customer example, a company was creating an I/O
switch chip that connected multiple PCI buses to an internal memory bus. At
the start of simulation they randomly chose the number of PCI buses (1–4),
the number of devices on each bus (1–8) and the parameters for each device
(master or slave, CSR addresses, etc.). They kept track of the tested combina-
tions using functional coverage so that they could be sure that they had
covered almost every possible one.
Other environment parameters include test length, error injection rates,
delay modes, etc. See Bergeron (2006) for more examples.
When you read about random stimulus, you probably thought of taking a
transaction such as a bus write or ATM cell and filling the data fields with
random values. Actually this approach is fairly straightforward as long as you
carefully prepare your transaction classes as shown in Chapters 4 and 8. You
need to anticipate any layered protocols and error injection, plus scoreboard-
ing and functional coverage.
There are few things more frustrating than when a device such as a PC or
cell phone locks up. Many times, the only cure is to shut it down and restart.
Chances are that deep inside the product there is a piece of logic that experi-
enced some sort of error condition and could not recover, and thus stopped the
device from working correctly.
How can you prevent this from happening to the hardware you are build-
ing? If something can go wrong in the real hardware, you should try to
simulate it. Look at all the errors that can occur. What happens if a bus trans-
action does not complete? If an invalid operation is encountered? Does the
design specification state that two signals are mutually exclusive? Drive them
both and make sure the device continues to operate.
12 SystemVerilog for Verification
Just as you are trying to provoke the hardware with ill-formed commands,
you should also try to catch these occurrences. For example, recall those
mutually exclusive signals. You should add checker code to look for these
violations. Your code should at least print a warning message when this
occurs, and preferably generate an error and wind down the test. It is frustrat-
ing to spend hours tracking back through code trying to find the root of a
malfunction, especially when you could have caught it close to the source
with a simple assertion. See Vijayaraghavan (2005) for more guidelines on
writing assertions in your testbench and design code. Just make sure that you
can disable the code that stops simulation on error so that you can easily test
error handling.
How fast should your testbench send in stimulus? Always use constrained-
random delays to help catch protocol bugs. A test that uses the shortest delays
runs the fastest, but it won’t create all possible stimulus. You can create a test-
bench that talks to another block at the fastest rate, but subtle bugs are often
revealed when intermittent delays are introduced.
A block may function correctly for all possible permutations of stimulus
from a single interface, but subtle errors may occur when data is flowing into
multiple inputs. Try to coordinate the various drivers so they can communi-
cate at different relative timing. What if the inputs arrive at the fastest possible
rate, but the output is being throttled back to a slower rate? What if stimulus
arrives at multiple inputs concurrently? What if it is staggered with different
delays? Use functional coverage as discussed in Chapter 9 to measure what
combinations have been randomly generated.
How should you run the tests? A directed test has a testbench that pro-
duces a unique set of stimulus and response vectors. To change the stimulus,
you need to change the test. A random test consists of the testbench code plus
a random seed. If you run the same test 50 times, each with a unique seed, you
will get 50 different sets of stimuli. Running with multiple seeds broadens the
coverage of your test and leverages your work.
You need to choose a unique seed for each simulation. Some people use
the time of day, but that can still cause duplicates. What if you are using a
batch queuing system across a CPU farm and tell it to start 10 jobs at mid-
night? Multiple jobs could start at the same time but on different computers,
and will thus get the same random seed, and run the same stimulus. You
Chapter 1: Verification Guidelines 13
should blend in the processor name to the seed. If your CPU farm includes
multiprocessor machines, you could have two jobs start running at midnight
with the same seed, so you should also throw in the process ID. Now all jobs
get unique seeds.
You need to plan how to organize your files to handle multiple
simulations. Each job creates a set of output files such as log
files and functional coverage data. You can run each job in a
different directory, or you can try to give a unique name to each
file.
The previous sections have shown how to create stimuli that can randomly
walk through the entire space of possible inputs. With this approach, your
testbench visits some areas often, but takes too long to reach all possible
states. Unreachable states will never be visited, even given unlimited simula-
tion time. You need to measure what has been verified in order to check off
items in your verification plan.
The process of measuring and using functional coverage consists of sev-
eral steps. First, you add code to the testbench to monitor the stimulus going
into the device, and its reaction and response, to determine what functionality
has been exercised. Next, the data from one or more simulations is combined
into a report. Lastly, you need to analyze the results and determine how to cre-
ate new stimulus to reach untested conditions and logic. Chapter 9 describes
functional coverage in SystemVerilog.
A random test evolves using feedback. The initial test can be run with
many different seeds, creating many unique input sequences. Eventually the
test, even with a new seed, is less likely to generate stimulus that reaches
areas of the design space. As the functional coverage asymptotically
approaches its limit, you need to change the test to find new approaches to
reach uncovered areas of the design. This is known as “coverage-driven
verification.”
14 SystemVerilog for Verification
100%
With
feedback
Coverage
Without
feedback
Time
What if your testbench were smart enough to do this for you? In a previous
job, I wrote a test that generated every bus transaction for a processor, and
additionally fired every bus terminator (Success, Parity error, Retry) in every
cycle. This was before HVLs, so I wrote a long set of directed tests and spent
days lining up the terminator code to fire at just the right cycles. After much
hand analysis I declared success – 100% coverage. Then the processor’s tim-
ing changed slightly! Now I had to reanalyze the test and change the stimuli.
A more productive testing strategy uses random transactions and termina-
tors. The longer you run it, the higher the coverage. As a bonus, the test could
be made flexible enough to create valid stimuli even if the design’s timing
changed. You could add a feedback loop that would look at the stimulus cre-
ated so far (generated all write cycles yet?) and change the constraint weights
(drop write weight to zero). This improvement would greatly reduce the time
needed to get to full coverage, with little manual intervention.
However, this is not a typical situation because of the trivial feedback
from functional coverage to the stimulus. In a real design, how should you
change the stimulus to reach a desired design state? There are no easy
answers, so dynamic feedback is rarely used for constrained-random stimulus.
Manual feedback is used in coverage-driven verification.
Feedback is used in formal analysis tools such as Magellan (Synopsys
2003). It analyzes a design to find all the unique, reachable states. Then it runs
a short simulation to see how many states are visited. Lastly, it searches from
the state machine to the design inputs to calculate the stimulus needed to
reach any remaining states and then Magellan applies this to the DUT.
Chapter 1: Verification Guidelines 15
Testbench
What goes into that testbench block? It is made of many bus functional
models (BFM), that you can think of as testbench components — to the DUT
they look like real components, but are part of the testbench, not RTL. If the
real device connects to AMBA, USB, PCI, and SPI buses, you have to build
equivalent components in your testbench that can generate stimulus and check
the response. These are not detailed, synthesizable models but instead, high-
level transactors that obey the protocol, but execute more quickly. If you are
prototyping using FPGAs or emulation, the BFMs do need to be
synthesizable.
16 SystemVerilog for Verification
Testbench
AMBA USB
PCI Design
Under
SPI Test
When you first learned Verilog and started writing tests, they probably
looked like the following low-level code that does a simplified APB (AMBA
Peripheral Bus) Write. (VHDL users may have written similar code.)
Chapter 1: Verification Guidelines 17
initial begin
// Drive reset
Rst <= 0;
#100 Rst <= 1;
// Toggle PEnable
@(posedge clk)
PEnable <= 1'b1;
@(posedge clk)
PEnable <= 1'b0;
After a few days of writing code like this, you probably realized that it is
very repetitive, so you created tasks for common operations such as a bus
write, as shown in Example 1-2.
18 SystemVerilog for Verification
// Toggle Penable
@(posedge clk)
PEnable <= 1'b1;
@(posedge clk)
PEnable <= 1'b0;
endtask
initial begin
reset(); // Reset the device
write(16’h50, 32’h50); // Write data into memory
By taking the common actions, such as reset, bus reads and writes, and
putting them in a routine, you became more efficient and made fewer mis-
takes. This creation of the physical and command layers is the first step to a
layered testbench.
Signal
DUT
At the bottom is the signal layer that contains the design under test and the
signals that connect it to the testbench.
The next level up is the command layer. The DUT’s inputs are driven by
the driver that runs single commands such as bus read or write. The DUT’s
output drives the monitor that takes signal transitions and groups them
together into commands. Assertions also cross the command/signal layer, as
they look at individual signals but look for changes across an entire command.
The functional layer feeds the command layer. The agent block (called the
transactor in the VMM) receives higher-level transactions such as DMA read
or write and breaks them into individual commands. These commands are
also sent to the scoreboard that predicts the results of the transaction. The
checker compares the commands from the monitor with those in the
scoreboard.
Signal
DUT
20 SystemVerilog for Verification
The functional layer is driven by the generator in the scenario layer. What
is a scenario? Remember that your job as a verification engineer is to make
sure that this device accomplishes its intended task. An example device is an
MP3 player that can concurrently play music from its storage, download new
music from a host, and respond to input from the user, such as volume and
track controls. Each of these operations is a scenario. Downloading a music
file takes several steps, such as control register reads and writes to set up the
operation, multiple DMA writes to transfer the song, and then another group
of reads and writes. The scenario layer of your testbench orchestrates all these
steps with constrained-random values for parameters such as track size and
memory location.
Signal
DUT
The blocks in the testbench environment (inside the dashed line) are writ-
ten at the start of development. During the project they may evolve and you
may add functionality, but these blocks should not change for individual tests.
This is done by leaving “hooks” in the code so that a test can change the
behavior of these blocks without having to rewrite them. You create these
hooks with callbacks (section 8.7) and factory patterns (section 8.3).
You are now at the top of the testbench, the test layer, as shown in Figure
1-12. Design bugs that occur between DUT blocks are harder to find as they
involve multiple people reading and interpreting multiple specifications.
Chapter 1: Verification Guidelines 21
This top-level test is a conductor who does not play any musical instru-
ment, but instead guides the efforts of others. The test contains the constraints
to create the stimulus.
Functional coverage measures the progress of all tests in fullfilling the
requirements in the verification plan. The functional coverage code changes
through the project as the various criteria complete. Because it is constantly
being modified, it is not part of the environment.
You can create a “directed test” in a constrained-random environment.
Simply insert a section of directed test case into the middle of or in parallel
with a random sequence. The directed code performs the work you want, but
the random “background noise” may cause a bug to become visible, perhaps
in an unanticipated block.
Test
Functional Coverage
Environment
Generator
DUT
Do you need all these layers in your testbench? The answer depends on
what your DUT looks like. A complicated design requires a sophisticated test-
bench. You always need the test layer. For a simple design, the scenario layer
may be so simple that you can merge it with the agent. When estimating the
effort to test a design, don’t count the number of gates; count the number of
designers. Every time you add another person to the team, you increase the
chance of different interpretations of the specifications.
You may need more layers. If your DUT has several protocol layers, each
should get its own layer in the testbench environment. For example, if you
have TCP traffic that is wrapped in IP and sent in Ethernet packets, consider
22 SystemVerilog for Verification
using three separate layers for generation and checking. Better yet, use exist-
ing verification components.
One last note about the above diagram: It shows some of the possible con-
nections between blocks, but your testbench may have a different set. The test
may need to reach down to the driver layer to force physical errors. These are
just guidelines – let your needs guide what you create.
Now it is time to take the previous diagrams and learn how to map the
components into SystemVerilog constructs.
Agent
Driver
DUT
The driver receives commands from the agent, may inject errors or add
delays, and then breaks the command down into individual signal changes
such as bus requests, handshakes, etc. The general term for such a testbench
block is a “transactor,” which, at its core, is a loop:
Chapter 4 presents basic OOP and how to create an object that includes the
routines and data for a transactor. Another example of a transactor is the
agent. It might break apart a complex transaction such as a DMA read into
multiple bus commands. Also in Chapter 4, you will see how to build an
object that contains the data and routines that make up a command. These
objects are sent between transactors using SystemVerilog mailboxes. In
Chapter 7, you will learn about many ways to exchange data between the dif-
ferent layers and to synchronize the transactors.
Up until now you have been learning what parts make up the environment.
When do these parts execute? You want to clearly define the phases to coordi-
nate the testbench so all the code for a project works together. The three
primary phases are Build, Run, and Wrap-up. Each is divided into smaller
steps.
The Build phase is divided into the following steps:
The Run phase is where the test actually runs. It has the following steps:
wait for the next lower layer. You should also use time-out checkers
to ensure that the DUT or testbench does not lock up.
Sweep: After the lowest layer completes, you need to wait for the
final transactions to drain out of the DUT.
Report: Once the DUT is idle, sweep the testbench for lost data.
Sometimes the scoreboard holds transactions that never came out,
perhaps because they were dropped by the DUT. With this informa-
tion you can create the final report on whether the test passed or
failed. If it failed, be sure to delete any functional coverage data, as it
may not be correct.
As shown in the layer diagram, Figure 1-12, the test starts the environ-
ment. This runs each of the steps. More details can be found in Chapter 8.
If this is the first time you have seen this methodology, you probably have
some qualms about how it works compared to directed testing. A common
objection is testbench performance. A directed test often simulates in less than
a second, while constrained-random tests will wander around through the
state space for minutes or hours. The problem with this argument is that it
ignores a real verification bottleneck – the time required by you to create a
test. You may be able to hand-craft a directed test in a day, and debug it and
manually verify the results by hand in another day or two. The actual simula-
tion run-time is dwarfed by the amount of your time that you invested.
Chapter 1: Verification Guidelines 25
1.17 Conclusion
Data Types
2.1 Introduction
SystemVerilog offers many improved data structures compared with Ver-
ilog. Some of these were created for designers but are also useful for
testbenches. In this chapter you will learn about the data structures most use-
ful for verification.
SystemVerilog introduces new data types with the following benefits.
The one thing in Verilog that always leaves new users scratching their
heads is the difference between a reg and a wire. When driving a port,
28 SystemVerilog for Verification
which should you use? How about when you are connecting blocks? System-
Verilog improves the classic reg data type so that it can be driven by
continuous assignments, gates and modules, in addition to being a variable. It
is given the new name logic so that it does not look like a register declara-
tion. The one limitation is that a logic variable cannot be driven by multiple
drivers such as when you are modeling a bidirectional bus. In this case, the
variable needs to be a net-type such as wire.
Example 2-1 shows the SystemVerilog logic type.
Example 2-1 Using the logic type
module logic_data_type(input logic rst_h);
parameter CYCLE = 20;
logic q, q_l, d, clk, rst_l;
initial begin
clk <= 0; // Procedural assignment
forever #(CYCLE/2) clk = ~clk;
end
endmodule
the 255 you may expect. (It has the range -128 to +127.) You could use byte
unsigned, but that is more verbose than just bit [7:0]. Signed variables
may cause unexpected results with randomization, as discussed in Chapter 6.
Be careful connecting two-state variables to the design under
test, especially its outputs. If the hardware tries to drive an X or
Z, these values are converted to a two-state value, and your
testbench code may never know. Don’t try to remember if they
are converted to 0 or 1; instead, always check for propagation
of unknown values. Use the $isunknown operator that returns 1 if any bit of
the expression is X or Z.
Example 2-3 Checking for four-state values
if ($isunknown(iport)
$display("@%0d: 4-state value detected on input port",
$time, iport);
Verilog requires that the low and high array limits must be given in the
declaration. Almost all arrays use a low index of 0, so SystemVerilog lets you
use the shortcut of just giving the array size, similar to C:
Example 2-4 Declaring fixed-size arrays
int lo_hi[0:15]; // 16 ints [0]..[15]
int c_style[16]; // 16 ints [0]..[15]
b_array[0] 76543210
b_array[1] Unused space 76543210
b_array[2] 76543210
You can initialize an array using an array literal that is an apostrophe and
curly braces.1 You can set some or all elements at once. You can replicate val-
ues by putting a count before the curly braces.
Example 2-7 Initializing an array
int ascend[4] = ’{0,1,2,3}; // Initialize 4 elements
int decend[5];
int md[2][3] = ’{’{0,1,2}, ’{3,4,5}};
1. VCS X-2005.06 follows the original Accellera standard for array literals and uses just the
curly braces with no leading apostrophe. VCS will change to the IEEE standard in an
upcoming release.
Chapter 2: Data Types 31
$display("New value:");
md = ‘{{9, 8, 7}, 3{5}}; // Replicate last 3 values
foreach (md[i,j]) // Yes, this is the right syntax
$display("md[%0d][%0d] = %0d", i, j, md[i][j]);
end
New value:
md[0][0] = 9
md[0][1] = 8
md[0][2] = 7
md[1][0] = 5
md[1][1] = 5
md[1][2] = 5
32 SystemVerilog for Verification
You can perform aggregate compare and copy of arrays without loops.
(An aggregate operation works on the entire array as opposed to working on
just an individual element.) Comparisons are limited to just equality and ine-
quality. Example 2-11 shows several examples of compares. The ? :
operator is a mini if-statement. Here it is choosing between two strings.
Example 2-11 Array copy and compare operations
initial begin
bit [31:0] src[5] = ’{0,1,2,3,4},
dst[5] = ’{5,4,3,2,1};
A common annoyance in Verilog-1995 is that you cannot use word and bit
subscripts together. Verilog-2001 removes this restriction for fixed-size
arrays. Example 2-12 prints the first array element (binary 101), its lowest bit
(1), and the next two higher bits (binary 10).
Chapter 2: Data Types 33
While this change is not new to SystemVerilog, many users may not know
about this useful improvement in Verilog-2001.
For some data types, you may want both to access the entire value and also
divide it into smaller elements. For example, you may have a 32-bit register
that sometimes you want to treat as four 8-bit values and at other times as a
single, unsigned value. A SystemVerilog packed array is treated as both an
array and a single value. It is stored as a contiguous set of bits with no unused
space, unlike an unpacked array.
The packed bit and word dimensions are specified as part of the type,
before the variable name. These dimensions must be specified in the
[lo:hi] format. The variable bytes is a packed array of four bytes, which
are stored in a single longword.
Example 2-13 Packed array declaration and usage
bit [3:0] [7:0] bytes; // 4 bytes packed into 32-bits
bytes = 32’hdead_beef;
$displayh(bytes,, // Show all 32-bits
bytes[3], // most significant byte "de"
bytes[3][7]); // most significant bit "1"
bytes[3] bytes[1][6]
bytes 76543210 76543210 76543210 76543210
You can mix packed and unpacked dimensions. You may want to make an
array that represents a memory that can be accessed as bits, bytes, or long-
34 SystemVerilog for Verification
The variable bytes is a packed array of four bytes, which are stored in a
single longword. barray is an array of three of these elements.
Figure 2-3 Packed arrays
barray[0][3] barray[0][1][6]
barray[0]7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
barray[1]7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
barray[2]7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
array until run-time? You may choose the number of transactions randomly
between 1000 and 100,000, but you do not want to use a fixed-size array that
would be half empty. SystemVerilog provides a dynamic array that can be
allocated and resized during simulation.
A dynamic array is declared with empty word subscripts []. This means
that you do not want to give an array size at compile time; instead, you specify
it at run-time. The array is initially empty, so you must call the new[] opera-
tor to allocate space, passing in the number of entries in the square brackets. If
you pass the name of an array to the new[] operator, the values are copied
into the new elements.
Example 2-15 Using dynamic arrays
int dyn[], d2[]; // Empty dynamic arrays
initial begin
dyn = new[5]; // Allocate 5 elements
foreach (dyn[j])
dyn[j] = j; // Initialize the elements
d2 = dyn; // Copy a dynamic array
d2[0] = 5; // Modify the copy
$display(dyn[0],d2[0]); // See both values (0 & 5)
dyn = new[20](dyn); // Expand and copy
dyn = new[100]; // Allocate 100 new integers
// Old values are lost
dyn.delete; // Delete all elements
end
You can make assignments between fixed-size and dynamic arrays as long
as they have the same base type such as int. You can assign a dynamic array
to a fixed array as long as they have the same number of elements.
When you copy a fixed-size array to a dynamic array, SystemVerilog calls
new[] constructor to allocate space, and then copies the values.
2.5 Queues
SystemVerilog introduces a new data type, the queue, which provides easy
searching and sorting in a structure that is as fast as a fixed-size array but as
versatile as a linked list.
Like a dynamic array, queues can grow and shrink, but with a queue you
can easily add and remove elements anywhere. Example 2-17 adds and
removes values from a queue.
Example 2-17 Queue operations
int j = 1,
b[$] = {3,4},
q[$] = {0,2,5}; // {0,2,5} Initial queue
initial begin
q.insert(1, j); // {0,1,2,5} Insert 1 before 2
q.insert(3, b); // {0,1,2,3,4,5} Insert whole q.
q.delete(1); // {0,2,3,4,5} Delete elem. #1
and deleting elements in the middle is slower, especially for larger queues, as
SystemVerilog has to shift up to half of the elements.
You can copy the contents of a fixed or dynamic array into a queue.
data
index 0…..3 42 1000 4521 200,000
Example 2-18 has the associative array, assoc, with very scattered ele-
ments: 1, 2, 4, 8, 16, etc. A simple for loop cannot step through them; you
need to use a foreach loop, or, if you wanted finer control, you could use the
first and next functions in a do...while loop. These functions modify the
index argument, and return 0 or 1 depending on whether any elements are left
in the array.
Associative arrays can also be addressed with a string index, similar to
Perl’s hash arrays. Example 2-19 reads name/value pairs from a file into an
associative array. If you try to read from an element that has not been allo-
cated yet, SystemVerilog returns a 0 for two-state types or X for 4-state types.
You can use the function exists to check if an element exists, as shown
below. Strings are explained in section 2.14.
Chapter 2: Data Types 39
initial begin
foreach (on[i])
on[i] = i; // on[i] gets 0 or 1
Other array reduction methods are product, and, or, and xor.
Example 2-22 uses a fixed-size array, f[6], a dynamic array, d[], and a
queue, q[$].The min and max functions find the smallest and largest ele-
ments in an array. Note that they return a queue, not a scalar as you might
expect. These methods also work for associative arrays. The unique method
returns a queue of the unique values from the array — duplicate values are not
included.
Example 2-21 Array locator methods: min, max, unique
int f[6] = ‘{1,6,2,6,8,6};
int q[$] = ‘{1,3,5,7}, tq[$];
tq = q.min; // {1}
tq = q.max; // {7}
tq = f.unique; // {1,6,2,8}
You could search through an array using a foreach loop, but SystemVer-
ilog can do this in one operation with a locator method. The with expression
tells SystemVerilog how to perform the search.
Example 2-22 Array locator methods: find
int d[] = ‘{9,1,8,3,4,4}, tq[$];
When you combine an array reduction such as sum using the with clause,
the results may surprise you. In Example 2-23, the sum operator is adding up
the number of times that the expression is true. For the first statement in
Example 2-23, there are two array elements that are greater than 7 (9 and 8) so
count is set to 2. Note that sum-with is a statement, not an expression, so
you need to store the result in a temporary variable, and cannot use it directly,
as in a $display statement.
42 SystemVerilog for Verification
2.9.1 Flexibility
If you want to reduce the simulation memory usage, use two-state ele-
ments. You should chose data sizes that are multiples of 32 bits to avoid
wasted space. Simulators usually store anything smaller in a 32-bit word. For
example, an array of 1024 bytes wastes ¾ of the memory if the simulator puts
each element in a 32-bit word. Packed arrays can also help conserve memory.
For arrays that hold up to a thousand elements, the type of array that you
choose does not make a big difference in memory usage (unless there are
Chapter 2: Data Types 43
many instances of these arrays). For arrays with a thousand to a million active
elements, fixed-size and dynamic arrays are the most memory efficient. You
may want to reconsider your algorithms if you need arrays with more than a
million active elements.
Queues are slightly less efficient to access than fixed-size or dynamic
arrays because of additional pointers. However, if your data set grows and
shrinks often, and you store it in a dynamic memory, you will have to manu-
ally call new[] to allocate memory and copy. This is an expensive operation
and would wipe out any gains from using a dynamic memory.
Modeling memories larger than a few megabytes should be done with an
associative array. Note that each element in an associative array can take sev-
eral times more memory than a fixed-size or dynamic memory because of
pointer overhead.
2.9.3 Speed
Choose your array type based on how many times it is accessed per clock
cycle. For only a few reads and writes, you could use any type, as the over-
head is minor compared with the DUT. As you use an array more often, its
size and type matters.
Fixed-size and dynamic arrays are stored in contiguous memory, so any
element can be found in the same amount of time, regardless of array size.
Queues have almost the same access time as a fixed-size or dynamic array
for reads and writes. The first and last elements can be pushed and popped
with almost no overhead. Inserting or removing elements in the middle
requires many elements to be shifted up or down to make room. If you need to
insert new elements into a large queue, your testbench may slow down, so
consider changing how you store new elements.
When reading and writing associative arrays, the simulator must search for
the element in memory. The LRM does not specify how this is done, but pop-
ular ways are hash tables and trees. These requires more computation than
other arrays, and therefore associative arrays are the slowest.
2.9.4 Sorting
If you have values that are noncontiguous such as ‘{1, 10, 11, 50}, and are
also unique, you can store them in an associative array by using them as an
index. Using the routines first, next, and prev, you can search an associa-
tive array for a value and find successive values. Lists are doubly linked, so
you can find values both larger and smaller than the current value. Both of
these support removing a value. However, the associative array is much faster
in accessing any given element given an index.
For example, you can use an associative array of bits to hold expected 32-
bit values. When the value is created, write to that location. When you need to
see if a given value has been written, use the exists function. When done
with an element, use delete to remove it from the associative array.
You can create an array of handles that point to objects, as shown in Chap-
ter 4 on Basic OOP.
You are not really creating a new type; you are just performing text substi-
tution. In SystemVerilog you create a new type with the following code. This
book uses the convention that user-defined types use the suffix “_t.”
Example 2-25 User-defined type in SystemVerilog
// New SystemVerilog style
parameter OPSIZE = 8;
typedef reg [OPSIZE-1:0] opreg_t;
In general, SystemVerilog lets you copy between these basic types with no
warning, either extending or truncating values if there is a width mismatch.
Note that the parameter and typedef statements can be made global by
putting them in $root, as shown in section 5.7.
One of the most useful types you can create is an unsigned, 2-
state, 32-bit integer. Most values in a testbench are positive
integers such as field length or number of transactions received.
Put the following definition of uint in $root so it can be used
anywhere in your simulation.
Example 2-26 Definition of uint
typedef bit [31:0] uint; // 32-bit unsigned 2-state
typedef int unsigned uint; // Equivalent definition
46 SystemVerilog for Verification
You can combine several variables into a structure. Example 2-27 creates
a structure called pixel that has three unsigned bytes for red, green, and blue.
Example 2-27 Creating a single pixel type
struct {bit [7:0] r, g, b;} pixel;
The problem with the above declaration is that it creates a single pixel of
this type. To be able to share pixels using ports and routines, you should cre-
ate a new type instead.
Example 2-28 The pixel struct
typedef struct {bit [7:0] r, g, b;} pixel_s;
pixel_s my_pixel;
Use the suffix “_s” when declaring a struct. This makes it easier for
you to share and reuse code.
SystemVerilog allows you more control in how data is laid out in memory
by using packed structures. A packed structure is stored as a contiguous set of
bits with no unused space. The struct for a pixel, shown above, used three
data values, so it is stored in three longwords, even though it only needs three
bytes. You can specify that it should be packed into the smallest possible
space.
Example 2-30 Packed structure
typedef struct packed {bit [7:0] r, g, b;} pixel_p_s;
pixel_p_s my_pixel;
Packed structures are used when the underlying bits represent a numerical
value, or when you are trying to reduce memory usage. For example, you
could pack together several bit-fields to make a single register. Or you might
pack together the opcode and operand fields to make a value that contains an
entire processor instruction.
initial begin
case (pstate)
IDLE: nstate = INIT; // data assignment
INIT: nstate = DECODE;
default: nstate = IDLE;
endcase
$display("Next state is %0s",
nstate.name); // Use name function
end
The actual values default to integers starting at 0 and then increase. You
can choose your own enumerated values. The following line uses the default
value of 0 for INIT, then 2 for DECODE, and 3 for IDLE.
Example 2-33 Specifying enumerated values
typedef enum {INIT, DECODE=2, IDLE} fsmtype_e;
Enumerated constants, such as INIT above, follow the same scoping rules
as variables. Consequently, if you use the same name in several enumerated
types (such as INIT in different state machines), they have to be declared in
different scopes such as modules, program blocks, routines, or classes.
Chapter 2: Data Types 49
The functions next and prev wrap around when they reach the beginning
or end of the enumeration.
Note that there is no easy way to write a for loop that steps through all
members of an enumerated type if you use an enumerated loop variable. You
get the starting member with first and the next member with next. The
problem is creating a comparison for the final iteration through the loop. If
you use the test current!=current.last, the loop ends before using the
last value. If you use current<=current.last, you get an infinite loop, as
next never gives you a value that is greater than the final value.
You can use a do...while loop to step through all the values.
50 SystemVerilog for Verification
The default type for an enumerated type is int (2-state). You can take the
value of an enumerated variable and put it in an integer or int with a sim-
ple assignment. But SystemVerilog does not let you store a 4-state integer
in an enum without explicitly changing the type. SystemVerilog requires you
to explicitly cast the value to make you realize that you could be writing an
out-of-bounds value.
Example 2-37 Assignments between integers and enumerated types
typedef enum {RED, BLUE, GREEN} COLOR_E;
COLOR_E color, c2;
integer c;
initial begin
c = color; // Convert from enum to integer
c++; // Increment integer
if (!$cast(color, c)) // Cast integer back to enum
$display("Cast failed for c=%0d", c);
$display("Color is %0d / %0s", color, color.name);
c2 = COLOR_E’(c); // No type checking done
end
2.13 Constants
There are several types of constants in SystemVerilog. The classic Verilog
way to create a constant is with a text macro. On the plus side, macros have
global scope and can be used for bit field definitions and type definitions. On
the negative side, macros are global, so they can cause conflicts if you just
need a local constant. Lastly, a macro requires the ` character so that it will be
recognized and expanded.
In SystemVerilog, parameters can be declared at the $root level so they
can be global. This approach can replace many Verilog macros that were just
being used as constants. You can use a typedef to replace those clunky mac-
ros. The next choice is a parameter. A Verilog parameter was loosely
typed and was limited in scope to a single module. Verilog-2001 added typed
parameters, but the limited scope kept parameters from being widely used.
SystemVerilog also supports the const modifier that allows you to make
a variable that can be initialized in the declaration but not written by proce-
dural code.
Example 2-38 Declaring a const variable
initial begin
const byte colon = ":";
...
end
2.14 Strings
If you have ever tried to use a Verilog reg variable to hold a string of char-
acters, your suffering is over. The SystemVerilog string type holds
variable-length strings. An individual character is of type byte. The elements
of a string of length N are numbered 0 to N-1. Note that, unlike C, there is no
null character at the end of a string, and any attempt to use the character “\0”
is ignored. Strings use dynamic memory allocation, so you do not have to
worry about running out of space to store the string.
Example 2-39 shows various string operations. The function getc(N)
returns the byte at location N, while toupper returns an upper-case copy of
the string and tolower returns a lowercase copy. The curly braces {} are
used for concatenation. The task putc(M) writes a byte into a string at loca-
tion M, that must be between 0 and the length as given by len. The
substr(start,end) function extracts characters from location start to
end.
52 SystemVerilog for Verification
initial begin
s = "SystemVerilog";
$display(s.getc(0)); // Display: 83 (‘S’)
$display(s.toupper()); // Display: SYSTEMVERILOG
Note how useful dynamic strings can be. In other languages such as C you
have to keep making temporary strings to hold the result from a function. In
Example 2-39, the $psprintf2 function is used instead of $sformat, from
Verilog-2001. This new function returns a formatted temporary string that, as
shown above, can be passed directly to another routine. This saves you from
having to declare a temporary string and passing it between the formatting
statement and the routine call.
2.
The function $psprintf is implemented in Synopsys VCS and submitted for the next
version of SystemVerilog. Other simulators may have already implemented it.
Chapter 2: Data Types 53
There are several tricks you can use to avoid this problem. First, avoid sit-
uations where the overflow is lost, as in addition A. Use a temporary, such as
b8, with the desired width. Or, you can add another value to force the mini-
mum precision, such as 2’b0. Lastly, in SystemVerilog, you can cast one of
the variables to the desired precision.
module first;
...
2.17 Conclusion
SystemVerilog provides many new data types and structures so that you
can create high-level testbenches without having to worry about the bit-level
representation. Queues work well for creating scoreboards where you con-
stantly need to add and remove data. Dynamic arrays allow you to choose the
array size at run-time for maximum testbench flexibility. Associative arrays
are used for sparse memories and some scoreboards with a single index. Enu-
54 SystemVerilog for Verification
merated types make your code easier to read and write by creating groups of
named constants.
But don’t go off and create a procedural testbench with just these con-
structs. Explore the OOP capabilities of SystemVerilog in Chapter 4 to learn
how to design code at an even higher level of abstraction, thus creating robust
and reusable code.
Chapter 3
Two new statements help with loops. First, if you are in a loop, but want to
skip over rest of the statements and do the next iteration, use continue. If
you want to leave the loop immediately, use break.
The following loop reads commands from a file using the amazing file I/O
code that is part of Verilog-2001. If the command is just a blank line, the code
just does a continue and skips any further processing of the command. If the
command is “done,” the code does a break to terminate the loop.
Example 3-2 Using break and continue while reading a file
initial begin
logic [127:0] cmd;
integer file, c;
Some simulators such as VCS allow you to ignore the return value without
using the above void syntax.
Chapter 3: Procedural Statements and Routines 57
3.
In general, a routine definition or call with no arguments does not need the empty parentheses (). This
book leaves them out except as needed for clarity.
58 SystemVerilog for Verification
requires you to declare some arguments twice, once for the direction, and
once for the type.
Example 3-6 Verilog-1995 routine arguments
task mytask2;
output [31:0] x;
reg [31:0] x;
input y;
...
endtask
With SystemVerilog, you can use the less verbose C-style. Note that you
should use the universal input type of logic.
Example 3-7 C-style routine arguments
task mytask1 (output logic [31:0] x,
input logic y);
...
endtask
The arguments a and b are input logic, 1 bit wide. The arguments u and v
are 16-bit output bit types.
Chapter 3: Procedural Statements and Routines 59
The second benefit of ref arguments is that a task can modify a variable
and is instantly seen by the calling function. This is useful when you have
several threads executing concurrently and want a simple way to pass
information. See Chapter 7 for more details on using fork-join.
60 SystemVerilog for Verification
In Example 3-11, the initial block can access the data from memory as
soon as bus.enable is asserted, even though the bus_read task does not
return until the bus transaction completes, which could be several cycles later.
Since the data argument is passed as ref, the @data statement triggers as
soon as data changes in the task. If you had declared data as output, the
@data statement would not trigger until the end of the bus transaction.
You can call this task in the following ways. Note that the first call is
compatible with both versions of the print_sum routine.
Example 3-13 Using default argument values
print_sum(a); // Sum a[0:size-1] – default
print_sum(a, 2, 5); // Sum a[2:5]
print_sum(a, 1); // Start at 1
print_sum(a,, 3); // Sum a[0:3]
print_sum(); // error: a has no default
The two arguments are input integers. As you are writing the task, you
realize that you need access to an array, so you add a new array argument, and
use the ref type so it does not have to be copied.
Example 3-15 Task header with additional array argument
task sticky(ref int array[50],
int a, b); // What direction are these?
62 SystemVerilog for Verification
What argument types are a and b? They take the direction of the previous
argument that is a ref. Using ref for a simple variable such as an int is not
usually needed, but you would not get even a warning from the compiler, and
thus would not realize that you were using the wrong type.
If any argument to your routine is something other than the default input
type, specify the direction for all arguments.
Example 3-16 Task header with additional array argument
task sticky(ref int array[50],
input int a, b); // Be explicit
languages. After all, how can you build a silicon representation of a recursive
routine? However, software engineers who were used to the behavior of
stack-based languages such as C were bitten by these subtle bugs, and were
limited in their ability to create complex testbenches with libraries of routines.
You can call this task multiple times concurrently, as the addr and
expect_data arguments are stored separately for each call. Without the
automatic modifier, if you called wait_for_mem a second time while the
first was still waiting, the second call would overwrite the two arguments.
The following task looks at the bus after five cycles and then creates a
local variable and attempts to initialize it to the current value of the address
bus.
Example 3-20 Static initialization bug
program initialization; // Buggy version
task check_bus;
repeat (5) @(posedge clock);
if (bus_cmd == ‘READ) begin
// When is local_addr initialized?
reg [7:0] local_addr = addr<<2; // Bug
$display("Local Addr = %h", local_addr);
end
endtask
endprogram
even more time aware by using the classic Verilog $timeformat and
$realtime routines.
Example 3-22 Time literals and $timeformat
module timing;
timeunit 1ns;
timeprecision 1ps;
initial begin
$timeformat(-9, 3, "ns", 8);
#1 $display("@%t", $realtime); // @1.000ns
#2ns $display("@%t", $realtime); // @3.000ns
#0.1ns $display("@%t", $realtime); // @3.100ns
#41ps $display("@%t, $realtime); // @3.141ns
end
endmodule
3.9 Conclusion
The new SystemVerilog procedural constructs and task/function features
make it easier for you to create testbenches by making the language look more
like other programming language such as C/C++. But stick with
SystemVerilog for the additional HDL constructs such as timing controls and
four-state logic.
Chapter 4
Basic OOP
4.1 Introduction
With procedural programming languages such as Verilog and C, there is a
strong division between data structures and the code that uses them. The dec-
larations and types of data are often in a different file than the algorithms that
manipulate them. As a result, it can be difficult to understand the functionality
of a program, as the two halves are separate.
Verilog users have it even worse than C users, as there are no structures in
Verilog, only bit vectors and arrays. If you wanted to store information about
a bus transaction, you would need multiple arrays: one for the address, one for
the data, one for the command, and more. Information about transaction N is
spread across all the arrays. Your code to create, transmit, and receive transac-
tions is in a module that may or may not be actually connected to the bus.
Worst of all, the arrays are all static, so if your testbench only allocated 100
array entries, and the current test needed 101, you would have to edit the
source code to change the size and recompile. As a result, the arrays are sized
to hold the greatest conceivable number of transactions, but during a normal
test, most of that memory is wasted.
Object Oriented Programming (OOP) lets you create complex data types
and tie them together with the routines that work with them. You can create
testbenches and system-level models at a more abstract level by calling rou-
tines to perform an action rather than toggling bits. When you work with
transactions instead of signal transitions, you are more productive. As a
bonus, your testbench is decoupled from the design details, making it more
robust and easier to maintain and reuse on future projects.
If you already are familiar with OOP, skim this chapter, as SystemVerilog
follows OOP guidelines fairly closely. Be sure to read section 4.18 to learn
how to build a testbench. Chapter 8 presents advanced OOP concepts such as
inheritance and more testbench techniques; it should be read by everyone.
grouped together into transactions. So the easiest way to organize the test-
bench is around the transactions, and the operations that you perform. In
OOP, the transaction is the object that is focus of your testbench.
You can think of an analogy with transportation. When you get into a car,
you want to perform discrete actions, such as starting, moving forward, turn-
ing, stopping, and listening to music while you drive. Early cars required
detailed knowledge about their internals to operate. You had to advance or
retard the spark, open and close the choke, keep an eye on the engine speed
and be aware of the traction of the tires if you drove on a slippery surface such
as a wet road. Today your interactions with the car are at a high level. If you
want to start a car, just turn the key in the ignition, and you are done. Get the
car moving by pressing the gas pedal; stop it with the brakes. Are you driving
on snow? Don’t worry: the anti-lock brakes will help you stop safely and in a
straight line.
Your testbench should be structured the same way. Traditional testbenches
were oriented around the operations that had to happen: create a transaction,
transmit it, receive it, check it, and make a report. Instead, you should think
about the structure of the testbench, and what each part does. The generator
creates transactions and passes them to the next level. The driver talks with
the design that responds with transactions that are received by a monitor. The
scoreboard checks these against the expected data. You should divide your
testbench into blocks, and then define how they communicate.
endclass : BusTran
Every company has its own naming style. This book uses the
following convention: Class names start with a capital letter
and do not use underscores, as in BusTran or Packet. Con-
stants are all upper case, as in CELL_SIZE, while variables are
lower case, as in count or trans_type. You are free to use
whatever style you want.
Verilog. Here are some OOP terms, definitions, and rough equivalents in Ver-
ilog 2001.
This book uses the more traditional terms from Verilog of “variable” and
“routine” rather than OOP’s “property” and “method.” If you are comfortable
with the OOP terms, you can skim this chapter.
In Verilog you build complex designs by creating modules and instantiat-
ing them hierarchically. In OOP you create classes and instantiate them
(creating objects) to create a similar hierarchy.
Here is a brief analogy to explain these OOP terms. Think of a class as the
blueprint for a house. This plans describe how the structure of house, but you
cannot live in a blueprint. An object is the actual house. Just as one set of
blueprints can be used to build a whole subdivision of houses, a single class
can be used to build many objects. The house address is like a handle in that it
uniquely identifies your house. Inside your house you have things such as
lights (on or off), with switches to control them. A class has variables that
hold values, and routines that control the values. A class for the house might
have many lights. A single call to turn_on_porch_light() sets the porch
light variable to ON in a single house.
When you declare the handle b, it is initialized to the special value null.
Next, you call the new function to construct the BusTran object. new allo-
cates space for the BusTran, initializes the variables to their default value (0
for 2-state variables and X for 4-state ones), and returns the address where the
object is stored. For every class, SystemVerilog creates a default new to allo-
cate and initialize an object. See section 4.6.2 for more details on this
function.
4.
Back to the house analogy: the address is normally static, unless your house burns down,
causing you to construct a new one. And garbage collection is never automatic.
72 SystemVerilog for Verification
function new;
addr = 3;
foreach (data[i])
data[i] = 5;
endfunction
endclass
The above code sets addr and data to fixed values but leaves crc at its
default value of X. (SystemVerilog allocates the space for the object automat-
ically.) You can use the function argument with default values to make a more
flexible constructor.
Example 4-4 A new function with arguments
class BusTran;
logic [31:0] addr, crc, data[8];
initial begin
BusTran b;
b = new(10); // data uses default of 5
end
Chapter 4: Basic OOP 73
How does SystemVerilog know which new function to call? It looks at the
type of the handle on the left side of the assignment. In Example 4-5, the call
to new inside the Driver constructor calls the new function for BusTran,
even though the one for Driver is closer. Since bt is a BusTran handle,
SystemVerilog does the right thing and create an object of type BusTran.
Example 4-5 Calling the right new function
class BusTran;
...
endclass : BusTran
class Driver;
BusTran bt;
function new(); // Driver’s new function
bt = new(); // Call the BusTran new function
endfunction
endclass : Driver
You may have noticed that this new() function looks a lot like the new[]
operator, described in section 2.4, used to set the size of dynamic arrays. They
both allocate memory and initialize values. The big difference is that the
new() function is called to construct a single object, while the new[] opera-
tor is building an array with multiple elements. new() can take arguments for
setting object values, while new[] only takes a single value for the array size.
New OOP users often confuse an object with its handle. The
two are very distinct. You declare a handle and construct
an object. Over the course of a simulation, a handle can
74 SystemVerilog for Verification
point to many objects. This is the dynamic nature of OOP and SystemVerilog.
Don’t get the handle confused with the object.
In Example 4-6, b1 first points to one object, then another.
Example 4-6 Allocating multiple objects
BusTran b1, b2; // Declare two handles
b1 = new; // Allocate first BusTran object
b2 = b1; // b1 & b2 point to it
b1 = new; // Allocate second BusTran object
b1 Second b2 First
BusTran BusTran
object object
Note that this dynamic creation of objects is different from anything else
offered before in the Verilog language. An instance of a Verilog module and
its name are bound together statically during compilation. Even with auto-
matic variables, which come and go during simulation, the name and storage
are always tied together.
An analogy for handles is people who are attending a conference. Each
person is similar to an object. When you arrive, a badge is “constructed” by
writing your name on it. This badge is a handle that can be used by the orga-
nizers to keep track of each person. When you take a seat for the lecture,
space is allocated. You may have multiple badges for attendee, presenter, or
organizer. When you leave the conference, your badge may be reused by writ-
ing a new name on it, just as a handle can point to different objects through
assignment. Lastly, if you lose your badge and there is nothing to identify
you, you will be asked to leave. The space you take, your seat, is reclaimed
for use by someone else.
cessfully, and you gather statistics, you don’t need to keep it around. You
should reclaim the memory; otherwise, a long simulation might run out of
memory, or at least run more and more slowly.
Garbage collection is the process of automatically freeing objects that are
no longer referenced. One way SystemVerilog can tell if an object is no
longer being used is by keeping track of the number of handles that point to it.
When the last handle no longer references an object, SystemVerilog releases
the memory for it.5
Example 4-7 Creating multiple objects
BusTran b; // Create a handle
b = new; // Allocate a new BusTran
b = new; // Allocate a second one, free the first
b = null; // Deallocate the second
The second line calls new to construct an object and store the address in
the handle b. The next call to new constructs a second object and stores its
address in b, overwriting the previous value. Since there are no handles point-
ing to the first object, SystemVerilog can deallocate it. (SystemVerilog may
delete the object immediately, or wait a while.) The last line explicitly clears
the handle so that now the second object can be deallocated.
If you are familiar with C++, these concepts of objects and handles might
look familiar, but there are some important differences. A SystemVerilog
handle can only point to objects of one type, so they are called “type-safe.” In
C++, a typical untyped pointer is only an address in memory, and you can set
it to any value or modify it with operators such as pre-increment. You cannot
be sure that a pointer really is valid. SystemVerilog does not allow any modi-
fication of a handle or using a handle of one type to refer to an object of
another type. (SystemVerilog’s OOP specification is closer to Java than C++.)
Secondly, since SystemVerilog performs automatic garbage collection
when no more handles refer to an object, you can be sure your code always
uses valid handles. In C / C++, a pointer can refer to an object that no longer
exists. Garbage collection in those languages is manual, so your code can suf-
fer from “memory leaks” when you forget to deallocate objects.
SystemVerilog cannot garbage collect an object that is refer-
enced by a handle. If you create linked lists (especially double-
linked lists) or circular lists, SystemVerilog does not deallocate
the object. You need to manually clear all handles by setting
them to null. If an object contains a routine that forks off a
5.
The actual algorithm to find unused objects varies between simulators. This section
describes reference counting, which is the easiest to understand.
76 SystemVerilog for Verification
thread, the object is not deallocated while the thread is running. Likewise, any
objects that are used by a spawned thread may not be deallocated until the
thread terminates. See Chapter 7 for more information on threads.
In SystemVerilog you can create a static variable inside a class. This vari-
able is shared between all instances of the class, but its scope is limited to the
class. In Example 4-9, the static variable count holds the number of objects
created so far. It is initialized to 0 in the declaration because there are no
transactions at the beginning of the simulation. Each time a new object is con-
structed, it is tagged with a unique value, and count is incremented.
Example 4-9 Class with a static variable
class BusTran;
static int count = 0; // Number of objects created
int id; // Unique instance ID
function new;
id = count++; // Set ID, bump count
endfunction
endclass
In Example 4-9, there is only one copy of the static variable count,
regardless of how many BusTran objects are created. You can think that
count is stored with the class and not the object. The variable id is not static,
so every BusTran has its own copy. Now you don’t need to make a global
variable for the count.
Figure 4-2 Static variables in a class
id: 0
class BusTran; count
static int count = 0;
int id;
id: 1
endclass
count
MyStatic s;
initial
s.initialize(42);
class PCI_Tran;
bit [31:0] addr, data; // Use realistic names
function void display();
$display("@%0d: PCI: addr=%h, data=%h",
addr, data);
endfunction
endclass
BusTran b;
PCI_Tran pc;
initial begin
b = new(); // Construct a BusTran
b.display(); // Display a BusTran
pc = new(); // Construct a PCT transaction
pc.display(); // Display a PCI Transaction
end
Here is how you create out-of-block declarations. Copy the first line of the
routine, with the name and arguments, and add the extern keyword at the
beginning. Now take the entire routine and move it after the class body, and
add the class name and two colons (:: the scope operator) before the routine
name.
The above classes could be defined as follows.
Example 4-12 Out-of-block routine declarations
class BusTran;
bit [31:0] addr, crc, data[8];
extern function void display();
endclass
class PCI_Tran;
bit [31:0] addr, data; // Use realistic names
extern function void display();
endclass
program p;
int limit; // $root.p.limit
class Foo;
int limit, array[]; // $root.p.Foo.limit
initial begin
int limit = $root.limit; // $root.p.$unnamed.limit
Foo bar;
bar = new;
bar.array = new[limit];
bar.print (limit);
end
endprogram
class Bug;
logic [31:0] data[9];
When you use a variable name, SystemVerilog looks in the current scope
for it, and then in the parent scopes until the variable is found. This is the
same algorithm used by Verilog. But what if you are deep inside a class and
want to unambiguously refer to a class-level object? This style code is most
commonly used in constructors, where the programmer uses the same name
for a class variable and an argument.6 In Example 4-16, the keyword “this”
removes the ambiguity to let SystemVerilog know that you are assigning the
local variable, oname, to the class variable, oname.
Example 4-16 Using this to refer to class variable
class Scoping;
string oname;
endclass
6. Some people think this makes the code easier to read; others think it is a shortcut by a lazy programmer.
84 SystemVerilog for Verification
class Buggy;
int data[10];
task transmit;
fork
for (i=0; i<10; i++) // i is not declared here
send(data[i]);
join_none
endtask
endclass
int i; // program-level i
Buggy b;
event receive;
initial begin
b = new;
for (i=0; i<10; i++) b.data[i] = i;
b.transmit;
The solution is to declare all your variables in the smallest scope that
encloses all uses of the variable. In Example 4-17, declare the index variables
inside the for loops, not at the program or scope level.
class BusTran;
bit [31:0] addr, crc, data[8];
Statistics stats;
endclass class Statistics;
time startT, stopT;
static int ntrans = 0;
static time total_elapsed_time;
endclass
function new();
stats = new(); // Make instance of stats
endfunction
task create_packet();
// Fill packet with data
stats.start();
// Transmit packet
endtask
endclass
Just as you may want to split up classes that are too big, you
should also have a lower limit on how small a class should be.
A class with just one or two members makes the code harder to
understand as it adds an extra layer of hierarchy and forces you
to constantly jump back and forth between the parent class and
all the children to understand what it does. In addition, look at how often it is
Chapter 4: Basic OOP 87
used. If a small class is only instantiated once, you might want to merge it into
the parent class.
One Synopsys customer put each transaction variable into its own class for
fine control of randomization. The transaction had a separate object for the
address, CRC, data, etc. In the end, this approach only made the class hierar-
chy more complex. On the next project they flattened the hierarchy.
See section 8.5 for more ideas on partitioning classes.
Sometimes you need to compile a class that includes another class that is
not yet defined. The declaration of the included class’s handle causes an error,
as the compiler does not recognize the new type. Declare the class name with
a typedef statement, as shown below.
Example 4-20 Using a typedef class statement
typedef class Statistics; // Define lower level class
class BusTran;
Statistics stats; // Use Statistics class
...
endclass
What happens when you pass an object into a routine? Perhaps the routine
only needs to read the values in the object, such as transmit above. Or, your
routine may modify the object, like a routine to create a packet. Either way,
when you call the routine, you pass a handle to the object, not the object itself.
Figure 4-4 Handles and objects across routines
task generator;
BusTran b;
b = new;
transmit(b);
endtask BusTransaction
In Figure 4-4, the generator task has just called transmit. There are
two handles, generator.b and transmit.btrans, that both refer to the
same object.
When you call a routine with a scalar variable (nonarray, nonobject) and
use the ref keyword, SystemVerilog passes the address of the scalar, so the
routine can modify it. If you don’t use ref, SystemVerilog copies the scalar’s
value into the argument variable, so any changes to the argument don’t affect
the original value.
Example 4-21 Passing objects
// Transmit a packet onto a 32-bit bus
task transmit(BusTran bt);
CBbus.rx_data <= bt.data;
bt.timestamp = $time;
...
endtask
BusTran b;
initial begin
b = new(); // Allocate the object
b.addr = 42; // Initialize values
transmit(b); // Pass object to task
end
Chapter 4: Basic OOP 89
In Example 4-21, the initial block allocates a BusTran object and calls the
transmit task with the handle that points to the object. Using this handle,
transmit can read and write values in the object. However, if transmit
tries to modify the handle, the result won’t be seen in the initial block, as the
bt argument was not declared as ref.
A routine can modify an object, even if the handle argument
does not have a ref modifier. This frequently causes confu-
sion for new users, as they mix up the handle with the object.
As shown above, transmit can write a timestamp into the
object. If you don’t want an object modified in a routine, pass a copy of it so
that the original data is untouched. See section 4.15 for more on copying
objects.
BusTran b;
initial begin
create_packet(b); // Call bad routine
$display(b.addr); // Fails because b=null
end
What are the symptoms of this mistake? The code above creates only one
BusTran, so every time through the loop, generator_bad changes the
object at the same time it is being transmitted. When you run this, the $dis-
play shows many addr values, but all transmitted BusTrans have the same
value of addr. The bug occurs if transmit stores the object and keeps using
it even after transmit returns. If your transmit task does not keep a refer-
ence to the object, you can recycle the same object over and over.
You need to create a new BusTran during each pass through the loop.
Example 4-25 Good generator creates many objects
task generator_good(int n);
BusTran b;
repeat (n) begin
b = new(); // Create one new object
b.addr = $random(); // Initialize variables
$display("Sending addr=%h", b.addr);
transmit(p); // Send it into the DUT
end
endtask
Chapter 4: Basic OOP 91
As you write testbenches, you need to be able to store and reference many
objects. You can make arrays of handles, each of which refers to an object.
Example 4-26 shows that storing of ten bus transactions in an array.
Example 4-26 Using an array of handles
task generator();
BusTran barray[10];
foreach (barray[i])
begin
barray[i] = new(); // Construct each object
transmit(barray[i]);
end
endtask
The array barray is made of handles, not objects. So you need to con-
struct each object in the array before using it, just as you would for a normal
handle. There is no way to call new on an entire array of handles
Using new to copy an object is easy and reliable. A new object is con-
structed and all variables from the existing object are copied.
92 SystemVerilog for Verification
function new;
stats = new;
id = count++;
endfunction
endclass
The initial block creates the first BusTran object and modifies a variable
in the contained object Statistics.
Chapter 4: Basic OOP 93
src id=3
stats startT=42
dst
When you call new to make a copy, the BusTran object is copied, but not
the Statistics one. This is because when you use new to copy an object, it
does not call your own new function. Instead, the values of variables and han-
dles are copied. So now both BusTran objects point to the same
Statistics object and both have the same id.
Figure 4-6 Objects and handles after copy with new
src id=3
stats startT=42
id=3
dst
stats
If you have a simple class that does not contain any references to other
classes, writing a copy function is easy.
Example 4-29 Simple class with copy function
class BusTran;
bit [31:0] addr, crc, data[8];
For nontrivial classes, you should always create your own copy function.
You can make it a deep copy by calling the copy functions of all the contained
objects. Your own copy function makes sure all your user fields (such as an
ID) remain consistent. The downside of making your own copy function is
that you need to keep it up to date as you add new variables – forget one and
you could spend hours debugging to find the missing value.7
Example 4-31 Complex class with deep copy function
class BusTran;
bit [31:0] addr, crc, data[8];
Statistics stats;
static int count = 0;
int id;
function new;
stats = new;
id = count++;
endfunction
7.
Perhaps the next version of SystemVerilog may include a deep object copy. However, this still does just
a copy, so your constructor (new function) won’t be called, and fields such as ID will not be updated.
Chapter 4: Basic OOP 95
Note that you also need to write a copy for the Statistics class, and
every other class in the hierarchy.
Figure 4-7 Objects and handles after deep copy
src id=3
stats startT=42
id=4
dst startT=96
stats
Test
Generator
Environment Functional Coverage
DUT
task run;
forever begin
// Get transaction from upstream block
...
// Do some processing
...
// Send it to downstream block
...
end
endtask
endclass
4.19 Conclusion
Using Object Oriented Programming is a big step, especially if your first
computer language was Verilog. The payoff is that your testbenches are more
modular and thus easier to develop, debug, and reuse.
Have patience — your first OOP testbench may look more like Verilog
with a few classes added. But as you get the hang of this new way of thinking,
you begin to create and manipulate classes for both transactions and the trans-
actors in the testbench that manipulate them.
In Chapter 8 you will learn more OOP techniques so your test can change
the behavior of the underlying testbench without having to change any of the
existing code.
Chapter 5
Testbench
spec, while you, the verification engineer, have to find ways to prove the
design does not match the spec.
Likewise, your testbench code is in a separate block from design code. In
classic Verilog, each goes in a separate module. However, using a module to
hold the testbench often causes timing problems around driving and sampling,
so SystemVerilog introduces the program block to separate the testbench,
both logically and temporally. For more details, see section 5.4.
As designs grow in complexity, the connections between the blocks
increase. Two RTL blocks may share dozens of signals, which must be listed
in the correct order for them to communicate properly. One mismatched or
misplaced connection and the design will not work. If it is a subtle bug, such
as swapping pins that only toggle occasionally, you may not notice the prob-
lem for some time. Worse yet is when you add a new signal between two
blocks. You have to edit not only the blocks to add the new port but also the
higher-level netlists that wire up the devices. Again, one wrong connection at
any level and the design stops working. Or worse, the system only works
intermittently!
The solution is the interface, the SystemVerilog construct that represents a
bundle of wires, with intelligence such as synchronization, and functional
code. An interface can be instantiated like a module but also connected to
ports like a signal.
Testbench
request[1:0] grant[1:0]
reset clk
Arbiter
Chapter 5: Connecting the Testbench and Design 101
The testbench is a module with ports. A small piece of the test is shown.
Example 5-2 Testbench using ports
module test (input logic [1:0] grant,
output logic [1:0] request,
output logic reset,
input logic clk);
initial begin
@(posedge clk) request <= 2'b01;
$display("@%0d: Drove req=01", $time);
repeat (2) @(posedge clk);
if (grant != 2'b01)
$display("@%0d: a1: grant != 2'b01", $time);
...
$finish;
end
endmodule
102 SystemVerilog for Verification
In Example 5-3, the netlists are simple, but real designs with hundreds of
pins require pages of signal and port declarations. All these connections can
be error prone. As a signal moves through several layers of hierarchy, it has to
be declared and connected over and over. Worst of all, if you just want to add
a new signal, it has to be declared and connected in multiple files. SystemVer-
ilog interfaces can help in each of these cases.
arb_if arbif(clk);
arb a1 (arbif);
test t1(arbif);
endmodule : top
@(posedge arbif.clk);
arbif.request <= 2'b01;
$display("@%0d: Drove req=01", $time);
repeat (2) @(posedge arbif.clk);
if (arbif.grant != 2'b01)
$display("@%0d: a1: grant != 2'b01", $time);
$finish;
end
endmodule : test
Lastly is the device under test, the arbiter, that uses an interface instead of
ports.
Example 5-7 Arbiter using a simple interface
module arb (arb_if arbif);
...
always @(posedge arbif.clk or posedge arbif.reset)
begin
if (arbif.reset)
arbif.grant <= 2'b00;
else
arbif.grant <= next_grant;
...
end
endmodule
You can see an immediate benefit, even on this small device: the connec-
tions become cleaner and less prone to mistakes. If you wanted to put a new
signal in an interface, you would just have to add it to the interface definition
and the modules that actually used it. You would not have to change any mod-
ule such as top that just pass the interface through. This language feature
greatly reduces the chance for wiring errors.
Example 5-8 connects the original arbiter from Example 5-1 to the interface
in Example 5-4.
Example 5-8 Connecting an interface to a module that uses ports
module top;
bit clk;
always #5 clk = ~clk;
arb_if arbif(clk);
arb_port a1 (.grant (arbif.grant),
.request (arbif.request),
.reset (arbif.reset),
.clk (arbif.clk));
test t1(arbif);
endmodule : top
endinterface
Here are the arbiter model and testbench, which need to specify the mod-
port in their port connection list. Note that you put the modport name, DUT or
TEST, after the interface name, arb_if. Other than the modport name, these
are identical to the previous examples.
106 SystemVerilog for Verification
The top model does not change from Example 5-5, as modports are speci-
fied in the module header, not when the module is instantiated.
While the code didn’t change much (except that the interface grew larger),
this interface more accurately represents the real design, especially the signal
direction.
are still in one central location, reducing the chance for making an
error.
You must now use the interface name in addition to the signal name,
possibly making the modules more verbose.
If you are connecting two design blocks with a unique protocol that
will not be reused, interfaces may be more work than just wiring
together the ports.
It is difficult to connect two different interfaces. A new interface
(bus_if) may contain all the signals of an existing one (arb_if),
plus new signals (address, data, etc.). But since interfaces cannot be
hierarchical, you have to break out the individual signals and drive
them appropriately.
An interface can contain multiple clocking blocks, one per clock domain,
as there is single clock expression in each block. Typical clock expressions
are @(posedge clk) or @(posedge clk1 or negedge clk2).
You can specify a clock skew in the clocking block using the default-
statement, but the default behavior is that input signals are sampled just before
the design executes, and the outputs are driven back into the design during the
current time slot. The next section provides more details on the timing
between the design and testbench.
Once you have defined a clocking block, your testbench can wait for the
clocking expression with @my_interface.cb rather than having to spell out
the exact clock and edge. Now if you change the clock or edge in the clocking
block, you do not have to change your testbench.
In Example 5-13, the clocking block cb declares that the signals in the
block are active on the positive edge of the clock. The signal directions are
relative to the modport where they are used. So request is an output of the
TEST modport, and grant is an input to the modport.
Example 5-13 Interface with a clocking block
interface arb_if(input bit clk);
logic [1:0] grant, request;
logic reset;
for nonblocking assignments, PLI execution, etc., but they can be ignored for
the purposes of this book. See the IEEE 1800-2005 Language Reference Man-
ual for more details.
Figure 5-4 Main regions inside a SystemVerilog time step
Active
(design) Loop back
if more events
Observed
(assertions)
Reactive
(testbench)
To next
time slot
Name Activity
Name Activity
endprogram : test
Section 5.5 explains more about the driving and sampling of interface
signals.
As discussed in section 3.7.1, you should always declare your
program block as automatic so that it behaves more like the
routines in stack-based languages you may have worked with,
such as C.
activity. So you get the output values just before the clock changes. The test-
bench outputs are synchronous by virtue of the clocking block, so they flow
directly into the design. The program block, running in the Reactive region,
retriggers the Active region during the same time slot. If you have a design
background, you can remember this by imagining that the clocking block
inserts a synchronizer between the design and testbench.
Figure 5-5 A clocking block synchronizes the DUT and testbench
Testbench
test test
out
in Design out d q
in
Under Test clk
at a clock edge (time 30ns), that value does not propagate to the testbench for
another cycle (time 40ns).
Figure 5-6 Sampling a synchronous interface
clk
DUT arb.grant 1 2 2 3
TEST arbif.cb.grant 1 2 2 3
10 20 30 40
initial begin
arbif.cb.request <= 2'b01;
$display("@%0d: Drove req=01", $time);
repeat (2) @arbif.cb;
if (arbif.cb.grant != 2'b01)
$display("@%0d: a1: grant != 2'b01", $time);
end
endprogram : test
8.
There are some cases where you could use a blocking assignment, such as the force
statement. The LRM is not clear about how a program block can force an interface signal or
a signal in the design. Additionally, if an interface signal is passed through a ref argument
into a routine, should the routine use a blocking or nonblocking assignment? SystemVerilog
is an evolving language.
Chapter 5: Connecting the Testbench and Design 117
in the Reactive region while design code is in the Active region. If your test-
bench drives arbif.cb.request at 100ns, the same time as arbif.cb
(which is @(posedge clk) according to the clocking block), request
changes in the design at 100ns. But if your testbench tries to drive
arbif.cb.request at time 101ns, between clock edges, the change does
not propagate until the next clock edge. In this way, your drives are always
synchronous. In Example 5-17, arbif.grant is driven by a module and can
use a blocking assignment.
If the testbench drives the synchronous interface signal at the active edge
of the clock, the value propagates immediately to the design. This is because
the default output delay is #0 for a clocking block. If the testbench drives the
output just after the active edge, the value is not seen in the design until the
next active edge of the clock.
Example 5-19 Interface signal drive
busif.cb.request <= 1; // Nonblocking sync drive
busif.cb.cmd <= cmd_buf; // Nonblocking sync drive
clk
TEST arb.cb.request 3 2 2 1
DUT arbif.request 3 1 2 1
10 20 30 40
If you want to wait for two clock cycles before driving a signal, you can
either use “repeat (2) @bus.cb;” or use the cycle delay ##2. This delay
only works as a prefix to a drive of a signal in a clocking block, as it need to
know which clock to use for the delay.
Example 5-21 Interface signal drive
##2 arbif.cb.request <= 0; // Wait 2 cycles then assign
##3; // Illegal - must be used with an assignment
initial begin
mif.cb.data <= 'z; // Tri-state the bus
@mif.cb;
$displayh(mif.cb.data); // Read from the bus
@mif.cb;
mif.cb.data <= 7’h5a; // Drive the bus
@mif.cb;
mif.cb.data <= 'z; // Release the bus
end
endprogram
would have to explicitly call $exit to signal that the program block
completed.
But don’t despair. If you really need an always block, you can use
“initial forever” to accomplish the same thing.
initial
forever @(posedge clk)
out_sig <= ~out_sig;
endprogram
The program block is not the place to put a clock generator. Example 5-23
tries to put the generator in a program block but just causes a race condition.
The clk and out_sig signals both propagate from the Reactive region to the
design in the Active region and could cause a race condition depending on
which one arrived first.
Avoid race conditions by always putting the clock generator in a module.
If you want to randomize the generator’s properties, create a class with
random variables for skew, frequency, and other characteristics. You can use
this class in the generator module, or in the testbench.
Chapter 5: Connecting the Testbench and Design 121
arb_if arbif(.*);
arb a1 (.*);
test t1(.*);
endmodule : top
not truly global as the parameter cannot be seen during compilation of other
files.
This leads to some confusion. Some tools, such as Synopsys VCS, com-
pile all the SystemVerilog code together, so $unit is global. But Synopsys
Design Compiler compiles a single module or group of modules at a time, so
$unit may be just the contents of one or a few files. Tools from other ven-
dors may compile all files or just a subset at once. As a result, $unit is not
portable.
This book calls the scope outside blocks the “top-level scope.” You can
define variables, parameters, data types and even routines in this space.
Example 5-26 declares a top-level parameter, TIMEOUT, that can be used any-
where in the hierarchy. This example also has a const string that holds an
error message. You can declare top-level constants either way.
Example 5-26 Top-level scope for arbiter design
// root.sv
`timescale 1ns/1ns
parameter int TIMEOUT = 1_000_000;
const string time_out_msg = "ERROR: Time out";
module top;
test t1(.*);
endmodule
module top;
bit clk;
test t1(.*);
endmodule
// Relative reference
$display("clk=%b", t1.clk)
endprogram
If the grant signal is asserted correctly, the test continues. If the signal
does not have the expected value, the simulator produces a message similar to
the following.
Chapter 5: Connecting the Testbench and Design 125
This message says that on line 7 of the file test.sv, the assertion
top.t1.a1 started at 55ns to check the signal bus.cb.grant, but it failed
immediately.
You may be tempted to use the full SystemVerilog Assertion
syntax to check an elaborate sequence over a range of time, but
use care. Assertions are declarative code, and execute very dif-
ferently than the surrounding procedural code. In just a few
lines of assertions, you can verify complex code; the equivalent
procedural code would be far more complicated and verbose.
If grant does not have the expected value, you’ll see an error message.
Example 5-32 Error from failed procedural assertion
"test.sv", 7: top.t1.a1: started at 55ns failed at 55ns
Offending '(bus.cb.grant == 2’b1)‘
property request_2state;
@(posedge clk) disable iff (reset);
$isunknown(request) == 0;
endproperty
assert_request_2state: assert property request_2state
endinterface
Testbench
Rx0 Tx0
4x4
Rx1 ATM Tx1
router
Rx2 Tx2
Rx3 Tx3
Example 5-37 shows the top of the testbench module. Once again, note
that the ports and wires take up the majority of the netlist.
130 SystemVerilog for Verification
initial begin
// Reset the device
Chapter 5: Connecting the Testbench and Design 131
rst <= 1;
Rx_data_0 <= 0;
...
end
endmodule
You just saw three pages of code, and it was all just connectivity — no
testbench, no design! Interfaces provide a better way to organize all this infor-
mation and eliminate the repetitive parts that are so error prone.
Testbench
Rx Tx
4x4 ATM
router
initial begin
// Reset the device
rst <= 1;
Rx0.cb.data <= 0;
...
receive_cell0();
...
end
task receive_cell0();
@(Tx0.cb);
Tx0.cb.clav <= 1; // Assert ready to receive
wait (Tx0.cb.soc == 1); // Wait for Start of Cell
bytes[i] = Tx0.cb.data;
@(Tx0.cb);
Tx0.cb.clav <= 0; // Deassert flow control
end
endtask : receive_cell0
endprogram : test
5.11 Conclusion
In this chapter you have learned how to use SystemVerilog’s interfaces to
organize the communication between design blocks and your testbench. With
this design construct, you can replace dozens of signal connections with a sin-
gle interface, making your code easier to maintain and improve, and reducing
the number of wiring mistakes.
SystemVerilog also introduces the program block to hold your testbench
and to reduce race conditions between the device under test and the testbench.
With a clocking block in an interface, your testbenches will drive and sample
design signals correctly relative to the clock.
Chapter 6
Randomization
6.1 Introduction
There are many ways to use randomization, and this chapter gives a wide
range of examples. It highlights the most useful techniques, but you should
choose what works best for you.
When you think of randomizing the stimulus to a design, the first thing
you may think of are the data fields. These are the easiest to create – just call
$random. The problem is that this approach has a very low payback in terms
of bugs found: you only find data-path bugs, perhaps with bit-level mistakes.
The test is still inherently directed. The challenging bugs are in the control
logic. As a result, you need to randomize all decision points in your DUT.
Everywhere control paths diverge, randomization increases the probability
that you’ll take a different path in each test case.
You need to think broadly about all design input such as the following.
Device configuration
Environment configuration
Primary input data
Encapsulated input data
Protocol exceptions
Delays
Transaction status
Errors and violations
What is the most common reason why bugs are missed during testing of
the RTL design? Not enough different configurations have been tried! Most
tests just use the design as it comes out of reset, or apply a fixed set of initial-
ization vectors to put it into a known state. This is like testing a PC’s
operating system right after it has been installed, and without any applica-
tions; of course the performance is fine, and there are no crashes.
Over time, in a real world environment, the DUT’s configuration becomes
more and more random. For example, a Synopsys customer had to verify a
time-division multiplexor switch that had 600 input channels and 12 output
channels. When the device was installed in the end-customer’s system, chan-
nels would be allocated and deallocated over and over, so at any point in time,
Chapter 6: Randomization 137
This is what you probably thought of first when you read about random
stimulus: take a transaction such as a bus write or ATM cell and fill it with
some random values. How hard can that be? Actually it is fairly straightfor-
ward as long as you carefully prepare your transaction classes. You should
anticipate any layered protocols and error injection.
Anything that can go wrong, will, eventually. The most challenging part of
design and verification is how to handle errors in the system. You need to
anticipate all the cases where things can go wrong, inject them into the sys-
tem, and make sure the design handles them gracefully, without locking up or
going into an illegal state. A good verification engineer tests the behavior of
the design to the edge of the functional specification and sometimes even
beyond
When two devices communicate, what happens if the transfer stops part-
way through? Can your testbench simulate these breaks? If there are error
detection and correction fields, you must make sure all combinations are tried.
The random component of these errors is that your testbench should be
able to send functionally correct stimuli and then, with the flip of a configura-
tion bit, start injecting random types of errors at random intervals.
6.2.6 Delays
can create constraints to limit the random values to legal values, or to test spe-
cific features.
Note that you can randomize individual variables, but this case is the least
interesting. True constrained-random stimuli is created at the transaction
level, not one value at a time.
Example 6-1 shows a class with random variables, constraints, plus test-
bench code to use this class.
Example 6-1 Simple random class
class Packet;
// The random variables
rand bit [31:0] src, dst, data[8];
randc bit [7:0] kind;
// Limit the values for src
constraint c {src > 10;
src < 15;}
endclass
Packet p;
initial begin
p = new; // Create a packet
assert (p.randomize());
transmit(p);
end
This class has four random variables. The first three use the rand modi-
fier, so that every time you randomize the class, the variables are assigned a
value. Think of rolling dice: each roll could be a new value or repeat the cur-
rent one. The kind variable is randc, which means random cyclic, so that the
random solver does not repeat a random value until every possible value has
been assigned. Think of dealing cards from a deck: you deal out every card in
the deck in random order, then shuffle the deck, and deal out the cards in a
different order.
Note that the constraint expression is grouped using curly braces: {}. This
is because this code is declarative, not procedural, which uses begin...end.
The randomize function returns 0 if a problem is found with the con-
straints. The procedural assertion is used to check the result, as shown in
section 5.9. You need find the tool-specific switches to cause the assertion to
terminate simulation. This book uses assert to test the result from
randomize, but you may want to test the result and then call your special
140 SystemVerilog for Verification
routine that prints any useful information and then gracefully shuts down then
simulation.
The constraint in Example 6-1 is an expression that limits the values for
the src variable. In this case, SystemVerilog chooses between the values of
11, 12, 13, or 14.
All variables in your classes should be random and public. This
gives your test the maximum control over the DUT’s stimulus
and control. You can always turn off a random variable, as
show in section 6.10.2. If you forget to make a variable ran-
dom, you must edit the environment, which you want to avoid.
Useful stimulus is more than just random values — there are relationships
between the variables. Otherwise, it may take too long to generate interesting
stimulus values, or the stimulus might contain illegal values. You define these
interactions in SystemVerilog using constraint blocks that contain one or
more constraint expressions. SystemVerilog solves these expressions concur-
rently, choosing random values that satisfy all the expressions.
At least one variable in each expression should be random,
either rand or randc. The following class fails when ran-
domized. The solution is to add the modifier rand or randc
to the variable son.
Example 6-2 Constraint without random variables
class bad;
bit [31:0] son; // Error – should be rand or randc
constraint c_teenager {son > 12;
son < 20;}
endclass
The following sections use this example of a random class with con-
straints. The specific constructs are explained later in this section.
142 SystemVerilog for Verification
constraint c_stim {
len < 1000;
len > 0;
src inside {0, [2:10], [100:107]};
if (congestion_test) {
dst inside {[CONGEST_ADDR-100:CONGEST_ADDR+100]};
}
}
endclass
Example 6-3 shows a class with a constraint block, with several expres-
sions. The first two control the values for the len variable. As shown above, a
variable can be used in multiple expressions.
There can be a maximum of only one relational operator (<,
<=, ==, >=, or >) in an expression. If you want to put multi-
ple variables in a fixed order, such as a, b, and c, use
multiple expressions.
Example 6-4 Constrain variables to be in a fixed order
class bad;
rand bit [15:0] a, b, c;
constraint good {0 < a; // Correct way
a < b;
b < c;}
constraint bad {0 < a < b < c;} // Error, won’t work!
endclass
You can create sets of values with the inside operator. SystemVerilog
gathers all the values and chooses between the values with equal probability,
unless you have other constraints on the variable. As always, you can use
variables in the sets. In Example 6-3, the first two expressions could be
replaced with len inside {[1:999]}.
Example 6-5 Random sets of values
rand int c; // Random variable
int lo, hi; // Nonrandom variables used as limits
constraint c_range {
c inside {[lo:hi]}; // lo <= c and c <= hi
}
All values in the set are chosen equally, even if they appear multiple times.
Example 6-7 Inverted random set constraint
constraint c_even_weight {
(c inside {0,1,1,1,1,1}); // 0 or 1, equal probability
}
If you need to weight some values more than others, use the dist opera-
tor, shown in section 6.4.6.
144 SystemVerilog for Verification
You can choose from a set of values by storing them in an array. Example
6-8 chooses a day of the week from a list of enumerated values. You can
change the list of choices on the fly. If you make choice a randc variable,
the simulator tries every possible value before repeating.
The name function returns a string with the name of an enumerated value.
Example 6-8 Choosing from an array of possible values
class Days;
typedef enum {SUN, MON, TUE, WED,
THU, FRI, SAT} DAYS_E;
DAYS_E choices[$];
rand DAYS_E choice;
constraint cday {choice inside choices;}
endclass
Days days;
initial begin
days = new;
If you want to dynamically add or remove values from a set, think twice
before using the inside operator because of its performance. For example,
perhaps you have a set of values that you want to be chosen just once. You
could use inside to choose values from a queue, and delete them to slowly
shrink the queue. This requires the solver to solve N constraints, where N is
the number of elements left in the queue. Instead, use a randc variable that
points into an array of choices. Choosing a randc value takes a short, con-
stant time, while solving a large number of constraints is very expensive,
especially for more than a few dozen values.
Chapter 6: Randomization 145
initial begin
RandcInside ri;
ri = new(’{1,3,5,7,9,11,13});
repeat (ri.array.size) begin
assert(ri.randomize());
$display("Picked %2d [%0d]", ri.pick(), ri.index);
end
end
constraint c_len {
len dist {BYTE := w_byte, // Choose a random
WORD := w_word, // length using
LWRD := w_lwrd}; // variable weights
}
endclass
Chapter 6: Randomization 147
In Example 6-11, the len enumerated variable has three values. The con-
straint defaults to choosing longword lengths, as w_lwrd has the largest
value.
By now you may have realized that constraint blocks are not procedural
code, executing from top to bottom. They are declarative code, all active at
the same time. If you constrain a variable with the inside operator with the
set [10:50] and have another expression that constrains the variable to be
greater than 20, SystemVerilog only chooses values between 21 and 50.
SystemVerilog constraints are bidirectional, which means that the solver
looks at the constraints on both side of an expression. Consider the following
constraint:
Example 6-12 Bidirectional constraint
rand logic [15:0] b, c, d;
constraint c_bidir {
b < d;
c == b;
d < 30;
c > 25;
}
Solution b c d
1 26 26 27
2 27 27 28
3 28 28 29
supports byte, word, and longword reads, but only longword writes. System-
Verilog supports two implication operators, -> and if-else.
When you are choosing from a list of expressions, such as an enumerated
type, the implication operator, ->, lets you create a case-like block. The
parentheses around the expression are not required, but do make the code eas-
ier to read.
Example 6-13 Constraint block with implication operator
class BusOp;
...
constraint c_io {
(io_space_mode) ->
addr[31] == 1’b1;
}
Whenever you deal with random values, you need to understand the prob-
ability of the outcome. SystemVerilog does not guarantee the exact solution
found by the random constraint solver, but you can influence the distribution.
Any time you work with random numbers, you have to look at thousands or
millions of values to average out the noise. Changing the tool version or ran-
dom seed can cause different results. Some simulators, such as Synopsys
VCS, have multiple solvers to allow you to trade memory usage vs.
performance.
6.5.1 Unconstrained
There are eight possible solutions. Because there are no constraints, each
has the same probability. You have to run thousands of randomizations to see
the actual results approach the listed probabilities.9
150 SystemVerilog for Verification
Solution x y Probability
A 0 0 1/8
B 0 1 1/8
C 0 2 1/8
D 0 3 1/8
E 1 0 1/8
F 1 1 1/8
G 1 2 1/8
H 1 3 1/8
6.5.2 Implication
Here are the possible solutions and probability. You can see that the ran-
dom solver recognizes that there are eight combinations of x and y, but all the
solutions where x==0 (A–D) have been merged together.
9.
The tables were generated with Synopsys VCS 2005.06 using the run-time switch
+ntb_solver_mode=1.
Chapter 6: Randomization 151
Solution x y Probability
A 0 0 1/2
B 0 1 0
C 0 2 0
D 0 3 0
E 1 0 1/8
F 1 1 1/8
G 1 2 1/8
H 1 3 1/8
Note that the implication operator says that when x==0, y is forced to 0,
but when y==0, there is no constraint on x. However, implication is bidirec-
tional in that if y were forced to a nonzero value, x would have to be 1.
Example 6-19 has the constraint y>0, so x can never be 0.
Example 6-19 Class with implication and constraint
class Imp2;
rand bit x; // 0 or 1
rand bit [1:0] y; // 0, 1, 2, or 3
constraint c_xy {
y > 0;
(x==0) -> y==0;
}
endclass
152 SystemVerilog for Verification
Solution x y Probability
A 0 0 0
B 0 1 0
C 0 2 0
D 0 3 0
E 1 0 0
F 1 1 1/3
G 1 2 1/3
H 1 3 1/3
The solve...before constraint does not change the solution space, just
the probability of the results. The solver chooses values of x (0, 1) with equal
probability. In 1000 calls to randomize, x is 0 about 500 times, and 1 about
500 times. When x is 0, y must be 0. When x is 1, y can be 0, 1, 2, or 3 with
equal probability.
Chapter 6: Randomization 153
Solution x y Probability
A 0 0 1/2
B 0 1 0
C 0 2 0
D 0 3 0
E 1 0 1/8
F 1 1 1/8
G 1 2 1/8
H 1 3 1/8
But, if you use the constraint solve y before x, you get a very differ-
ent distribution.
Solution x y Probability
A 0 0 1/8
B 0 1 0
C 0 2 0
D 0 3 0
E 1 0 1/8
F 1 1 1/4
G 1 2 1/4
H 1 3 1/4
A class can contain multiple constraint blocks. Your class might naturally
divide into two sets of variables, such as data vs. control, so you may want to
constrain them separately. Or you might want to have a separate constraint for
each test. Perhaps one constraint would restrict the data length to create small
transactions (great for testing congestion), while another would make long
transactions.
At run-time, you can use the constraint_mode() routine to turn con-
straints on and off. When used with handle.constraint, this method
controls a single constraint. When used with just handle, it controls all con-
straints for an object.
Example 6-21 Using constraint_mode
class Packet;
rand int length;
constraint c_short {length inside {[1:32]}; }
constraint c_long {length inside {[1000:1023]}; }
endclass
Packet p;
initial begin
p = new;
transmit(p);
constraint valid_RMW_LWRD {
(opc == RMW) -> length == LWRD;
}
endclass
Now you know the bus transaction obeys the rule. Later, if you want to
violate the rule, use constraint_mode to turn off this one constraint. You
should have a naming convention to make these constraints stand out, such as
using the prefix valid as shown above.
As you write more tests, you can end up with many constraints. They can
interact with each other in unexpected ways, and the extra code to enable and
disable them adds to the test complexity. Additionally, constantly adding and
editing constraints to a class could cause problems in a team environment.
Many tests only randomize objects at one place in the code. SystemVer-
ilog allows you to add an extra constraint using randomize() with. This is
equivalent to adding an extra constraint to any existing ones in effect. Exam-
ple 6-23 shows a base class with constraints, then two randomize() with
statements.
156 SystemVerilog for Verification
Transaction t = new();
initial begin
int s;
t = new();
driveBus(t);
driveBus(t);
end
The extra constraints are added to the existing ones in effect. Use
constraint_mode if you need to disable a conflicting constraint. Note that
inside the with{} statement, SystemVerilog uses the scope of the class. That
is why Example 6-23 used just addr, not t.addr.
A common mistake is to surround your in-line constraints
with parenthesis instead of curly braces {}. Just remember
that constraint blocks use curly braces, so your in-line con-
straint must use them too. Braces are for declarative code.
function does not return a value, but, because it is not a task, does not con-
sume time. If you want to call a debug routine from pre_randomize or
post_randomize, it must be a function.
Sum is a
Left bathtub Right
Exponential Exponential
0 Values WIDTH
Example 6-24 Building a bathtub distribution
class Bathtub;
int value; // Random variable with bathtub dist
int WIDTH = 50, DEPTH=4, seed=1;
endclass
158 SystemVerilog for Verification
Every time this object is randomized, the variable value gets updated.
Across many randomizations, you will see the desired nonlinear distribution.
You can use all the Verilog-1995 distribution functions this way, plus sev-
eral that are new for SystemVerilog. Some of the useful functions include the
following.
How can you create constrained-random tests that can be easily modified?
There are several tricks you can use.
Most constraint examples in this book use constants to make them more
readable. In Example 6-25, size is randomized over a range that uses a vari-
able for the upper bound.
Chapter 6: Randomization 159
By default, this class creates random sizes between 1 and 100, but by
changing the variable max_size, you can vary the upper limit.
You can use variables in the dist constraint to turn on and off values and
ranges. In Example 6-26, each bus command has a different weight variable.
Example 6-26 dist constraint with variable weights
typedef enum (READ8, READ16, READ32) read_t;
class ReadCommands;
rand read_t read_cmd;
int read8_wt=1, read16_wt=1, read32_wt=1;
constraint c_read {
read_cmd dist {READ8 := read8_wt,
READ16 := read16_wt,
READ32 := read32_wt};
}
endclass
If you have a set of constraints that produces stimulus that is almost what
you want, but not quite, you could call randomize, and then set a variable to
the value you want – you don’t have to use the random one. However, your
stimulus values may not be correct according to the constraints you created to
check validity.
If there are just a few variables that you want to override, use rand_mode
to make them nonrandom.
160 SystemVerilog for Verification
Packet p;
initial begin
p = new();
If you randomize an object and then modify some variables, you can check
that the object is still valid by checking if all constraints are still obeyed. Call
Chapter 6: Randomization 161
A simple testbench may use a data class with just a few constraints. What
if you want to have two tests with very different flavors of data? You could
use the implication operators (-> or if-else) to build a single, elaborate
constraint controlled by nonrandom variables.
Example 6-28 Using the implication constraint as a case statement
class Instruction;
typedef enum {NOP, HALT, CLR, NOT} OPCODE_T;
rand OPCODE_T opcode;
bit [1:0] n_operands;
...
constraint c_operands {
if (n_operands == 0)
opcode == NOP || opcode == HALT;
else if (n_operands == 1)
opcode == CLR || opcode == NOT;
...
}
endclass
You can see that having one large constraint can quickly get out of control
as you add further expressions for each operand, addressing modes, etc. A
more modular approach is to use a separate constraint for each flavor of
instruction, and then disable all but the one you need.
While the constraints are simpler with this approach, the process of turn-
ing them on and off is more complex. For example, when you turn off all
constraints that create data, you are also disabling all the ones that check the
data’s validity.
162 SystemVerilog for Verification
Instruction instr;
initial begin
instr = new;
put it in a routine in a separate file and then call it as needed. But at that point
it has become nearly the same as an external constraint.
The body of a constraint does not have to be defined within the class, just
as a routine body can be defined externally, as shown in section 4.11. Your
data class could be defined in one file, with one empty constraint. Then each
test could define its own version of this constraint to generate its own flavors
of stimulus.
Example 6-30 Class with an external constraint
// packet.sv
class Packet;
rand bit [7:0] length;
rand bit [7:0] payload[];
constraint c_valid {length > 0;
payload.size == length;}
constraint c_external;
endclass
Example 6-31 Program defining external constraint
// test.sv
program test;
constraint Packet::c_external {length == 1;}
...
endprogram
In Chapter 8, you will learn how to extend a class. With this, you can take
a testbench that uses a given class, and swap in an extended class that has
additional or redefined constraints, routines, and variables. Learning OOP
techniques requires a little more study, but the flexibility of this new approach
repays with great rewards.
You may be comfortable with procedural code, but writing constraints and
understanding random distributions requires a new way of thinking. Here are
some issues you may encounter when trying to create random stimulus.
When creating a testbench, you may be tempted to use the int, byte, or
other signed types for counters and other simple variables. Don’t use them in
random constraints unless you really want signed values. What values are pro-
duced when the class in Example 6-32 is randomized? It has two random
variables and wants to make the sum of them 64.
Example 6-32 Signed variables cause randomization problems
class SignedVars;
rand byte pkt1_len, pk2_len;
constraint total_len {
pkt1_len + pk2_len == 64;
}
endclass
Obviously, you could get pairs of values such as (32, 32) and (2, 62). But
you could also see (-64, 128), as this is a legitimate solution of the equation,
even though it may not be what you wanted. To avoid meaningless values
such as negative lengths, use only unsigned random variables.
Example 6-33 Randomizing unsigned 32-bit variables
class Vars32;
rand logic [31:0] pkt1_len, pk2_len; // unsigned type
constraint total_len {
pkt1_len + pk2_len == 64;
}
endclass
Chapter 6: Randomization 165
The constraints presented so far allow you to specify limits on scalar vari-
ables. What if you want to randomize an array? The foreach statement and
several array functions let you shape the distribution of the values.
Using the foreach constraint creates many constraints and
slow down simulation. A good solver can quickly solve hun-
dreds of constraints but may slow down with thousands.
Especially slow are nested foreach constraints, as they pro-
duce N2 constraints for an array of size N. See section 6.12.5
for an algorithm that used randc variables instead of nested foreach.
The easiest array constraint to understand is the size function. You are
specifying the number of elements in a dynamic array or queue.
Example 6-35 Constraining dynamic array size
class dyn_size;
rand reg [31:0] d[];
constraint d_size {d.size inside {[1:10]}; }
endclass
Using the inside constraint lets you set a lower and upper boundary on
the array size. In many cases you may not want an empty array, that is,
size==0. Remember to specify an upper limit; otherwise, you can end up
166 SystemVerilog for Verification
with thousands or millions of elements, which can cause the random solver to
take an excessive amount of time.
You can send a random array of data into a design, but you can also use it
to control the flow. Perhaps you have an interface that has to transfer four data
words. The words can be sent consecutively or over many cycles. A strobe
signal tells when the data is valid. Here are some legitimate strobe patterns,
sending four values over ten cycles.
Figure 6-2 Random strobe waveforms
You can create these patterns using a random array. Constrain it to have
four bits enabled out of the entire range using the sum function.
Example 6-36 Random strobe pattern class
parameter MAX_TRANSFER_LEN = 10;
class StrobePat;
rand bit strobe[MAX_TRANSFER_LEN];
constraint c_set_four { strobe.sum == 3’h4; }
endclass
Chapter 6: Randomization 167
sp = new();
assert (sp.randomize);
The sum function looks simple but can cause several problems because of
Verilog’s arithmetic rules.
Start with a simple concept. You want to generate from one to eight trans-
actions, such that the total length of all of them is less than 1024 bytes. Here is
a first attempt. The len field is a byte in the original transaction.
Example 6-38 First attempt at sum constraint: bad_sum1
class bad_sum1;
rand byte len[];
constraint c_len {len.sum < 1024;
len.size inside {[1:8]};}
function void display;
$write("size=%d, sum=%4d ", len.size, len.sum);
foreach(len[i]) $write("%4d ", len[i]);
$display;
endfunction
endclass
168 SystemVerilog for Verification
This generates some smaller lengths, but the sum is sometimes negative
and is always less than 127 — definitely not what you wanted! Try again, this
time with an unsigned field. (The display function is unchanged.)
Example 6-41 Second attempt at sum constraint: bad_sum2
class bad_sum2;
rand bit [7:0] len[]; // 8 bits
constraint c_len {len.sum < 1024;
len.size inside {[1:8]};}
endclass
Example 6-42 Output from bad_sum2
len: sum= 79, val= 88 100 246 2 14 228 169
len: sum= 120, val= 74 75 141 86
len: sum= 39, val= 39
len: sum= 193, val= 31 156 172 33 57
len: sum= 173, val= 59 150 25 101 138 212
Example 6-42 has a subtle problem: the sum of all transaction lengths is
always less than 256, even though you constrained the array sum to be less
than 1024. The problem here is that in Verilog, the sum of many 8-bit values
is computed using an 8-bit result. Bump the len field up to 32 bits using the
uint type from Chapter 2.
Chapter 6: Randomization 169
Wow – what happened here? This is similar to the signed problem in sec-
tion 6.11.1, in that the sum of two very large numbers can wrap around to a
small number. You need to limit the size based on the comparison in the
constraint.
Example 6-45 Fourth attempt at sum_constraint: bad_sum4
class bad_sum4;
rand bit [9:0] len[]; // 10 bits
constraint c_len {len.sum < 1024;
len.size inside {[1:8]};}
endclass
Example 6-46 Output from bad_sum4
len: sum= 989, val= 787 202
len: sum=1021, val= 564 76 132 235 0 8 6
len: sum= 872, val= 624 101 136 11
len: sum= 978, val= 890 88
len: sum= 905, val= 663 242
This does not work either as the individual len field fields are more than 8
bits, so the len values are often greater than 255. You need to specify that
each len field is between 1 and 255, but use a 9-bit field so they sum cor-
rectly. This requires constraining every element of the array.
170 SystemVerilog for Verification
The addition of the constraint for individual elements fixed the example.
Note that the len array can be 10 or more bits wide, but must be unsigned.
You can specify constraints between array elements as long as you are
careful about the endpoints. The following class creates an ascending list of
values by comparing each element to the previous, except for the first.
Example 6-49 Creating ascending array values with foreach
class Ascend;
rand uint d[10];
constraint c {
foreach (d[i]) // For every element
if (i>0) // except the first
d[i] > d[i-1]; // compare with previous element
}
endclass
How complex can these constraints become? Constraints have been writ-
ten to solve Einstein’s problem (a logic puzzle with five people, each with
five separate attributes), the Eight Queens problem (place eight queens on a
chess board so that none can capture each other), and Sudoku.
Chapter 6: Randomization 171
How can you create an array of random values that is unique? For exam-
ple, you may need to assign ID numbers to N bus drivers, which are in the
range of 0 to MAX-1 where MAX >=N.
Your may be tempted to use a constraint with nested foreach specifying
a[i] != a[j]. The SystemVerilog solver expands out these equations, so
an array of 30 values creates almost 1000 constraints.
Instead, try procedural code in a post_randomize function that uses a
randc variable. You have to put the randc variable in a helper class so that
you can randomize the same variable over and over.
Example 6-50 UniqueArray class
// Generate a random array of unique values
class UniqueArray;
int max_array_size, max_value;
rand bit [7:0] a[]; // Array of unique values
constraint c_size {a.size inside {[1:max_array_size]};}
Up until now, you have seen atomic random transactions. You have
learned how to make a single random bus transaction, a single network
packet, or a single processor instruction. But your job is to verify that the
design works with real-world stimuli. A bus may have long sequences of
transactions such as DMA transfers or cache fills. Network traffic consists of
extended sequences of packets as you simultaneously read e-mail, browse a
web page, and download music from the net, all in parallel. Processors have
deep pipelines that are filled with the code for routine calls, for loops, and
interrupt handlers. Generating transactions one at a time is unlikely to mimic
any of these scenarios.
Chapter 6: Randomization 173
6.13.2 Randsequence
endsequence
end // for
end
task cfg_read_task;
...
endtask
174 SystemVerilog for Verification
At this point you may be thinking that this process is a great way to create
long streams of random input into your design. Or you may think that this is a
lot of work if all you want to do is occasionally to make a random decision in
your code. You may prefer a set of procedural statements that you can step
through using a debugger.
LenDist lenD;
initial begin
lenD = new;
assert (lenD.randomize());
$display("Chose len=%0d", lenD.len);
end
Code using randcase is more difficult to override and modify than ran-
dom constraints. The only way to modify the random results is to rewrite the
code or use variable weights.
Be careful using randcase, as it does not leave any tracks behind. For
example, you could use it to decide whether or not to inject an error in a trans-
action. The problem is that the downstream transactors and scoreboard need
to know of this choice. The best way to inform them would be to use a vari-
able in the transaction or environment. But if you are going to create a
variable that is part of these classes, you could have made it a random variable
and used constraints to change its behavior in different tests.
You can use randcase when you need to create a decision tree. Example
6-56 has just two levels of procedural code, but you can see how it can be
extended to use more.
Chapter 6: Randomization 177
// Level 2
task do_one_write;
randcase
mem_write_wt: do_mem_write();
io_write_wt: do_io_write();
cfg_write_wt: do_cfg_write();
endcase
endtask
task do_one_read;
randcase
mem_read_wt: do_mem_read();
io_read_wt: do_io_read();
cfg_read_wt: do_cfg_read();
endcase
endtask
Verilog uses a simple PRNG that you could access with the $random
function. The generator has an internal state that you can set by providing a
seed to $random. All IEEE-1364-compliant Verilog simulators use the same
algorithm to calculate values.
178 SystemVerilog for Verification
Example 6-57 shows a simple PRNG, not the one used by SystemVerilog.
The PRNG has a 32-bit state. To calculate the next random value, square the
state to produce a 64-bit value, take the middle 32 bits, then add the original
value.
Example 6-57 Simple pseudorandom number generator
reg [31:0] state = 32’h12345678;
function reg [31:0] my_random;
reg [63:0] s64;
s64 = state * state;
state = (s64 >> 16) + state;
my_random = state;
endfunction
You can see how this simple code produces a stream of values that seem
random, but can be repeated by using the same seed value. SystemVerilog
calls its PRNG for a new values for randomize and randcase.
Verilog has a single PRNG that is used for the entire simulation. What
would happen if SystemVerilog kept this approach? Testbenches often have
several stimulus generators running in parallel, creating data for the design
under test. If two streams share the same PRNG, they each get a subset of the
random values.
Figure 6-3 Sharing a single random generator
PRNG
a b,c
d e,f
g h,i
class Gen1; class Gen2;
Transaction tr; Transaction tr1, tr2;
In Figure 6-3, there are two stimulus generators and a single PRNG pro-
ducing values a, b, c, etc. Gen2 has two random objects, so during every
cycle, it uses twice as many random values as Gen1. A problem can occur
Chapter 6: Randomization 179
when one of the classes changes. Gen1 gets an additional random variable,
and so consumes two random values every time it is called.
Figure 6-4 First generator uses additional values
PRNG
a,b c,d
e,f g,h
i,j k,l
This approach changes the values used not only by Gen1, but also by
Gen2.
In SystemVerilog, there is a separate PRNG for every object and thread.
Changes to one object don’t affect the random values seen by others.
Figure 6-5 Separate random generators per object
PRNG 1 PRNG 2
a,b m,n
c,d o,p
e,f q,r
Every object and thread have its own PRNG and unique seed. When a new
object or thread is started, its PRNG is seeded from its parent’s PRNG. Thus a
180 SystemVerilog for Verification
single seed specified at the start of simulation can create many streams of ran-
dom stimulus, each distinct.
function new;
cfg = new; // Construct the cfg
endfunction
task run;
// Start the testbench transactors
endtask
task wrapup;
// Not currently used
endtask
enclass : Environment
Now you have all the components to build a test, which is described in a
program block. The test instantiates the environment class and then runs each
step.
182 SystemVerilog for Verification
initial begin
env = new; // Construct environment
env.gen_cfg; // Create random configuration
env.build; // Build the testbench environment
env.run; // Run the test
env.wrapup; // Clean up after test & report
end
endprogram
You may want to override the random configuration, perhaps to reach a
corner case. The following test randomizes the configuration class and then
enables all the ports.
Example 6-61 Simple test that overrides random configuration
program test;
Environment env;
initial begin
env = new; // Construct environment
env.gen_cfg; // Create random configuration
6.17 Conclusion
Constrained-random tests are the only practical way to generate the stimu-
lus needed to verify a complex design. SystemVerilog offers many ways to
create a random stimulus and this chapter presents many of the alternatives.
A test needs to be flexible, allowing you either to use the values generated
by default or to constrain or override the values so that you can reach your
goals. Always plan ahead when creating your testbench by leaving sufficient
“hooks” so that you can steer the testbench from the test without modifying
existing code.
Chapter 7
Generator Environment
DUT
The SystemVerilog scheduler is the traffic cop that chooses which thread
runs next. You can use the techniques in this chapter to control the threads and
thus your testbench.
Each of these threads communicates with its neighbors. In Figure 7-1, the
generator passes the stimulus to the agent. The environment class needs to
know when the generator completes and then tell the rest of the testbench
threads to terminate. This is done with interprocess communication constructs
such as the standard Verilog events, event control and wait constructs, and
the SystemVerilog mailboxes and semaphores.10
184 SystemVerilog for Verification
10.
The SystemVerilog LRM uses “thread” and “process” interchangeably. The term “process” is most com-
monly associated with Unix processes, in which each contains a program running in its own memory
space. Threads are lightweight processes that may share common code and memory, and consume far
less resources than a typical process. This book uses the term “thread.” However, “interprocess commu-
nication” is such a common term that it is used in this book.
Chapter 7: Threads and Interprocesses Communication 185
Note in the output below that code in the fork...join executes in parallel,
so statements with shorter delays execute before those with longer delays. The
fork...join completes after the last statement, which starts with #50.
Example 7-2 Output from begin...end and fork...join
@0: start fork...join example
@10: sequential after #10
@10: parallel start
@20: parallel after #10
@40: sequential after #30
@50: sequential after #10
@60: parallel after #50
@60: after join
@140: final after #80
186 SystemVerilog for Verification
The diagram for this block is similar to Figure 7-3. Note that the statement
after the join_none block executes before any statement inside the
fork...join_none.
Example 7-4 Fork...join_none output
@0: start fork...join_none example
@10: sequential after #10
@10: after join_none
@10: parallel start
@20: parallel after #10
@40: sequential after #30
@50: sequential after #10
@60: parallel after #50
@90: final after #80
fork
repeat (n) begin
p = new;
if (!p.randomize) begin
$display("Packet randomize failed");
$finish;
end
transmit(p);
end
join_none
endtask
endclass
Generator gen;
initial begin
gen = new;
gen.run(10);
// Start the checker, monitor, and other threads
end
There are several points you should notice with Example 7-7.
First, the transactor is not started in the new task. The
constructor should just initialize values, not start any threads.
Separating the constructor from the code that does the real
work allows you to change any variables before you start
executing the code in the object. This allows you to inject errors, modify the
defaults, and alter the behavior of the object.
Next, the run task starts a thread in a fork...join_none block. The thread
is an implementation detail of the transactor and should be spawned there, not
in the parent class.
Transaction tr;
initial
repeat (10)
begin
// Create a random transaction
tr = new;
if (!tr.randomize) $finish;
When the wait_for_tr task is called, it spawns off a thread to watch the
bus for the matching transaction address. During a normal simulation, many
of these threads run concurrently. In this simple example, the thread just
prints a message, but you could add more elaborate controls.
190 SystemVerilog for Verification
The #0 delay blocks the current thread and reschedules it to start later
during the current time slot. In Example 7-10, the delay makes the current
Chapter 7: Threads and Interprocesses Communication 191
thread run after the threads spawned in the fork...join statement. This delay
is useful for blocking a thread, but you should be careful, as excessive use
causes race conditions and unexpected results.
You should use automatic variables inside a fork...join statement to
save a copy of a variable as shown in Example 7-11.
Example 7-11 Automatic variables in a fork...join_none
initial begin
for (int j=0; j<3; j++)
fork
automatic int k = j; // Make copy of index
$write(k); // Print copy
join_none
#0 $display;
end
begin
// Wait for response, or some maximum delay
fork : timeout_block
wait (bus.cb.addr != tr.addr);
#TIME_OUT $display("@%0d: Error: timeout", $time);
join_any
disable timeout_block;
join_none
endtask
Example 7-15 is the more robust version of Example 7-14, with disable
with a label that explicitly names the threads that you want to stop.
Example 7-15 Using disable label to stop threads
initial begin
wait_for_tr(tr0); // Spawn thread 0
begin : threads_1_2
wait_for_tr(tr1); // Spawn thread 1
wait_for_tr(tr2); // Spawn thread 2
end
7.4 Events
A Verilog event synchronizes threads. It is similar to a phone, where one
person waits for a call from another person. In Verilog a thread waits for an
event with the @ operator. This operator is edge sensitive, so it always blocks,
waiting for the event to change. Another thread triggers the event with the ->
operator, unblocking the first thread. SystemVerilog enhances the Verilog
event in several ways.
First, an event is now a handle to a synchronization object that can be
passed around to routines. This feature allows you to share events across
objects without having to make the events global. So you can give a phone
number to an object to be called later.
There is always the possibility of a race condition in Verilog where one
thread blocks on an event at the same time another triggers it. If the triggering
thread executes before the blocking thread, the trigger is missed.
SystemVerilog introduces the triggered function that lets you check
whether an event has been triggered, including during the current time-slot. A
thread can wait on this function instead of blocking with the @ operator.
initial begin
$display("@%0d: 2: before trigger", $time);
-> e2;
@e1;
$display("@%0d: 2: after trigger", $time);
end
196 SystemVerilog for Verification
initial begin
$display("\n@%0d: 1: before trigger", $time);
-> e1;
wait (e2.triggered);
$display("@%0d: 1: after trigger", $time);
end
initial begin
$display("@%0d: 2: before trigger", $time);
-> e2;
wait (e1.triggered);
$display("@%0d: 2: after trigger", $time);
end
When you run this code, one initial block starts, triggers its event, and then
blocks on the other event. The second block starts, triggers its event (waking
up the first) and then blocks on the first event.
Example 7-20 Output from waiting for an event
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
@0: 2: after trigger
task run;
fork
begin
... // Create transactions
-> done; // Tell the test we are done
end
join_none
endtask
endclass
initial begin
gen = new(gen_done); // Instantiate testbench
gen.run; // Run transactor
wait(gen_done.triggered); // Wait for finish
end
endprogram
initial begin
foreach (gen[i]) begin
gen[i] = new; // Create N generators
gen[i].run; // Start them running
end
Another way to solve this problem is to keep track of the number of events
that have triggered.
Example 7-23 Waiting for multiple threads by counting triggers
event done[N_GENERATORS];
initial begin
foreach (gen[i]) begin
gen[i] = new; // Create N generators
gen[i].run; // Start them running
end
That was slightly less complicated. Why not get rid of all the events and
just wait on a count of the number of running generators? This count can be a
static variable in the Generator class. Note that most of the thread
manipulation code has been replaced with a single wait construct.
The last block in Example 7-24 waits for the count using the handle
gen[0]. Any handle to an object gives you access to the static variables.
Example 7-24 Waiting for multiple threads using a thread count
class Generator;
static int thread_count = 0;
task run;
thread_count++; // Start another thread
fork
begin
// Do the real work in here
// And when done, decrement the thread count
thread_count--;
end
join_none
endtask
endclass
Generator gen[N_GENERATORS];
initial begin
// Create N generators
foreach (gen[i])
gen[i] = new;
7.5 Semaphores
A semaphore allows you to control access to a resource. Imagine that you
and your spouse share a car. Obviously, only one person can drive it at a time.
You can manage this situation by agreeing that whoever has the key can drive
it. When you are done with the car, you will give up the car so that the other
200 SystemVerilog for Verification
person can use it. The key is the semaphore that makes sure only one person
has access to the car. In operating system terminology, this is known as
“mutually exclusive access,” so a semaphore is known as a mutex and is used
to control access to a resource.
Semaphores can be used in a testbench when you have a resource, such as
a bus, that may have multiple requestors from inside the testbench but, as part
of the physical design, can only have one driver. In SystemVerilog, a thread
that requests a key when one is not available always blocks. Multiple blocking
threads are queued in FIFO order.
task sequencer;
repeat($urandom%10) // Random wait, 0-9 cycles
@bus.cb;
sendTrans; // Execute the transaction
endtask
task sendTrans;
sem.get(1); // Get the key to the bus
@bus.cb; // Drive signals onto bus
bus.cb.addr <= t.addr;
...
sem.put(1); // Put it back when done
endtask
endprogram
Chapter 7: Threads and Interprocesses Communication 201
7.6 Mailboxes
How do you pass information between two threads? Perhaps your
generator needs to create many transactions and pass them to a driver. You
might be tempted to just have the generator thread call a task in the driver. But
then the generator needs to know the hierarchical path to the driver task,
making your code less reusable. Additionally, this style forces the generator
to run at the same speed as the driver, that can cause synchronization
problems if one generator needs to control multiple drivers.
Think of your generator and driver as transactors that are
autonomous objects that communicate through a channel. Each
object gets a transaction from an upstream object (or creates it,
as in the case of a generator), does some processing, and then
passes it to a downstream object. The channel must allow its
driver and receiver to operate asynchronously. You may be tempted to just
use a shared array or queue, but it can be difficult to create code that reads,
writes, and blocks between threads.
The solution is a SystemVerilog mailbox. From a hardware point of view,
the easiest way to think about a mailbox is that it is just a FIFO, with a source
and sink. The source puts data into the mailbox, and the sink gets values from
the mailbox. Mailboxes can have a maximum size or can be unlimited. When
the source puts a value into a sized mailbox that is full, it blocks until data is
removed. Likewise, if a sink tries to remove data from a mailbox that is
empty, it blocks until data is put into the mailbox.
202 SystemVerilog for Verification
generator
mailbox
driver
class Generator;
Transaction tr;
mailbox mbx;
task run;
repeat (10) begin
tr = new;
assert(tr.randomize);
mbx.put(tr); // Send out transaction
end
endtask
endclass
class Driver;
Transaction tr;
mailbox mbx;
task run;
repeat (10) begin
mbx.get(tr); // Fetch next transaction
@(posedge busif.cb.ack);
bus.cb.kind <= tr.kind;
...
end
endtask
endclass
drv = new(mbx);
fork
gen.run(); // Spawn the generator
drv.run(); // Spawn the driver
join
end
endprogram
initial begin
mbx = new(1); // Size = 1
fork
// Producer
for (int i=1; i<4; i++) begin
$display("@%0d: Producer: putting %0d", $time, i);
mbx.put(i);
$display("@%0d: Producer: put(%0d) done %0d",
$time, i);
end
// Consumer
repeat(3) begin
int j;
#1ns mbx.get(j);
$display("@%0d: Consumer: got %0d", $time, j);
end
join_any
end
endprogram
Chapter 7: Threads and Interprocesses Communication 205
Example 7-27 creates the smallest mailbox which stores a single message.
The Producer thread tries to put three messages (integers) in the mailbox,
while the Consumer thread slowly gets messages every 1ns. As Example 7-28
shows, the first put succeeds, then the Producer tries put(2) which blocks.
The Consumer wakes up, gets a message 1 from the mailbox, so now the
Producer can finish putting the message 2.
Example 7-28 Output from bounded mailbox
@0: Producer: before put(1)
@0: Producer: put(1) done
@0: Producer: before put(2)
@1: Consumer: got 1
@1: Producer: put(2) done
@1: Producer: before put(3)
@2: Consumer: got 2
@2: Producer: put(3) done
The bounded mailbox acts as a buffer between the two processes. You can
see how the Producer gets ahead of the Consumer.
mailbox mbx;
Producer p;
Consumer c;
initial begin
// Construct mailbox, producer, consumer
mbx = new;
p = new;
c = new;
event handshake;
class Producer;
task run;
for (int i=1; i<4; i++) begin
$display("Producer: before put(%0d)", i);
mbx.put(i);
@handshake;
$display("Producer: after put(%0d)", i);
end
endtask
endclass
208 SystemVerilog for Verification
Now the Producer does not advance until the Consumer triggers the event.
Example 7-34 Output from producer–consumer with event
Producer: before put(1)
Consumer: after get(1)
Producer: after put(1)
Producer: before put(2)
Consumer: after get(2)
Producer: after put(2)
Producer: before put(3)
Consumer: after get(3)
Producer: after put(3)
You can see that the Producer and Consumer are in lockstep.
class Consumer;
task run;
int i;
repeat (3) begin
$display("Consumer: before get");
mbx.get(i);
$display("Consumer: after get(%0d)", i);
rtn.put(-i);
end
endtask
endclass
endprogram
The return message in the rtn mailbox is just a negative version of the
original integer.
210 SystemVerilog for Verification
function build;
// Empty for now
endfunction
task run;
forever begin
// Get transaction from upstream block
gen2agt.get(tr);
// Do some processing
task wrapup;
// Empty for now
endtask
endclass
Generator gen;
Agent agt;
Driver drv;
Monitor mon;
Checker chk;
Scoreboard scb;
Config cfg;
mailbox gen2agt, agt2drv, mon2chk;
function Environment::new;
// Initialize mailboxes
gen2agt = new;
agt2drv = new;
mon2chk = new;
// Initialize transactors
gen = new(gen2agt);
agt = new(gen2agt, agt2drv);
drv = new(agt2drv);
mon = new(mon2chk);
chk = new(mon2chk);
scb = new;
cfg = new;
endfunction
chk.build;
scb.build;
endfunction
task Environment::run;
fork
gen.run(run_for_n_trans);
agt.run;
drv.run;
mon.run;
chk.run;
scb.run(run_for_n_trans);
join
endtask
task Environment::wrapup;
fork
gen.wrapup;
agt.wrapup;
drv.wrapup;
mon.wrapup;
chk.wrapup;
scb.wrapup;
join
endtask
Environment env;
initial begin
env = new;
env.gen_cfg;
env.build;
env.run;
env.wrapup;
end
endprogram
214 SystemVerilog for Verification
7.8 Conclusion
Your design is modeled as many independent blocks running in parallel,
so your testbench must also generate multiple stimulus streams and check the
responses using parallel threads. These are organized into a layered testbench,
orchestrated by the top-level environment. SystemVerilog introduces
powerful constructs such as fork...join_none and fork...join_any for
dynamically creating new threads, in addition to the standard fork...join.
These threads communicate and synchronize using events, semaphore,
mailboxes, and the classic @ event control and wait statement. Lastly, the
disable command is used to terminate threads.
These threads and the related control constructs complement the dynamic
nature of OOP. As objects are created and destroyed, they can run in
independent threads, allowing you to build a powerful and flexible testbench
environment.
Chapter 8
Test
Environment
Generator
Driver
DUT
A diagram for the class shows both the variables and routines.
Figure 8-2 Base Transaction class diagram
crc
data[7]
endclass : BadTr
Note that in Example 8-2, the variable crc is used without a hierarchical
identifier. The BadTr class can see all the variables from the original
Transaction plus its own variables such as bad_crc. The calc_crc
function in the extended class calls calc_crc in the base class using the
super prefix. You can call one level up, but going across multiple levels such
218 SystemVerilog for Verification
crc data[7]
display()
task main;
Transaction tr;
forever begin
// Get transaction from upstream generator
gen2drv.get(tr);
This class stimulates the DUT with Transaction objects. OOP rules say
that if you have a handle of the base type (Transaction), it can also point to
an object of an extended type (BadTr). This is because the handle tr can only
reference the variables src, dst, crc, and data, and the routine calc_crc.
So you can send BadTr objects into the driver without changing it.
When the driver calls tr.calc_crc, SystemVerilog looks at the type of
object stored in tr, because the task was declared as virtual. If the object is of
type Transaction, SystemVerilog calls Transaction::calc_crc. If it is
of type BadTr, SystemVerilog calls BadTr::calc_crc.
task run;
forever begin
tr = new; // Construct transaction
assert(tr.randomize); // Randomize it
gen2drv.put(tr); // Send to driver
end
endtask
Generated
stream
The beauty of this technique is that if you change the blueprint object,
your factory creates a different-type object. Using the sign analogy, you
change the cutting die from a square to a triangle to make Yield signs.
222 SystemVerilog for Verification
Generated
stream
The blueprint is the “hook” that allows you to change the behavior of the
generator class without having to change the class’s code.
Here is the generator class using the factory pattern. The important thing
to notice is that the blueprint object is constructed in one place (the build
task) and used in another (the run task). Previous coding guidelines said to
separate the declaration and construction; similarly, you need to separate the
construction and randomization of the blueprint object.
Example 8-6 Generator class using factory pattern
class Generator;
mailbox gen2drv;
Transaction blueprint;
function build;
blueprint = new;
endfunction
task run;
Transaction tr;
forever begin
assert(blueprint.randomize);
tr = blueprint.copy; // * see below
gen2drv.put(tr); // Send to driver
end
endtask
endclass
The copy function is discussed again in section 8.6. For now, remember
that you must add it to the Transaction and BadTr classes.
Chapter 8: Advanced OOP and Guidelines 223
function new;
gen = new(gen2drv);
drv = new(gen2drv);
endfunction
function build;
gen.build;
drv.build;
endfunction
task run;
fork
gen.run;
drv.run;
join_none
endtask
task wrapup;
gen.wrapup;
drv.wrapup;
endtask
endclass
Environment env;
initial begin
env = new; // Construct the environment
env.build; // Build testbench objects
env.run; // Run the test
env.wrap_up; // Clean up afterwards
end
endprogram
Environment env;
initial begin
env = new;
env.build; // Construct a blueprint
begin
BadTr bad; // Create a bad transaction
bad = new; // Replace blueprint with
env.drv.tr = bad; // the "bad" one
end
Transaction tr;
BadTr bad, b2;
When a class is extended, all the variables and routines are inherited, so
the integer src exists in the extended object. The assignment on the second
line is permitted, as any reference using the base handle tr is valid, such as
tr.src and tr.display.
But what if you try going in the opposite direction, copying a base object
into an extended handle, as shown in Example 8-12? This fails because the
base object is missing properties that only exist in the extended class, such as
bad_crc. The SystemVerilog compiler does a static check of the handle
types and will not compile the second line.
Example 8-12 Copying a base handle to an extended handle
tr = new; // Construct base object
bad = tr; // ERROR: WILL NOT COMPILE
$display(bad.bad_crc); // bad_crc is not in base object
When you use $cast as a task, SystemVerilog checks the type of the
source object at run-time and gives an error if it is not compatible. You can
Chapter 8: Advanced OOP and Guidelines 227
eliminate this error by using $cast as a function and checking the result - 0
for incompatible types, and non-0 for compatible types.
initial begin
tr = new;
tr.calc_crc; // Calls Transaction::calc_crc
bad = new;
bad.calc_crc; // Calls BadTr::calc_crc
When you use virtual methods, SystemVerilog uses the type of the object,
not the handle to decide which routine to call. In the final statement, tr points
to an extended object (BadTr) and so BadTr::calc_crc is called.
If you left out the virtual modifier on calc_crc, SystemVerilog
would use the type of the handle (Transaction), not the object. That last
statement would call Transaction::calc_crc – probably not what you
wanted.
The OOP term for multiple routines sharing a common name is
“polymorphism.” It solves a problem similar to what computer architects
faced when trying to make a processor that could address a large address
space but had only a small amount of physical memory. They created the
concept of virtual memory, where the code and data for a program could
reside in memory or on a disk. At compile time, the program didn’t know
where its parts resided — that was all taken care of by the hardware plus
operating system at run-time. A virtual address could be mapped to some
RAM chips, or the swap file on the disk. Programmers no longer needed to
worry about this virtual memory mapping when they wrote code — they just
knew that the processor would find the code and data at run-time. See also
Denning (2005).
8.4.3 Signatures
There is one downside to using virtual routines – once you define one, all
extended classes that define the same virtual routine must use the same
“signature,” i.e., the same number and type of arguments. You cannot add or
remove an argument in an extended virtual routine. This just means you need
to plan ahead.
1. Are there several small classes that you want to combine into a larger
class? For example, you may have a data class and header class and
now want to make a packet class. SystemVerilog does not support
multiple inheritance, where one class derives from several classes at
once. Instead you have to use composition. Alternatively, you could
extend one of the classes to be the new class, and manually add the
information from the others.
2. In Example 8-14, the Transaction and BadTr classes are both bus
transactions that are created in a generator and driven into the DUT.
Thus inheritance makes sense.
3. The lower-level information such as src, dst, and data must
always be present for the Driver to send a transaction.
4. In Example 8-14, the new BadTr class has a new field bad_crc and
the extended calc_crc function. The Generator class just trans-
mits a transaction and does not care about the additional information.
If you use composition to create the error bus transaction, the
Generator class would have to be rewritten to handle the new type.
If two objects seem to be related by both “is-a” and “has-a,” you may need
to break them down into smaller components.
section 4.16 on public vs. private attributes, testbenches are not standard
software development projects. Concepts such as information hiding (using
private variables) conflict with building a testbench that needs maximum
visibility and controllability. Similarly, dividing a transaction into smaller
pieces may cause more problems than it solves.
When you are creating a class to represent a transaction, you may want to
partition it to keep the code more manageable. For example, you may have an
Ethernet MAC frame and your testbench uses two flavors, normal (II) and
Virtual LAN (VLAN). Using composition, you could create a basic cell
EthMacFrame with all the common fields such as da and sa and a
discriminant variable, kind, to indicate the type. There is a second class to
hold the VLAN information, which is included in EthMacFrame.
Example 8-16 Building an Ethernet frame with composition
// Not recommended
class EthMacFrame;
typedef enum {II, IEEE} kind_t;
rand kind_t kind;
rand bit [47:0] da, sa;
rand bit [15:0] len;
...
rand Vlan vlan_h;
endclass
class Vlan;
rand bit [15:0] vlan;
endclass
There are several problems with composition. First, it adds an extra layer
of hierarchy, so you are constantly having to add an extra name to every
reference. The VLAN information is called eth_h.vlan_h.vlan. If you
start adding more layers, the hierarchical names become a burden.
A more subtle issue occurs when you want to instantiate and randomize
the classes. What does the EthMacFrame constructor create? Because kind
is random, you don’t know whether to construct a Vlan object when new is
called. When you randomize the class, the constraints set variables in both the
EthMacFrame and Vlan objects based on the random kind field. However,
randomization only works on objects that have been instantiated. But you
can’t instantiate this object until kind has been chosen.
The only solution to the construction and randomization problems is to
always instantiate all object in EthMacFrame::new. But if you are always
using all alternatives, why divide the Ethernet cell into two different classes?
Chapter 8: Advanced OOP and Guidelines 231
On the downside, a set of classes that use inheritance always requires more
effort to design, build, and debug than a set of classes without inheritance.
Your code must use $cast whenever you have an assignment from a base
handle to an extended. Building a set of virtual routines can be challenging, as
they all have to have the same prototype. If you need an extra argument, you
need to go back and edit the entire set, and possibly the routine calls too.
There are also problems with randomization. How do you make a
constraint that randomly chooses between the two kinds of frame and sets the
proper variables? You can’t put a constraint in EthMacFrame that references
the vlan field.
The final issue is with multiple inheritance. In Figure 8-7, you can see how
the VLAN frame is derived from normal MAC frame. The problem is that
these different standards reconverged. SystemVerilog does not support
multiple inheritance, so you could not create the VLAN / Snap / Control
frame through inheritance.
232 SystemVerilog for Verification
Define the typical behavior and constraints in the class, and then use
inheritance to inject new behavior at the test level.
When you extend the Transaction class to make the class BadTr, the
copy function still has to return a Transaction object. This is because the
extended virtual function must match the base Transaction::copy,
including all arguments and return type.
234 SystemVerilog for Verification
endclass : BadTr
endclass : BadTr
8.7 Callbacks
One of the main guidelines of this book is to create a verification
environment that you can use for all tests with no changes. The key
requirement is that this testbench must provide a “hook” where the test
program can inject new code without modifying the original classes. Your
driver may want to do the following.
Inject errors
Drop the transaction
Delay the transaction
Synchronize this transaction with others
Put the transaction in the scoreboard
Gather functional coverage data
11. This OOP-based callback technique is not related to Verilog PLI callbacks or SVA callbacks.
Chapter 8: Advanced OOP and Guidelines 237
task run;
bit drop;
Transaction tr;
forever begin
agt2drv.get(tr);
foreach (cbs[i]) cbs.pre_tx(tr, drop);
if (!drop) transmit(tr);
endclass
Environment env;
initial begin
env = new;
env.gen_cfg;
env.build;
// Callback injection
begin
Driver_cbs_drop dcd;
dcd = new; // Create scb callback
env.drv.cbs.push_back(dcd); // Put into driver’s Q
end
env.run;
env.wrapup;
end
endprogram
Chapter 8: Advanced OOP and Guidelines 239
endclass
Environment env;
initial begin
env = new;
env.gen_cfg;
env.build;
begin
Driver_cbs_scoreboard dcs;
dcs = new; // Create scb callback
env.drv.cbs.push_back(dcs); // Put into driver’s Q
end
env.run;
env.wrapup;
end
endprogram
8.8 Conclusion
The software concept of inheritance, where new functionality is added to
an existing class, parallels the hardware practice of extending the design’s
features for each generation, while still maintaining backwards compatibility.
For example, you can upgrade your PC by adding a larger capacity disk.
As long as it uses the same interface as the old one, you do not have to replace
any other part of the system, yet the overall functionality is improved.
Likewise, you can create a new test by “upgrading” the existing driver
class to inject errors. If you use an existing callback in the driver, you do not
have to change any of the testbench infrastructure.
You need to plan ahead if you want use these OOP techniques. By using
virtual routines and providing sufficient callback points, your test can modify
the behavior of the testbench without changing its code. The result is a robust
testbench that does not need to anticipate every type of disturbance (error-
injection, delays, synchronization) that you may want as long as you leave a
hook where the test can inject its own behavior. The tests become smaller and
easier to write as the testbench does the hard work of sending stimulus and
checking responses, so the test only has to make small tweaks to cause
specialized behavior.
Chapter 9
Functional Coverage
9.1 Introduction
As designs become more complex, the only effective way to verify them
thoroughly is with constrained-random testing (CRT). This approach elevates
you above the tedium of writing individual directed tests, one for each feature
in the design. However, if your testbench is taking a random walk through the
space of all design states, how do you know if you have reached your destina-
tion? Whether you are using random or directed stimulus, you can gauge
progress using coverage.
Functional coverage is a measure of which design features have been exer-
cised by the tests. Start with the design specification and create a verification
plan with a detailed list of what to test and how. For example, if your design
connects to a bus, your tests need to exercise all the possible interactions
between the design and bus, including relevant design states, delays, and error
modes. The verification plan is a map to show you where to go. For more
information on creating a verification plan, see Bergeron (2006).
Figure 9-1 Coverage convergence
Constrained Many runs,
random tests Many runs,
different seeds
different seeds
Add Directed
Add testcase
constraints Functional
constraints
Coverage
Identify
Minimal code Identify
holes
modifications holes
Use a feedback loop to analyze the coverage results and decide on which
actions to take in order to converge on 100% coverage. Your first choice is to
run existing tests with more seeds; the second is to build new constraints.
Only resort to creating directed tests if absolutely necessary.
Back when you exclusively wrote directed tests, the verification planning
was limited. If the design specification listed 100 features, all you had to do
was write 100 tests. Coverage was implicit in the tests — the “register move”
test moved all combinations of registers back and forth. Measuring progress
242 SystemVerilog for Verification
was easy: if you had completed 50 tests, you were halfway done. This chapter
uses “explicit” and “implicit” to describe how coverage is specified. Explicit
coverage is described directly in the test environment using SystemVerilog
features. Implicit coverage is implied by a test — when the “register move”
directed test passes, you have hopefully covered all register transactions.
With CRT, you are freed from hand crafting every line of input stimulus,
but now you need to write code that tracks the effectiveness of the test with
respect to the verification plan. You are still more productive, as you are
working at a higher level of abstraction. You have moved from tweaking indi-
vidual bits to describing the interesting design states. Reaching for 100%
functional coverage forces you to think more about what you want to observe
and how you can direct the design into those states.
Design Verification
Specification Plan
still being found, you may not be measuring true coverage for some areas of
your design.
Each simulation vendor has its own format for storing coverage data and
as well as its own analysis tools. You need to perform the following actions
with those tools.
– Run a test with multiple seeds. For a given set of constraints (and cov-
erage groups), compile the testbench and design into a single execute-
able. Now you need to run this constraint set over and over with
different random seeds. You can use the Unix system clock as a seed,
but be careful, as your batch system may start multiple jobs simulta-
neously. These jobs may run on different servers or may start on a
single server with multiple processors.
– Check for pass/fail. Functional coverage information is only valid for
a successful simulation. When a simulation fails because there is a
design bug, the coverage information must be discarded. The cover-
age data measures how many items in the verification plan are com-
plete, and this plan is based on the design specification. If the design
does not match the specification, the coverage data is useless. Some
verification teams periodically measure all functional coverage from
scratch so that it reflects the current state of the design.
– Analyze coverage across multiple runs. You need to measure how
successful each constraint set is, over time. If you are not yet getting
100% coverage for the areas that are targeted by the constraints, but
the amount is still growing, run more seeds. If the coverage level has
plateaued, with no recent progress, it is time to modify the con-
straints. Only if you think that reaching the last few test cases for one
particular section may take too long for constrained-random simula-
tion should you consider writing a directed test. Even then, continue
to use random stimulus for the other sections of the design, in case
this “background noise” finds a bug.
The reset logic was accidently left out. A code coverage tool would report
that every line had been exercised, yet the model was not implemented
correctly.
you are performing “design” coverage. For example, the verification plan for
a D-flip flop would mention not only its data storage but also how it resets to
a known state. Until your test checks both these design features, you will not
have 100% functional coverage.
Functional coverage is tied to the design intent and is sometimes called
“specification coverage,” while code coverage measures the design imple-
mentation. Consider what happens if a block of code is missing from the
design. Code coverage cannot catch this mistake, but functional coverage can.
Integration
Bugs/week
Design
review
Tape-out
Time
The bug rate can vary per week based on many factors such as project
phases, recent design changes, blocks being integrated, personnel changes,
and even vacation schedules. Unexpected changes in the rate could signal a
potential problem. As shown in Figure 9-3, it is not uncommon to keep find-
ing bugs even after tape-out, and even after the design ships to customers.
simulated along with the design and testbench, or proven by formal tools.
Sometimes you can write the equivalent check using SystemVerilog proce-
dural code, but many assertions are more easily expressed using
SystemVerilog Assertions (SVA).
Assertions can have local variables and perform simple data checking. If
you need to check a more complex protocol, such as determining whether a
packet successfully went through a router, procedural code is often better
suited for the job. There is a large overlap between sequences that are coded
procedurally or using SVA. See Vijayaraghavan (2005), Cohen (2005), and
Chapters 3 and 7 in the VMM book, Bergeron et al. (2006) for more informa-
tion on SVA.
The most familiar assertions look for errors such as two signals that should
be mutually exclusive or a request that was never followed by a grant. These
error checks should stop the simulation as soon as they detect a problem.
Assertions can also check arbitration algorithms, FIFOs, and other hardware.
These are coded with the assert property statement.
Some assertions might look for interesting signal values or design states,
such as a successful bus transaction. These are coded with the cover
property statement. You can measure how often these assertions are trig-
gered during a test by using assertion coverage. A cover property observes
sequences of signals, while a cover group (described below) samples data val-
ues and transactions during the simulation. These two constructs overlap in
that a cover group can trigger when a sequence completes. Additionally, a
sequence can collect information that can be used by a cover group.
empty the FIFO is? You would still have 1K coverage values. If your test-
bench pushed 100 entries into the FIFO, then pushed in 100 more, do you
really need to know if the FIFO ever had 150 values? Not as long as you can
successfully read out all values.
The corner cases for a FIFO are Full and Empty. If you can make the FIFO
go from Empty (the state after reset) through Full and back down to Empty,
you have covered all the levels in between. Other interesting states involve the
indices as they pass between all 1’s and all 0’s. A coverage report for these
cases is easy to understand.
You may have noticed that the interesting states are independent of the
FIFO size. Once again, look at the information, not the data values.
Design signals with a large range (more than a few dozen possible values)
should be broken down into smaller ranges, plus corner cases. For example,
your DUT may have a 32-bit address bus, but you certainly don’t need to col-
lect 4 billion samples. Check for natural divisions such as memory and IO
space. For a counter, pick a few interesting values, and always try to rollover
counter values from all 1’s back to 0.
constraints and tests to explore new areas. Save test/seed combinations that
give high coverage, so that you can use them in regression testing.
Figure 9-4 Coverage comparison
Functional Coverage
Need more FC
High
Is design complete?
Low
Low High
Code Coverage
What if the functional coverage is high but the code coverage is low? Your
tests are not exercising the full design, and you need to revise your verifica-
tion plan and add more functional coverage points to locate untested
functionality.
A more difficult situation is high code coverage but low functional cover-
age. Even though your testbench is giving the design a good workout, you are
unable to put it in all the interesting states. First, see if the design implements
all the specified functionality. If the functionality is there, but your tests can’t
reach it, you might need a formal verification tool that can extract the design’s
states and create appropriate stimulus.
The goal is both high code and functional coverage. But don’t plan your
vacation yet. What is the trend of the bug rate? Are significant bugs still pop-
ping up? Worse yet, are they being found deliberately, or did your testbench
happen to stumble across a particular combination of states that no one had
anticipated? On the other hand, a low bug rate may mean that your existing
strategies have run out of steam, and you should look into different
approaches. Try different approaches such as new combinations of design
blocks and error generators.
class Transaction;
rand bit [31:0] data;
rand bit [ 2:0] port; // Eight port numbers
endclass
covergroup CovPort;
coverpoint tr.port; // Measure coverage
endgroup
Transaction tr = new;
initial begin
CovPort ck = new; // Instantiate group
A cover group inside a class can sample variables in that class, as well as data
values from embedded classes.
Don’t define the cover group in a data class, such as a transac-
tion, as doing so can cause additional overhead when gathering
coverage data. Imagine you are trying to track how many beers
were consumed by patrons in a pub. Would you try to follow
every bottle as it flowed from the loading dock, over the bar,
and into each person? No, instead you could just have each patron check off
the type and number of beers consumed, as shown in van der Schoot (2006).
In SystemVerilog, you should define cover groups at the appropriate level
of abstraction. This level can be at the boundary between your testbench and
the design, in the transactors that read and write data, in the environment con-
figuration class, or wherever is needed. The sampling of any transaction must
wait until it is actually received by the DUT. If you inject an error in the mid-
dle of a transaction, causing it to be aborted in transmission, you need to
change how you treat it for functional coverage. You need to use a different
cover point that has been created just for error handling.
A class can contain multiple cover groups. This approach allows you to
have separate groups that can be enabled and disabled as needed. Addition-
ally, each group may have a separate trigger, allowing you to gather data from
many sources.
A cover group must be instantiated for it to collect data. If
you forget, no error message about null handles is printed at
run-time, but the coverage report will not contain any men-
tion of the cover group. This rule applies for cover groups
defined either inside or outside of classes.
task main;
forever begin
tr = mbx_in.get; // Get next transaction
ifc.cb.port <= tr.port; // Send into DUT
ifc.cb.data <= tr.data;
CovPort.sample(); // Gather coverage
end
endtask
endclass
data is sampled. And if you need an extra “hook” in the environment for a
callback, you can always add one in an unobtrusive manner, as a callback
only “fires” when the test registers a callback object. You can create many
separate callbacks for each cover group, with little overhead. As explained in
section 8.7.3, callbacks are superior to using a mailbox to connect the test-
bench to the coverage objects. You might need multiple mailboxes to collect
transactions from different points in your testbench. A mailbox requires a
transactor to receive transactions, and multiple mailboxes cause you to juggle
multiple threads. Instead of an active transactor, use a passive callback.
Example 8-25 shows a driver class that has two callback points, before and
after the transaction is transmitted. Example 8-24 shows the base callback
class, and Example 8-26 has a test with an extended callback class that sends
data to a scoreboard. Make your own extension, Driver_cbs_coverage,
of the base callback class, Driver_cbs, to call the sample task for your
cover group in post_tx. Push an instance of the coverage callback class into
the driver’s callback queue, and your coverage code triggers the cover group
at the right time. The following two examples define and use the callback
Driver_cs_coverage.
Example 9-6 Test using functional coverage callback
program automatic test;
Environment env;
initial begin
Driver_cbs_coverage dcc;
env = new;
env.gen_cfg;
env.build;
env.run;
env.wrapup.
end
endprogram
Chapter 9: Functional Coverage 255
The advantage of using an event over calling the sample routine directly
is that you may be able to use an existing event such as one triggered by an
assertion, as shown in Example 9-10.
cover property
(@(posedge sb.clock) sb.write_ena==1)
-> write_event;
endmodule
256 SystemVerilog for Verification
Write_cg wcg;
initial begin
wcg = new;
// Apply stimulus here
sb.write_ena <= 1;
...
#10000 $finish;
end
endprogram
all the groups are combined to give a coverage percentage for all the simula-
tion databases.
This is the status for a single simulation. You need to track coverage over
time. Look for trends so you can see where to run more simulations or add
new constraints or tests. Now you can better predict when verification of the
design will be completed.
The coverage report from VCS shows the two bins. This simulation
achieved 100% coverage because the eight port values were mapped to two
bins. Since both bins have sampled values, your coverage is 100%.
Example 9-12 Report with auto_bin_max set to 2
Bin # hits at least
==================================
auto[0:3] 15 1
auto[4:7] 17 1
==================================
Example 9-11 used auto_bin_max as an option for the cover point only.
You can also use it as an option for the entire group.
Example 9-13 Using auto_bin_max for all cover points
covergroup CovPort;
options.auto_bin_max = 2; // Affects port & data
coverpoint tr.port;
coverpoint tr.data;
endgroup
Transaction tr;
covergroup CovLen;
len16: coverpoint (tr.hdr_len + tr.payload_len);
len32: coverpoint (tr.hdr_len + tr.payload_len + 5’b0);
endgroup
A quick run with 200 transactions showed that the len16 had 100% cov-
erage, but this is across only 16 bins. The cover point len32 had 68%
coverage across 32 bins. Neither of these cover points are correct, as the max-
imum length has a domain of 0:23. The auto-generated bins just don’t work,
as the maximum length is not a power of 2.
After sampling 2000 random transactions, the group has 95.83% coverage.
260 SystemVerilog for Verification
A quick look at the report shows the problem — the length of 23 (17 hex)
was never seen. The longest header is 7, and the longest payload is 15, for a
total of 22, not 23! If you change to the bins declaration to use 0:22, the cov-
erage jumps to 100%. The user-defined bins found a bug in the test.
When you define the bins, you are restricting the values used for coverage
to those that are interesting to you. SystemVerilog no longer automatically
creates bins, and it ignores values that do not fall into a predefined bin. More
importantly, only the bins you create are used to calculate functional cover-
age. You get 100% coverage only as long as you get a hit in every specified
bin.
Values that do not fall into any specified bin are ignored. This
rule is useful if the sampled value, such as transaction length, is
not a power of 2. In general, if you are specifying bins, always
use the default bin specifier to catch values that you may
have forgotten.
262 SystemVerilog for Verification
Alternately, you can use the start and stop functions to control individ-
ual instances of cover groups.
Example 9-20 Using stop and start functions
initial begin
CovPort ck = new; // Instantiate cover group
Here is part of the coverage report from VCS, showing the bins for the
enumerated types.
Chapter 9: Functional Coverage 263
If you want to group multiple values into a single bin, you have to define
your own bins. Any bins outside the enumerated values are ignored unless
you define a bin with the default specifier. When you gather coverage on enu-
merated types, auto_bin_max does not apply.
You can quickly specify multiple transitions using ranges. The expression
(1,2 => 3,4) creates the four transitions (1=>3), (1=>4), (2=>3), and
(2=>4).
You can specify transitions of any length. Note that you have to sample
once for each state in the transition. So (0 => 1 => 2) is different from (0
=> 1 => 1 => 2) or (0 => 1 => 1 => 1 => 2). If you need to repeat
values, as in the last sequence, you can use the shorthand form:(0 => 1[*3]
=> 2). To repeat the value 1 for 3, 4, or 5 times, use 1[*3:5].
auto_bin_max: 0:1, 2:3, 4:5, and 6:7. But then the uppermost bin is elimi-
nated by ignore_bins, so in the end only three bins are created. This cover
point can have coverage of 0%, 33%, 66%, or 100%
cross coverage of a variable with N values, and of another with M values, Sys-
temVerilog needs NxM cross bins to store all the combinations.
Transaction tr;
covergroup CovPort;
kind: coverpoint tr.kind; // Create cover point kind
port: coverpoint tr.port // Create cover point port
cross kind, port; // Cross kind and port
endgroup
If you define bins that contain multiple values, the coverage statistics
change. In the report below, the number of bins has dropped from 128 to 88.
This is because kind has 11 bins: zero, lo, hi_8, hi_9, hi_a, hi_b, hi_c,
hi_d, hi_e, hi_f, and misc. The percentage of coverage jumped from
87.5% to 90.91% because any single value in the lo bin, such as 2, allows
that bin to be marked as covered, even if the other values, 0 or 3, are not seen.
Example 9-31 Cross coverage report with labeled bins
Summary
Coverage: 90.91
Number of Coverpoints Crossed: 2
Coverpoints Crossed: kind port
Number of Expected Cross Bins: 88
Number of Automatically Generated Cross Bins: 80
Automatically Generated Cross Bins
The first ignore_bins just excludes bins where port is 7 and any value
of kind. Since kind is a 4-bit value, this statement excludes 16 bins. The sec-
ond ignore_bins is more selective, ignoring bins where port is 0 and
kind is 9, 10, or 11, for a total of 3 bins.
The ignore_bins can use the bins defined in the individual cover points.
The ignore_bins lo uses bin names to exclude kind.lo that is 1, 2, or 3.
The bins must be names defined at compile-time, such as zero and lo. The
bins hi_8, hi_a,... hi_f, and any automatically generated bins do not have
names that can be used at compile-time in other statements such as
ignore_bins; these names are created at run-time or during the report
generation.
Note that binsof uses parentheses () while intersect specifies a range
and therefore uses curly braces {}.
270 SystemVerilog for Verification
covergroup CrossBinNames;
a: coverpoint tr.a
{ bins a0 = {0};
bins a1 = {1};
option.weight=0;} // Only count cross
b: coverpoint tr.b
{ bins b0 = {0};
bins b1 = {1};
option.weight=0;} // Only count cross
ab: cross a, b
{ bins a0b0 = binsof(a.a0) && binsof(b.b0);
bins a1b0 = binsof(a.a1) && binsof(b.b0);
bins b0 = binsof(b.b0); }
endgroup
Example 9-35 gathers the same cross coverage, but now uses binsof to
specify the cross coverage values.
Example 9-35 Cross coverage with binsof
class Transaction;
rand bit a, b;
endclass
covergroup CrossBinsofIntersect;
a: coverpoint tr.a
{ option.weight=0; } // Only count cross
b: coverpoint tr.b
{ option.weight=0; } // Only count cross
ab: cross a, b
{ bins a0b0 = binsof(a) intersect {0} &&
binsof(b) intersect {0};
bins a1b0 = binsof(a) intersect {1} &&
binsof(b) intersect {0};
bins b1 = binsof(b) intersect {1}; }
endgroup
Use the style in Example 9-34 if you already have bins defined for the
individual cover points and want to use them to build the cross coverage bins.
Use Example 9-35 if you need to build cross coverage bins but have no pre-
defined cover point bins. Use Example 9-36 if you want the tersest format.
This option affects only the report, and may be deprecated in a future ver-
sion of the SystemVerilog IEEE standard.
274 SystemVerilog for Verification
CoverPort cp;
initial
cp = new(5); // lo=0:4, hi=5:7
12. The 12/2005 SystemVerilog LRM mentions a default of both 100% and 90%, but the latter is an error.
Chapter 9: Functional Coverage 275
The problem with this class is that len is not evenly weighted.
276 SystemVerilog for Verification
1400
1200
1000
800
Count
600
400
200
0
0 5 10 15 20 25
Transaction length
If you want to make total length be evenly distributed, use a
solve...before constraint.
Example 9-44 solve...before constraint for transaction length
constraint sb {solve len before hdr_len, payload_len;}
1000
900
800
700
600
Count
500
400
300
200
100
0
0 5 10 15 20 25
Transaction length
approach allows you to check whether you have reached your coverage goals,
and possibly to control a random test.
The most practical use for these functions is to monitor coverage over a
long test. If the coverage level does not advance after a given number of trans-
actions or cycles, the test should stop. Hopefully, another seed or test will
increase the coverage.
While it would be nice to have a test that can perform some sophisticated
actions based on functional coverage results, it is very hard to write this sort
of test. Each test / random seed pair may uncover new functionality, but it
may take many runs to reach a goal. If a test finds that it has not reached 100%
coverage, what should it do? Run for more cycles? How many more? Should
it change the stimulus being generated? How can you correlate a change in the
input with the level of functional coverage? The one reliable thing to change
is the random seed, which you should only do once per simulation. Otherwise,
how can you reproduce a design bug if the stimulus depends on multiple ran-
dom seeds?
You can query the functional coverage statistics if you want to create your
own coverage database. Verification teams have built their own SQL data-
bases that are fed functional coverage data from simulation. This setup allows
them greater control over the data, but requires a lot of work outside of creat-
ing tests.
Some formal verification tools can extract the state of a design and then
create input stimulus to reach all possible states. Don’t try to duplicate this in
your testbench!
9.13 Conclusion
When you switch from writing directed tests, hand-crafting every bit of
stimulus, to constrained-random testing, you might worry that the tests are no
longer under your command. By measuring coverage, especially functional
coverage, you regain control by knowing what features have been tested.
Using functional coverage requires a detailed verification plan and much
time creating the cover groups, analyzing the results, and modifying tests to
create the proper stimulus. But this effort is less than would be required to
write the equivalent directed tests, and the coverage work helps you better
track your progress in verifying your design.
Chapter 10
Advanced Interfaces
10.1 Introduction
In Chapter 5 you learned how to use interfaces to connect the design and
testbench. These physical interfaces represent real signals, similar to the wires
that connected ports in Verilog-1995. The testbench uses these interfaces by
statically connecting to them through ports. However, for many designs, the
testbench needs to connect dynamically to the design.
In a network switch, a single Driver class may connect to many interfaces,
one for each input channel of the DUT. You wouldn’t want to write a unique
Driver for each channel — instead you want to write a generic Driver, instan-
tiate it N times, and have it connect each of the N physical interfaces. You can
do this in SystemVerilog by using a virtual interface that is merely a handle to
a physical interface.
You may need to write a testbench that attaches to several different config-
urations of your design. In one configuration, the pins of the chip may drive a
USB bus, while in another the same pins may drive an I2C serial bus. Once
again, you can use a virtual interface in the testbench so you can decide at run-
time which drivers to use.
A SystemVerilog interface is more than just signals — you can put execut-
able code inside. This might include routines to read and write to the
interface, initial and always blocks that run code inside the interface, and
assertions to constantly check the status of the signals. However, do not put
testbench code in an interface. Program blocks have been created expressly
for building a testbench, including scheduling their execution in the Reactive
region, as described in the LRM.
initial begin
// Reset the device
rst <= 1;
Rx0.cb.data <= 0;
...
receive_cell0;
...
end
task receive_cell0;
@(Tx0.cb);
Tx0.cb.clav <= 1; // Assert ready to receive
wait (Tx0.cb.soc == 1); // Wait for Start of Cell
bytes[i] = Tx0.cb.data;
@(Tx0.cb);
Tx0.cb.clav <= 0; // Deassert flow control
end
endtask
endprogram
Driver drv[4];
Monitor mon[4];
Scoreboard scb[4];
initial begin
foreach (scb[i]) begin
scb[i] = new(i);
drv[i] = new(scb[i].exp_mbx, i, vRx[i]);
mon[i] = new(scb[i].rcv_mbx, i, vTx[i]);
end
...
end
endprogram
The driver class looks similar to the code in Example 10-2, except it uses
the virtual interface name Rx instead of the physical interface Rx0.
Chapter 10: Advanced Interfaces 283
#ac.delay;
284 SystemVerilog for Verification
ac.byte_pack(bytes);
@Rx.cb;
Rx.cb.soc <= 1'bz; // Tristate SOC at end
Rx.cb.clav <= 0;
Rx.cb.data <= 8'bz; // Clear data lines
$display("@%0d: Driver::drive_cell(%0d) finish",
$time, stream_id);
endtask // drive_cell_t
endclass // Driver
always @cb
$strobe("@%0d:%m: out=%0d, in=%0d, ld=%0d, r=%0d",
$time, dout, din, load, reset_l);
module top;
// Clock generator
bit clk;
initial forever #20 clk = ~clk;
// Instantiate N interfaces
X_if xi [NUM_XI] (clk);
endmodule : top
The key line in the testbench is where the local virtual interface array,
vxi, is assigned to point to the array of physical interfaces in the top module,
top.xi. To simplify Example 10-7, the environment class has been merged
with the test, while the generator, agent, and driver layers have been com-
pressed into the driver.
The testbench assumes there is at least one counter and thus at least one X
interface. If your design could have zero counters, you would have to use
dynamic arrays, as fixed-size arrays cannot have a size of zero.
Chapter 10: Advanced Interfaces 287
initial begin
// Connect local virtual interface to top
vxi = top.xi;
// Create N drivers
driver = new[NUM_XI];
foreach (driver[i]) begin
driver[i] = new(vxi[i], i);
driver[i].reset;
end
foreach (driver[i])
driver[i].load;
endprogram
The Driver class uses a single virtual interface to drive and sample sig-
nals from the counter.
288 SystemVerilog for Verification
task reset;
fork
begin
$display("@%0d: %m: Start reset [%0d]",
$time, id);
// Reset the device
xi.reset_l <= 1;
xi.cb.load <= 0;
xi.cb.din <= 0;
@(xi.cb)
xi.reset_l <= 0;
@(xi.cb)
xi.reset_l <= 1;
$display("@%0d: %m: End reset [%0d]",
$time, id);
end
join_none
endtask
task load;
fork
begin
$display("@%0d: %m: Start load [%0d]",
$time, id);
##1 xi.cb.load <= 1;
xi.cb.din <= id + 10;
endclass
// Instantiate N interfaces
X_if xi [NUM_XI] (clk);
...
// Instantiate the testbench
test tb(xi);
endmodule : top
Driver driver[];
virtual X_if vxi[NUM_XI];
initial begin
// Connect the local virtual interfaces to the top
if (NUM_XI <= 0) $finish;
driver = new[NUM_XI];
vxi = xi; // Assign the interface array
endprogram
signals and also routines to perform commands such as a read or write. The
inner workings of these routines are hidden from the external blocks, allowing
you to defer the actual implementation. Access to these routines is controlled
using the modport statement, just as with signals. A task or function is
imported into a modport so that it is then visible to any block that uses the
modport.
These routines can be used by both the design and the testbench. This
approach ensures that both are using the same protocol, eliminating a com-
mon source of testbench bugs.
Assertions in an interface are used to verify the protocol. An assertion can
check for illegal combinations, such as protocol violations and unknown val-
ues. These can display state information and stop simulation immediately so
that you can easily debug the problem. An assertion can also fire when good
transactions occur. Functional coverage code will uses this type of assertion to
trigger the gathering of coverage information.
// Parallel send
task initiatorSend(input bus_cmd_e c,
logic [7:0] a, d);
@(posedge clk);
cmd <= c;
addr <= a;
data <= d;
endtask
// Parallel receive
task targetRcv(output bus_cmd_e c, logic [7:0] a, d);
@(posedge clk);
a = addr;
d = data;
c = cmd;
endtask
endinterface: simple_if
modport TARGET
(input addr, cmd, data,
import task targetRcv (output bus_cmd_e c,
logic [7:0] a, d));
modport INITIATOR
(output addr, cmd, data,
import task initiatorSend(input bus_cmd_e c,
logic [7:0] a, d)
);
// Serial send
task initiatorSend(input bus_cmd_e c,
logic [7:0] a, d);
@(posedge clk);
start <= 1;
cmd <= c;
foreach (a[i]) begin
addr <= a[i];
data <= d[i];
@(posedge clk);
start <= 0;
end
cmd <= IDLE;
endtask
// Serial receive
task targetRcv(output bus_cmd_e c, logic [7:0] a, d);
@(posedge start);
c = cmd;
foreach (a[i]) begin
@(posedge clk);
a[i] = addr;
d[i] = data;
end
endtask
endinterface: simple_if
294 SystemVerilog for Verification
10.5 Conclusion
The interface construct in SystemVerilog provides a powerful technique to
group together the connectivity, timing, and functionality for the communica-
tion between blocks. In this chapter you saw how you can create a single
testbench that connects to many different design configurations containing
multiple interfaces. Your signal layer code can connect to a variable number
of physical interfaces at run-time with virtual interfaces. Additionally, an
interface can contain the procedural code that drives the signals and assertions
to check the protocol.
In many ways, an interface can resemble a class with pointers, encapsula-
tion, and abstraction. This lets you create an interface to model your system at
a higher level than Verilog’s traditional ports and wires. Just remember to
keep the testbench in the program block.
References
Symbols A
$cast 50, 225–226, 231 Accellera xxviii
$dist_exponential 158 accumulate operator 55
$dist_normal 158 Active region 111–112, 114, 117
$dist_poisson 158 Agent class 96
$dist_uniform 158 always blocks in programs 119
$error 125 anonymous enumerated type 48
$fatal 125 arguments
$feof 56 default value 60–61
$fopen 56 sticky 58
$fscanf 56 task and function 57
$info 125 type 59
$isunknown 29 array
$psprintf 52 assignment 36
$random 158 associative 37–39, 41, 43–44
$realtime 65 compare 32
$root 45, 122–123 constraint 165
$sformat 52 copy 32
$size 30–31, 35 dynamic 34, 36–37, 41–43
$timeformat 65 fixed-size 29, 36–37, 41–43
$unit 122 handle 91
$urandom 158 linked list 39
$urandom_range 158 literal 30
$warning 125 locator methods 40
+= 55 methods 40
? : operator 32 multidimensional 29, 31
`default_nettype 53 packed 33–34
`define 45 queue 36–37, 42–43
reduction methods 40
unpacked 30, 34
Numerics assertion
2-state types 28–29
concurrent 126
4-state types 29
coverage 245
298 SystemVerilog for Verification
E I
enumerated types 47–48 implicit nets 53
enumerated values 48 implicit port connection 121
enumeration 47 increment 55
Environment class 96 inheritance 215–216, 228
event 195 initialization in declaration 63
event triggered 196 in-line constraint 162
expression width 52 inout argument type 59
extended class 218 input argument type 59
external constraints 163 insert queue 36
external routine declaration 80–81, 212 inside 142–144, 147, 149
inside constraint 165
instantiate 71
F int data type 28
file I/O 56
integer data type 27
find_first method 41
interface 102, 121
find_index method 41
procedural code 290
find_last method 41
virtual 279
find_last_index method 41
interprocess communication 183
first 38
first method 38, 44, 49–50
fixed-size array 29, 36–37, 41–43 L
for loop 30–31, 38, 49, 55 last method 49
force design signals 116, 123 linked list 39
foreach constraint 165, 170–171 logic data type 27–28
foreach loop 30–31, 35–36, 38 longint data type 28
fork...join 184–185 LRM SystemVerilog 43, 69, 116, 119,
fork...join_any 184, 186–187 163, 184, 274, 279, 295
fork...join_none 184, 186–188, 191
four-state types 29
function 56
M
macro 45
arguments 57
macromodule 121
functional coverage 295
Magellan 295
using callbacks 239
mailbox 201, 205, 208
bounded 204–205
G unbounded 204
garbage collection 75 makes Jack a dull boy xxxi
Generator class 96, 187, 199 malloc 71
getc method 51–52 max method 41
method 70, 218
min method 41
H modport 105, 131
handle 70–71
module 69
array 91
Monitor class 96
Hardware Description Language xxviii
multidimensional array 29, 31
Hardware Verification Language xxviii, 1
HDL xxviii
hook 222, 236 N
HVL xxviii, 1–2, 14 name function 48, 144
300 SystemVerilog for Verification
U
uint data type 45, 168
uint user-defined type 45
unbounded mailbox 204
union 46–47
unique method 41
unpacked array 30, 34
V
Verification Methodology Manual for
SystemVerilog 1, 295
Verilog-1995 xxvii–xxviii, 27, 29, 32,