Systemverilog For Design and Verification: Engineer Explorer Series
Systemverilog For Design and Verification: Engineer Explorer Series
Trademarks: Trademarks and service marks of Cadence Design Systems, Inc. (Cadence) contained in this document are
attributed to Cadence with the appropriate symbol. For queries regarding Cadence trademarks, contact the corporate legal
department at the address shown above or call 1-800-862-4522.
All other trademarks are the property of their respective holders.
Restricted Print Permission: This publication is protected by copyright and any unauthorized use of this publication may
violate copyright, trademark, and other laws. Except as specified in this permission statement, this publication may not be
copied, reproduced, modified, published, uploaded, posted, transmitted, or distributed in any way, without prior written
permission from Cadence. This statement grants you permission to print one (1) hard copy of this publication subject to the
following conditions:
The publication may be used solely for personal, informational, and noncommercial purposes;
The publication may not be modified in any way;
Any copy of the publication or portion thereof must include all original copyright, trademark, and other proprietary
notices and this permission statement; and
Cadence reserves the right to revoke this authorization at any time, and any such use shall be discontinued immediately
upon written notice from Cadence.
Disclaimer: Information in this publication is subject to change without notice and does not represent a commitment on the
part of Cadence. The information contained herein is the proprietary and confidential information of Cadence or its licensors,
and is supplied subject to, and may be used only by Cadence customers in accordance with, a written agreement between
Cadence and the customer.
Except as may be explicitly set forth in such agreement, Cadence does not make, and expressly disclaims, any representations
or warranties as to the completeness, accuracy or usefulness of the information contained in this document. Cadence does not
warrant that use of such information will not infringe any third party rights, nor does Cadence assume any liability for
damages or costs of any kind that may result from use of such information.
Restricted Rights: Use, duplication, or disclosure by the Government is subject to restrictions as set forth in FAR52.227-14
and DFAR252.227-7013 et seq. or its successor.
Table of Contents
SystemVerilog for Design and Verification
Version 21.10
Estimated time: 5 Days
Module 1
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
Course Prerequisites
Before taking this course, you need to have:
● Taken a Verilog language course
● A good working knowledge of Verilog
Course Objectives
In this course, you
● Explore the new SystemVerilog features for design and verification:
▪ Creating code using new standard and user-defined data types
▪ Using new procedural statements and operators
▪ Modifying design and testbench modules to use interfaces
▪ Writing object-oriented verification code using classes
▪ Creating stimulus using constrained randomization
▪ Defining coverage to measure the effectiveness of stimulus
▪ Adding dynamic array objects to testbench code
▪ Writing assertions to check design behavior
▪ Integrating C code into a SystemVerilog testbench
Course Agenda
● SystemVerilog Overview ● Basic Classes
● Standard Data Types and Literals ▪ Lab: Using Classes
● Procedural Statements and Procedural Blocks ● Polymorphism and Virtuality
▪ Lab: Modeling a Simple Register ▪ Lab: Using Class Polymorphism and Virtual Methods
▪ Lab: Modeling a Simple Multiplexor (Optional)
● Class-Based Random Stimulus
● Operators ▪ Lab: Using Class-Based Randomization
▪ Lab: Modeling a Simple Counter
● Interfaces in Verification
● User-Defined Data Types and Structures
▪ Lab: Using Virtual Interfaces
▪ Lab: Modeling a Sequence Controller
● Covergroup Coverage
● Hierarchy and Connectivity ▪ Lab: Simple Covergroup Coverage
▪ Lab: Modeling an Arithmetic Logic Unit (ALU) (Optional) ▪ Lab: Analyzing Cross Coverage (Optional)
● Static Arrays ● Queues and Dynamic and Associative Arrays (QDA)
● Tasks and Functions ▪ Lab: Using Dynamic Arrays and Queues
▪ Lab: Testing a Memory Module
● Introduction to Assertion-Based Verification (ABV)
● Interfaces
● Introduction to SystemVerilog Assertions (SVA)
▪ Lab: Using a Memory Interface
▪ Lab: Simulating Simple Implication Assertions
▪ Lab: Verifying the VeriRISC CPU (Optional)
▪ Lab: Sequence-Based Properties
● Simple Verification Features ● Direct Programming Interface (DPI)
● Clocking Blocks ▪ Lab: Simple DPI Use
▪ Lab: Using a Simple Clocking Block ● Interprocess Synchronization
● Random Stimulus ▪ Lab: Synchronizing Processes with a Mailbox
▪ Lab: Using Scope-Based Randomization ▪ Lab: Synchronizing Processes with a Semaphore
▪ Lab: Synchronizing Processes with Events (Optional)
If there is additional information regarding the specific software, it is detailed in the lab document
and/or the README file of the database provided with this course.
Benefits of Cadence Certified Digital Badges How do I register to take the exam?
Problem & Solution Quick Reference Tool Command How To Lab List
SystemVerilog Overview
Module 2
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Identify the purpose of SystemVerilog
● Analyze the segmentation of SystemVerilog constructs
What Is SystemVerilog?
SystemVerilog is a unified hardware design, specification, and verification language based
on the Accellera SystemVerilog 3.1a extensions to the Verilog hardware description
language.
SystemVerilog is the IEEE Std. 1800 “IEEE Standard for SystemVerilog – Unified Hardware Design,
Specification, and Verification Language”.
SystemVerilog includes the IEEE Std. 1364-2001 “IEEE Standard Verilog Hardware Description
Language” by reference.
Several companies donated proprietary content to the Accellera EDA advocacy group, which integrated
those Verilog extensions into SystemVerilog and championed the creation of the new IEEE standard.
Verilog-2001
ANSI C style ports standard file I/O ** (power operator)
multi dimensional arrays
generate $value$plusargs configurations
signed types
localparam `ifndef `elsif `line memory part selects
automatic
constant functions @* variable part select
Verilog-1995
modules $finish $fopen $fclose initial wire reg
parameters $display $write disable integer real +=*/
parameters
function/tasks $monitor events time %
function/tasks
always @ `define `ifdef `else wait # @ packed arrays >> <<
assign
assign `include `timescale fork–join 2D memory
The IEEE Std. 1364 standardized in 1995 was previously the proprietary Verilog hardware description
language.
The 2001 update to the Verilog standard added several convenience features, still primarily targeted at
hardware design.
Accellera proposed an update to the Verilog standard that added many design convenience features and
more than tripled the verification features. This slide illustrates just how much SystemVerilog adds to
Verilog. To hopefully minimize confusion, Accellera requested and obtained the new IEEE standard
number 1800 for these new features.
Verification Extensions
Classes and Object-Oriented Design
Dynamic Data Structures Assertions
Scope and Class Based Constrained Randomization Coverage
Increase Verification Efficiency!
Intraprocess Synchronization Clocking Blocks
Direct Programming Interface (DPI) Program Blocks
Design Subset
Relaxed Datatype Rules Procedural Statements
Static Data Types Connectivity Enhancements Express more with less code!
Interfaces Convenience Features Increase synthesis quality!
Specialized Procedures
Verilog-2001
ANSI C style ports standard file I/O (* attributes *) multi dimensional arrays
generate $value$plusargs configurations signed types
localparam `ifndef `elsif `line memory part selects automatic
constant functions @* variable part select ** (power operator)
Verilog-1995
modules $finish $fopen $fclose initial wire reg modules
parameters $display $write disable integer real parameters +=*/
function/tasks $monitor events time function/tasks %
always @ `define `ifdef `else wait # @ packed arrays always @ >> <<
assign `include `timescale fork–join 2D memory assign
Obviously, most SystemVerilog enhancements are for verification, have no hardware implementation
and therefore are not synthesizable. These are the SystemVerilog verification extensions and include
classes, coverage, randomization and assertions.
However, there are several SystemVerilog enhancements that are intended to fix common Verilog
synthesis issues and which have hardware implementation-defined by the Language Reference Manual
(LRM). This is the design subset of SystemVerilog intended for RTL code, and includes relaxed and
enhanced data types, additional procedural statements and connectivity enhancements.
Obviously, there are many features that can be used in both verification and design. There is a large
“grey area” of language features for which synthesizability and implementation are not defined by the
LRM but depend on the synthesis tool you are using. There are even constructs that are definitely
synthesizable with a known implementation in one form but vendor-specific in another. This can affect
the portability of design code in that some constructs may be synthesizable in one tool, but not in
another, or may be synthesizable, but implemented differently in different tools.
Normally, a company will make a decision as to whether portability and implementation are important
and refine their existing RTL coding guidelines accordingly.
Module Summary
SystemVerilog has a lot of new content, including:
● Nearly 100 new keywords
SystemVerilog is a substantial upgrade on Verilog:
● Based heavily on Verilog-2001
There is a huge learning curve for both designers and tool vendors.
● Parts of the language are still being refined
● Tools may not support some language constructs
● Most users will adopt SystemVerilog incrementally
This course covers the most useful, the most frequently used, and the most widely supported features
of the language.
Many design teams have not yet fully adopted the Verilog-2001 standard. As SystemVerilog is based on
Verilog-2001, such teams will need to learn about Verilog-2001 as well as SystemVerilog.
Having many new keywords means that existing Verilog designs may not compile with SystemVerilog
compilers. For example, if you have a signal named do, then your code will not compile in
SystemVerilog because do is a SystemVerilog keyword.
However, SystemVerilog does not take features away from Verilog users. Verilog-2001 features are still
available and work the same as earlier.
Module 3
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Use the new logic data type
● Analyze the relaxed rules for data types in SystemVerilog
● Examine two-state data types and their uses
● Differentiate the changes between time literals and timing specification
What Is a Datatype?
A datatype is a set of values (2-state or 4-state) that can be used to declare data objects or
to define user-defined data types.
All Verilog data types except real are 4-state logic. 4-state logic is essential for gate-level modeling
and for initialization at RTL, but simulating everything in 4-state logic can add significant performance
overhead.
Therefore SystemVerilog adds a 2-state bit type, containing only 0 and 1 values, with the intention of
enhancing simulation performance at the higher abstraction levels.
As with the 4-state register type integer, SystemVerilog defines a number of predefined bit types of
various widths – shortint is 16 bits, int is 32 bits, and longint is 64 bits, on all platforms.
Objects of the bit type are by default unsigned, and objects of the predefined types are, by default,
signed.
SystemVerilog also defines a 32-bit floating-point type, shortreal, to complement the 64-bit Verilog
real type.
What Is a 'logic'?
The keyword logic defines that the
variable or net is a 4-state data type.
Verilog
reg clock;
reg [15:0] b_reg;
reg [7:0] mem8x32 [0:31];
● The Verilog reg data type is potentially confusing:
▪ It can synthesize to a net or register depending on
the usage.
SystemVerilog changes the terminology for data types, but in a way that is backward compatible with
Verilog.
Verilog has two forms of data type: net (wire) and register (reg). However, the term register is
misleading. A synthesis tool may infer either combinational or sequential logic for a Verilog register
type, depending on how it is used. Therefore, SystemVerilog uses the term variable (or var) to refer
to Verilog register types.
SystemVerilog also defines the name logic for the basic 4-state Verilog type (0, 1, X, Z).
Therefore, the Verilog declaration reg clk; becomes var logic clk; in SystemVerilog. The
default SystemVerilog data type for logic is variable, making the var keyword optional. Therefore
the SystemVerilog form becomes logic clk; So logic becomes a direct replacement for reg.
You can also specify the logic type for net data types like wire. So Verilog wire clk; becomes
wire logic clk; in SystemVerilog. However, SystemVerilog assumes that all net data types are
logic, making the logic keyword optional. Therefore, the SystemVerilog form becomes wire
clk;
Although 2-state types are more efficient for simulation, they have some issues for RTL design.
Remember that the x and z values in 4-state logic are used for more than modeling unknown and tristate
values. X is used for un-initialized logic (not yet assigned) and z for un-driven logic (declared but never
assigned). Hence 4-state types always initialize to x at the start of the simulation, allowing the detection
of un-initialized code.
2-state types initialize to 0 at the start of the simulation; hence un-initialized code cannot be so easily
detected.
SystemVerilog allows 2- and 4-state types to be easily mixed, although this can be dangerous. If a 4-
state type is assigned to a 2-state type, any x or z value is converted to 0. This can hide un-initialized or
un-driven values. The conversion occurs with no warning or error, and so mixing 4-state and 2-state
types is generally not a good idea.
Due to the initialization and conversion issues with 2-state types, they are rarely used in RTL code.
However, they can be useful for testbench code and for the definition of the clock and reset variables
which should not need x or z values.
wire
a_in reg
y_out
SystemVerilog relaxes the rules for a variable by: module mone
(output assigned procedurally)
● Assigning a SystemVerilog variable:
▪ In any number of initial or always blocks: wire
logic
To ease confusion about when to use a Verilog register type and when to use a net type, SystemVerilog
relaxes the data type rules. A SystemVerilog variable can be driven from a continuous assignment or a
module or primitive output, as long as there is only one driver of that form. Therefore, in most
situations, you can simply use the logic keyword instead of reg or wire.
The next page contains an example using the relaxed SystemVerilog data type rules.
module test;
and_out, or_out are
reg a, b; test
wire in module test
wire and_out, or_out;
– instance outputs a a_in mone
mone u1 (and_out, a, b);
mtwo u2 (or_out, a, b); b_in y_out and_out
initial begin
a = 0;
b = 0; a , b are reg in module test – a_in mtwo
... assigned in an initial
b b_in y_out or_out
end
procedural block
endmodule
23 © Cadence Design Systems, Inc. All rights reserved.
Here, we are using fully defined Verilog-2001 ANSI C syntax for module port declarations for clarity.
In both modules mone and mtwo, the input ports a_in and b_in must be declared as net data types.
In module mone, the output y_out must be declared as a register data type as it is assigned from the
always procedural block.
In module mtwo, the output y_out must be declared as a net data type as it is driven from the
continuous assign statement.
In module test, a and b must be declared as register data types as they are assigned from the
initial procedural block, but and_out and or_out must be declared as net data types as they are
driven from the instance output ports.
Therefore, a connection like a-> a_in, or y_out -> and_out actually changes data type as it
crosses the module boundary.
module test;
logic a, b; and_out, or_out are test
logic and_out, or_out; logic in module test
a_in mone
mone u1 (and_out, a, b); – single instance outputs a
mtwo u2 (or_out, a, b); b_in y_out and_out
initial begin
a = 0;
b = 0; a , b are logic in module test – a_in mtwo
... assigned in an initial
end b b_in y_out or_out
procedural block
endmodule
24 © Cadence Design Systems, Inc. All rights reserved.
Using the relaxed data type rules of SystemVerilog, our example becomes easier to read and write.
In both modules mone and mtwo, the input ports a_in and b_in can now be declared as register data
types (logic) as they are both driven from single input ports of the module.
In module mtwo, the output y_out can now be declared as a register data type (logic) as it is driven
from a single continuous assign statement.
In module test, and_out and or_out can now be declared as register data types (logic) as they
are driven from single output ports of the module instances.
We can simply use logic everywhere in this example.
Therefore, a connection like a-> a_in or y_out -> and_out no longer changes data type as it
crosses the module boundary.
● You cannot combine procedural assignments with continuous assignments or module output drivers on
the same variable.
● You cannot have multiple continuous assignments or multiple output ports drive the same variable.
● Only net types can have multiple drivers.
Although SystemVerilog relaxes the rules on data type declaration, there are still some restrictions on
assignment to variables. These restrictions aim to prevent multiple drivers on a single variable.
If a variable is assigned from a procedural block, then it cannot also be driven from a continuous
assign statement or from a module output port.
A variable cannot be driven by multiple assign statements or multiple module output ports or by a
combination of assign and port drivers.
Only net data types (like wire) are allowed to have multiple drivers.
The intention is that signals with single drivers are declared as variables, and net types are reserved
solely for connections with multiple drivers only.
However, you still have the problem of driving a variable from multiple procedural blocks.
SystemVerilog has a different solution for this problem.
● Special procedural blocks (always_ff, always_comb, always_latch) allow only a single driver to
variables.
▪ See the Procedures and Procedural Statements module.
logic op;
R logic op;
Error
always @(sel, a, b) always_comb
if (sel) if (sel)
op = a; op = a;
else else
op = b; op = b; always_comb
allows only a
always @(sel2, c, d) always_comb single driver on
if (sel2) if (sel2) logic op.
op = c; op = c;
else else
op = d; op = d;
26 © Cadence Design Systems, Inc. All rights reserved.
SystemVerilog has a set of new procedural blocks for RTL code. These allow the designer to clearly
identify how they intend the procedural block to be synthesized. For example, always_comb is an
always block that should synthesize to combinational logic.
Another feature of these new procedural blocks is that they enforce a “single driver per variable” policy.
If a variable is assigned from one of these procedural blocks, then it is a compilation error to drive the
same variable from any other procedural block, or assign statement or module output.
The Procedures and Procedural Statements module describes these new procedural blocks in more
detail.
A Verilog literal has three parts: a size (default value 32 in Verilog-1995), a base (default decimal), and a
value (required).
Verilog-1995 extends an unsized literal to 32 bits. If the leftmost bit is the high impedance value or the
unknown value, it extends to 32 bits with that value. It extends beyond 32 bits with zeros (with the
normal padding rules for vector assignment where the target is smaller than the source).
Verilog-2001 extends the leftmost high impedance value or unknown value to the size of the enclosing
expression.
SystemVerilog has a new syntax that allows single-bit literals that are both unsized and unbased.
When you assign an unsized literal, SystemVerilog fills all bit positions of the target with the specified
single-bit 4-state value.
This makes it very easy to set a logic vector to, for example, all ones, all high impedance, or all
unknowns.
You write a time literal as an integer or fixed-point number, followed immediately by a time unit (fs
ps ns us ms s) without an intervening space character.
The 1step delay refers to the simulation step, that is, the simulation precision.
Time literals allow delay values to be explicitly defined and so isolated from the current time unit,
traditionally specified with a `timescale compiler directive. However, SystemVerilog still rounds the
time literal to the current time precision. This implies that you must have a current time unit and
precision specified for the scope where the time literal appears.
You can use a time literal anywhere you would use a unitless integer or real number to represent time.
This example uses time literals to specify procedural and intra-assignment delays.
Although the time literal isolates a delay value from the current time unit, the delay is still scaled by the
current time precision. In the example above, the #5.18ns delay is scaled to #5.2ns as the precision
is 100ps (0.1ns).
The timeunit and timeprecision declarations are equivalent to the timescale compiler directive
(`timescale), but without the file-order dependency of compiler directives. timeunit and
timeprecision must be the first declarations in the module or design element. The declarations are
visible only within that unit declaration and within any further nested declarations.
Syntax: timeunit <literal><units> ;
timeprecision <literal><units> ;
Where:
literal::= 1, 10, 100
units::= s ms us ns ps fs step
• step advances time by the simulation time precision.
The current time unit is defined using the following highest to lowest precedence:
▪ 1st – The timeunit of the current interface, module, package or program definition.
▪ 2nd – The timeunit inherited from the enclosing interface or module definition.
▪ 3rd – The timescale (`timescale) directive of the current compilation unit.
▪ 4th – The timeunit declaration of the current compilation unit.
▪ 5th – The implementation-specific default time unit.
Quiz
Which data type can be used for the majority of logic
SystemVerilog variables?
In which design situations would you need to use Where there is more than one driver, for example tristates
nets? and bidirectionals.
What is the issue with using 2-state data types 2-state logic initializes to 0 and so hides initialization
with RTL code? problems in RTL code. Also assigning 4-state to 2-state
variables converts X and Z values to 0.
Module 4
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Use new SystemVerilog procedural statements and enhancements to existing constructs
● List enhancements for addressing common Verilog synthesis issues
● Use synthesizable procedural blocks
Verilog-1995 did not allow variables to be declared in procedural blocks or local scopes within a block.
Verilog-2001 allowed local variables to be declared within blocks, but only if the block was explicitly
named.
SystemVerilog allows local variables to be declared in unnamed blocks. These variables are visible in
the unnamed block and any blocks nested below it, but as the block has no name, the variables are not
accessible through hierarchical references.
IEEE 1800-2012 12.7.1 You can declare a for loop variable within the for statement.
● Variables are visible only in the loop.
● The same identifier name can be safely used in multiple (un-nested)
loops.
● Good use of 2-state data types as loop variables.
Verilog
One benefit of allowing variables in unnamed blocks is that you can now declare a for loop variable
within the for statement, removing the need to separately declare the variable outside the loop. This has
the added benefit of avoiding issues where a single loop variable is accidentally or deliberately used in
multiple, overlapping loops.
In fact, you can declare multiple variables in the loop, separately initialize each variable, and separately
reassign each variable. Thus, SystemVerilog for loops can be a function of more than one loop variable.
foreach Loop
The SystemVerilog foreach loop iterates over all the elements of an array. It is equivalent to a for loop,
which iterates over the full width of the array from left bound to right bound. This construct is useful for
simple loop-based array initialization or processing.
The foreach loop variable is automatically declared (and typed) as part of the foreach statement. As a
local variable, it is visible only within the foreach statement and any nested blocks. In a change to
normal Verilog behavior, this variable cannot be written but only read.
If the array is multidimensional, you can declare a loop variable for each dimension. This creates nested
loops. Loop variables are mapped to dimensions in order of cardinality (see the IEEE1800 Language
Reference Manual). In the example above, k iterates from 7 to 0, and l from 2 to 0. Also, higher
cardinality dimensions change more rapidly, so in the example above, k is set at 7 while l iterates from 2
to 0, then k decrements to 6 while l changes, etc.
Syntax:
foreach ( array_identifier [ loop_variables ] )
statement(s)
● The while loop executes a group of statements ● The expression is checked after statements
until expression becomes false. execute.
● expression is checked at the beginning. ● The statement block executes at least once.
● This makes certain loop functions easier to create.
Verilog SystemVerilog
... ...
while (enable) do
@(posedge clk) @(posedge clk) if enable false on
if enable false on loop
count = count + 1; entry; count = count + 1; loop entry;
... count not incremented count incremented
while (enable); once
...
The Verilog while loop executes statements in the loop while the condition is true. The condition is
checked before the loop is entered, and if the condition is false at this point, no loop statements are
executed.
The SystemVerilog do…while loop executes the loop statements once before the condition is tested.
Therefore even if the condition is false, the loop statements are executed once. This form of the while
loop makes certain functionality easier to create.
Syntax:
do
statement_or_null
while ( expression ) ;
You can use break and continue statements only within loop statements (do, for, foreach,
repeat, while).
break terminates the current loop, i.e., it forces a jump to the end of the loop. The break is usually
conditional. This break example rotates data to the left until the rightmost bit is 1.
continue starts the next iteration of the loop, i.e., it forces a jump to the beginning of the loop.
Again, continue is usually conditional. This continue example counts the number of zeros in the
data vector. The foreach loop iterates through the data bits and jumps over the increment statement
if the current data bit is 1.
Use of these statements may make your code more readable than using branching statements or the
Verilog disable statement.
Verilog case statement branches are prioritized. The simulator checks the branches in the order in
which they appear; branches can overlap and there need not be a branch for every possible value of the
case expression. The synthesis tool must initially produce hardware that represents this priority
structure, though it can later optimize out the priority structure if it is not needed. As the designer, you
may know that in real operation, the case match items are mutually exclusive. You can use the
parallel_case attribute to direct the logic synthesis tool to not build the priority structure.
As the designer, you may know that in real operation, certain case expression values cannot occur. If
you do not specify match items for those values and do not specify default assignments, the synthesis
tool will, for combinational logic, infer a latch to hold the existing result values for those unmatched
case expression values. You can use the full_case attribute to direct the logic synthesis tool to
produce binary result values for those unmatched case expression values, rather than to infer a latch.
Use of the full_case attribute should never be necessary, as you can always simply add a default
branch.
If incorrectly applied, these directives can create incorrect hardware. As the directives are ignored by
simulation tools, this incorrect behavior may only be detected (if at all) in a post-synthesis netlist.
The priority case modifier replaces and enhances the full_case attribute. SystemVerilog
performs runtime checks to verify that for every execution of the case statement, a matching branch is
found. Hence, the full case behavior of the case statement can be tested in simulation.
Note: If simulation is insufficient and a legal value that does not have a matching branch is never
applied to the priority case, then a runtime warning is not received. For example, in the priority
casez example, if pri = 0 was a legal value, but never applied in simulation, a runtime warning
would not be seen and the issue would not be detected.
The unique case modifier replaces and enhances the parallel_case attribute. SystemVerilog
performs runtime checks to verify that for every entry into this statement, one and only one matching
branch is found.
Note: The simulation may report that a case statement is not unique (for example, when fullc has the
value 3'bxxx) even though for synthesis purposes the case is full and parallel.
SystemVerilog priority if
The priority and unique modifiers can also be applied to if statements. An if statement can be
made complete by appending an unqualified else with a null statement (else ;). A null statement is
simply a semicolon.
It is important to understand that SystemVerilog treats a unique if very much like a unique case
in that it evaluates each conditional expression independently of the other conditional expressions.
SystemVerilog unique if
Hardware Equivalent
always @(avec)
if (enable)
The iff keyword qualifies a procedural event control. y <= avec;
You can add an “if and only if”” (iff) qualifier expression to any event, net, or variable in an event
expression. The event guard applies only to the immediately preceding identifier.
In the top example, the enable signal guards the avec signal. Transitions of enable have no effect.
Transitions of avec trigger block execution only when enable is 1. The expression is evaluated when
avec changes, and not when enable changes.
Note that iff has precedence over or. This can be made clearer by the use of parentheses.
Use for iff in RTL is limited to modeling latches and gated clocks.
iff is more useful in verification code, particularly in embedded event expressions for stimulus code.
In the last example, data can only be driven into the design on the negative edge of clk when the DUT
output suspend signal is low. Using the iff guard makes the code much simpler.
A Verilog always block can synthesize to combinational, latched, or sequential logic, depending upon
the sensitivity list and your coding style. The simulator does not know what you intend, so it cannot
verify that your block matches your intentions.
SystemVerilog adds implementation-specific procedural blocks (always_comb, always_latch,
always_ff). These blocks reduce design ambiguity by clearly indicating the hardware intent for a
procedural block. Simulation, formal checking, linting, synthesis, equivalence checking and other
downstream tools can have a consistent specification.
The always combinational (always_comb) procedural block provides special functionality for
combinational logic:
▪ It infers a sensitivity list that includes every variable read by the procedural block.
▪ It prohibits its assigned variables from being assigned in any other procedural block. ( With these
processes, it is illegal for a variable to be written to by more than one process, even when these
processes are in different modules, interfaces or test programs)
▪ It cannot contain any embedded timing or event control.
▪ It is initially run once at time zero after all initial and always blocks have run to suspension.
This is to ensure that the outputs of the block are consistent with any changes to the inputs.
Normally, an always block will be executed only at time 0 if there is an event on a signal in its
sensitivity list. In addition, the execution order of always and initial blocks is indeterminate. A
testbench initial block may apply an initial value to a design input that does not trigger an event
(e.g., assignment of 0 to a 2-state type). Therefore, the outputs of a combinational block may not be
consistent with the input values at time 0. Use of an always_comb fixes this initialization
inconsistency by automatically executing the always_comb after initial and always blocks at
time 0.
Software tools can perform additional checks to warn whether behavior within an always_comb
procedural block does not represent combinational logic, e.g., whether it infers latches.
● Can include timing and additional event controls. ● Cannot contain any timing or event controls.
● Can assign to variables which are assigned ● Cannot assign to variables which are assigned
elsewhere. elsewhere.
● Triggered at time 0 with other blocks only if event ● Automatically triggered at time 0 after always and
on sensitivity list. initial blocks.
● Only sensitive to changes in the arguments of a ● Sensitive to changes in any input to a called
called function. function.
always @* always_comb
if (sel == 1) if (sel == 1)
op = a; op = a;
else else
op = b; op = b;
48 © Cadence Design Systems, Inc. All rights reserved.
Verilog-2001 added a wildcard sensitivity list for combinational always procedures. However, this is
only an enhancement for the event expression; it does not prevent the always block from containing
other event controls or timing, from driving variables that are assigned elsewhere nor trigger execution
of the block at time 0.
Also, the wildcard sensitivity has a limitation. If the always block contains a function call, then only
the arguments of the function are added to the sensitivity list. If the function reads a variable that is not
passed via the argument list (i.e., reads the variable by side effects), then that variable is not included in
the wildcard sensitivity list.
The SystemVerilog always_comb block includes the extra features which prevent additional event or
delay expressions in the block; it prevents multiple drivers on assigned variables and triggers the
execution of the block at the start of simulation. In addition, the complete sensitivity list of
always_comb includes all variables read by a function call, including those read by side effects.
Therefore, in every way, the always_comb block is far better for combinational synthesis code than
the always block.
● Tools can issue warnings if the block does not infer latched logic.
The always latch (always_latch) procedural block provides special functionality for latched
logic, similar to an always_comb block.
▪ It infers a sensitivity list that includes every variable read by the procedural block.
▪ It prohibits its assigned variables from being assigned in any other procedural block.
▪ It cannot contain any embedded timing or event control.
▪ It is initially run once at time zero after all initial and always blocks have run to suspension.
This is to ensure that the outputs of the block are consistent with any changes to the inputs.
However, the best feature of the always_latch is that it clearly documents that a designer intends to
create latched logic.
Software tools can perform additional checks to warn whether the behavior within an always_latch
procedural block does not represent latched logic.
The always flip-flop (always_ff) procedural block requires one and only one event control
preceding the block body.
▪ It prohibits its assigned variables from being assigned in any other procedural block.
▪ It cannot contain any additional embedded timing or event control.
Software tools can perform additional checks to warn whether the behavior within an always_ff
procedural block does not represent sequential logic.
Module Summary
In this module, you learned how to
● Write block names that can also appear at the block end, which enhances readability
● Declare local variables in unnamed blocks that are especially useful as for and foreach loop variables
● Write the full_case and parallel_case attributes that can be replaced for proper case statement
synthesis
● Set up runtime checks with the event control iff qualifier
● Analyze always_comb, always_latch, always_ff which can be used to prohibit multiple signal drivers,
better initialization, and no embedded events or timing
//ONE //THREE
always @(posedge clk iff rst == 0)
logic [2:0] myvec, tmp; // synchronous reset
if (rst)
data_out <= 8<92>h00;
always@(myvec) begin else
tmp = myvec>>1; data_out <= data_in;
unique case (tmp)
3'b000 : $display("zero"); Procedure never
triggered when rst=1,
3'b001 : $display("one");
3'b010 : $display("two"); so never reset
endcase;
end No case branch
for tmp=3'b011 //FOUR
always_comb
if (ctrl == 1)
//TWO
always_ff op <= a;
begin Does not infer
#(period/2) clk <= 0; combinational logic
#(period/2) clk <= 1;
end
Block timing inside
an always_ff
52 © Cadence Design Systems, Inc. All rights reserved.
Labs
Lab 1 Modeling a Simple Register rst_
8 8
● Model a simple register using logic data types, data out
timeunit and timeprecision statements, and
enable
an always_ff procedural block.
clk
Operators
Module 5
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Analyze new SystemVerilog assignment operators
● Use wildcard equality and inequality operators
● Examine the inside operator
initial begin
a = 1;
b = 2;
a += b; // same as a = a + b, so, a = 3
...
Assignment operators are blocking assignments. Therefore, they are suitable for use only in RTL
combinational logic, temporary variables in RTL sequential code, or testbench and stimulus.
--total;
Pre- and post-increment and decrement operators are blocking assignments. Therefore, they are only
suitable for use in RTL combinational logic, temporary variables in RTL sequential code, or testbench
and stimulus.
if (a == b) // unknown
...
● Allows bits to be defined as a “wildcard”:
▪ Don’t-care – like casex if (a === b) // false
▪ An X, Z or ? in the right operand matches any value ...
in the left
if (a ==? b) // true
▪ Asymmetric – only right side can have wildcard bits ...
▪ Also wild inequality (!=?)
if (a !=? b) // false
● Remember that Verilog has two (in)equality ...
operators:
▪ Logical (in)equality (==) if (a ==? 4'b?1?1) // true
...
▪ Identity (in)equality (===)
The wildcard equality operator (==?) and the wildcard inequality operator (!=?) treat unknown (X) and
high-impedance (Z) values in the given bit position of the right operand as “don’t-care” bits to ignore
when doing the comparison. These operators return a 1-bit result that can be 0, 1 or unknown.
Remember that you can use the Z and question mark (?) characters interchangeably in a Verilog 4-state
literal.
The set membership (inside) operator performs a comparison between a left-side expression and a
right-side list of values. The right-side list can utilize wildcards, value ranges and the values can
overlap.
Range values must be expressed in the form [lower_bound : higher_bound ]. Either bound
can be $ indicating the minimum or maximum value for the expression.
For nonintegral expressions, SystemVerilog performs an equality match operation, which returns 1 or 0.
For integral expressions, SystemVerilog performs a wild equality match operation, which is an
asymmetric match. Therefore, the result can be 0, 1 or unknown.
Syntax:
▪ expression inside { value_range { , value_range } }
▪ value_range ::= expression | [ expression : expression ]
The set membership (inside) operator performs a comparison between a left-side expression and a
right-side list of values. The right-side list can utilize wildcards, value ranges and the values can
overlap.
Range values must be expressed in the form [lower_bound : higher_bound ]. Either bound
can be $ indicating the minimum or maximum value for the expression.
For nonintegral expressions, SystemVerilog performs an equality match operation, which returns 1 or 0.
For integral expressions, SystemVerilog performs a wild equality match operation, which is an
asymmetric match. Therefore, the result can be 0, 1 or unknown.
Syntax:
▪ expression inside { value_range { , value_range } }
▪ value_range ::= expression | [ expression : expression ]
*/% Left
+ - (binary) Left
^ ~^ ^~ (binary) Left
| (binary) Left
&& Left
|| Left
–> Right
You can make pattern assignments to unpacked arrays and unpacked structures (structures described in
the User-Defined Data Types module).
Assignment patterns are an aggregate assignment, equivalent to an individual assignment to each array
element. It is a compilation error if the number of values in the pattern is more than or fewer than the
number of elements in the target array.
The assignment pattern starts with an acute accent ', as opposed to a Verilog compilation directive,
which begins with a grave accent `.
Assignment pattern syntax is very similar to a concatenation, but unlike concatenation, individual values
do not need to be sized. As the construct expands to individual assignments, SystemVerilog pads or
truncates each assignment as needed.
You can make pattern assignments by position or by key, but not by both in the same assignment. For
arrays, the key can be an element index or the default keyword, which assigns the default value to all
elements not otherwise specified in the pattern.
Use of the default key is the only situation in which it is allowed to have fewer values in the pattern
than elements in the array.
For structures, the key can be a field name, a field type, or the default keyword. See the User-
Defined Data Types module for more information.
Module Summary
In this module, you learned how to
● Use blocking only assignment operators for combinational logic or verification code
● Employ pre- and post-increment/decrement operators, particularly useful in for loop constructs
● Assignment patterns (plus enhancements!):
▪ Useful for multidimensional arrays
▪ Also essential for structures (see next module)
Quiz
initial begin
What is the value of each a = 4'b00xx;
expression or the output of each b = 4'b0001;
conditional branch? c = 4'b0100;
b++;
c += b;
c >>= b;
if (b ==? a)
$display("branch one");
else
$display("branch two");
if ( c inside {4'b0110, 4'b0010} )
$display("branch three");
else
$display("branch four");
OUTPUT ...
branch one
branch four
Lab
Lab 3 Modeling a Simple Counter
● Model a simple counter using SystemVerilog constructs
rst_
5 5
data count
load
enable
clk
Module 6
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Name type declarations in SystemVerilog with typedef
● Create types with user-defined, enumerated values
● Use structures to create arrays of different data types
● Identify the use of packages
A typedef declares and names a user-defined type, allowing the name to be used in the declaration of
variables, ports and other types. You must declare a typedef before you use the name, either:
▪ Inside any declarative scope (for example, a module), which makes it visible only within that scope,
and any nested declarative scopes.
▪ In the compilation unit. (See the Hierarchy and Connectivity module for more details.)
Or
▪ Inside a package, which makes it visible in all design elements that import the package. (See the
Hierarchy and Connectivity module for more details.)
An enumerate type is a user-defined type where the user defines the value set of the type. A
SystemVerilog enumerate type is declared with the keyword enum followed by a list of values for the
type. The value names must be unique in whichever scope the enumerate type is used. A common policy
is to use uppercase names for values to ensure uniqueness.
Enumerate type declarations can be typed (with typedef) or anonymous. There are significant
benefits to naming the enumerate type as we shall see.
SystemVerilog enumerate types are strongly typed. An enumerate variable can be assigned only from a
variable of the same type or from an enumeration value of its type.
To assign any other value requires a static cast to the enumerated type. A cast can only be made using
the name of the enumerate type. Casting the value to the enumerated type does not check the validity of
the value (see following slides).
Traditionally, Verilog is not a strongly typed language. Therefore some compilers allow the direct
assignment of an integral value to an enumerated type variable, but generate a warning.
Tip: Always use a typedef to declare your enumerate types.
start
idle
pause
done
▪ Casting does not check value.
aint = state * 2; // 16
...
is legal, even though there is no named value in the enumerate declaration corresponding to the integral
value 8.
When you use an enumerate variable in an expression, SystemVerilog automatically converts the
enumeration value to its base integral value and uses the integral value to evaluate the expression. If you
wish to assign the expression result back to the enumerate variable, you must use an explicit cast:
state = state_t'(state *2);
idle
done
start
pause
▪ Allows one-hot coding, etc.
S1
S0
S2
S3
3. Value encodings must be unique:
Error
enum {P0=0, P1, P2=1, P3} bad_encs;
▪ Compilation error is reported if encodings overlap.
P2 duplicates P1 encoding
Explicit encoding can be included in an enumerated type declaration, either typed or anonymous. This
allows one-hot, grey or other encodings to be defined for enumerated values.
For 2-state types like the default int base type, you can use only the binary values (no Z or X).
You can mix explicit and implicit value encoding in the same enumerated type declaration. For any
value without an explicit encoding, the encoding is simply an increment of the encoding for the previous
enumerate value; hence for the variable encs, S1 has the encoding 1, and S3 = 7.
It is a compilation error if two or more values have the same encoding; e.g., for variable badencs,
both P1 and P2 have an encoding of 1, P1 by implicitly following the encoding for P0, and P2 by
explicit declaration.
Note: For these encodings to be reflected in hardware, the synthesis tool must support explicit encoding.
If, for example, you want to optimize the encoding of a state variable, it may be better to allow the
synthesis tool to choose its own encoding based on your synthesis goals.
You can specify a range of enumeration elements. This automatically creates names for the elements
and assigns incremental values to the names.
idle
pause
start
done
Enum Declaration with explicit
● This allows you to constrain the enumerate value base type and implicit encoding
range.
● The default value encoding will match the specified
typedef enum bit[2:0]
base type.
{idle = 3'b000,
● Explicit value encoding must match type and (if start = 3'b001,
explicit) length. pause = 3'b010,
done = 3'b100} onehot0_t;
Enum Declaration with explicit
An explicit base type base type and explicit encoding onehot0_t state;
makes enumerates ...
easier and safer to use.
state <= onehot0_t'(3'b001);
The base type of an enumerated type defaults to int. As we have seen, this means a default enumerate
variable can be assigned any value in the int range (32 bits).
You can define an explicit base type for the enumerate. This allows you to constrain the size of the base
type and hence limit the range of values that can be assigned into the type by casting.
You can declare base types of the atomic integer types (byte, shortint, int, longint,
integer, time) or integer vector types (bit, logic, reg).
If you define an explicit base type, then any explicit value encodings must match the base type and size.
In the example, for type onehot0_t, as the base type is of size 3, the explicit encodings must all be
sized to length 3. Truncation and padding assignment rules do not apply when defining explicit
encodings. This is to ensure every value has a unique encoding.
initial begin
$display("value %b", var_l);
$display("name %s", var_l.name());
value xxx
name LX
76 © Cadence Design Systems, Inc. All rights reserved.
The initial value of an enumerate variable depends on the base type of the enumerated declaration.
▪ For the default int base type, the initial value is 0, which would match the default encoding of the
first enumeration value. Therefore, by default, the variable initializes to the first enumerate value.
▪ For any explicit 2-state type (e.g., bit array), the initial value is 0, which would match the default
encoding of the first enumeration value.
▪ For an explicit 4-state base type (e.g., logic array), the initial value is all bits unknown ('x),
which would match only an explicitly encoded value.
In the second example, the initial value of enumerate var_l is 3'bxxx. As this value does not
match the encoding for any of the declared values, the name() method returns an empty string.
With a 4-state explicit base type, you can encode a value with unknowns ('x). This could be used, for
example, to indicate that the variable is invalid and detect sampling problems. However, an explicit
unknown value will cause problems if the variable is intended to be synthesized.
What Is a Structure?
typedef struct {
A structure represents a collection of logic id, par;
data types that can be referenced as a logic[3:0] addr;
whole, or the individual data types that logic[7:0] data;
make up the structure can be referenced } frame_t;
by name. frame_t f1, two_frame[1:0];
logic[7:0] data_in;
● Declare using the struct keyword. ...
// individual field access
● Use the “dot” notation to access individual fields. f1.id = 1'b1;
data_in = f1.data;
● You can make pattern assignments to structures:
// ordered assignment pattern
▪ Ordered or keyed:
f1 = '{1'b1, 1'b1, 4'h0, 8'hff};
o Not both in the same pattern.
▪ Keys can be by name, type, default or a mix of these. // named assignment pattern
f1 = '{id:0, par:1, addr:0, data:0};
● You can declare arrays of structures.
// nested ordered assignment pattern
● Structures can be nested. two_frame = '{'{0,0,0,255}, '{1,1,1,0}};
A structure is an array of elements that can be of different data types. It can be used to group complex
data information, which would be otherwise spread over several separate variables into one object.
The individual fields of the structure can be accessed from a structure variable using the dot notation.
By default, the fields of a structure are stored and must be accessed separately. Therefore, the easiest
way to load a structure is using a pattern assignment. The assignment must be made to every field but
can be either ordered or keyed. With structures, the key can include assignment by type as well as
assignment by name and default (see next slide).
Structures are just like any other type in SystemVerilog, and therefore you can declare arrays of
structures or structures containing fields that are of another structure type (nested structures).
The next slide describes pattern assignments in more detail.
▪ Value must be type compatible with all remaining // assignment by name, type & default
fields. f1 = '{data:8'hff, logic:1, default:0};
Assignment patterns can use either ordered or keyed assignments but cannot use both in the same
pattern.
When using an assignment pattern, all fields must be assigned a value, either explicitly or using the
default key.
Key precedence is by (highest to lowest):
▪ Name, using the field name.
▪ Type, selecting all fields of the specified type.
▪ Default, which applies to all remaining unassigned fields. All fields to which the default value
applies must be of a type compatible with the default value specified.
By default, a structure's fields are stored and assigned individually. However, a structure can be stored,
and accessed, as a single one-dimensional array by using the keyword packed.
There are some limitations on the field types for a packed structure. Only integral fields can be packed.
Multidimensional arrays are allowed if they are also packed (see module on static arrays) and any
nested structure fields must also be packed.
A packed structure can be assigned, read, sliced, shifted and operated upon as if it were a single one-
dimensional array. There are two ways to access the fields of a packed structure. You can still use the
dot notation to access a field, but you can also slice the structure.
For example, f1.addr and f1[11:8] both access the same information.
In general, you should avoid mixing 2-state types like bit and 4-state types like logic in a structure
declaration. Mixing the types means the entire structure is stored as a 4-state type, but automatic
conversion to 0 or 1 is carried out when a 2-state field is accessed. If you wrote X into a 4-state field
and shifted the value into a 2-state field, the packed structure will still be storing an X in the 2-state
field, but it would be read as a 0.
You must declare types before you attempt to use them. This presents a problem when you need to
declare ports of a user-defined type using Verilog-2001 syntax. To resolve this, SystemVerilog adds two
new declarative regions which can be used for sharing type declarations:
▪ The package is a new design element for shared declarations. The package is like a module – it is
usually declared in its own file and must be separately compiled before any other modules or
packages which reference the package. You reference declarations from the package by using
import statements or by using resolved pathnames.
▪ The Compilation Unit Scope is a declarative region immediately before the module definition in a
SystemVerilog file. The Compilation Unit Scope should generally be avoided if possible.
The Hierarchy and Connectivity module explores both mechanisms in more detail.
Module Summary
In this module, you learned about
User-defined data types (typedef):
● Lets you define a type that is used throughout the design
▪ Example: Modifying bus widths without using parameters
Enumerations (enum):
● Name states in a state machine or op-codes in an instruction set
● Easier and safer than using parameters or ‘define
Structures (struct):
● A vector of different data types, referenced by a single name
● Makes for more readable code, with less typing
● Simplifies the passing of data across module/task/function boundaries
Packed data types:
● Allows bit-vector operations across packed structs
Quiz
Which of these statements in the code snippet are incorrect? How could you correct them?
Lab
Lab 4 Modeling a Sequence Controller
● Model a simple FSM using SystemVerilog constructs including enumerate data types, enumeration
methods and packages.
3 load_ac
opcode mem_rd
zero mem_wr
inc_pc
clk
load_pc
load_ir
halt
rst_
Module 7
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Use implicit .name and .* port connections in module instantiations
● Define and reference declarations in packages
● Identify the issues involved with the compilation unit scope
Ordered Port Connections Ordered port connections, which have the risk of
Ordered
incorrectly ordered connections
counter c1 (clk, rst, ld, data, cnt);
Badly ordered
counter c2 (clk, ld, rst, data, cnt);
Named Port Connections Named port connections, which are safer but very
verbose
counter c3 (.clk(clk), .rst(rst), .ld(ld),
.cnt(cnt), .data(data));
module counter (
input logic clk, rst, ld,
SystemVerilog adds two options to
input logic [7:0] data,
simplify port connections: output logic [7:0] cnt);
...
● .name (dot-name)
endmodule
● .* (dot-star)
For Verilog, you can use implicit ordered port connections or explicitly named port connections.
Ordered port connections map the signals of the instantiation to the ports of the module in the order of
declaration, the first signal to the first port, etc. This runs the risk of listing the signals in the wrong
order. Detecting badly ordered port connections in simulation can be very difficult.
Named port connections explicitly identify which signal is mapped to which port, greatly reducing the
risk of incorrect connections. Named port connections are usually the preferred option but are very
verbose.
SystemVerilog adds two options to simplify named port connections, referred to as dot-name and dot-
star.
Where the net or variable name matches the port to which it is connected, SystemVerilog allows you to
just use the name once (dot-name). This is equivalent to a named connection but is easier to write.
For example, where a variable clk is connected to a port clk, instead of writing the named connection
.clk(clk) SystemVerilog allows you to simply write .clk.
You can use a dot-name port connection only if the name, size and type of the port match that of the
connecting net or variable.
Dot-name can be mixed with full-named connections in the same module instance to make connections
where the port name and signal name do not match.
A SystemVerilog dot-name port connection is equivalent to a Verilog named port connection with the
following exceptions:
▪ Signals used in a dot-name connection must be explicitly declared. In Verilog, an undeclared
identifier defaults to a scalar wire. It is not uncommon to use such implicit net declarations in
Verilog module instantiations; however, these implicit declarations cannot be used for dot-name
connections.
▪ Dot-name does not allow explicit or implicit typecasting between the port and the connected signal,
not even padding or truncation. However, there are two exceptions:
1. It automatically converts between 2-state and 4-state types of the same length.
2. It automatically converts between a net and a variable of the same length.
Where all net or variable names match the port to which they are connected, SystemVerilog allows you
to simply write .* (dot-star). This is equivalent to a named connection for every port, but is far easier to
write, if much harder to read.
You can use a dot-star port connection only if the name, size and type of the port match that of the
connecting net or variable.
Dot-star can be mixed with full named connections in the same module instance to make connections
where the port name and signal name do not match.
The dot-star connection is subject to the same rules as the dot-name connection. In particular, signals
used in a dot-star connection must be explicitly declared. In Verilog, an undeclared identifier defaults to
a scalar wire. It is not uncommon to use such implicit net declarations in Verilog module
instantiations; however, these implicit declarations cannot be used for dot-name connections.
● Connecting hierarchy is much ● Connecting hierarchy is much ● Needed where port and variable
easier: more likely to be correct: names do not match:
▪ Much less verbose than any of the ▪ Less connection debugging ▪ Low-level netlists
alternatives ▪ Structural designs
● Port-to-signal association is
▪ As safe as a named port connection ▪ Multiple instances of a module
retained:
● Exceptions to matching port and ▪ Parent and child both have port list
signal names clearly stand out
● It is very useful for top-level block
connections and testbenches:
▪ Block and testbench variable names
can match port names
In Verilog, we use the `include compiler directives to allow declarations to be shared among multiple
modules. The compiler replaces the directive with the referenced code. This can cause issues; for
example, an included file may compile in one location but not another. To resolve these issues,
SystemVerilog adds two new declarative regions which can be used for sharing declarations:
▪ The package is a new design element for shared declarations. The package is like a module – it is
usually declared in its own file and must be separately compiled, before any other modules or
packages which reference the package. You reference declarations from the package by using
import statements or by using resolved pathnames.
▪ The Compilation Unit Scope is a declarative region immediately before the module definition in a
SystemVerilog file. The Compilation Unit Scope should generally be avoided if possible.
The Compilation Unit Scope (CUS) is a new scope for declarations. Declarations are placed inside a file
but outside of a design element (module, package, interface, etc.). The scope of declarations in a CUS
are, by default, restricted to the file in which they are declared. By declaring several design elements in
the file, the CUS declarations can be shared between the design elements. The SystemVerilog standard
also requires a compiler to be able to extend the visibility of a CUS declaration to all files compiled at
the same time. Vendors may also provide other options.
A CUS is equivalent to an anonymous package. As it is anonymous, there is no name for referencing the
scope, so you cannot use a hierarchical pathname to reference any CUS declaration.
A CUS type declaration is visible to any design elements defined in the same compilation unit scope.
Local declarations within the design element can override those in the CUS by using the same identifier.
This follows the normal Verilog rules that declarations within the most local scope take precedence over
all others. You can explicitly access a CUS declaration by using a scope resolution operator (::)
prefixed with dollar-unit ($unit). This allows a design element to access a CUS declaration that is
otherwise overridden by a local declaration.
By default, Cadence® tools will treat each file list in a compile run as a separate CUS. Multiple compile
runs (followed by a separate elaboration step) will create multiple CUSs. There is also the simulator
option –scu, which puts each individual file in a separate CUS.
2. Packages Package
package mytypes;
● New design element similar to a module: typedef enum {start,done} mode_t;
▪ Must be compiled separately. endpackage : mytypes
mytypes::mode_t mode,
...
94 © Cadence Design Systems, Inc. All rights reserved.
The package is a design element. A design element is a top-level simulation building block that is
typically declared in a separate file and must be separately compiled. Verilog has three design elements
– module, configuration and primitive. SystemVerilog adds package, interface and program.
A package allows you to share declarations between multiple design elements. You can place almost any
declaration – types, variables, tasks and functions – within a package.
You can reference packages from within other design elements – modules, interfaces, programs, and
other packages. There are three ways to reference the declarations from a package:
▪ An explicit import allows you to reference selected package declarations.
▪ An implicit wildcard import allows you to reference all the package declarations.
▪ You can also reference a declaration using the package name and the scope resolution operator.
The first example explicitly imports the mode_t type.
The second example implicitly imports the entire contents of the package.
The last example uses a resolved pathname to the package declaration and does not import the
declaration. Any other references to the package declarations must also use a resolved name in the
absence of an import statement. Resolved names are sometimes used in addition to package imports for
clarity.
An explicit import directly loads a package declaration into the design element as if it was declared in
the design element. As such, if the design element can have visibility of another declaration using the
same identifier in the same scope, either through a local declaration or another explicitly imported
package declaration, then it is a compilation error.
An explicit import only makes the identifiers in the import statement visible. For example, if you
explicitly import an enumerated type, that does not automatically also import the enumeration value
names. However, if you explicitly import a structure type, the field names are visible in the design
element.
Explicit import statements in the Compilation Unit Scope (CUS) place those type declarations in the
Compilation Unit Scope. These declarations are then subject to the normal rules for CUS declarations.
Specifically, those local declarations can override the imported package declarations, even if the
package declarations are explicitly imported. Also the extent of the import visibility can be changed
with compilation options. For these reasons, use of the Compilation Unit scope should be avoided if
possible.
Local c1
Wildcard import makes declaration candidate for module mone(...); overrides
P1::c1
import: import P1::*;
● Local or explicitly imported declarations can logic [7:0] c1;
override wildcard imported declarations. mode_t mode; All package
... declarations
● Package declarations still visible through resolved if (mode==stop) visible
name. ...
$display("%h",P1::c1);
...
An implicit wildcard import makes the package types candidates for import. It imports each type only
when the type is used. If the design element can have visibility of another declaration using the same
identifier, either through a local declaration or an explicitly imported package declaration, and the other
declaration is visible before the identifier is used, then the wildcard imported declaration is overridden.
However, declarations from the package are still visible even if overridden by local or explicitly
imported declarations, by using a resolved name.
A wildcard import makes all the declarations in the imported package visible, including enumerate type
values.
● Better readability.
● Imports do not chain: module mone(...);
▪ Importing P1 into P2, and then P2 into mone does not
make P1 declarations visible in mone. logic [7:0] d = c2;
▪ P1 must be separately imported into mone...
import P2::*; Error –
▪ ...or add an export to P2 to make P1 visible as part of c2 undefined
P2. initial begin
o export P1::*; $display("%0d",c2);
o Rarely used.
$display("%0d",c1);
Place imports at the beginning of the ... Error –
design element. c1 undefined
97 © Cadence Design Systems, Inc. All rights reserved.
The import statement is a declaration. You can place an import in any design element, such as an
interface, module, program or other package, but not within a class scope.
Imported declarations are only visible from the point of the import statement onward, so you must
import a declaration before you attempt to use it. It is a good tip to place all your imports at the top of
the module, for better readability and fewer visibility issues.
In the example, the first reference to c2 fails because it comes before the import statement.
Another issue with imports is that they do not chain. In the example, package P2 contains an import
for package P1, which allows it to access the value of c1. However, when we import P2 into the
module mone, we only see the declarations from P2, not P1. Therefore, the reference to c1 fails. To
see declarations from P1 in module mone, package P1 must be separately imported into the module.
An alternative is to make the declarations imported from P1 visible as part of the P2 package by adding
an export statement to P2, although exports are rarely used in practice.
export P1::*;
The export can be wildcard or explicit. A special wildcard export form exports all declarations imported
into a package:
export *::*;
It is an error to import the same identifier more than once. For wildcard imports, the error occurs when
you actually attempt to use the identifier.
In this example, packages P1 and P2 both declare the identifier c1. Wildcard importing of the contents
of both packages gives a compilation error only upon the first use of c1.
An error would also occur if both c1 declarations were explicitly imported.
There are several solutions to this issue:
▪ Avoid duplicate identifiers where possible.
▪ Partition your packages carefully to separate out declarations that clash. It is generally better to have
more, smaller packages rather than a fewer larger ones. This allows better control of declaration
visibility.
▪ Use resolved names to explicitly identify which package declaration is visible.
▪ Explicitly import only the parts of each package that you need.
If you are using package declarations in an ANSI C module port or parameter list, then using an
import statement inside the module is insufficient. The import is compiled after the package
declaration use and so the declarations cause compilation errors.
There are several solutions to this issue:
1. Use resolved names for all package delcarations, although this is inefficient if you are using
multiple declarations from the package.
2. Move the import statement to the Compilation Unit Scope. This means the import statement
is subject to the normal rules of this scope, specifically in that the visibility of the scope can be
modified with compiler options. Therefore we should avoid this option if possible.
3. Packages can also be imported as part of a module header. This makes all the package items
visible throughout the module and these package items can also be used in parameter or port
declaration of the module. This option can also be used with other design elements such as
interfaces and programs.
The module import must come before the ANSI C port or parameter list of the module and must be
terminated with a semicolon, as if you were executing an import statement.
Module Summary
In this module, you learned how to
Do Connectivity Changes
● New options make it easier to build hierarchy
● dot-name connection gives best tradeoff on safety, verbosity and readability
Use Compilation Unit Scope
● Definitions external to module
● Rarely (if ever) needed
Use Packages
● Define types and subroutines external to modules
● Definitions reusable throughout a design hierarchy
● Good for system constants, common user-defined types and functions like printing out error messages
You should restrict your use of the compilation unit scope. The actual scope can be tool-dependent and
not necessarily identical for each compilation session.
Use of packages places related declarations in one file that you can separately compile and then import
into the design element where needed.
One situation where you might want to use the compilation unit scope is for Verilog-2001 port
declarations that use user-defined types. You can import just those type declarations from a package into
the compilation unit scope just before the port declarations. Other port declarations would do the same,
thus restricting the type definition itself to one package rather than potentially multiple compilation
units.
Quiz
Find the problems in the following module
package P2;
top code and correct them. typedef logic [15:0] mytype2;
endpackage : P2
Redundant package P1;
import P2::*;
import P1::load; typedef enum {one, two} mytype1;
module top; localparam int load = 10;
import P1::*; endpackage : P1
Import P2 to
define mytype2 mytype2 data; module inc (input logic clk, rst, ld,
logic [7:0] count, cnt; input logic [7:0] data,
logic clk, rst, ld, load; output logic [7:0] cnt);
typedef logic [7:0] mytype1; ...
endmodule
inc U1 (clk, ld, rst, data[7:0], cnt);
inc U2 (.clk, .ld, .rst, .data, .cnt); rst, ld order swapped
inc U3 (.*);
inc U4 (.*, .count); No port count
inc U5 (.*, .data(data[7:0]));
... data size mismatch for
endmodule U2, U3 and U4
Multiple drivers on cnt variable
for U1, U2, U3, and U5
101 © Cadence Design Systems, Inc. All rights reserved.
Lab
Lab 5 Modeling an Arithmetic Logic Unit (ALU) (Optional)
● Model a simple ALU using SystemVerilog constructs including enumerate data types, enumeration
methods and packages.
3
opcode
8
accum
8
out
8 zero
data
clk
Static Arrays
Module 8
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Examine array enhancements in SystemVerilog
● Analyze the difference between packed and unpacked arrays
● Explore array-querying functions
Arrays
IEEE 1800-2012 7.4 2D array
int inta [2:0][1:0]; of int
Verilog-2001 allows arrays of any number of dimensions of any type except events.
SystemVerilog allows arrays of any number of dimensions of any type, including events.
Traditional Verilog multidimensional arrays are defined as unpacked by SystemVerilog. Unpacked
means that each element of the array is stored separately, and operations or accesses over multiple
elements are not possible. Unpacked arrays are distinguished by having the index defined after the array
identifier.
Traditional Verilog single-dimensional arrays are defined as packed by SystemVerilog. Packed means
that the elements of the array are stored as one item, allowing operations or accesses over the full width
of the element, as well as operations or accesses on each individual element.
This means that standard vectors, such as logic [7:0] avec, are defined as packed.
Pattern assignment is the easiest method to access multiple unpacked array dimensions. You can make
pattern assignments by order or by key but not by both in the same assignment. The key can be the
element index, and you can also use the default keyword to assign a value to all elements which are
otherwise unassigned.
Packed Arrays
A packed array is a mechanism for 15 8 7 0
subdividing a vector into subfields, which logic[7:0][1] logic[7:0][0]
can be conveniently accessed as array
elements.
● Packed arrays: Packed array of bytes
▪ Index declared before name
▪ Stored as a single vector
▪ Can be operated upon and accessed as a single vector logic[1:0] [7:0] packarr;
▪ Indexing precedence from left to right
logic[7:0] short = 8'hAA;
logic[23:0] long = 24'h555555;
● Only single-bit data types can be packed: bit, ...
logic, reg: packarr = 16'h0;
▪ And recursively, other packed objects of these types packarr = packarr << 1;
packarr = ~packarr;
● Packed arrays obey assignment rules for padding
and truncation. packarr = short; //16'h00aa
● A packed array is equivalent to a vector with packarr = long; //16'h5555
predefined bit fields.
107 © Cadence Design Systems, Inc. All rights reserved.
SystemVerilog also has the concept of packed arrays. In a packed array, elements are stored together as
a single vector, allowing operations and access to the whole array, as well as individual elements. For
example, a packed array can be inverted, shifted, or assigned as a single vector while still having access
to the individual elements.
The packed dimensions are declared before the array identifier. Therefore, traditional Verilog one-
dimensional vectors are packed.
You can pack only the single-bit types (bit, logic, reg), and recursively, other packed objects
(array, struct) of those types.
A packed array obeys the traditional Verilog rules for truncation and padding when assigned to or from
an array of different lengths.
A packed array is essentially just a vector with predefined fields.
You can use the packed array name as an integer in any expression in which an integer may appear.
Remember that a packed array is by default unsigned and all multiple bit integer types are by default
signed.
Packed arrays are generally synthesizable as they are defined of synthesizable type (bit, logic, reg)
and have a defined hardware interpretation (single vector).
● Generally, only fully packed arrays are bit [3:0] [5:1] v1 [7:0];
synthesizable. // v1 = array of 2D packed arrays
// v1[3] = 2D packed array[3:0][5:1]
// v1[3][2] = 1D packed array[5:1]
// v1[3][2][1] = scalar bit
SystemVerilog allows arrays to have both packed dimensions (declared before the array identifier) and
unpacked dimensions (declared after the array identifier).
Therefore, traditional Verilog memories are interpreted as a one-dimensional unpacked array of one-
dimensional packed arrays.
reg [7:0] mem [0:255];
Elements in the packed dimensions can be accessed either individually or all at once. Elements in the
unpacked dimensions can only be accessed individually.
Mixed packed and unpacked array types can be declared with typedef, as can purely packed or
unpacked arrays. With mixed arrays, the unpacked dimensions are placed after the type name.
When indexing a mixed array, precedence is from left to right through the unpacked dimensions first,
followed by a left to right through the packed dimensions.
Generally, only fully packed arrays are synthesizable as they have a direct hardware interpretation
(single vector).
These methods return an int value, typedef logic [7:0] mem_t [1:1024];
and are applicable to all arrays. mem_t mem ;
...
$display($dimensions(mem)); // 2
$display($unpacked_dimensions(mem_t)); // 1
$display($left(mem)); // 1
$display($high(mem_t,2)); // 7
$display($increment(mem)); //-1
Array querying system functions return an int value and apply to all array types, including fixed-size
integer types (equivalent to one-dimensional packed arrays). You can apply them to any array identifier
and to any static array type.
Syntax:
$dimensions | $unpacked_dimensions ( identifier | type )
Array dimension system functions (from slowest varying = 1)
<function> ( identifier | type [, dimension_expr=1 ] )
<function> one of { $left $right $low $high $increment $size }
▪ $dimensions returns the number of dimensions in the array: 1 for strings and simple bit vectors
or 0 for any other type.
▪ $unpacked_dimensions returns the number of unpacked dimensions of an array or 0 for any
other type.
▪ $left returns the left declared bound of the dimension.
▪ $right returns the right declared bound of the dimension.
▪ $low returns the lowest declared bound of the dimension.
▪ $high returns the highest declared bound of the dimension.
▪ $increment returns 1 if the left bound is greater than or equal to the right bound and -1
otherwise.
▪ $size returns the number of elements in the dimension.
Dimensions are numbered from number 1 in the order of index precedence.
Quiz
Module 9
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Create functions with output arguments
● Use void functions
● Apply default argument values and named argument passing
● Review the issues with the default "pass-by-value" argument operation
● Examine the SystemVerilog "pass-by-reference" option for arguments
● Concurrent calls each have their own set of arguments and variables. initial
begin
● Argument assignment must be blocking. count_clocks(10);
...
function automatic [63:0] factorial; end
input [7:0] n;
begin always @(posedge trig)
if (n == 1) begin
factorial = 1; count_clocks(6);
else
...
factorial = n * factorial(n-1);
end
Verilog-2001
endfunction
Verilog-2001
114 © Cadence Design Systems, Inc. All rights reserved.
SystemVerilog
Void Functions
function void printerr (input int errs);
IEEE 1800-2012 7.4 if (errs == 0)
$display ("No Errors");
else
$display ("%0d Errors", errs);
endfunction
● You can declare functions that do not return a value. ...
▪ Define return type as void. initial begin
...
● You call a void function as a statement: printerr(status);
▪ All restrictions of functions still apply. ...
In Verilog, a function must return a value even if this value is redundant or superfluous. SystemVerilog
allows a function not to return a value by declaring the return type of the function as void. Such a
function is called a void function.
As the function no longer returns a value, it cannot be called as part of an expression or assignment.
Therefore, a void function is called as a statement, like a task. Apart from this difference, a void
function follows all the normal rules for a SystemVerilog function.
void can also be used in another capacity with functions. If you call a function which does have a return
value, and you wish to ignore this value, you can cast the return value to the void type.
void’( nonVoidFunctionCall(...) )
● You can provide input, output, and inout logic [7:0] a, b, sum;
function arguments. logic cry;
▪ Argument default is input of type logic.
initial begin
▪ output/inout arguments let functions return sum = addcarry(a, b, cry);
multiple values. ...
▪ output/inout arguments let void functions
return values. cry is an output
always @(a or b)
// void function call
add (a, b, sum);
117 © Cadence Design Systems, Inc. All rights reserved.
In SystemVerilog, a function can have output and inout arguments as well as input. Output
arguments are passed out when the function returns.
To remain compatible with Verilog, an undefined argument direction defaults to input and an
undefined argument type defaults to logic.
Output arguments allow nonvoid functions to return more than one value – the return type of the
function plus any output arguments. In the examples above, the function addcarry returns two values,
one by the output argument carry and the other via the function name addcarry.
Output arguments also allow void functions to return values.
You can use functions with output or inout arguments only within procedural blocks and not in
continuous assignments.
The purpose of these changes is to remove the one key advantage of a task over a function – the ability
to return multiple output arguments. Now functions can return multiple values; there is no longer any
reason to use tasks in synthesizable code.
Subroutines can have large numbers of input arguments, and some of these arguments may be mapped
to the same literal value for the majority of calls. Consider, for example, a debug switch on a task,
which is usually disabled, but when enabled reports additional information about the task execution. In
such cases, it may be convenient to define default values for these arguments, allowing the argument to
be omitted in the task call when the default is used, but included in the call if the default value must be
overridden.
SystemVerilog allows a subroutine declaration to specify a default value for each singular argument that
is not an output port. You can use default values only with ANSI-C-style port declarations. When you
call the subroutine, you can omit any arguments that have default values. If necessary, you can use
commas as placeholders for default arguments, although this is not very readable.
When actual arguments are mapped to formals in the call, every input or inout argument must have an
unambiguous value, either though an explicit mapping or a default value.
SystemVerilog allows you to bind subroutine argument actuals to their formals by name, similar to
Verilog named port mapping in module instantiations. This is especially useful with default values, to
avoid using commas as position placeholders.
If you are calling a void function or class function method that has no arguments, or calling a task, void
function or class method for which all arguments have default values, SystemVerilog allows the
parentheses for the call to be omitted. For a directly recursive nonvoid function method call, you need to
either include the parentheses or hierarchically qualify the function called. This is to differentiate
between the recursive function call and the function name variable.
SystemVerilog adds an exit explicitly for subroutines using the return statement. For a function, the
return statement can provide an explicit return value function, and this value overrides any other value
previously assigned to the function name.
If the subroutine contains output or inout arguments, you should remember to assign these before
executing the return statement. Unassigned output or inout arguments will have either default
initialization values, or for static subroutines, values from any previous calls.
endtask : cpu_drive_side
...
Only literals in call
cpu_drive_side(8'hff);
...
The traditional solution to the argument passing issue is to use side-effects. Here the variables driven
and read by the task are not passed as arguments, but are accessed directly. Only literals (in this case
write_data) are passed as arguments. The variables accessed by side-effects must be declared in the
same scope as the task declaration. These are not necessarily the variables from the scope of the task
call.
There are some restrictions to this solution.
▪ The task cannot be reused for a different set of such nets or variables than those used in the
declaration. This can lead to duplication.
▪ The task must be declared local to the variables it accesses, preventing the task from being declared
in a package, for example.
These restrictions can be avoided through the use of interfaces (see later). The side-effect approach is
also extensively used in class methods, where subroutines are declared local to the variables (or class
properties) they access (see later).
Formal arguments
mapped to actuals in call
SystemVerilog supports pass-by-reference (ref) for variable arguments. ref cannot be used for net
arguments – they can only be passed by value.
Pass by reference passes a handle to the actual argument object rather than its value. The subroutine
directly accesses the variable whose reference is passed. Changes to input arguments are immediately
seen inside the subroutine call. Changes to output arguments are immediately seen outside the
subroutine call.
ref arguments can only be used with automatic subroutines.
You can hierarchically access the arguments of a static subroutine throughout the simulation. The static
reference would be valid only while the referenced variable exists. For this reason, SystemVerilog does
not permit passing static arguments by reference. You must in general be careful not to use a reference
that might be invalid, for example, in an automatic task that consumes time and returns after the calling
process goes out of scope.
With the use of the ref keyword, the direction information of the argument is lost. This may lead to
inadvertently updating arguments meant only as inputs. To prevent an argument from being updated,
declare it as const ref.
When the arguments are large, it can be undesirable to pass arguments by value. Pass by value creates
multiple copies of the same data, and is thus inefficient. Passing large arguments by reference does not
create additional copies every time the subroutine is called. This can be much more efficient.
Module Summary
In this module, you learned how to
● Use following features that describe more functionality with less code:
▪ return statements
▪ Default argument values
● Apply below features for reduced potential syntax and functional errors:
▪ Function output arguments
▪ Argument passing by name
▪ Argument passing by reference
Quiz
Under what circumstances can you call a If all arguments have a default value
subroutine and provide no arguments?
How do you prevent a subroutine from writing an Declare the formal parameter to be
argument passed by reference? const ref
Lab
Lab 6 Testing a Memory Module
● Model stimulus tasks to verify a memory module using subroutine enhancements.
top
5 addr
test memory
read
write
8 data_in
8 data_out
clk
Interfaces
Module 10
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Investigate the benefits and capabilities of interfaces
● Create simple interfaces
● Refine simple interfaces with ports and parameters
● Define variable direction and visibility with modports
● Define interface methods – tasks/functions declared in interfaces
mem
start
cpu
Using Interface
mem busa cpu
rdy
mode
addr
data
module top;
logic req, gnt, start, rdy;
logic clk = 0;
module memory( module cpucore( logic [1:0] mode;
input logic clk, req, start, input logic clk, gnt, rdy, logic [7:0] addr;
logic [1:0] mode, inout wire [7:0] data, wire [7:0] data;
logic [7:0] addr, output logic req, start,
inout wire [7:0] data, logic [7:0] addr, memory mem(.clk, .req, .start,
output logic gnt, rdy); logic [1:0] mode); .mode, .addr, .data, .gnt, .rdy);
... ...
endmodule endmodule cpucore cpu(.clk, .gnt, .rdy, .data,
.req, .start, .addr, .mode);
131 © Cadence Design Systems, Inc. All rights reserved.
...
To add a single connection between two modules in one level of hierarchy, you:
▪ Add a port to each module.
▪ Declare a net or variable in the parent module.
▪ Map the net or variable to the port of each module instance.
A standard communications bus may be composed of many signals, which may be connected to multiple modules.
Creating and maintaining such a bus connection can be difficult in Verilog.
SystemVerilog supports interfaces. In its simplest form, an interface is a separately declared and named group of
signals. All connections associated with a specific interface are declared and maintained in one place.
More complex interfaces can have their own ports through which they can share external nets and variables with the
connected modules. They can be parameterized to allow, for example, different bus widths, and can contain subroutines
to convert abstract data communications to low-level signal transitions.
Declaration of the bus ports and signals is time consuming and error prone. Maintaining bus connections is also
difficult, particularly if the bus is connected to many separate modules. Modifying one signal connection requires you
to:
▪ Modify the port list of every module that connects to the bus.
▪ Modify every instantiation of the changed modules.
▪ Modify signal declarations in the parent module.
A typical module may have many port connections, representing perhaps several buses. In such cases, it can be difficult
to remember which ports belong to which bus.
In this example, a connection between the memory and cpucore modules is composed of the req, gnt, start,
rdy, mode, addr and data signals. Each of these connections is declared in the memory, cpucore and top
modules.
What Is an Interface?
A SystemVerilog interface is, at its most basic
level, simply a bundle of external nets and/or B2
variables normally shared by one or more B1
modules by virtue of port connections.
B3
An interface is a new kind of design block that captures inter-module communication in one place.
Rather than declare many ports and signals at each hierarchy level and connect them, you can declare
the signals in one interface and declare ports of that interface type, which when connected to the
interface, automatically make all those individual signal connections for you.
How to
Bind Interface Instance to Module Ports of Interface Type
SystemVerilog port mapping rules also module modone ( module modtwo (
apply to interfaces. input bit clk, input bit clk,
ifa busa); ifa busb);
... ...
1. You can map by position or name.
module top;
2. You can use dot-name (.name) mapping if logic clk = 0; Creating Interface Instances
instance name matches port name.
ifa oneif(), twoif(), busa(), busb();
3. You can use dot-star (.*) mapping if all names
match.
modone mem_pos(clk, oneif);
Positional
modone mem_nam(.clk(clk), .busa(twoif));
Named modone mem_dotnam(.clk, .busa);
dot-star
135 © Cadence Design Systems, Inc. All rights reserved.
The port map syntax for ports of an interface type is identical to that for ports of any other type. You can
use ordered connections, named connections, dot-name (.name) connections, and dot-star (.*)
connections.
How to
Access Interface Items Through Module Ports Bound to the Interface Instance
For a module with an interface port, interface ifa;
you access interface items using the logic req, start, gnt, rdy;
port name as a prefix. logic [1:0] mode;
logic [7:0] addr;
wire [7:0] data;
...
endinterface : ifa
In a module that has a port of an interface type, you can reference the contents of the interface by the
port name (port_name.interface_signal_name).
How to
Access Interface Items Through the Interface Instance Identifier
For a module with an interface
interface ifa;
instance, you access interface items logic req, start, gnt, rdy;
using the instance name as a prefix. logic [1:0] mode;
logic [7:0] addr;
wire [7:0] data;
...
module top; endinterface : ifa
logic clk;
From within a module which has an instance of an interface type, you reference the contents of the
interface via the instance name (instance_name.interface_signal_name).
How to
Define Interface Ports
top
An interface can have its own ports: clk
Interfaces can contain ports similar to modules. Ports of the interface are visible to modules with ports
of the interface type as if the interface ports were signals declared within the interface itself.
When an interface with ports is instantiated in a module, the ports must be connected to local signals of
the interface, just like a module instantiation.
This allows an interface to include a signal declared (and driven) external to the interface or to map
different signals into instances of the same interface.
This example adds the clk port to the ifa interface definition. The memory module and cpucore
module no longer require separate clk ports, as they are now part of the interface. The module top
maps its local clk signal to the clk port of the interface when it instantiates the interface. The modules
can access this interface signal the same way they access any other interface signal.
Connections from mem1 to cpu1, and from mem2 to cpu2 use different interface types (pds_if and
hbus_if), but must be synchronized to the same clock signal. In module top, we pass the same signal
clk to the input port of both the bus1 and bus2 interface instances.
The connection from mem3 to cpu3 uses the same interface type as that between mem2 and cpu2, but
a completely different clock signal (clk1) is mapped to the interface clock port for the bus3 interface
instance.
How to
Define Interface Parameters
You can parameterize interface just
like a module.
1. You can define them as 2. You can also define them inside
part of interface header. interface with 'parameter' type.
module test;
fastbus #(8, 5) bus8x5(clk); // 8-bit DBUS and 5-bit ABUS 3. You can override parameters
during interface instantiation.
fastbus #(8) bus8x8(clk); // 8-bit DBUS and 8-bit ABUS
What Is modport?
To restrict interface access within a module, there are modport lists with directions declared
within the interface. The keyword modport indicates that the directions are declared as if
inside the module.
A simple interface does not contain any direction information for the interface signals. Therefore, a
module to which the interface is connected could read or write any interface signal. Typically, a bus
connection between two modules will have a direction associated with it. There will be a transmitter or
master to send data over the connection and a receiver or slave to read the information. This gives us
two views of the bus connection. You can define these views using the modport keyword.
An interface can have any number of modports, and each defines a different view of the interface
contents. A modport can restrict how connected modules use the interface contents by defining a subset
of the interface signals.
The interface mod_if declares the following modports:
▪ Modport master which defines signals a and b as input and signals c and d as output.
▪ Modport slave which defines signals a and b as output and signals c and d as input.
▪ Modport subset which defines signal a as output and b as input. Any connections to the interface
via modport subset would not be able to access signals c or d.
A module can specify which modport to use in its port list declaration or a parent module can specify a
modport when making the port connection.
This example defines an interface mod_if with master and slave modports.
▪ The busmaster module declares an interface port mbus of type mod_if and linked to the
master modport of mod_if.
▪ The busslave module declares an interface port sbus of type mod_if and linked to the slave
modport of mod_if.
▪ The testbench module instantiates the interface and maps it to the ports of the busmaster and
busslave instances.
This modport selection method works well when all instances of a module use an interface in the same
way.
In this example, the module top specifies which modport the module interface ports will use.
▪ In the instantiation M1 of busmaster, the interface port mbus is mapped to the master modport
of the interface instance busb.
▪ In the instantiation S1 of busslave, the interface port sbus is mapped to the slave modport of
the interface instance busb.
This modport selection method works well when different instances of a module definition use an
interface in different ways.
Typically, operations on interface signals, such as read or write operations, are defined as tasks in a
testbench. The tasks are then called in required order with different arguments to meet verification
goals.
One key aspect of an interface is that it can be re-used everywhere a specific bus or collection of signals
is used. When an interface is used with multiple testbenches, each testbench must have a copy of the
stimulus tasks used to perform operations on the interface signals. This makes task declarations and
calls difficult to maintain.
For SystemVerilog, you can declare the task as part of the interface. Then any module which connects to
the interface can access the task.
● Call methods through interface port or instance interface ifa (input clk);
logic req, start, gnt, rdy;
logic [1:0] mode;
logic [7:0] addr;
wire [7:0] data;
You can place subroutine definitions within the interface declaration. Connected modules can call the
subroutines using the interface port name as a prefix to the subroutine call, just like accessing an
interface signal.
This promotes the interface as a reusable block as it now contains the stimulus tasks which operate on
the interface signals.
If you are accessing an interface using modports, then you must import subroutines into the modport
to make them visible. Without the import, the subroutines are not visible to a module connected via the
modport.
Within a modport, apart from importing a subroutine from the interface to make it available to the
connected module, you can export a subroutine from a connected module to make it available to the
interface.
How to
Declare a Module Port of a Generic Interface Type
You can use the interface keyword rather
than a specific interface type in your module
declaration’s list of ports. This serves as a module memory(interface bus);
placeholder for an interface type and you can ...
select the interface when instantiating the endmodule : memory
module.
module cpucore(interface bus);
You can declare a module port of a generic ...
interface type: endmodule : cpucore
● This defers actual interface selection until module 1. Use a generic interface
instantiation. port in module definition.
module top;
● Must use Verilog-2001 ANSI-style port declaration. 2. Map Specific interface
ifa busa(); during instantiations.
interface ifa;
logic req, start, gnt, rdy; memory mem (.bus(busa));
logic [1:0] mode; cpucore cpu (.bus(busa));
...
endinterface : ifa endmodule : top
147 © Cadence Design Systems, Inc. All rights reserved.
You can use the interface keyword rather than a specific interface type in your module declaration’s
list of ports. This serves as a placeholder for an interface type. You can select the interface when
instantiating the module. This allows a module to connect to different versions of a specific interface.
These versions may implement the interface methods in different ways, or there may be versions which
contain extra logic, coverage or assertions checking. The unspecified interface is called a “generic
interface reference”. You can use it only with the Verilog named port connections.
Note: You cannot just connect a module interface port to any arbitrary interface. The interface mapped
to the port must contain declarations of the signals referenced within the module via the interface port
name.
Module Summary
Interfaces simplify design block communication.
● You can represent a number of signals as a single port
● This reduces code verbosity and promotes code reuse
● It “should” be synthesizable if there are no other unsynthesizable constructs
Interfaces raise the level of abstraction
● All declarations are contained in a single location
● They produce a much more readable design
You can define tasks and functions inside an interface
● This encapsulates the inter-module communications protocols
● Example: A “Bus Read” task that you define and execute inside an interface
With modports, you can define different views of the interface
● Example: Define master and slave views of a bus that has shared signals but different port declarations and
restricted capabilities
148 © Cadence Design Systems, Inc. All rights reserved.
Quiz
Define an interface to represent the depicted interconnect:
● All integral types in interconnect are of 4-state logic
interface quizif (input clk, rst);
type. logic as, rw, ds, da;
● Use modports to ensure correct signal flow. logic [7:0] addr,
logic [7:0] data;
● Include clk and rst interface ports.
Write code to instantiate and connect the modules and modport RD (input clk, rst, da, data, output ad
interface. dr, as, rw, ds);
addr[7:0]
as module testbench;
logic clk = 0;
rw
U1 U2 logic rst = 0;
ds quizif busa(clk, rst);
busread busmgr
busread U1 (busa.RD);
da busmgr U2 (busa.MGR);
data[15:0] endmodule
clk
rst
Labs
Lab 7 Using a Memory Interface
● Modify your memory module test to use an interface.
top
clk
Module 11
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you explore simple verification features, including the following:
● Declaring strings and string methods
● Analyzing immediate assertions
● Implementing fork-join enhancements
Strings
IEEE 1800-2012 6.16
string message = "test ";
initial begin
string is a new datatype. if (pass)
● Dynamic array of bytes message = {message, "passed"};
else
● Grows/shrinks automatically to hold contents
message = {message, "failed"};
● Initial value of "" (empty string) $display("%s", message);
It is indexed as a normal array. $display("%c", message[0]); // "t"
● With 0 as left-most character ...
It can be concatenated, replicated and compared.
It can be assigned a string literal: string repstr;
● Some additional special characters are:
▪ \f – form feed repstr = {2{"go "}}; // "go go "
▪ \v – vertical tab repstr[2] = "t"; // "gotgo "
▪ \a – bell
▪ \xdd – hex ASCII
In Verilog, you can model strings using arrays of bytes, where each byte contains the ASCII encoding
for a single character.
SystemVerilog provides a string type, which is essentially a dynamic array of byte elements
together with defined methods for string manipulation.
The default initial value of a string variable is the special value "", (empty string). Indexing an empty
string variable generates an out-of-bounds access error.
The indices of a string are numbered from 0 to N-1, where N is the number of characters in the string,
and index 0 accesses the left-most character of the string.
Verilog provides escaped character sequences for including newline (\n), tab (\t), backslash (\\) and
quote (\") characters, and any octal representation of a character, within a string literal.
To these, SystemVerilog adds the form feed, vertical tab and bell characters, and any hexadecimal
representation of a character.
String Operators
All of these string operators are just Verilog operators, so they should look familiar to you.
Equality and comparison operators are evaluated using the lexicographical ordering of the two strings,
from left to right.
Passing an out-of-bounds index to a string array returns 0 (character NUL).
String Methods
Method Description Syntax & Usage
len() Returns length of the string (length excludes any function int len();
terminating character).
aint = str.len();
putc() Replaces ith character of the string with the integral task putc (int i, byte c);
value c.
str.putc(i, c);
getc() Returns ASCII code of the ith character in the function byte getc(int i);
string.
abyte = str.getc(i);
compare() Compares strings with regard to lexical ordering. function int compare(string s);
aint = str.compare(s);
icompare() Compares strings with regard to lexical ordering function int icompare(string s);
and is insensitive to character case.
aint = str.icompare(s);
substr() Returns new string that is a substring formed by function string substr(int i, int j);
characters in position i through j of str.
mystr = str.substr(i,j);
▪ toupper(), tolower() – Case-conversion methods return the calling string with all characters
converted to upper or lower case, respectively. The original string is left unchanged.
▪ ato<bin/oct/i/hex>() – ASCII conversion methods return the integral representation of the
string. The conversion scans leading digit and underscore characters and stops the scan when an un-
recognized character is found. For example, atohex() will recognize characters a to f;
atooct() will only recognize characters 0 to 7. The conversions do not accept standard Verilog
sized or based literal syntax. They return 0 if no valid integer is found.
▪ atoreal() – ASCII conversion method returns the real representation of the string. The method
accepts all Verilog syntax that constructs a real literal, and stops at any other character. It returns 0 if
it finds no valid real literal.
▪ <bin/oct/i/hex/real>toa() – Conversion methods return the string representation of the
integral or real value.
Immediate Assertions
test.sv
module test;
IEEE 1800-2012 16.3
...
always @(negedge clk)
A1: assert ( ~(wr_en && rd_en) );
● A procedural assert statement is: ...
Immediate assertion
▪ Similar to an if statement.
▪ Ignored by synthesis.
Verilog Equivalent
● When executed, assertion verifies a Boolean always @(negedge clock)
expression: if (wr_en && rd_en)
▪ Pass if expression evaluates to 1. $display("error");
▪ Fail if expression evaluates to 0, Z, X.
● Reports error message on failure: Failure message
▪ You can change this behavior. *E,ASRTST (./test.sv,9): (time 10 NS) Assertion test.A1 has failed
● You should label your assertions:
▪ Label is used in the failure message and helps to
manage assertions.
Immediate assertions are similar to if statements. The assertion passes if the asserted expression
evaluates to 1 and fails if the expression evaluates to any other value.
When an assertion fails, the simulator must display the following minimum information:
▪ The severity level of the assertion.
▪ The file name and line number of the assertion.
▪ The simulation time at which the assertion failed.
▪ The assertion label, or if unlabelled, the scope of the assertion.
The output message can be customized – see the next slide.
The assertion label is optional, but you are strongly encouraged to use meaningful labels for your
assertions. You will typically have large numbers of assertions in your design. If you do not label your
assertions, the simulator will label them for you, and as the label is used in the failure message, it is
much easier to debug, manage and maintain large numbers of assertions with meaningful user-defined
labels.
Action Blocks
Assertions can execute statements when they always @(negedge clock)
pass or fail: rwchk: assert (~(wr_en && rd_en))
$display ("%m: success");
● Called action blocks. else
begin
● Can contain any SystemVerilog constructs. $display("read/write fail");
err_count++;
● Has verification functionality only. -> rw_err_event;
else block is executed on assertion failure: end
You can associate an action block with the assertion. In the action block, you place code to execute on
either the success or the failure of an assertion. The pass block executes if the assertion succeeds. The
fail block executes if the assertion fails. You have the option to include neither, either or both. It is very
common to omit the pass block and just include the fail block.
As the assertion is ignored by synthesis tools, only verification code can be placed in action blocks.
The assertion label can be accessed in an action block via the %m format specifier. However, as the
hierarchical pathname and label for the assertion is always included in the failure message, this is only
meaningful for the pass action block.
In the examples above, any $display output in the failure action block is printed in addition, and
after, the standard assertion failure message.
Severity Levels
● Assertion failure can be graded with severity levels:
▪ $info always @(negedge enable)
▪ $warning valchk: assert (valid)
▪ $error else
begin
o Default
$warning("invalid enable");
▪ $fatal err_count++;
o Terminates simulation end
● Severity level is reported.
● You can append output information using syntax
identical to that for $display: always @(negedge clock)
rw_chk: assert (~(wr_en && rd_en))
● This is output in addition to the standard failure else
message. $error("wr_en && rd_en");
Assertion severity is error by default. The simulator reports assertion failure as an error and should
return from execution with a nonzero error code. You can override the default severity by using these
system tasks in the action block. These system tasks can only be used in an action block. The fatal
severity has the effect of terminating the simulation. These system tasks accept the same arguments you
would use with the $display system task. Just remember that prior to your own output, you will also
see the standard assertion failure message.
The procedural block follows a standard procedure for checking protocol behavior:
▪ It synchronizes to the rising edge of ce which indicates the start of a frame.
▪ For 16 iterations, it checks for either a rising edge on clk (the expected behavior) or a falling edge
on ce which indicates a short pulse.
▪ After every triggering of the event expression, assertion SP is used to check ce is still high,
otherwise ce has fallen before 16 rising edges on clk were seen, indicating a short pulse. If the
assertion fails, we use a Verilog disable statement after the error message to terminate the current
execution of the procedural block. This is to ensure we only get one error message on a short pulse.
▪ After the loop, we check for the same event expression, but now a falling edge on ce is expected
and another rising edge on clk indicates a long pulse. Assertion LP is used to check that ce is low,
otherwise we have another rising edge on clk indicating a long pulse.
The code performs only a very basic check that ce stays high for 16 clocks. In practice, you would have
to write much more SystemVerilog code to check other properties of the protocol.
With the conventional Verilog fork-join, all forked processes must complete before the parent procedure
can continue.
SystemVerilog adds the join_any and join_none variants:
▪ With join_any, the parent procedure continues when any forked process completes.
▪ With join_none, the parent procedure continues without waiting for any forked processes to
complete.
As these variants allow the parent procedure to continue while forked processes are still running,
SystemVerilog adds statements to control forked processes from outside the fork statement.
fork wait
executed
wait fork stops further execution until d;
join_none
all forked blocks complete:
● Affects all forks in current scope. e;
wait fork; f
f; wait completes
The disable fork statement terminates all sub-processes of the procedure that executes the
statement.
Using a fork-join_any with a later disable fork allows a SystemVerilog testbench to spawn
several concurrent processes to wait for separate events, for example, separate system interrupts, and as
soon as one event is received, terminate the other outstanding processes to handle the received event.
You may need to enclose the disable fork in an additional fork...join layer to restrict its scope of
operation. The LRM allows the disable to terminate any sub-process, not only forked blocks, and this
could also include the procedural block where the disable fork is executed.
The wait fork statement causes the executing procedure to wait until all of its sub-processes have
completed.
With the combination of join_any, join_none, and wait fork, you can model complex
concurrent and serial behavior. The lower example illustrates this. The parent procedure spawns three
subprocesses (a, b and c), and when the first subprocess completes (b), spawns another subprocess (d)
with a join_none and continues with its own execution (e). Note that without timing controls, the
execution order between the parent (e) and spawned processes (d) is indeterminate. After (e)
completes, the procedure executes a wait fork to allow all spawned processes to complete (a and d)
complete before continuing its own execution (f).
task t1();
fork disable_fork
begin : a1
#1 $display("get_preamble");
end
begin: a2
#2 $display("get_header");
end
join_none
fork
begin : a3
#5 $display("get_payload"); Not desired
end
begin : a4 ● Terminates all active descendants of calling process
#6 $display("packet_timeout"); (f1)
end ● Terminates descendants of process descendants
join_any
disable fork;
$display("Processing packet");
endtask
endmodule
The disable fork statement terminates all active descendants (subprocesses) of the calling process.
The syntax for disable fork is as follows:
disable fork ; // from A.6.5
The disable fork statement terminates all descendants of the calling process as well as the descendants
of the process's descendants. In other words, if any of the child processes have descendants of their own,
the disable fork statement shall terminate them as well.
SystemVerilog provides the disable fork statement, which disables all active threads of a calling process,
including any sub-processes spawned by any of those threads. Unlike the disable statement, which
terminates the execution of a named block regardless of its relationship to the calling process, the
disable fork statement terminates only the processes that were forked by the calling thread.
The disable fork statement kills children of the currently running thread. It means the thread executing
the disable fork statement. In this example, the parent thread is enclosed by the i1 scope. It starts a child
thread, f1, then calls the t1 task.
Inside t1 task, the first fork starts the a1 and a2 threads. Then, the second fork starts a3 and a4. Since the
second fork has join_any, it waits for one of the threads to complete before executing the disable fork
statement.
The disable fork will not execute until the a3 thread completes at time 5, and only f1 and a4 threads are
still active and will be killed
fork
begin : a3
#5 $display("get_payload");
end
● Terminates all active executions of task and tasks
begin : a4
called from it
#6 $display("packet_timeout");
end ● Does not terminate subprocesses of the task
join_any
disable t1;
$display("Processing packet"); ● disable fork is replaced with disable t1
endtask
endmodule
165 © Cadence Design Systems, Inc. All rights reserved.
The disable statement terminates all active executions of the task and of tasks called from it. It does not
terminate subprocesses of the task.
In this same example, we have replaced the 'disable fork' with 'disable t1'. This only kills the active
processes of the task, which is the display statement after the 'disable t1' statement.
It cannot kill the subprocesses spawned by same task which is a4. It also cannot kill the processes of the
calling process, which is 'f1'.
If you want to do these, you need to use 'disable fork' instead of 'disable t1'.
For certain nested fork join_none/join_any scenarios, as shown, “disable fork” might still not work.
If the 'f2' block is not encapsulated, "#171 disable fork" does not disable the thread in the fork:join_none
f2 block. You might expect that it should.
To disable the fork:join_none f2 block, an additional begin:end procedural encapsulating block around
block f2 and the disable statement is required.
This encapsulating block ensures that disable fork does not kill any other child processes of the process
that spawned it.
With the encapsulation in place, thread 4 of block f2 is killed by disable fork.
Quiz
Fill in the following blanks.
string
● ________ type variables allow easy creation and manipulation of messages.
▪ Arrays are _______
dynamic , so there is no need to worry about fixed array sizes.
▪ Supported by large number of conversion and translation _____
methods , for example:
itoa() converts from decimal integer to ASCII.
o ______
atohex() converts from ASCII to Hex.
o ______
● Immediate _________
assertions simplify verification and error checking.
Action _______
▪ ______ Blocks can execute code on pass or fail.
$error while a $fatal
▪ Default severity is _______, ______ severity terminates simulation.
Clocking Blocks
Module 12
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Analyze the concepts of clocking blocks
● Use clocking blocks to
▪ Avoid race conditions when driving clock and design inputs
▪ Control sampling of design outputs
Testbench
clk clk
data
δ
data Clocking data
Stimulus DUT
Block
enab
enab
clk enab
Cycle-based
Clocking block
stimulus
adds delay timing
● Driving or sampling DUT ports on active clock edges can lead to race conditions.
● Clocking blocks are a verification construct to help avoid this:
▪ Driving testbench outputs via a clocking block adds delay (skew) to transitions.
▪ A clocking block can also sample testbench inputs with a set delay.
● Clocking blocks separate signal timing from signal function and allow stimulus to be written in terms of cycles
and transactions only.
Driving testbench outputs and sampling testbench inputs on the active edge of a clock can potentially
lead to race conditions between data and clock. Clocking blocks are a verification construct to make the
timing of drives and samples explicit and relative to a specific clocking event.
The aim of a clocking block is to:
1. Separate signal timing from structural, functional and procedural elements of a testbench.
2. Make explicit and unambiguous the timing of input drives and output samples.
3. Simplify stimulus by writing cycle-based transactions and using a clocking block to add fine-grain
timing delays. (Stimulus is further simplified by using cycle delays and synchronous events using
clocking blocks.)
A clocking block defines a set of timing, relative to a specified clock, for a set of signals. Signals are
divided into outputs to be driven into the design and inputs to be sampled from the design.
When you drive an output via a clocking block on the edge of a clock, the clocking block adds delay
(called skew) to the output transition, relative to the clock event.
Likewise, a clocking block can sample an input at a specific skew before the edge of a clock. Reading
the output via the clocking block retrieves the output value from the last sample time.
A clocking block is both a declaration and instance of that declaration. You do not separately instantiate
a clocking block. A clocking block can be declared in an interface, module or program. It cannot be
declared in a package or a Compilation Unit Scope.
A clocking block declaration defines:
▪ The clocking block event: Usually an edge on a clock signal.
▪ Signal direction: A clocking block does not declare signals, but only qualifies the direction of pre-
declared signals for the clocking block. The direction is always from the perspective of the
testbench. Clocking block outputs are design inputs, and clocking block inputs are design outputs.
Bidirectional signals can be qualified as inout, which is simply a shorthand notation for separate
input and output qualifiers on the same signal.
▪ Input and output delay (skew): Skew can be defined explicitly for each input or output, or by default
for all inputs and outputs. Default and explicit skew can be mixed, with explicit skew takes
precedence.
Outputs are delayed in the clocking block by skew delay after the clocking block event, before being
driven into the design.
Inputs are sampled by the clocking block at skew delay before the clocking block event.
● @(cb); 3 Output
data Driving
▪ Similar to @(posedge clk);
5
Timing applies only when output is driven through the clocking block. enab
Avoid mixing clocking block and regular assignment.
To apply output skew delay, the outputs must be driven via the clocking block using a nonblocking
assignment.
The drive value is stored in the clocking block, and the block waits for the clocking event. If a clocking
event occurred in the current time step, then the output skew is applied from the current time.
Otherwise, the block synchronizes to the next time step in which the clocking event occurs.
When synchronized to the clocking event, the block waits for the specified skew delay.
After the delay has passed, the signal is assigned in the Re-NBA region. See Appendix B for more
details on the SystemVerilog Event Scheduler.
Outputs with explicit zero skew (#0) are driven in the Re-NBA region of the time step in which the
clocking event occurs.
You can synchronize to the clocking block event by using the clocking block name as an event
expression (for example @(cb) ). However, to avoid a race condition with sampled inputs, the @(cb)
event is triggered in the Observed region of the time step in which the clocking event occurs. See
Appendix B for more details on the SystemVerilog Event Scheduler.
Clocking block output drive can be mixed with regular procedural assignment, but achieving the
required stimulus can be difficult. Driving outputs only via the clocking block is more efficient,
readable and maintainable. Stimulus is simplified by describing it purely in terms of clocking block
drives and clocking block events.
To apply input skew delay, the inputs must be read via the clocking block.
A clocking block automatically samples inputs at the specified skew delay before the clocking event.
These values are stored in the clocking block, and can be read at any time by reading the input via the
clocking block. Obviously, the sampled value read from the clocking block may be different from the
current value of the signal.
Inputs with nonzero skew are sampled in the Postponed region of the time step skew time units before
the clocking block event.
To avoid a race condition, inputs with explicit zero skew (#0) are sampled in the Observed region of the
time step in which the clocking event occurs. See Appendix B for more details on the SystemVerilog
Event Scheduler.
You can synchronize to a change in a clocking block input sample, e.g., @(cb.dout);, or to a
specific edge in an input or slice of input vector, e.g., @(posedge cb.dout[1]);
By reading sampled inputs via a clocking block, stimulus can be greatly simplified.
Skew can be specified with a default value or explicitly in the signal direction identifier. Explicit skew
overrides the default skew. There can be separate default values for inputs and/or outputs.
A clocking block can be declared without any default or explicit skew, in which case input skew is
#1step and output skew #0.
A skew can be defined a positive number or constant expression, which can include parameters.
Alternatively, skew can be defined as an edge (which refers to the clocking block event) or a time literal
(e.g., #1ns or #1step).
An input with skew of #1step is sampled at the end of the previous time step before the clock event,
specifically in the Postponed region. An input with skew of #0 is sampled at the same time as the clock
event, but to avoid races is sampled in the Observed region.
An output with skew of #0 is driven at the same time as the clock event in the Re-NBA region.
Clocking block skews are declarations, not procedural statements – in particular a #0 skew does not
suspend any process.
Assuming a clock period of 10ns, and given a timescale of 1ns and timeprecision of 100ps, the
timing for this example is as follows:
▪ reset and data outputs are driven 3ns after the clock event, at 8ns, 18ns, 28ns, etc.
▪ enab is driven on the negative edge of the clock, at 10ns, 20ns, 30ns, etc.
▪ dout input is sampled one simulation time step (100ps), before the clock event, at 4.9ns, 14.9ns,
etc.
Clocking blocks are intended for use with cycle-based stimulus. If you add delays to cycle-based
stimulus, synchronization can be affected and unintuitive results seen.
If clocking block output drives are executed in a time slice at which the clocking block event occurs,
then skew is added from the current time. For example, in the first waveform, we synchronize to clk
cycle 1 with @(cb), assign the drives to done and dtwo, and add skew from the current time (cycle
1).
Note that the SystemVerilog LRM is not explicit on this behavior, but most vendors conform to this
interpretation.
If output drives are executed at a time slice in which the clocking block event has not occurred, then the
clocking block waits until the next clocking block event, unless a transition occurs on the clocking
signal which matches a skew specified with that edge. For example, in the second waveform, we delay
until 1ns after cycle 2, then assign the drives. Because dtwo has a skew of negedge and a falling edge
of clk ( for cycle 2) occurs before cycle 3, dtwo is driven at the falling edge of cycle 2, whereas done
is not driven until 3ns after the rising edge of cycle 3.
In the final waveform, you wait until 1ns after the falling edge of cycle 4 before executing the output
drives. In this case, you synchronise to the rising edge of cycle 5, drive done 3ns after the rising edge
and drive dtwo on the falling edge of cycle 5.
Defining an edge skew for an output drive effectively redefines the clocking event for that output – the
skew is no longer relative to the clocking block event, but is the absolute next edge event on the
clocking signal.
● You can define multiple clocking blocks in a scope: clocking si1 @(posedge clk2);
▪ For multiple clocks, different signals, or different timing input #2 ip1;
output #2 op1;
● One block in a scope can be defined as a default endclocking
clocking block: default clocking si2 @(posedge clk2);
▪ Add default to the beginning of the clocking block input #2 ip2;
declaration output #5 op2;
endclocking
▪ Or use a default statement separate from the
declaration
● Default clocking blocks allow cycle delays. clocking si2 @(posedge clk2);
input #2 ip2;
output #5 op2;
endclocking
A scope can contain multiple clocking blocks. This may be required for multiple interfaces with
different timing or clock events, different timings for the same signals depending on the clock event
(e.g., DDR interfaces) or different timings for the same signals on the same clocking event depending
on different design modes.
One block only in a scope can be defined as the default clocking block. There are two ways to define the
default block:
1. In its declaration, using the keyword default.
2. You can define an existing clocking block to be the default.
Only one default clocking block can exist in a module, interface or program.
A default clocking block is valid only in the scope declaring it and any nested declarations.
Default clocking blocks allow the use of cycle delay constructs.
Cycle Delay
default clocking cb1 @(posedge clk);
IEEE 1800-2012 14.11 input #2 dout;
output #3 reset, data;
endclocking
A default clocking block allows cycle delays. The delays are of the clocking block event.
The number of cycles can be defined using a number or identifier or any expression which evaluates to a
positive integer. Expressions must be enclosed in brackets.
##0 is allowed, and is treated as a special case. ##0 means wait for a clocking event in this time step but
do not wait if it has already occurred.
The semantics of the cycle delay (##N) operator differ depending on how you use it:
▪ When used as an intra-assignment delay in an assignment to a clocking block signal, it refers to the
clock event of the specified clocking block. These assignments must use the nonblocking operator.
▪ When not used as intra-assignment delay in an assignment to a clocking block signal, it refers to the
default clocking block. For this situation, the compiler shall issue an error if no default clocking
block has been declared.
You can use clocking blocks to drive or, more likely, sample signals in other scopes using hierarchical
pathnames. For example, you may need to sample internal signals of the design at specific time points.
You can also use clocking blocks to drive or sample slices or concatenations of signals from other
scopes.
However, you cannot directly qualify a hierarchical signal as an input or output within a clocking block.
You must declare a clocking block virtual signal and associate it with an expression that references the
hierarchical signal. The semantics of this association are much like those for a port connection – you
can use any expression, including slices, concatenations and part selects, that you can legally connect to
a port of that direction.
Quiz
5 15 25 35 45 55
Draw the waveform for the following stimulus,
assuming the following: clk
● clk period of 10ns
X
reset
● An initial value of X for reset
initial begin
cb.reset <= 0; //at first posedge clk + 5ns
##2 cb.reset <= 1; //wait 2x posedge clk then drive @ +5ns
##3 cb.reset <= 0; //wait 3x posedge clk then drive @ +5ns
...
Lab
Lab 9 Using a Simple Clocking Block
● Use a testbench for a simple register to explore clocking block behavior.
Testbench
qout
Clocking reset
Stimulus block 8
8
qin
clk Register
Random Stimulus
Module 13
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Identify the benefits of using random data generation in verification
● Create random data using the randomize method
● Analyze implementation issues in randomization such as seeding and stability
● Use constraints to control randomization
● Randomly select statements using the randcase construct
● 4 registers:
▪ REG0, REG1, REG2, REG3
In the following double byte format…
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Let us take a look at why you might want to generate random stimulus and constrain it to meaningful
sets of values.
To do this, we will use a simplified example of an 8-bit CPU on which we want to test the defined
immediate instruction set with four internal registers.
Stimulus is sent to the CPU as an 8-bit data field, a 2-bit register field to address four registers, and a 3-
bit opcode field to specify one of the seven immediate instructions.
op_t opc;
regs_t regs; ADDI REG0 00 - FF
logic[7:0] data; ADDI REG1 00 - FF
ADDI REG2 00 – FF
initial begin ADDI REG3 00 - FF
opc = opc.first(); SUBI REG0 00 – FF
regs = regs.first(); SUBI REG1 00 – FF
repeat(opc.num()) begin SUBI REG2 00 – FF
repeat (regs.num()) begin SUBI REG3 00 - FF
for (data=0; data<255; ++data) AND1 REG0 00 – FF
@(posedge clk); ...
regs=regs.next();
end
opc = opc.next();
end
... Use nested loops to index
through 7168 combinations
A simple approach uses nested loops to generate all possible values over the ranges of the three
variables.
The outer loop iterates over the opcode values, the middle loop over the register identifiers, and the
inner loop over the range of data values. This generates 7168 separate combinations of values.
We then measure the structural and functional coverage of the 7168 patterns.
Structural metrics look good – we have applied every possible value to each variable. Functional
metrics do not look good. We have missed most of the transitions between values.
Applying all such transitions requires a much more complex set of stimulus loops!
What we really want to do is to generate random sequences of operations, registers and data, and
constrain those random combinations to meaningful combinations.
Randomization allows you to produce large amounts of stimulus with a minimal amount of code. This
lets you more thoroughly and effectively verify the design.
However, full, unrestricted randomization is rarely useful. Control over generated values is required to
maintain dependencies between different variables, exclude illegal or undesired values or just restrict
data to specific areas of interest.
With randomization constraints, you can restrict the generation and probability of random values to
specific values, ranges or areas of interest.
SystemVerilog offers two forms of randomization:
▪ Randomization of local variables (called scope randomization) covered in this module.
▪ Randomization of class properties, covered in a later module.
Pure randomization is useless for verification. Pure randomization creates different data patterns for
every simulation run. Hence if you find, debug and define a fix for an error, you cannot check the fix
because you cannot repeat the sequence of random values which originally caused the error. The
sequence of values created by multiple randomization must be repeatable to successfully debug the
design.
For verification and debug, we need a repeatable (pseudo-random) sequence. These sequences are
generated algorithmically. The algorithms are controlled by starting values called seeds. The seed is
input to the algorithm to generate the first random value. This random value then becomes the input to
the algorithm to generate the next random value, and so on. Hence, for a given seed, the sequence of
multiple random values is always the same, but the sequence can be changed simply by using a different
seed.
It is really easy to generate random values on scope variables. You simply pass the variables as
arguments to a built-in function called randomize(). The function returns an int value, which is 1
for successful randomization and 0 for failure. Typically, randomization will fail if constraints conflict
and prevent the random number generator reaching a valid solution. Every time the function is called,
random values are applied to the variable arguments.
You can only randomize singular integral variables.
The singular restriction prevents you from randomizing unpacked arrays or unpacked structures. The
integral restriction prevents you from randomizing real numbers and the variable restriction prevents
you from randomizing any nets.
The randomize() function is declared in a predefined package called std, which is automatically
and implicitly imported into every SystemVerilog module. If you wish, you can use the full declarative
path for the function std::randomize(), but the package reference is optional.
An issue with pseudo-randomization is that the current value is used to generate the next random value.
Therefore, to maintain a repeatable sequence, the randomize calls need to be made in the same order
every time. Code changes and conditional randomization can change this order, leading to a different
order of random values. This is called random instability.
SystemVerilog addresses this by creating a separate, independent Random Number Generator (RNG) for
every design element instance, class instance and process thread.
▪ Each package and design element instance (interface, module) RNG is seeded with an initial seed,
usually 1. This value can be changed on the command line (+svseed=<seed>).
▪ When created, the RNG of every static thread or class object is seeded with the next random value
of its parent design element’s RNG. This is called hierarchical seeding.
▪ When created, the RNG of each dynamic thread or class object is seeded with the next random value
of its parent thread’s RNG.
▪ The RNG of thread and class objects are stable to the extent that their creation order and
randomization order are stable.
To maintain random stability, add new threads, class objects and randomization calls after previous
threads, class objects and randomization calls.
In the example, the RNG of the module is seeded with 1, and its subsequent values are used to set the
RNGs of its procedural blocks (P1 gets 5, P2 56 and P3 3). Inserting another procedural block (P0)
before P1 will shift the seeds for all the RNGs (P0 gets 5, P1 56, P2 3, P3 42) breaking the stability.
New blocks should be added after P3 to avoid seed stealing.
Normally, process Random Number Generators (RNGs) are seeded hierarchically from the parent RNG.
However, as we saw on the previous slide, adding new processes in front of existing ones can shift
seeding and affect the random stability.
You can remove the dependency of the process RNG on the hierarchical seeding by manually seeding
each procedural block using the srandom() method via the following syntax:
process::self.srandom(seed);
where self() is a static function of the built-in process class which returns a handle to the process
making the call.
Objects of type process are automatically created when processes are created. A process is either
static, created with the always or initial keyword or dynamic, created with the fork-join
construct. You cannot create process instances or extend the process class type.
In the example, manually seeding the process P1 would allow us to add new processes before P1
without affecting the randomization within P1. You can also reseed the RNG at any time. In P2, the
first randomization uses the hierarchical seed, which could be affected by new processes added before
P1, but the RNG is then reseeded with 20 for subsequent randomizations, removing the stability
dependency on the hierarchical seed.
Manual seeding should only really be used to address issues of random stability when they arise. By
fixing the seed manually, changing the default seed with the +svseed command option no longer
affects the process. A manually seeded process will always generate the same random data.
typedef enum bit[2:0] {ADDI, SUBI, ANDI, XORI, JMP, JMPC, CALL} op_t;
typedef enum bit[1:0] {REG0, REG1, REG2, REG3} regs_t;
op_t opc;
regs_t regs;
logic[7:0] data; Relational: data between 32 and 126
int ok;
initial begin
ok = randomize(data) with { data>=32; data<=126; }; List: opc in range ADDI to ANDI or JMP or JMPC
So far our randomization has been unconstrained, with every possible value of a variable equally
probable. Constraints allow us to define a subset of values or different probability distribution. They are
rules which the randomizer must follow to generate a value.
A constraint block can be attached to the randomize method using the with clause, and enclosing
constraint expressions in curly brackets {}. Every constraint expression within the brackets must be
terminated in a semicolon.
Constraint expressions can be relational, as in the data constraint.
Constraint expressions can define a list of values and value ranges using the inside operator, as in the
opc constraint. The choice of opc is limited to the values in the list. Be careful with syntax – the inside
operator uses curly brackets to define the value list, but the range and value items within the list are
comma separated.
Constraints can also change the probability of values using the dist operator. dist is an enhancement
of inside, where the values or ranges in the list are given weights – relative probabilities – such as in
the regs constraint. Here, values REG0 and REG1 each has a weight of 2, whereas REG2 and REG3
each has a weight of 1. Hence REG0 has a probability of 2 (its relative weight) divided by 6 (sum of all
value weights) i.e., 1/3. The default weight is 1.
A constraint expression can be almost any integral expression. It cannot have side-effects such as using
assignment operators. You can use functions in constraints, but there are restrictions. Please refer to the
SystemVerilog Language Reference Manual (LRM).
logic[7:0] data;
typedef enum bit[1:0] {SMALL, MEDIUM, LARGE, XL} mode_t;
mode_t mode;
If mode is SMALL, data constraint is <100
...
ok = randomize(data) with If mode is LARGE, data constraint is >200
{mode == SMALL -> data < 100;
mode == LARGE -> data > 200;};
If mode is neither SMALL nor LARGE,
data is unconstrained
ok = randomize(data) with
{ if (mode == SMALL) data < 100; else
Same constraints
if (mode == LARGE) data > 200; }; with if/else
...
Conditional constraints allow you to select between different sets of constraints, depending on the value
of another variable.
There are two ways of defining conditional constraints:
▪ Implication, using the -> implication operator from SystemVerilog concurrent assertion syntax:
• expression –> constraint_set
▪ If-else (using the if-else construct):
• if ( expression ) constraint_set [ else constraint_set ]
Both forms are equivalent, i.e., choice of form is on syntax preference, not functionality.
If no condition in a conditional constraint block is true, then the variables in the block are
unconstrained.
Multiple constraints can be defined in each conditional branch by enclosing them in an additional layer
of curly brackets.
You can use the randcase keyword to randomly select a statement or group of statements for
execution.
For each randcase branch, you can define weights (relative probabilities). The default weight is 1.
Weights can be defined with expressions which may be either constant or variable, but must evaluate to
non-negative integral values. A weight of 0 indicates that the item shall never be selected. Fractional
weights are allowed.
Module Summary
With simple randomization features, you can:
● Generate large amounts of stimulus data from the compact code:
▪ Run longer simulations with more stimulus that more thoroughly tests the design
▪ Spend more of your own time crafting directed tests of corner cases
You can use these SystemVerilog randomization features to easily generate large amounts of stimulus
from small amounts of code.
Random stimulus is usually used in conjunction with directed testing. Randomization can test the more
easily controlled and observed functionality of the design, while allowing you to spend your time
crafting directed tests to verify the less easily controlled and observed functionality.
Lab
Lab 10 Using Scope-Based Randomization
● Modify your memory testbench to use constrained scope-based randomization.
top interface
test memory
5 addr
read
write
8 data_in
8 data_out
clk
Basic Classes
Module 14
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Identify the basic principles and benefits of SystemVerilog classes
● Declare simple classes, properties and methods
● Use explicit constructors to initialize properties
● Define static properties and methods
● Create aggregate classes to group class instances
● Apply inheritance and examine issues with constructors
● Use data encapsulation and information hiding for robust classes
where when
A+ A+
Jack John
1234 how what 2341
Data Encapsulation means that each object “hides” its data from external access.
An object typically prohibits direct external access to its data.
In the object oriented paradigm, objects interact by “sending messages.”
A message can request the receiving object to change a data value or reply with a data value.
In SV we simply term this “calling a function.”
Here, we illustrate students interacting by sending messages.
What Is a Class?
A class is a type that includes data and ● A class is a user-defined data type:
subroutines (functions and tasks) that ▪ Declared in a module, package, interface or other
operate on those data. design element
A class is a user-defined type that includes data items of different types (like a struct) but also
contains tasks/functions to access and manipulate the data items.
Variables of class types are dynamic, meaning they can be created and deleted during a simulation run,
which makes them extremely useful for verification. For example, we can use class variables (handles)
to create a random number of data items for stimulus, drive the data into the design, capture the output
data and once the data is checked and verified, delete both input and output items to save memory
space.
Classes allow the creation of generic, reusable objects that can be later extended, inherited, constrained,
overridden, enabled, disabled, and merged with or separated from other objects.
A class declaration is like any other type declaration and must be placed inside a design element
(module, program, interface or package).
As we know, class is an user-defined data type, and can contain properties and methods as shown. An
object is one instance of the class type. An object is an area of memory allocated for your program and
associated with a type. In this example, we have a class called, "my class". It has a property called
"number". It also has two methods. on the left side, you are declaring a variable C1 of type, "myclass".
Now, C1 is called as handle. We cannot access class properties or methods with this handle, since we
have not yet created an instance. In the next line, when you say, "new", then, this creates an instance to
the class. we will look more into this in upcoming slides.
o Is no longer used.
myclass c2 = new;
o Is assigned null.
...
▪ Automatic “garbage collection.” Handle
memory
--
c1 F89CD7 c1 : myclass
--
Memory pointer
A class is defined as a data type. The declaration contains data items (class properties) and optionally
subroutines to manipulate properties (methods).
To use a class, you declare a variable of the class type, called a handle. The handle contains a pointer to
a location in memory which holds the class data. An uninitialized handle has the value null. A class
instance is created by calling a special class method called new, which allocates an area of memory for
the class instance and places a pointer to that area in the class handle. The new method is called the
class constructor.
A class instance can be declared and constructed in one line or the handle declared and the instance
constructed later in a procedural statement.
In Object-Oriented languages such as C++, there is a corresponding deconstructor method, which must
be called when the instance is no longer used. In such languages, memory management (construction of
new instances and recycling of used instances) is solely under the user control.
In SystemVerilog, class memory management is handled automatically. There is no deconstructor
method and the recycling of used instances is handled by the simulator. Once created, an instance will
persist until either:
▪ The instance is no longer used. For example, if its handle has been used to create a new instance, or
the region where the instance was created no longer exists. For example, for a instance declared in a
task, the instance can be recycled when the task returns.
▪ All handles on the instance are explicitly assigned the value null. This is not necessary and rarely
done.
class StudentRecord;
int number;
string grade; New
string name;
4
string surname; “Jane”
endclass “Doe”
‘B’
The class type declaration contains the declaration of class members. Class members can be data items,
called properties or fields. Only variable declarations are allowed as class properties; net data types are
not allowed. Class members can also be tasks or functions, called methods, which manipulate the class
properties. You reference the members of a class instance by using the “dot” (.) notation.
By default, you have full and unrestricted access to all members of a class. We will look at restricting
access later in this module.
Compared to other Object-Oriented languages such as C++, SystemVerilog heavily restricts what you
can do with a class handle. Things you cannot do include:
▪ Arithmetic operations, for example incrementing and decrementing
▪ Using with any arbitrary data type
▪ Assigning the address of a variable
▪ Dereferencing
▪ Casting to any arbitrary pointer type
A simpler class handle implementation leads to fewer issues and errors in using classes.
A key benefit of the SystemVerilog Object-Oriented implementation is that memory management is
automatic. The simulator is responsible for recycling used class instances and for preventing memory
leaks – where class instances are repeatedly created without being recycled, leading to a gradual
reduction in free system memory and performance, and an eventual software crash.
SystemVerilog is closer to modern Object-Oriented languages such as Java in this respect.
Purely for convenience and readability, you can declare the implementation of a class method outside of
the class declaration using the extern keyword.
This gives the class user a quick, compact description of the class properties and methods, uncluttered
by the full implementation of every method.
To use external method declarations, you declare the method prototype, prefixed with the keyword
extern, in the class declaration. The method prototype is the first line of the method declaration,
without the method code body. This identifies the type of method (function or task), the return type (for
a function), the method name and the arguments. The full method description, including the code body,
is then declared outside the class declaration. However, as the method is a class member, not a
subroutine of the external scope, you must link its declaration to the class by prefixing the method name
with the class name, using the scope resolution operator.
External (or out-of-block) method declarations are purely for readability and have no effect on
simulation.
endclass endclass
By default, class properties initialize to their default values, e.g., x for 4-state logic types, 0 for 2-state
logic types, etc.
An explicit constructor can be implemented to initialize properties as required. An explicit constructor is
defined as a function named new in the class declaration. When you call the new constructor to create a
class instance, the new function in the class declaration is automatically called. The code body of the
new function can execute whatever code is required, for example, initialize class properties to required
values. The function does not have a return type (this is derived from the class declaration), but
otherwise follows all the normal rules of a SystemVerilog function. For example, the constructor can
have multiple input arguments with default values, if required.
A SystemVerilog class declaration can only contain a single explicit constructor. SystemVerilog does not
support subroutine overloading, which would allow multiple declarations of a task or function with the
same name (e.g., new) but different numbers or types of arguments.
Here is a class example of a frame object with three properties and three methods (including an
explicit constructor).
Class properties can be given initial values, just like module variables. In this example, the initial value
on parity is redundant because the parity is updated in the constructor.
The addr and payload properties are initialized in an explicit constructor using the add and dat
arguments. In the constructor, we also call the genpar() method to update the parity property to be
consistent with the addr and payload values. The parity generation is defined as a separate method
as this will be a common operation to be executed on the frame instances. In the declaration of
genpar(), the return type of the function is void and no arguments are passed to the method. As the
function is declared inside the class, local to the properties it needs to access, then, by convention, the
properties are accessed directly and are not passed as method arguments.
The other class method, getframe(), is a “packing” method in that it packs all the properties of the
class into a single vector for passing into the design.
Normally, each class instance has a separate, unique copy of every class property.
However, a property can be defined as static, which means that only one copy of the property exists
and this copy is shared among all instances of the class. An update to a static property from one instance
updates the property for all instances.
Here a static property is used to keep track of the number of frames created, and also to assign a unique
identity to each frame. The static property frmcount is incremented in the class constructor and
assigned to a nonstatic property tag.
When frame instance f1 is created, frmcount is incremented to 1 and assigned to f1.tag. When
instance f2 is created, frmcount is incremented to 2, which sets both f1.frmcount and
f2.frmcount to 2, and f2.tag is assigned to 2.
Static property frmcount initializes to 0 (being of type int), but could also be explicitly initialized in
the declaration:
static int frmcount = 0;
Note that we do not want to reset frmcount in the class constructor. The aim is to keep an
independent count of the number of frames created, so every time we create a frame, we want to
increment the current frame count, not set it to zero.
A static method knows nothing about the current object and so can access only static properties or other
static methods.
The advantage of a static method is that it can be called even if there are no current instances of the
class. A static method can be accessed from any handle of the class, regardless of whether the handle
contains an instance, using normal method access or from the class name using the resolution operator
(::):
<class_name>::<static_method>
This example uses a static function to return the current frame count, which is a static property.
Some coding guidelines mandate the calling of static methods using resolution operator access from the
class type name. This serves to clearly indicate the method is static, and aids readability. For example, at
first glance the getcount() call from f2 at the beginning of the initial block could be interpreted as
a nonstatic method call from an object instance. The resolution operator syntax is unambiguous as a
static method call.
It is common for class objects to be building blocks for other classes. One way to achieve this is to
declare class properties which are themselves handles of other classes. This is known as an aggregate or
composite class (an aggregation, or collection, of other class instances).
We can use this to create a class which hold multiple frames with consecutive addresses.
Here class twoframe contains two object handles (f1, f2) of the class frame as properties. The
constructor for twoframe should create the instances of frame by a call to the frame constructor.
Without this call, the frame object handles in twoframe will be null. When we create an instance
tf1 of twoframe, the constructor creates the instances f1 and f2 of frame within the tf1
instance.
To access the properties of f1 within tf1, we must chain handles:
tf1.f1.addr
This accesses the property addr of the frame instance f1, which is a property of the twoframe
instance tf1.
This is similar to a module hierarchy in Verilog. We can use a hierarchical pathname to access variables
in a module-based hierarchy; likewise, we can use an handle pathname to access members of a class-
based hierarchy.
Class Inheritance is extending a class by defining a derived class that inherits members of the base
class and may override inherited members and may add new members.
Inheritance is essential to the object-oriented paradigm. Each initial description or refinement of the
class is defined in one place and easily reused by further refinements to the class.
In an inheritance hierarchy, an inherited class is a “superclass” in object-oriented terms and a “base”
class in SV terms, and the inheriting class is a “subclass” in object-oriented terms and a “derived” class
in other language terms. Base classes and derived classes are relative terms, as a derived class can be a
base for a further derivation.
SystemVerilog supports only single inheritance. That is each class can be derived from a single base
class.
one.addr = 0;
215 © Cadence Design Systems, Inc. All rights reserved.
An instance of the sub-class inherits all the members of its parent class and, by default, they can be
referenced as if they were members of the sub-class.
A sub-class can also declare additional members and even override members from the parent class. An
instance of the sub-class will, by default, always access its local overridden member declarations (if
present) in preference to those from the parent class.
Hence using classes as building blocks for more specialized or generic classes is very easy.
The sub-class can include an explicit or default constructor, but the sub-class constructor always calls
the parent class constructor as the first line. This is to make sure the properties inherited from the parent
class are correctly initialized before the sub-class properties are processed. For example, in the code
above, the compiler automatically inserts a call to the frame constructor as the first line of the
badtagframe constructor. If you are using default constructors, this is no problem, but, in our
example, the frame class has an explicit constructor with arguments which do not have default values.
The call to the parent constructor inserted by the inheritance mechanism lacks the arguments required
and so a compilation error is reported.
In this example, the frame constructor requires arguments to set the initial values of addr and
payload. The implicit constructor call automatically inserted by the inheritance mechanism (derived
from the extends) does not have these arguments, and so a compilation error results.
By explicitly adding the constructor call, we can overwrite the implicit call and pass the required
arguments up to the frame constructor. However, in the goodtagframe class constructor, any
reference to new would access the local constructor, creating a recursive function call. Nor can we use
this as it would point to the goodtagframe instance. The keyword super allows us to access any
member of the parent class which is otherwise hidden by being overridden in the sub-class. Therefore
super.new(add,dat) allows us to call the frame constructor and pass the required arguments.
Note that the add and dat arguments must be added to the goodtagframe constructor and passed
up to the frame constructor in the super call.
An explicit super.new call must be the first line of the sub-class constructor. If we do not make it the
first line, an implicit constructor call is added automatically. This means that the sub-class constructor
cannot process any arguments before they are passed up to the parent class constructor.
Aggregation refers to a class which collects (aggregates) a number of other classes into one object.
There is no limit on the number of classes which can be aggregated, but each object handle must be
explicitly constructed. This is referred to as a “has a” relationship, in that the class twoframe has a
property f1, which is an instance of class frame and also has a property f2, which is also an instance
of frame. In traditional Verilog, this is similar to module instantiation. Any instance of class
twoframe instantiates two instances of frame f1 and f2 and to access the frame instances of
twoframe, we use a hierarchical pathname.
Inheritance is a different way of using classes as building blocks, and is the basis of object-oriented
design. Inheritance allows a class (sub-class) to extend another class (parent). We can use this to
separate the tag functionality of the frame class from the packet properties. The tagframe class
extends frame. The sub-class inherits all the base class members (addr, parity etc.) and can add
new members (frmcount, tag) or override existing members. This is referred to as a “is a”
relationship in that the sub-class tagframe is a frame class, albeit with additional members. In
traditional Verilog, this is similar to an `include directive in that the declarations from the parent
class appear as if they were declared in the sub-class.
Crucially, the sub-class is still compatible with the base class and handles of both classes can be mixed.
SystemVerilog only allows single inheritance in that a sub-class can only extend from one base class.
The base class is also called the super class. The sub-class is also called the derived class or child class.
Multi-Layer Inheritance
frame
class frame; Base class
You can layer inheritance to multiple addr
logic [4:0] addr; payload
generations. Each new level inherits logic [7:0] payload; parity
bit parity;
the members of the previous levels new
and can override any of these function new(int add, int dat); ...
members and can add new members. ...
Sub-class
endfunction
class tagframe extends frame;
...static int frmcount; tagframe
endclass
errframe is a sub-class of tagframe. int tag;
frmcount
function new(input int add, dat); tag
● Induces parity errors.
super.new(add,dat); new
● Has access to tagframe and frame members. ... ...
endclass Sub-class
● Constructors are explicit and cascaded. class errframe extends tagframe;
static int errcount; errframe
function new(input int add, dat); errcount
You can only pass arguments one level at a time: super.new(add,dat); new
endfunction ...
● super.super.new() is not allowed. ...
endclass
Inheritance can, and often is, layered to many levels. Each new level inherits the members of the
previous levels and can override any of these members and can add new members.
In this example, errframe is a specialized version of tagframe which inserts errors in the frame
data. It has its own methods and a static property errcount.
tagframe is a parent class of errframe and a sub-class of frame. frame is the parent class of
tagframe. The highest parent class at the base of the inheritance hierarchy is referred to as the base
class.
Because the base class constructor is parameterized, we must explicitly define constructors for each sub-
class and pass the parameters up the inheritance hierarchy. Note that super refers to the immediate
parent class only. There is no way to reach higher (super.super.new is not allowed).
Remember, protected members are inherited, whereas local members are not.
class stack #(parameter type sign = int); ● Parameter values can be overridden for individual
local sign data[100]; instances.
static int depth;
▪ Methods using type parameters must work for all
task push(input sign indat... ); expected type overrides.
...
endtask ● Each new parameter value effectively creates a new
task pop( output sign outdat ... ); class declaration or class specialization.
...
▪ Each class specialization has a separate set of static
endtask
endclass properties.
Classes can have parameters, just like modules and interfaces, and these parameters can be mapped to
different values for each instance of a class. For example, this would allow the user to change the size of
an array property for each instance of a class.
Verilog-2001 allows type parameters, and type parameters can also be used with classes. A class with a
type parameter is known as a template class, and typically defines some functionality (here a stack)
which can be carried out on many different types. An issue with template classes is defining methods so
they work with all combinations of expected types, including SystemVerilog types such as struct and
enum.
Each new value or type for a parameter effectively creates a new class declaration (or class
specialization). One consequence is that each class specialization has a separate set of static properties.
Note that there are alternative, and better, constructs in SystemVerilog for implementing stack-like
functionality, such as a queue dynamic array, where stack elements are of the same type, or a
mailbox where the elements can be of different types.
Module Summary
With the SystemVerilog Object-Oriented testbench features, you can:
● Declare dynamic data types:
▪ Create and destroy objects during simulation
Quiz
What is the role of a class constructor? ▪ The class constructor creates an instance and
initializes the class properties.
What is the difference between a static and non- ▪ A static method of a class can access only the
static method? static members of the class.
Lab
Lab 11 Using Classes
● Create a simple class using user-defined constructors and methods, and explore inheritance and
aggregate classes.
counter
# count : int
+ new(int)
+ load(int): void
+ getcount() : int
upcounter downcounter
+ new(int) + new(int)
+ next(): void + next(): void
Module 15
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Analyze the basic concepts of polymorphism
● Use casting to copy class instances between handles
● Analyze polymorphic class member resolution
● Create virtual methods and virtual method prototypes
● Evaluate the role of virtual classes
What Is Polymorphism?
Polymorphism allows the use of a variable of the
superclass type to hold subclass objects and to frame
reference the methods of those subclasses directly
from the superclass variable. tagframe frame
errframe
● An inherited class creates a new type.
▪ You need to easily change between sub-class types.
▪ Example: Switch to tagframe without updating the type of all
handles. Any sub-class ...can be stored in
instance ... a parent handle
● A class handle of a given type can be assigned any class
extension instance.
▪ Polymorphism.
frame c1; tagframe
▪ This introduces the concept of a handle type.
▪ Class type is used in declaration.
● It aims to look at the contents of the handle. Handle type Handle Instance
▪ Class instance held in the handle.
Every sub-class declaration defines a new type e.g., frame, tagframe and errframe are all
different type declarations in SystemVerilog. However, these types are closely related to each other
(having a common base type of frame) and we need to easily change between different sub-class types
without having to change the type of our class handles. A key feature of object-oriented design is the
ability to assign to a class handle of a specific type, any instance of a sub-class of the handle type i.e.,
we can create our design using object handles of type frame, and then assign any instance of the sub-
classes of frame to that handle. This is polymorphism.
This introduces the concept that a handle has a declaration type e.g., frame, but its contents – the
instance to which the handle points – may be of a sub-class of the handle type e.g., tagframe.
How to
Copy a Sub-Class Instance to a Parent Handle
A sub-class instance can always be directly copied to class frame;
a parent class handle. ...
function void iam();
However, by default, only parent class members are $display ("frame");
accessible from the handle: endfunction
...
● Even though it contains a sub-class instance endclass
In polymorphism, we can always directly copy a sub-class instance to a handle of a parent class type.
Here, we create an instance of tagframe in the handle t1, and copy the instance to the frame handle
f1. As frame is a parent class of tagframe, the assignment is valid. Remember that both f1 and t1
are handle pointers to memory addresses where the instances are actually stored. Therefore, when we
copy t1 to f1, we are overwriting the current contents of f1 (if any) with the address stored in t1,
which is a pointer to an instance of tagframe. This is called a reference copy.
Now f1 has a handle type of frame, but contains (points to) an instance of tagframe. However,
when we try to access class members from f1, by default we can only see members of the handle type
class, i.e., frame. Calling the overridden iam() method from f1 executes the function defined in the
frame class, even through f1 contains an instance of tagframe. Likewise, trying to access
additional properties of tagframe, such as tag, reports errors as all member access is resolved by
reference to the handle type (frame).
How to
Copy a Parent Instance to a Sub-Class Handle
It is never legal to directly assign a parent handle to a class frame;
handle of one of its sub-classes. It is only legal to assign a ...
parent handle to a sub-class handle, if the parent handle function void iam();
contains an instance of the given sub-class. $display ("frame");
endfunction
...
1. A parent-class instance cannot be copied to a sub-class handle:
endclass
▪ Unless the parent class handle contains a sub-class instance
2. Requires use of $cast: class tagframe extends frame;
...
▪ Checks that the parent handle contains a sub-class instance function void iam();
$display ("tagframe");
frame f1; endfunction
tagframe t1 = new(...); endclass
1. Copy tagframe instance to frame handle tagframe t2;
One way to access the members of a sub-class instance copied to a parent class handle is to copy the
sub-class instance back into a sub-class handle. However, although it is always legal to assign a sub-
class instance to a handle of a class higher in the inheritance tree, it is never legal to directly assign a
parent handle to a handle of one of its sub-classes. It is only legal to assign a parent handle to a sub-class
handle if the parent handle contains an instance of the given sub-class. To check whether the assignment
is legal, a dynamic cast must be used.
$cast is called with two arguments – the first is the target handle for the assignment, and the second is
the source handle. $cast checks whether the contents of the source are compatible with the handle
type of the target, and if so, it makes the assignment from source to target.
Once the sub-class instance is back in a sub-class handle, the correct members can be accessed.
Here we copy an instance of tagframe into the frame handle f1. When we access the method
iam() from f1, the call is directed to the handle class type frame. Using the cast, we can copy the
contents of f1 to the handle t2. The cast checks that the contents of f1 (an instance of tagframe)
are compatible with the tagframe handle t2. The check is successful and so the assignment
completes. We can now call the iam() method from t2 and access the tagframe implementation.
Using $cast:
Casting lets sub-class instances use resources defined for parent
classes:
IEEE 1800-2012 8.16
$cast is actually a subroutine:
Receiver
... assigned to sub1 s1;
Sender
base resource sub2 s2; Successful cast
base if ($cast(s1, b1)) identifies contents
// sub1 instance
sub1 ? else if ($cast(s2, b1))
sub2 base b1; // sub2 instance
Random selection else
of instance ... // base instance
229 © Cadence Design Systems, Inc. All rights reserved.
Casting allows us to use a base or parent class resource, such as a handle, with any instance of a sub-
class of the base or parent type. We can assign the sub-class instance directly to the base class resource,
and then extract the sub-class instance back into a sub-class handle using the cast.
In the example, we have a resource b1 (e.g., a handle) of the base class type. A Sender randomly
creates an instance of a base, sub1 or sub2 class and assigns it to b1. At the receiver, we do not know
which instance has been sent, but by casting to a handle of each possible sub-class, we can either
identify the sub-class instance or deduce it is a base instance if both casts fail.
The syntax for $cast() is as follows:
task $cast(destination_handle, source_handle );
function int $cast(destination_handle, source_handle);
The function form is preferable because if the cast fails, we can detect the failure and act accordingly.
The task form gives a runtime error with no possibility of recovery or debug.
The cast will be successful if the destination is the same type or a superclass (parent) of the source type.
Advantages of Polymorphism
frame framearr[7:0];
Polymorphism usage:
tagframe tf;
● A base class is used for a handle array declaration. errframe ef;
Polymorphism allows a variable to be assigned different classes at different times, and for operations on
that variable to be dependent on the current class it is assigned.
The variable framearr is declared as an array of type frame. The testbench is written using the array
of frame handles. framearr can then be assigned from any sub-class of frame, dynamically during
simulation.
Polymorphism is an essential part of object-oriented design. It allows a basic, generic coding
infrastructure to be written using base class handles which can later be easily extended, inherited,
overridden, enabled, disabled, and generally modified to suit requirements.
When you copy a sub-class instance into a parent class handle, then any references to class members are
resolved by checking the declaration of the handle class type.
Here we create an instance of the parent class and copy it into the base class handle b1. When we
call b1.iam(), the method call is resolved by looking in the class type of b1 i.e., the base class. So
b1.iam() returns "Base" even though b1 contains a parent instance.
Likewise, when we copy an instance of child into the parent handle p1, the call p1.iam() is
resolved by looking in the parent class. If iam() was not declared in class parent, then the base
class would be checked (as parent extends base). If iam() was not declared in base, the call
would fail to compile, even if iam() was declared in child.
By default, resolution of class member access is always according to the type of a handle, not its
contents.
This can be a serious limitation, so there must be a solution. One solution is to use casting to copy sub-
class instances back into sub-class handles, but this requires much modification to code when using sub-
classes. A better solution is to use virtual methods.
Simply declaring a method as virtual means a call is resolved according to the contents of a handle, not
the type of the handle.
Here we create an instance of the parent class and copy it into the base class handle b1. When we
call b1.iam(), the method call is resolved by first looking in the class type of b1, i.e., the base
class. In class base, iam() is declared as virtual, and this forces the resolution to go back to the
call and examine the contents of b1. Here we find b1 contains an instance of parent, so the
resolution is directed to the parent class. So b1.iam() returns Parent when iam() is declared as
virtual.
Likewise, when we copy an instance of child into the parent handle p1, the call p1.iam() is
resolved by looking in the parent class first. Again we find iam() is declared as virtual, so the
resolution is redirected to the type of the contents of p1, i.e., Child.
Only methods can be virtual, not properties. If you need to access a sub-class property from a base class
handle, you need to access the property via a virtual method.
Once a method is declared as virtual, it is automatically virtual in every sub-class. Therefore declaring
iam() as virtual in base means it is virtual in parent and child, and the virtual keyword can
be omitted for the parent and child declaration (although this can be considered bad practice).
How to
Resolve Class Method Access
class base;
With Polymorphism, when a method is function void nvirt();
accessed off a class handle, how do you ...
identify which class method is used? endfunction
Calls to a normal method resolve to the handle class, if implemented there; else the nearest higher class
where implemented i.e., compile-time resolution.
Calls to a virtual method resolve to the object class in the handle, if implemented there; else the nearest
higher class where implemented, i.e., runtime resolution.
You can use the virtual keyword to declare a virtual class. A virtual (or abstract) class type cannot be
instantiated; it only exists as a base class for sub-classes to use for inheritance. You cannot create an
instance of a virtual class, although you can declare handles (for use in polymorphism).
The implication is that virtual class declares a set of class members which can be shared by many sub-
classes, but that the set is not sufficient to be used by itself – the sub-classes must add extra members to
make the class usable.
An abstract class (and only an abstract class) can declare pure virtual methods. A pure virtual method is
a method prototype declared with the pure and virtual keywords i.e., only the first line is defined.
This identifies the method as a task, or function, and defines its name and arguments.
A sub-class which extends the abstract class must provide a full implementation of the pure virtual
method. Specifically, a sub-class instance cannot be created unless all pure virtual methods have an
implementation.
So a virtual (abstract) class can define:
▪ Methods
▪ Virtual methods
▪ Pure virtual methods
A nonvirtual class can define:
▪ Methods
▪ Virtual methods
Module Summary
Polymorphism, virtual methods and virtual classes are essential object-oriented features, which allow:
● A generic testbench to be written using base classes
● A testbench to be easily extended, inherited, overridden, and modified to suit requirements
▪ While changing very little of the existing code
Lab
Lab 12 Using Class Polymorphism and Virtual Methods
● Analyze polymorphism and virtual methods using simple classes.
counter
# count : int
+ new(int)
+ load(int): void
+ getcount() : int
upcounter downcounter
+ new(int) + new(int)
+ next(): void + next(): void
Module 16
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Define class properties for randomization
● Create random data using the randomize(), pre_randomize() and post_randomize() methods
● Select random properties with in-line and rand_mode() options
● Control random data with constraint blocks and constraint_mode()
● Analyze the order in which properties are randomized and explore how to control the order
Class properties are defined as random using either the rand or the randc modifier. Random
properties must be integral types.
rand properties have a uniform distribution, i.e., every possible value has an equal probability every
time the property is randomized. Repetition of specific values over a small number of randomizations is
possible and likely.
randc properties are random-cyclic, where every possible value must appear once, in random order,
before a specific value can be repeated. randc randomization is broken down into a series of iterations.
Within each iteration, randomization cycles through all possible values, in random order, without
repetition. Once all values have been generated, a new iteration automatically starts. The new iteration
randomly cycles through all possible values, but in a different order than the previous iteration.
Repetition of values over a small number of randomizations is possible, but these values will be from
different iterations.
At the start of an iteration, all values are equally probable. Once a value is generated, it is excluded until
the start of the next iteration.
randc generation is computationally intensive, therefore implementations can impose a limit on the
maximum size of a randc variable, but SystemVerilog dictates that a minimum of 8 bits should be
supported.
class randclass;
rand bit[1:0] p1; p1 is a random variable
p2 is a random-cyclic variable
randc bit[1:0] p2;
endclass
randclass myrand = new(); Randomizes all random
variables in a class instance
int ok;
initial begin
ok = myrand.randomize(); Can check return value
if ( !myrand.randomize() )
$display ("myrand randomize failure");
end
...
A clocking block is both a declaration and instance of that declaration. You do not separately instantiate
a clocking block. A clocking block can be declared in an interface, module or program. It cannot be
declared in a package or a Compilation Unit Scope.
A clocking block declaration defines:
▪ The clocking block event: Usually an edge on a clock signal.
▪ Signal direction: A clocking block does not declare signals, but only qualifies the direction of pre-
declared signals for the clocking block. The direction is always from the perspective of the
testbench. Clocking block outputs are design inputs, and clocking block inputs are design outputs.
Bidirectional signals can be qualified as inout, which is simply a shorthand notation for separate
input and output qualifiers on the same signal.
▪ Input and output delay (skew): Skew can be defined explicitly for each input or output, or by default
for all inputs and outputs. Default and explicit skew can be mixed, with explicit skew takes
precedence.
Outputs are delayed in the clocking block by skew delay after the clocking block event, before being
driven into the design.
Inputs are sampled by the clocking block at skew delay before the clocking block event.
Although you cannot re-declare the randomize function, you can customize randomization by
declaring either of two “callback” methods in the class. When randomize is called, the simulator
automatically searches the class for a declaration of pre_randomize and, if it finds one, executes
this before randomization. Then after successful randomization, the simulator automatically searches the
class for a declaration of post_randomize and, if it finds one, executes it.
The pre/post_randomize declarations must match the prototypes shown, i.e., they must be void
functions with no arguments.
A good use for post_randomize is to calculate parity so it is consistent with the new randomized
properties, as shown in this example.
It is considered bad practice to call pre/post_randomize directly. They should only be executed as
part of the randomize call. If pre/post-randomize needs to contain functionality which can be
called outside of randomization, then a separate method should be declared for this and called from
within the randomization callback methods.
Be careful with naming. An error in the name of the callback method will mean it is not executed when
randomize is called.
pre/post randomize are effectively virtual despite not being explicitly defined as such.
initial begin
ok = mywrap.randomize();
...
An aggregate class may contain class instance properties which themselves contain random properties.
Here the randwrap aggregate class contains an instance of randclass with two random variables,
p1 and p2.
The randomize method can push down into the randclass instance within randwrap, and
randomize P1 and P2, but only if the randclass handle (c1) is declared as rand. If c1 is not
declared as rand, properties mywrap.c1.p1 and mywrap.c1.p2 will not be randomized, even
though they are random properties.
Effectively, the rand modifier acts as a gate for randomization. If a class instance property is rand,
randomization pushes into the property to randomize its contents, otherwise the instance is skipped.
Normally, the randomize method randomizes all random properties of the class. However, you can
select specific properties to be randomized, even non-random state properties, by passing property
names as arguments to the randomize method. Once any argument is passed as an argument, then only
the arguments are randomized, and all other properties are left unchanged.
Randomization of state variables passed as randomize arguments is always as if they were declared as
rand, i.e., randomization is not cyclic. If you need cyclic randomization, then the properties must be
explicitly declared as randc.
Every random property in a class has an enable bit named rand_mode. If the mode is disabled (set to
0), then the property will not be affected by randomization, however the property value will still be used
in constraints. The default mode setting is enabled.
The mode can be changed using the built-in task method rand_mode(). The task can be applied to an
individual random property, in which case the mode is changed just for that property. Alternatively, the
task can be applied to a class instance, in which case the mode of all of the random properties in that
instance are changed. For aggregate classes, setting the rand_mode of a random property, which is a
class instance, does not change the mode of the random properties within the instance, but can affect
whether or not they are randomized.
The mode can be read using the built-in function method rand_mode(). The function can only be
applied to an individual random property and returns the current value of the mode.
You can also apply these methods to individual elements of an unpacked array and individual elements
of an unpacked structure providing that the array or structure are declared as random properties.
Example: rand_mode()
class randclass;
p1, p2 are random properties
rand bit[1:0] p1;
randc bit[1:0] p2;
bit[1:0] s1, s2; s1, s2 are state properties
endclass
The example uses the rand_mode() task call to set the mode of instance myrand of class
randclass to 0. This sets the rand_mode of myrand.p1 and myrand.p2 to 0.
The rand_mode() task is then used to set the mode of myrand.p2 to 1.
The rand_mode() function reads the mode of myrand.p2 and stores the value in state, which
will contain the value 1.
When the myrand instance is randomized, only myrand.p2 is affected.
Applying the rand_mode task or function to a non-random property (such as s1) is a compilation
error. Only random properties have a rand_mode.
Constraint Blocks
IEEE 1800-2012 18.5
Constraints apply restrictions to randomization to exclude values or change the probability distribution.
You declare a constraint block as a class member with the constraint keyword, followed by an
identifier, followed by a list of constraint items enclosed within curly braces {}. Each constraint item in
the list is terminated by a semicolon, but the constraint block itself is terminated by the closing curly
bracket } and does not end with a semicolon.
Constraint block items can be constraint expressions and statements ordering the constraint solution.
Constraint expressions can be almost any integral expression. They cannot have side effects (for
example through assignment operators). The use of functions is also restricted (see LRM for more
details).
Constraint Inheritance
IEEE 1800-2012 18.5.2 class randclass;
rand bit [1:0] p1;
constraint not0 {p1 != 2'b00;}
endclass
not0 constraint in base class
class rcx1 extends randclass;
constraint not3 {p1 != 2'b11;} rcx1 adds constraint not3 to
constraint not0 from randclass
endclass
Constraints are class members and are inherited just like all other inherited class members.
Here we declare a base class randclass with a single random property p1 and single constraint
not0 which restricts p1 to 01, 10 or 11.
Sub-class rcx1 defines a new constraint not3. When instances of rcx1 are randomized, local
constraint not3 and inherited constraint not0 will both apply and p1 is restricted to 01 or 10.
Sub-class rcx2 redefines the constraint not0 and so overrides the inherited constraint. When instances
of rcx2 are randomized, the single local constraint not0 applies and p1 is restricted to 00, 01 or 10.
Sub-class rcx3 redefines the constraint not0, but does not define an expression. This has the effect of
removing the constraint. We then define a new constraint not1. If you want to replace a constraint, it
may be more readable to remove the existing constraint by defining it as empty, and define a new
constraint with a better name. If we had not declared not1, then removal of not0 would mean there
are no restrictions on p1.
In cases such as rcx2 when you want to redefine an existing constraint, it may be confusing to use the
same name. It may be better to remove the existing constraint (as in rcx3) and then define a new
constraint with a more meaningful name.
class randclass;
rand bit[7:0] p3;
constraint c1 {p3 inside {3, 7, [11:20]};}
endclass
The inside operator allows you to define a constraint which constrains a random property to a value
in a list. The list can contain ranges as well as individual values. The inside operator uses curly
brackets {} to define the list of allowed values. Values or ranges in the list are comma separated.
An inside constraint expression can also be negated to constrain a random property to a value outside of
a list of values. Indeed, any constraint expression can be negated, by enclosing the expression in
parentheses and applying an inversion operator.
For example, constraint c2 constrains p3 to be 0, 2, 3, 4, 5, 6, 8 or 9.
● You can change distribution by defining weights for values using dist.
▪ Default weight is 1
101 to 200 each
● There are two ways to assign weight: gets a weight of 200
▪ := assigns weight to the item or every value in a range.
class randclass;
rand int p4;
constraint c3 {p4 dist {7:=5, [11:20]:=3, [26:30]:/1};}
endclass
7 has a 11-20 each has 26-30 each has
weight of 5 a weight of 3 a weight of 1/5
249 © Cadence Design Systems, Inc. All rights reserved.
Normal distribution (probability) for randomization is uniform, where every possible value is equally
likely.
You can change the distribution of values using the dist operator. This is very similar to the inside
operator in that you constrain randomization to a list of values. However, the dist operator allows you
to define weights, or relative probabilities, to each value in the list.
The default weight is 1.
There are two forms of weight assignment, and these only differ when assigning a weight to a range.
The := operator assigns the weight to every value in the range.
The :/ operator divides the weight by the number of values in the range, i.e., for a range of n values
and a weight of w, the weight of each individual value is w/n.
You can apply individual constraints to each element of an array dimension using the foreach loop.
Iterative constraints are very powerful. Consider the following constraint for the example above:
constraint c3 {
foreach ( arr[ k ] )
(k < 7) -> arr[k + 1] > arr[k];
}
This constrains each array value to be greater than the preceding element, giving you a random array
with element values sorted in descending order.
Every constraint block in a class has an enable bit named constraint_mode. If the mode is enabled
(set to 1), then the constraint block will be used by the randomize method. If the mode is disabled (0),
then the constraint block will be ignored by randomize. The default mode setting is enabled.
The mode can be changed using the built-in task method constraint_mode(). The task can be
applied to an individual constraint block, in which case the mode is changed just for that block.
Alternatively, the task can be applied to a class instance, in which case the mode of all of the constraint
blocks in that instance are changed.
The mode can be read using the built-in function method constraint_mode(). The function can
only be applied to an individual constraint block and returns the current value of the mode.
The example uses the constraint_mode() task call to set the mode of instance myrand of class
randclass to 0. This sets the constraint_mode of myrand.blue and myrand.green to 0.
The constraint_mode() task is then used to set the mode of myrand.blue to 1.
The constraint_mode() function reads the mode of myrand.green and stores the value in
state, which will contain the value 1.
When the myrand instance is randomized, only the myrand.blue constraint is used.
Applying the constraint mode task or function to a class property is a compilation error. Only constraint
blocks have a constraint mode.
When a class instance is randomized, randc properties are always randomized first, simultaneously.
Then all the rand properties are randomized together. Then the constraints are checked to see whether
the solution is correct, and if not, the process is repeated until either a solution is found or the
randomization options are exhausted. If a solution cannot be found, then randomize fails, the randomize
method returns 0 and the simulator reports a constraint violation.
Obviously, this is the conceptual view and the process is heavily optimized in the simulator.
Randomization of properties in parallel can lead to unexpected results.
For example, the constraint c1 defines that if mode is 1, then vect is zero. This may lead you to
expect mode will be one (1) 50% of the time. However, this is not seen in simulation, because both
mode and vect are generated simultaneously. The next slide describes this in more detail.
You can control the order in which rand properties are generated by using the solve... before
constraint. If you generate mode first, then it will be 1 half the time and so vect will be zero half the
time. The other half of the time, mode is zero and vect is unconstrained.
Keep in mind the following restrictions:
▪ You cannot order randc variables. The solver solves randc variables first, then ordered rand
variables, then unordered rand variables.
▪ You can only order rand variables of integral types.
▪ You must avoid circular dependencies e.g.,
• {solve a before b; solve b before c; solve c before a;}
● All solutions equally likely 0 0,0 0,1 0,2 0,3 0,4 0,5 0,6 0,7
mode
● Probability mode = 1 is 1/9 1 1,0 1,1 1,2 1,3 1,4 1,5 1,6 1,7
Excluded by constraint
Conceptual ordered solution
In the un-ordered solution, both mode and vect are randomized simultaneously. This generates 16
possible combinations for mode and vect. Then the constraints are applied and the invalid
combinations are removed – when mode is 1, any value of vect other than 0 is invalid. The remaining
solutions are all equally likely, meaning the combination of mode = 1 and vect = 0 has a probability
of 1/9.
In the ordered solution, the solve constraint requires the simulator to generate mode before vect. This
gives a 50% chance of mode = 1, from which the only possible value of vect is 0. If mode is 0, then
vect is unconstrained and there are 8 possible values for vect. As mode is generated first, the
combination of mode = 1 and vect = 0 has a probability of 1/2 .
These are conceptual explanations to aid understanding. Optimizations in the Random Number
Generation may use different processes, but will generate the same results.
Randomization Failure
module randfail; randfail.sv ● Warning on randomization failure:
▪ Identifies conflicting constraints.
class randclass;
rand bit [4:0] var1; Declarative constraint ▪ Identifies variables used in constraints.
constraint c1 {var1 < 4;}
endclass ● In batch mode, simulation continues
▪ GUI launches Constraints Debugger
int ok; Window (see lab).
Procedural constraint
randclass myrand = new();
initial begin
ok = myrand.randomize() with {var1 == 5;});
... Randomization failure warning
ok = myrand.randomize() with {var1 == 5;};
| Read green mode (0)
xmsim: *W,SVRNDF (./randfail.sv,12|24): The randomize method call failed. ...
green constraint disabled:
xmsim: *W,RNDOCS: These constraints contribute to the set of be
p1 will conflicting
01, 10, 11 constraints:
constraint c1 {var1 < 4;} (./randfail.sv,5)
ok = myrand.randomize() with {var1 == 5;}; (./randfail.sv,12) Conflicting constraints
Randomization will fail if a solution cannot be found which meets all applicable constraints. By default,
the failure will generate a warning message, not an error or fatal message. The message identifies the
conflicting constraints and the variables used in the constraints. If the list of variables includes static
(non-random) variables, then their value at the time of randomization will be shown.
The actual format of the message may vary between tool versions but the essential information is
always included.
If randomization fails for one variable, then it fails for all variables used in the randomize call, and no
values are changed.
In batch-mode, by default, the simulation generates the warning and continues. Therefore you need to
check the simulation logs carefully for failures.
In the GUI, by default, the simulation generates the warning; stops the simulation and opens the
Constraints Debugger Window showing the information on the conflicting constraints and variables
affected.
Module Summary
With these class randomization features, you can:
● Generate large amounts of stimulus data from compact code:
▪ Run longer simulations with more stimulus:
o That more thoroughly tests the design
▪ Spend more of your own time crafting directed tests of corner cases
Quiz
Which variables are
randomized in below class one;
code? randc bit bit1; three two one
rand byte byte1;
Both these are rand obj2 obj1 randc bit1
enabled with endclass
obj3.obj2.rand_mod rand int3 randc byte2 rand byte1
e(1) real3 rand int2
class two;
randc byte byte2;
rand int int2; three obj3 = new;
one obj1; int ok;
function new();
This is not declared obj1 = new; initial begin
as random, hence
obj1 properties not
endfunction obj3.rand_mode(0);
randomized endclass obj3.obj2.int2.rand_mode(0);
obj3.obj2.rand_mode(1);
class three; ok = obj3.randomize();
rand int int3; end
obj3.int3 – Disabled
hierarchically by real real3;
obj3.rand_mode(0)
rand two obj2;
function new(); Class 'two' properties
obj3.real3 – Not a
random property
obj2 = new; byte2 and int2 will be
endfunction randomized
257 © Cadence Design Systems, Inc. All rights reserved.
endclass
Quiz module m;
class myrand;
bit [1:0] s1, s2;
1. What value(s) will be printed for r1 after rand bit [1:0] r1, r2;
the 1st randomize()? constraint c1 { r1 != r2; }
constraint c2 { r1 inside {s1,s2}; }
function void post_randomize;
$display("s1=%1d, s2=%1d, r1=%1d, r2=%1d",s1,s2,r1,r2);
'0' because s1 and s2 are initialized to 0. endfunction
endclass
2. What value(s) could be generated for r2 in myrand ci = new;
the 1st randomize()? initial begin
void'(ci.randomize());
void'(ci.randomize(s1,s2));
end
r2 cannot be 0 so would be 1, 2 or 3.
endmodule
Initialize s1 or s2 to a non-zero value, or disable s1 and s2 are subject to constraints c1 and c2, we
know r1 == 0 after the 1st randomize(), so one
one of the constraints. of s1 or s2 must be 0, and the other can take any
value 0,1,2,3.
Lab
Lab 13 Using Class-Based Randomization
● Modify your memory module testbench to use constrained class-based randomization.
top interface
test memory
5 addr
read
write
8 data_in
8 data_out
clk
Interfaces in Verification
Module 17
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Analyze the concept of Transaction-Based Verification
● Identify the architecture of a typical Verification Component (VC)
● Write virtual interfaces to create a reusable module and class-based VCs using generic stimulus tasks
● Apply clocking blocks to interface declarations
Class-Based Testbench
Note: SystemVerilog provides very little class infrastructure and hence requires the UVM
verification class library which provides everything from print() methods to simulation control.
Sequence Verification
Component
register_write
... address =
Transactions ff001000
burst_write(64'h00112233);
data = 00112233
... 00 11 22 33
Signals
DUT
ff*
00 11 22 33
Each separate bus protocol or group of related signals will have a separate Verification Component (VC)
which reads and writes the signals of the bus.
A sequence defines the stimulus that the VC will drive onto the bus. This is a sequence of high-level
transactions abstracted from the protocol of the bus.
The VC takes the transactions of the sequence, one at a time, and drives them onto the signals of the
DUT with the correct protocol.
Within the Verification Component, there will be a driver, or Bus Functional Model (BFM), which
converts between the high-level transactions and the low-level signal transitions at the Design Under
Test (DUT). This conversion can go both ways. For example, the next data operation from the sequence
may depend on the response of the DUT from the last operation. Therefore, the VC needs to convert the
response from low-level signal transitions to a high-level transaction so the sequence can understand the
DUT response.
A class-based verification component needs to be able to drive stimulus on the DUT ports. The most
convenient approach is to instantiate interface instances connected to the DUT at one end, and
read/written from the verification component at the other. However we don't want to reference a specific
interface instance in the verification component class.
Firstly, this breaks reusability. If we hardware the class to drive interface instance bus1, then the user
must always use that instance name for their interface. This prevents the class from being used with a
different instance name, and would require separate classes to drive multiple instances of the interface
and DUT.
Secondly, interface instances are static. Therefore, the class is to be declared in the same scope as the
interface instance, preventing its declaration in a more convenient package.
A virtual interface is a variable that can be mapped to different interface instances (of the same type)
during simulation.
Declaration syntax is:
virtual interface <interface type> <variable name>;
The interface keyword is optional, but as the keyword virtual is used for many different
purposes, interface is recommended for clarity.
The signals of the interface can be referenced by using the virtual interface name as a prefix, instead of
a specific interface instance. The virtual interface must then be individually assigned to an actual
interface instance for every VC. A virtual interface can be defined as a class property, allowing the
dynamic class to reference a static interface instance.
By writing the VC to access RTL signals via a virtual interface, the VC can then be connected to any
instance of that interface as required, thus ensuring reusability.
Also the VC does not have to be declared local to a specific interface instance. This means the VC can
be declared somewhere more convenient like a package.
The default value of a virtual interface is null – it must be assigned before it is used.
A virtual interface can only be assigned or compared to an interface instance or virtual interface of the
same type, or to null.
A virtual interface can be declared as a local variable of a module, as an argument to a task or function
or as a class property (see next slide). Virtual interfaces cannot be declared as ref arguments, although
they behave as such.
Use of virtual interfaces promotes code reuse (write once, use often).
You can also iterate through an array of interface variables and pass each element in turn to a testbench
task, so that a single task can access multiple instances of similar design blocks, each through its own
interface instance.
DUT
VC
virtual interface. But, Virtual interfaces cannot hdata_w hdata_w
write to nets:
● As procedural assignment to a net (wire) is illegal.
...
● Bidirectional connections must be declared as wire. virtual interface hbus_if vif;
Solution 1: Dual Representation
Procedural
initial begin
Solution 1: Utilize a clocking block @(posedge vif.clk);
assignment to
wire illegal
//vif.hdata_w <= 8'hff;
vif.hdata <= 8'hff;
logic used as
dreg <= vif.hdata_w;
intermediary
Solution1 is to define two representations for an ...
Read from
interface bidirectional connection. wire OK
● wire (hdata_w) for mapping to DUT inout port and
for reading via virtual interface. interface hbus_if (input clk);
● logic (hdata) for writing from virtual interface. wire[7:0] hdata_w;
logic[7:0] hdata;
● An assign statement is needed to drive logic writes assign hdata_w = hdata;
onto wire. endinterface
Interface signals can only be driven via a virtual interface using procedural assignment. This is a
problem for bidirectional connections. They must be declared as nets (wire) which precludes driving
them via procedural assignment. One solution is for the interface to provide a way of driving the net
procedurally. To do this, we need two representations of the net connection in the interface:
1. A wire (hdata_w in the HBUS interface): This is for mapping to the inout port of the DUT in
the module instantiation and for reading the bidirectional connection, via the virtual interface, in the
VC.
2. A logic (hdata in the HBUS interface): This is for driving the bidirectional connection, via the
virtual interface, from the VC.
The interface must assign the logic variable to the wire signal in order to allow driven values from
the VC to pass into the DUT.
There is an alternative to the above “dual representation” option, and that is to use a clocking block. See
the next slide.
You can define clocking blocks within an interface, and you can then access design signals using the
timing of the clocking block. You can also access the interface clocking block via an interface instance
or virtual interface using the following notations:
<interface instance>.<clocking block>.<signal name>
<virtual interface>.<clocking block>.<signal name>
Procedural assignment to a net is allowed via a clocking block; therefore, this example is an alternative
to the “dual representation” option for driving bidirectional signals as described on the previous slide.
The specified direction of the clocking block signals is from the perspective of the testbench. Instead of
re-entering this information for the testbench modport, you can reuse the clocking block direction
information. In the modport, the direction information is replaced by a reference to the clocking block
using the keyword clocking:
modport <modport name> (clocking <clocking block name>);
Module Summary
You can verify designs at the transaction level instead of at the signal level
● This allows you to develop and debug at a higher level of abstraction:
▪ Rather than wading through a “sea of waveforms” at the signal level
Transaction patterns are defined in sequences which execute on Verification Components (VCs):
● You typically have one VC for every protocol interface on your design
You can partition test development into stimulus (sequences) and structure (Verification Component):
● This partition separates stimulus development, done by test writers, from environment development
For reusability, VCs connect to the DUT ports through virtual interfaces
This methodology is most effective for designs with well-defined interfaces
Transaction-based verification is all about performing system-level development and verification with
abstract transactions, without getting involved with an ocean of individual signal transitions.
Stimulus is defined as a pattern of transactions in a sequence, which are then executed on a Verification
Component (VC). The VC processes the individual transactions of the sequence and converts them to
and from DUT signal transitions. There can be many sequences defined for a specific VC, each focussed
on achieving a particular verification goal.
Each VC is associated with a particular signal protocol, bus or interface, and you typically have one VC
for every protocol or bus in your design. VCs for common bus protocols are usually available as
Verification IP (VIP) from Cadence® or third-party vendors.
This structure allows you to partition test development into stimulus, which deals solely with
transactions, and structure – the development of the VC.
This methodology works only as well as your design team adheres to a top-down design flow, where the
design specification is defined before it is implemented.
Transaction-Based Verification (TBV) has been used for many years. What is relatively new is the
increasing tool support, in simulators and waveform viewers, for the creation, recording and analysis of
transactions, and recently developed high-level verification languages such as e, SystemC and
SystemVerilog, which support more sophisticated transaction-based verification by borrowing from the
software world, powerful proven concepts such as object-oriented design and dynamic resource
generation.
Lab
Lab 14 Using Virtual Interfaces
● Modify your memory module classes to use virtual interfaces and connect these to multiple
instances of memories.
top
if1 memory1
test
if2 memory2
Covergroup Coverage
Module 18
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Identify the role of functional coverage in verification
● Create simple coverage models using SystemVerilog covergroups
● Refine simple coverage using user-defined bins
● Implement cross-coverage
● Apply common options for modifying coverage behavior
add;sub;divide
add typedef enum bit[1:0] add;divide;sub
sub {add, sub, divide} op_t; add;add;add;add
divide sub;sub;sub;divide
Code coverage Functional coverage
Code coverage can measure the number of times a line, statement, block or branch has executed by a
given simulation run. More advanced code coverage can measure how many times a given subterm
determines whether an expression is true.
For example, assume the following code is given:
if (a || b || c) ...
Code coverage could measure the number of times the expression was true due to a, b, c or any
combination of these terms.
Code coverage is automatic and can be simply enabled with a simulation option.
Code coverage does not automatically check transitions between design states as the transition space is
extremely large.
Transition or temporal checks are important. If you know that two consecutive sub-operations
followed by a divide has caused an overflow error on previous designs, you need to check that
stimulus has been applied to the current design. This is where user-directed functional coverage
becomes imperative.
Functional coverage, as the name suggests, checks whether the test exercises the functionality of the
design. You identify the critical combinations and sequences that should be exercised to verify the
design functionality.
SystemVerilog offers both data-oriented functional coverage and control-oriented functional coverage.
This module focuses on data-oriented functional coverage. Later modules introduce SystemVerilog
Assertions (SVA), which provide control-oriented functional coverage.
SystemVerilog Assertions (SVA) are covered in more detail in a separate training class.
Unlike structural coverage, functional coverage is not automatic. You must define a coverage model
using SystemVerilog constructs to capture the value changes and signal transitions to confirm how
thoroughly the design is being tested. A coverage model is usually developed as part of the design test
plan.
Working with the design specification, you develop a test plan to define the following:
▪ How to test the design features (stimulus to be applied).
▪ How to confirm whether a given test passes (checkers or assertions).
▪ How to confirm whether the test is successfully applied (coverage).
Based on the test plan, you can define a SystemVerilog data-oriented functional coverage model,
capturing the data values and value transitions to confirm how thoroughly the design is tested. During
the simulation run, the simulator counts the value and transition occurrences and saves them in a
database. Separate analysis tools allow you to interpret the coverage post-simulation.
Each coverpoint in a covergroup creates a series of counters, called bins, which are stored in a coverage
database. When coverage is sampled, the bin corresponding to the variable value is incremented.
By default, a coverpoint creates a single bin for every value in the variable range. These are called
automatic, or implicit, bins and are named using the identifier auto and the value for the bin. There is a
configurable limit on the number of automatic bins created. The default limit is 64.
For an enumerated coverpoint, there is one bin for each valid value. For an integral coverpoint variable,
the number of automatic bins is at most 2M where M is the number of bits required to represent the
variable. When 2M is greater than the default limit, the values are distributed as evenly as possible, with
the last bin getting any extra values. Later slides show how to define explicit bins and how to change the
default limit for automatic bin creation.
One way of optimizing and managing coverage information is to define explicit bins to track only a
subset of variable values or to group related values into the same bin. For example, instead of tracking
every address value individually, it may be sufficient to track address ranges, and have a bin for all the
small, medium and large address values sampled.
To define explicit bins, the semicolon at the end of the coverpoint is removed, and a series of bins
clauses are added, enclosed in curly braces {}. Note that each bins clause is terminated with a
semicolon inside the braces, but there is not a semicolon after the closing brace } at the end of the
coverpoint.
Each bins clause contains the bins keyword, a bin name and a list of values or value ranges
corresponding to the bin. The list is a comma-separated series of individual values or ranges. For ranges,
the maximum value only can be defined using $.
With explicit bins, only the values explicitly defined in the bins clause list are tracked. Coverage for
unlisted values is not collected.
A scalar bin is one bin that counts occurrences of any of the values in its list.
A vector bin is an array of bins. Using the unconstrained array declaration [] a separate bin is created
for every unique value in the list.
Coverpoints may have multiple bins clauses defining both scalar and vectored bins. The options are as
follows:
▪ illegal_bins specifies a bin for illegal values of the coverpoint variable. Once a value is
defined in the list for illegal bins, it is automatically excluded from all other bins clauses in the same
coverpoint, even if explicitly listed. The simulator issues an error when a sampled variable has an
illegal value.
▪ ignore_bins specifies bins for ignored values. Once a value is defined in the list for ignored
bins, it is automatically excluded from all other bins clauses in the same coverpoint, even if
explicitly listed. Illegal bins takes precedence over ignore bins.
▪ A scalar bin is one bin for all the specified values in the list.
▪ For a vector bin of unconstrained size, each unique value in the list has its own bin. The standard
implies this to mean duplicate values are not retained. Illegal and ignored values are removed after
the values are distributed. Here, that results in an empty bin (e[0]), which is removed.
▪ For a vector bin of a specified size, the values are distributed as evenly as possible by order of
appearance in the list, with the later bins getting any extra values. Duplicate values are retained, so
can show up in multiple bins. Illegal and ignored values are removed after the values are distributed.
▪ The default keyword specifies bins for values that do not appear in any other bins. You cannot
use the illegal_bins or ignore_bins keywords with default.
typedef enum bit[2:0] {ADDI, SUBI, ANDI, XORI, JMP, JMPC, CALL} op_t;
typedef enum bit[1:0] {REG0, REG1, REG2, REG3} regs_t;
c1: 7 explicit vectored bins named
op_t opc;
c1.op[ADDI] to c1.op[CALL]
regs_t regs;
logic[7:0] data;
typedef enum bit[2:0] {ADDI, SUBI, ANDI, XORI, JMP, JMPC, CALL} op_t;
You can track cross-products of: typedef enum bit[1:0] {REG0, REG1, REG2, REG3} regs_t;
op_t opc;
● Coverpoints within the covergroup. regs_t regs;
● Other scope variables.
c1.auto[CALL]
c1.auto[ADDI]
c1.auto[SUBI]
c1.auto[ANDI]
c1.auto[XORI]
c1.auto[JMPC]
covergroup cg @(posedge clk);
c1.auto[JMP]
▪ Creates implicit coverpoint. c1: coverpoint opc;
▪ Participates in cover cross. c2: coverpoint regs;
opcxreg: cross c1, c2;
▪ No separate coverage for variable.
endgroup
Use the cross keyword and provide a:
● Cross name (optional). c2.auto[REG0]
You can declare cover crosses of two or more already declared coverpoints within the covergroup and/or
other integral variables. SystemVerilog automatically creates implicit coverpoints for cover cross
variables which do not have a coverpoint. It does not report coverage data separately for implicit
coverpoints.
Cross coverage effectively creates a multidimensional matrix of bins. By default, one automatic bin is
created for every cross-combination with no limit on the number of bins created. In the example here,
with two coverpoints, cross coverage is a 7x4 2-dimensional bin matrix containing 28 separate bins.
Now you can see coverage showing the number of times each specific opcode was used with each
specific register.
This example declares a cover cross of one variable (a) and two previously declared coverpoints (bcp,
ccp).
▪ Variable a has four possible values for which four implicit bins are automatically created
(a.auto[0] to a.auto[3]).
▪ Coverpoint bcp has one explicit scalar bin (bcp.b1) and one explicit vector bin containing three
elements (bcp.b2[13] to bcp.b2[15]), for a total of four bins. Note that the vectored bin
bcp.restof[] is not included in cross coverage as it uses the default keyword.
▪ Coverpoint ccp has two possible values for which two bins (ccp.auto[0] and ccp.auto[1])
are automatically created.
The cover cross thus has 32 automatic bins, one for each combination of coverpoint bins that make up
the cross.
Defining a Cover Cross Bin (Explicit Cross Bin and Select Expressions)
CALL
ADDI
SUBI
ANDI
XORI
JMPC
JMP
SystemVerilog by default automatically creates a single bin for every product of the cover cross. It does
not include any coverpoint bins declared with the default range or any declared with
illegal_bins or ignore_bins.
You can explicitly declare cover cross bins enclosed in curly braces ({}) immediately after the list of
coverpoints to cross. As with coverpoint bins, each cross bins clause in the list is separately terminated
with a semicolon, but the cross declaration line itself is terminated with the close curly brace }, not a
semicolon.
In explicit cross coverage bins, you use the binsof keyword to select which coverpoint bins should
participate in the cross. You can filter the bins selected to intersect with a list of values or value ranges
using the intersect keyword. You can then perform conjunction, disjunction and negation
operations on the resulting bin selections.
The example declares the cover cross bin x1 to include the bins of coverpoint c1 that intersect with
range ADDI to XORI, and to include the bins of c2 which intersect with the values REG0 and REG3.
Therefore x1 is a single cross coverage bin which is incremented for any combination of logical
opcode (ADDI to XORI) on registers REG0 or REG3.
JMPC
CALL
JMP
}
c2 endgroup
REG0
REG1
REG2
x3 – select all bins not of REG2 or REG3, but
REG3 ignore all JMP or JMPC bins (x4)
As with coverpoint bins, you can declare cover cross bins to be illegal or to be ignored. Ignored bins are
removed from any other cover cross bin in the same cross. Illegal bins are also removed from any other
cover cross bin in the same cross. In addition, the simulator must issue an error upon the occurrence of
any illegal cross-product.
This example declares the cross coverage bin x3 to exclude the bins of coverpoint c2 that contain the
values REG2 and REG3. The cross coverage ignore bin x4 removes all bins of c1 which contain the
values JMP or JMPC from any bins in the cross opxr.
Therefore, x3 is a single bin which increments for any opcode other than JMP or JMPC on REG0 or
REG1.
SUBI
ADDI
ANDI
XORI
CALL
endgroup
c201
REG0
One bin –
REG1 opxr.<opnj,rg01>
Select all bins not of REG2 or REG3, but
ignore all JMP or JMPC bins
Cross coverage expressions can become complex and hard to interpret. An alternative to such complex
expressions may be to define multiple coverpoints which select the ranges or values required, and
simply cross these coverpoints directly.
Remember you can have as many coverpoints as you wish on the same variable, as long as each is
uniquely named.
In the example above, we define a coverpoint c1nj which creates a single bin, opnj, for all the
opcodes except JMP and JMPC. We also define a coverpoint c201, creating a single bin, rg01, for
registers REG0 and REG1. Then we can simply cross c1nj and c201 to create a single cross coverage
bin opxr.<opnj,rg01>. Note that this is a single bin – using vectored bins in the coverpoints would
generate multiple cross coverage bins if this was required.
The disadvantage of this approach is that we have individual coverage on all the extra coverpoints. If
these coverpoints are purely to help create cross coverage and this individual coverage is meaningless,
we may need to filter away individual coverage by, for example, setting weight option to zero (see
covergroup options).
● The cover group definition can access any class function new();
properties – even if protected or local. cg1 = new();
2. Construct the cover group instance. endfunction A class covergroup
instance is not
3. Define cover group sampling. endclass separately named
● For a design or test component class object, define a covclass one = new();
sampling event.
initial begin
● For a data class object, upon communicating the data ...
object, call the cover group method sample(). one.cg1.sample();
When a covergroup is declared inside a class, the syntax to create a covergroup instance is different than
that of a module covergroup.
In a class, the covergroup is declared as normally, but you do not create a property of the covergroup
type to create an instance. In a class, the instance is created by calling new directly off the covergroup
type name.
Each instance of the class will track coverage separately.
With class-based coverage, sampling is more likely to be manual, either by calling sample() directly
from the covergroup of the class instance, or by defining a method to call sample (see later for list of
coverage methods).
typedef enum bit[2:0] {ADDI, SUBI, ANDI, XORI, JMP, JMPC, CALL} op_t;
op_t opc;
1 transition
covergroup cg;
c1: coverpoint opc {bins adsu=(ADDI
A class => SUBI);
covergroup instance 1 sequence
bins suan=(ADDI
is not => SUBI => ANDI);
separately named transition
bins su3= (ADDI,SUBI => ANDI); }
endgroup
Coverpoints can also track transitions: (ADDI => ANDI), (SUBI => ANDI)
Transitions are enclosed in parentheses and use => to specify transition. Transitions can be defined as
being between specific values (ADDI => SUBI). The bin will be incremented if the variable had the
value ADDI on the last sample event, and SUBI on the current. Transitions can be longer than 2 values
(ADDI => SUBI => ANDI). Arrays or ranges of values can also be used to indicate multiple
changes. For example, (ADDI, ANDI => JMP, JMPC) defines 4 transitions: (ADDI => JMP),
(ADDI => JMPC), (ANDI => JMP) and (ANDI => JMPC).
Transition bins can also use sequence syntax similar to that used in SystemVerilog Assertions.
Specifically, repetition operators can be used to indicate repetition of a value over several sample
events.
Syntax and rules for transition coverage can differ slightly than for value-based functional coverage.
Please see the SystemVerilog Language Reference Manual (LRM) for more details.
▪ The covergroup construct has a number of built-in options to control the behavior of covergroups,
coverpoints and crosses. This information is usually passed into the coverage database for the
analysis tool to act upon.
▪ There are two types of option:
• Type options: These options affect every instance of a covergroup, similar to static properties of
a class. They are set with type_option and can be embedded in coverpoint or cross
definitions, or set externally using a scope resolution operator (::) just like a static method call.
• Instance options: These options can affect the individual instance of a covergroup. They are set
with the instance option and are usually applied procedurally to specific instances of
covergroups. They can also be embedded in coverpoint or cross definitions to affect every
instance or to redefine default values.
weight, goal and comment options, which are set at the covergroup level, do not affect the values
set at the coverpoint or cover cross level. You can also set weight, goal and comment options as
instance-specific options. The next slide describes the instance-specific options.
▪ Set weight to modify the relative weight of the coverpoint or cover cross when calculating the
enclosing covergroup coverage metric, and the relative weight of the covergroup when calculating
the overall coverage metric, for a type-based coverage report.
▪ Set goal to modify the target goal for the element. The standard does not state how the vendor
should use this field.
▪ Set comment to provide a string information for the coverage report.
▪ Set strobe to defer sampling to the postponed region at the end of the time slot, where
$monitor and $strobe system tasks execute (see Appendix B).
You can set instance-specific options within a covergroup definition, and except for the
per_instance option, you can also set them for each individual instance of the covergroup.
Instance-specific options other than weight, goal and comment that you set at the covergroup level
provide new default values for coverpoint and cover cross options.
▪ Set name to provide a name for the covergroup instance. The simulator generates a unique name for
each covergroup instance for which you do not supply a name.
▪ Set weight to modify the relative weight of the coverpoint or cover cross when calculating the
enclosing covergroup coverage metric, and the relative weight of the covergroup when calculating
the overall coverage metric (instance-based coverage report).
▪ Set goal to modify the target goal for the element. The standard does not state how the vendor
should use this field.
▪ Set comment to provide a string for the coverage report.
▪ Set at_least to modify the hit count for considering a bin covered.
▪ Set auto_bin_max to modify the maximum number of automatic bins.
▪ Set cross_num_print_missing to modify the number of uncovered cover cross bins that
must be saved to the coverage database and printed in the coverage report.
▪ Set detect_overlap to have the simulator issue a warning when the values of two coverpoint
bins overlap.
▪ Set per_instance to track coverage data for each covergroup instance as well as each
covergroup type. You may want to track data on a per-instance basis if you differently parameterize
each covergroup instance.
Method cg cp cc Description
function void sample() ✓ Force an immediate sample
These are methods of the covergroup base class that you call for specific covergroup instances. The
get_coverage() method is a static method that you can also call for the covergroup type using the
scope resolution operator.
▪ Use sample() to force an immediate sample.
▪ Use get_coverage() to obtain the current coverage percentage for all instances of the
covergroup type. If you provide the optional reference arguments, the simulator places the covered
bin count and total bin count in the referenced variables.
▪ Use get_inst_coverage() to obtain the current coverage percentage for only the one specific
instance of the covergroup type.
▪ Use set_inst_name() to provide a new instance name for the covergroup instance.
▪ Use start() and stop() to start and stop collecting coverage data for the specific covergroup
instance.
Module Summary
SystemVerilog data-oriented functional coverage with covergroups offers:
● Specification of the sampling event
● Specification of variables to sample:
▪ Automatic value bins
▪ Explicit scalar, vector, ignore, illegal and default value bins
▪ Explicit bins for transitions between values
● Specification of cross-products:
▪ Automatic and explicit binning of cross counts
▪ Filtering and combining cross-product bins
Quiz
How many coverpoint bins are there in each of the
cross c bins? What are their names?
Solution
Labs
Lab 15 Simple Covergroup Coverage top
5 addr
● Apply covergroup coverage to the memory testbench. test memory
read
write
8 data_in
8 data_out
clk
Module 19
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Use the following array constructs:
▪ Dynamic arrays
▪ Associative arrays
▪ Queues
Dynamic Arrays
Dynamic array of 8-bit logic
IEEE 1800-2012 7.5
logic [7:0] dynarr[];
Use a dynamic array when the array size changes during the simulation. Declare the dynamic array by
leaving an unpacked dimension unsized. Create, and re-create, the array during run time using the new
operator. As an argument to the new operator, you can provide an existing array of the same type to
initialize the new array. Dynamic arrays have the size() and delete() methods, as well as the
standard array manipulation methods and standard array query system functions.
This example declares a dynamic array to store 8-bit logic vectors. During run time, it allocates space
for 32 elements. In a loop, it writes data to a memory component and stores the data in the dynamic
memory. In a second loop, it reads data from the memory component and compares it to the data in the
dynamic memory. It deletes the dynamic memory when finished with it.
Effectively, we use the dynamic array as a scoreboard, to store the data we write to the memory, and
then check the data read from the memory against the stored write data in the array.
Associative Arrays
IEEE 1800-2012 7.8
Use an associative array when the data space is unbounded or sparsely populated.
● Declare the array by specifying a type instead of a size for its one dimension.
▪ The key can be any nonreal type for which the equality operator is defined.
▪ Array elements do not exist until you assign to them.
▪ Array elements are “pairs” of associated key (address) and data values.
▪ Elements are stored by key, ordered by key – see slide notes*.
Use an associative array when the address space is unbounded or sparsely populated. Declare the
associative array by specifying a type instead of a size for its one dimension. The key can be any type
for which a relative order can be determined, that is, any type to which SystemVerilog can apply a
relational operator. An associative array is dynamic and elements do not exist until you assign them,
therefore the associative array is, by default, empty at the start of simulation.
The address or index of an associative array is known as a key. When you write to an associative array,
the key and its data are stored as a pair in the array. Subsequent writes to the array create new pairs
which are ordered in memory according to the index type.
For an integral type, ordering is numeric. For a string type, ordering is lexicographical. For a class type,
ordering is deterministic but arbitrary.
Reading an index value that has not yet been written returns the default value for the array data type.
The key can be wildcard (using *) to permit indexing by any integral type. It is recommended to avoid
using a wildcard key as it affects optimization (and hence performance) and can lead to confusing
results if different data types are assigned.
*- 1800-2012-7.8.5 - If the relational operator(<,>,==) is defined for the index type, the ordering is as
defined in the preceding clauses. If not, the relative ordering of any two entries in such an associative
array can vary, even between successive runs of the same tool. Index<5, index==10…etc.
initial begin
repeat (32) begin
success = randomize(rand_a, rand_d); Random data generation
write_mem (rand_a, rand_d);
assoc[rand_a] = rand_d;
Associative array assign
end
This example declares an associative array to store 8-bit logic vectors with keys of the int type. In a
loop, it randomizes address and data 32 times, writes the data to a memory component, and stores the
address/data pairs in an associative memory. As the address vector is only 5 bits wide, it is very likely
that some addresses are written multiple times. In a second loop, the example iterates through the
potential address space. For each address, if the associative array has an entry for that address, it verifies
that the memory component has the correct data, then deletes the entry for that address. Deleting entries
individually is inefficient, but it allows us to verify that all locations have been checked by printing the
final size of the array.
initial begin
repeat (32) begin Random data generation
success = randomize(rand_a, rand_d);
write_mem (rand_a, rand_d);
assoc[rand_a] = rand_d; Associative array assign
end
This algorithm is more efficient. It utilizes associative array methods to iterate through only the
assigned entries. It deletes the entire associative array at once when finished with it.
Queues
IEEE 1800-2012 7.10
● A queue supports access to all its elements as well as insertion and removal at the beginning or
the end of the queue.
● Each element is identified by a number defining its position in the queue.
▪ 0 represents the first location.
▪ $ represents the last location.
Use a queue when the insertion and extraction order are important. Declare a queue by using the dollar
($) character as the size of its one dimension. You can optionally limit the queue size by appending a
colon (:) followed by a constant expression after the dollar character. You can use the $ to reference the
end of the queue and 0 to reference the start.
If a write to a bounded queue would exceed the maximum queue size, then a warning is issued and the
write is discarded.
Queue Methods
Queue methods support inserting and extracting elements. This example illustrates the use of several of
the methods.
initial begin
q_int = {0,q_int}; // {0} push_front
q_int = {q_int,1}; // {0,1} push_back
q_int = {2,q_int}; // {2,0,1} push_front
q_int = {q_int[0],3,q_int[1:$]}; // {2,3,0,1} insert
q_int = {q_int[0:2],4,q_int[3]}; // {2,3,0,4,1} insert
q_int = {q_int[0:1],q_int[3:4]}; // {2,3,4,1} delete
q_int = {q_int[0:1],5,q_int[2:3]}; // {2,3,5,4,1} insert
data = q_int[$]; q_int = q_int[0:$-1]; // {2,3,5,4} data = 1 pop_back
data = q_int[0]; q_int = q_int[1:$]; // {3,5,4} data = 2 pop_front
while (q_int.size() > 0) begin // checking queue size
data = q_int[$]; // loop executes 3 times pop_back
q_int = q_int[0:$-1]; end
...
end
A queue is an array, so if you really want to, you can still use unpacked array indexing and
concatenation to insert and extract elements. As this example illustrates, you probably don’t really want
to. The queue methods are much more friendly.
● Locator
▪ Search array for elements or indexes that satisfy an Locator Method Example
expression. ● Extract all elements from array_int greater than 5:
o Attached using with. q_int = array_int.find with (item>5);
▪ Return a queue of those elements or indexes.
Ordering Method Example
● Ordering ● Sort q_int in ascending order:
▪ Reorder an array. q_int.sort;
▪ Cannot be applied to associative arrays.
Reduction Method Example
● Reduction ● Extract xor reduction of all elements in q_int:
▪ Reduce an array of integral values to a single value. var_int = q_int.xor;
The locator methods return a queue of either indexes or elements. The return queue type is of the same
time as the array index or element.
The ordering methods affect the array to which they are applied directly.
The reduction methods return a single integral value.
Array locator methods apply to any unpacked array except associative arrays using the wildcard index
type. The min(), max(), and both unique methods further require that relational operators be defined
for the expression to be evaluated.
A with clause allows you to define an expression for the array method. The expression uses an iterator
to refer to the current array element when processing the array. You can define your own iterator or use
the default item identifier.
For example, the following are equivalent:
q_int = array_int.find with (item>5);
q_int = array_int.find(i) with (i>5);
Within the with expression you may refer to the current element’s index by using the iterator’s
index() method and passing to it the dimension number for which you want the current index. The
dimension number defaults to 1, that is, the first dimension.
string qstr[$];
initial begin
qstr = {"A","D","C","B","E"};
qstr.sort; // {"A","B","C","D","E"}
qstr.reverse; // {"E","D","C","B","A"}
qstr.shuffle; // {"D","C","E","A","B"}
...
Random shuffle
Array ordering methods apply to any unpacked array except associative arrays. The sorting methods
further require that relational operators be defined for the expression to be evaluated for sorting
purposes. As this expression is by default the array element itself, for some array types you will want to
specify some other appropriate expression.
Method Description
sum() Sum of expressions
product() Product of expressions
and() Conjunction of expressions (bitwise)
or() Disjunction of expressions (bitwise)
xor() Parity of expressions (bitwise)
Module Summary
Features of SystemVerilog arrays allow efficient and flexible definition, storage and access to large
test data sets:
● Dynamic arrays are useful for contiguous data which varies in size during simulation
▪ Track an undetermined number of dynamic objects
● Queues are useful where insertion and extraction order are important
▪ Synchronize data using FIFO and LIFO (stack) mechanisms
Quiz
Lab
Lab 17 Using Dynamic Arrays and Queues
● Implement a scoreboard using:
▪ Dynamic Array
top
▪ Associative Array addr
5
▪ Queues test memory
read
write
8 data_in
8 data_out
clk
Module 20
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Gain an overview of Assertion-Based Verification (ABV)
● Discuss who writes assertions and for what purpose
● Describe the benefits and issues of using assertions
What Is an Assertion?
An assertion is a directive to an EDA
tool to verify that a property is always
true.
Testbench
● A property is a description of design behavior.
● We instruct the tool what to do with the property A A
A A A
using verification directives:
▪ assert, cover, assume, restrict
B1 B2
● An assertion is a check that a property is always
true. DUV
● During verification, assertions continually observe: HDL
▪ If a specific condition occurs, or
▪ If a specific sequence of events occurs.
● Assertions offer a way to significantly enhance
productivity for designers:
▪ Find bugs earlier and more easily.
An assertion is a directive to an EDA tool to verify that a property is always true (the terminology is that
it “holds”). The simulator will report any failure of that design property to hold. Stating the assertion is
the easy part. Crafting useful design properties is more difficult. However, it is far easier than debugging
a broken system without the help of assertions.
The graphic illustrates where you can place an assertion. Note that you can place them on design inputs
and design outputs and design interconnect and in the design blocks, as well as in a testbench.
The verification directives assume and restrict are used only for formal verification.
Cover and assert can be used in simulation or formal verification.
● Writing assertions helps designers to better understand their design and do more debugging
You can place assertions anywhere in your design hierarchy, enabling you to monitor design behavior
locally and highlight problems as soon as they occur at the source of the problem. Conventional
testbenches need to propagate a problem to the design outputs to detect it, and then you have to trace the
error back through the design, and probably back through time as well, to determine the source.
Assertions have the potential to detect problems during the early stages of testbench development, when
the testbench might not yet be sufficiently robust to even propagate a problem to the outputs.
You can have the simulation terminate upon failure of critical assertions, thus permitting other projects
to use the compute cycles that would otherwise be wasted.
Writing design properties encourages you to think more clearly about your design. This itself can
highlight problems before you even finish the property. Overall verification efficiency is improved by
designers producing better designs to begin with.
Your embedded assertions travel with your design as it is used in different projects, warning against
unexpected or incorrect use of your design block.
Your embedded assertions also travel with your design from tool to tool, for example, from simulation
to formal verification, avoiding duplication of the verification effort.
Implementation has properties the RTL When Read and Write pointers both 0,
RTL Designer Functional Property
designer intended it to have. the FIFO indicates empty.
A system architect develops abstract properties that represent the desired behavior of the system at the
functional level. At this point, the actual implementation is not known.
The block designer develops objective properties that represent their intended behaviour of the system
at the RTL level.
As an IP designer, the RTL designer will want to embed input assertions in the design to detect future
misuse of the design.
The verification engineer focuses on design functional coverage points, which should include that the
testbench actually exercised the asserted properties. Thus, functional coverage is a fundamental part of
an Assertion-Based Verification methodology.
The most prominent issue with the Assertion-Based Verification methodology is that you, the user, must
craft the design properties. It is easy to miss nuances of design behavior, some corner cases, that really
ought to be checked. There is also the issue of “how do you know when your property set is sufficient”.
The simulator can check only those properties that the testbench exercises, so testbench quality is
critical. This demands coverage metrics, which help to solve the “how do you know when you have
tested enough” question, but do nothing for the “how do you know when your property set is sufficient”
question.
DUV
● Assertions monitor and report:
▪ Expected behavior
▪ Forbidden behavior Arbiter
● Assertions are used by: PCI
Bus
▪ Static verification tools
o No test vectors, formal (mathematical) proof
FIFO FSM
▪ Dynamic verification tools
Processor
o Dependent upon simulation test effectiveness (as AHB
measured using functional coverage) Bus
● ABV also encompasses SVA constructs for
defining functional coverage.
Assertion-based verification is a verification methodology that utilizes assertions. Assertions are not
limited to just simulation tools. Formal verification tools, that do their verification statically, typically
with very little helper test stimulus, if any, also use assertions. Coverage is an essential part of any
verification activity whether one is using a simulator or a formal tool. SVA includes constructs for
defining functional coverage.
ABV implies that we have a verification plan which details which properties are required to verify the
design and which technologies the properties should be used with, for example, simulation, formal.
`ifndef SYNTHESIS
always @GNT
@(negedge clk)
if ( GNT != 4'h0 )
begin: GNT_CHK
integer cnt, idx;
cnt = 0 ;
for ( idx = 0; idx <= 3; idx = idx+1 )
if ( GNT[idx] != 1'b0 )
cnt = cnt + 1;
if ( cnt > 1 )
$display ( "ERROR: Multiple GNT" );
end
`endif
Verification personnel writing Verilog testbenches have been using an informal type of assertion right
along. They write a process, that upon appropriate events, checks that some expression has the desired
value, and if not, takes appropriate action. They can even write a check that spans multiple cycles,
although that is more difficult, and thus rare.
The use of assertions is not new. What is relatively new is the standardization of language constructs
with which you can concisely express temporal design behaviors, and consistent support of those
constructs, with failure messages, statistics collection and reporting, and user-friendly debug features.
Module Summary
Complex properties are difficult to write using Verilog.
● A dedicated syntax like SystemVerilog Assertions helps greatly
Assertions can reduce debugging time by identifying incorrect design behavior when and where it
occurs. The Assertion-Based Verification methodology does the following:
● Captures specifications
● Captures design assumptions
● Documents interfaces
● Provides white-box visibility in simulation
● Checks properties of legacy designs without modifying existing code
● Supports advanced verification technologies:
▪ Simulation, acceleration, emulation
▪ Functional coverage
▪ Static (formal) verification
Module 21
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Examine the structure of a concurrent assertion
● Create a simple instantaneous concurrent assertion
● Use implication to create conditional assertions
● Define sequences to create multicycle assertions
● Use simple cycle and sequence repetition to create more efficient assertions
We have learned that an immediate assertion is a procedural statement that some Boolean condition is
true. The assertion is checked when the procedural statement executes.
You typically place concurrent assertions outside procedural blocks to execute concurrently with the
procedural blocks and utilize their own clock event. In simulation, this means each assertion runs
continually in parallel and in the background.
A property is a design behavior that may span multiple cycles. It is a design requirement, similar to a
synthesis constraint that the design must work with a 250MHz clock. SystemVerilog has special
operators you use to develop concise property expressions to represent complex multicycle design
behavior.
Temporal Condition:
Property: @(negedge clk) is the clock
Required by expression. The behavior is only
the syntax. checked on this condition.
This is one form of a concurrent assertions. There are other forms as you will see.
▪ The assertion starts with an optional statement label and the keyword assert. As designs typically
contain hundreds if not thousands of assertions, a user-defined label will help with assertion
management and debugging. The assert keyword tells the simulator to check this property.
▪ The property keyword defines this is a concurrent as opposed to an immediate assertion. The two
terms of the property must be enclosed in parentheses.
▪ The first part of the property is the temporal condition which defines the event expression upon
which the assertion is evaluated. All properties must be clocked, although the temporal condition
can be defined elsewhere.
▪ The second part of the property is the Boolean expression, defining the design behavior which will
be checked at the temporal expression. If the Boolean expression evaluates to 1, this simple
assertion passes. If the expression evaluates to any other value (0 , X, Z), the assertion fails and an
error message is reported.
The assertion label must be unique in the current scope. It is a good practice to use an uppercase label to
avoid the likelihood of clashes with any other local identifiers. If you fail to label your assertions, the
simulator just reports the scope of the assertion if it fails.
`ifndef USING_OLD_TOOL
property RW_CHK_CLK;
@(negedge clk) en1 || en2;
What about tools that cannot compile SystemVerilog? endproperty
● Use text macro conditional compilation. A1: assert property (RW_CHK_CLK);
● Or use bind construct. `endif
endmodule
You can declare a property, like a normal SystemVerilog type declaration, in a compilation unit scope,
package, interface, module or program.
The property is asserted as a procedural statement, only in an interface, module, or program. You can
also assert a property in an always or initial block, although there are many issues with doing this, which
are covered in the full SystemVerilog Assertions class.
If your tool flow contains tools which cannot compile SystemVerilog, then there are two common
options:
▪ Declare your own text macro and use conditional compilation (`ifndef) to conceal the code from
the non-compliant tool.
▪ Use the SystemVerilog bind construct. This allows you to place assertions within a module, and
then instantiate the assertion module into an existing design module from a higher hierarchical
scope, usually the testbench, without modifying the design module. Bind is covered in the full
SystemVerilog Assertions class.
Concurrent assertions can also be placed inside of procedural blocks.
However, this makes there behavior very difficult to understand.
It is strongly recommended that you don’t place concurrent assertions inside of an always or
initial block.
en1 || en2;
A property expression can be simply a Boolean expression. Use parentheses where needed to
appropriately group the operands and to enhance readability.
property RW_CHK_CLK;
@(negedge clk iff VALID) en1 || en2;
endproperty property P3;
@(negedge addr_en) addr <= 7;
endproperty
All assertions must be clocked. You can specify the clock in a sequence expression that the property
uses or in the property specification.
The clocking event is typically just a Verilog event expression, exactly the same as that used in an RTL
always procedure, or in embedded event control in verification code. SystemVerilog event expression
enhancements, such as iff (if-and-only-if) qualifiers, can also be used.
Default clocks can be defined for properties which do not have an explicit clock in their declaration.
This is done with a clocking block and the keyword pair “default clocking”.
The default clock applies only to the scope in which it is declared.
A SystemVerilog simulation time instant is divided into several regions. The regions separate various
activities to help avoid races between the writing and reading of a variable.
Assertions are sampled, clocked, and evaluated at strictly defined points in the simulation cycle.
Variables in a property are sampled at the beginning of the simulation cycle in the preponed region. In
the Active, Inactive and NBA regions, procedures are evaluated and variables updated. It can take
several passes through these regions until you reach a steady state at this point in simulation time.
Then properties are evaluated in the observed region. If the temporal condition is currently true, such as
a rising edge of clk occurred in the NBA region, then the assertion is triggered. However, the property
is evaluated using the sampled variable values from the preponed region, which might be different from
the current value of these variables in the observed region, that is, they were updated in the Active or
NBA region.
This may seem counter-intuitive, but if you consider gate-level behavior, where the input sampled by a
register is that in the setup time before the clock edge, then it makes more sense.
In the example above, en1 and en2 are updated on the negedge of clk, and the property
RW_CHK_CLK is evaluated on the negedge of clk. en1 and en2 are sampled in the preponed
region. clk, en1 and en2 are updated in the NBA region, and if clk fell, the property is evaluated in
the observed region, using the sampled values of en1 and en2. Therefore, assertion evaluation uses the
preclock values, and the values of en1 and en2 in the observed region (post-clk values) might be
different from the values used in the property evaluation (pre-clk values).
This diagram of the time slots is greatly simplified and omits all the regions used for PLI callbacks.
clk
RW_CHK_CLK succeeds
Clock asynchronous properties on any change of any signal used in the property expression.
Clock synchronous properties on the appropriate edge of the appropriate clock signal.
Property expression evaluation uses the sampled values, that is, the values before the clock event.
A side effect of using sampled values in assertions rather than instantaneous values is that conflicting
behavior is not immediately detected. Notice that the failure of RW_CHK occurs some time after the
incorrect behavior started. Indeed if we end the simulation before en2 changes from 0->1, then we
would never be aware that there was a problem.
Bind allows you to insert an instantiation of an assertion module (or indeed any module) into any design
module in the hierarchy, by adding a bind construct to, typically, the top-most module in a design.
Hence, you can add modules into a design hierarchy from the testbench without having to edit the
design modules.
Syntax for bind:
▪ Bind
▪ mone //design module name/instance
▪ mone_prop //assertion module
▪ mp1 //instantiation of assertion module
▪ req,ack,clk); //map to variables in design module
You can bind an assertion module to a specific instantiation of a design module by using the
instantiation name instead of the module name:
bind m1 mone_prop mp1(req,ack,clk);
Bind creates an implicit instantiation of an assertion module and links this to all instances, or a specific
instance, of a design module via hierarchical variable names.
Here, DUT module has internal vectors named 'half', 'empty' and 'full'. And fifo_check module has ports
with same names, but these vectors are not declared in the top module, because these vectors do not
exist in top module. These vectors are present only in fifo_check and dut modules. Since, fifo_check
module is indirectly instantiated into the dut module through the use of the 'bind' mechanism, and does
not really exist in top module, there is no need to make the declarations of half, empty and full in top
module.
Assume the port names of the fifo_check module have different names that have to be connected to half,
empty and full signals of the dut module. Then, you can simply use dot name notation to connect those
signals to all remaining signals using the dot star connection.
Again, it is important to remember that, if it is permitted to instantiate fifo_check module directly in the
dut module, then bind construct is not required at all.
Simple Boolean properties are instantaneous – they will pass or fail at a single evaluation point.
However, most properties are temporal. They require different checks to be performed over different
cycles to confirm the design behavior.
A sequence is a series of Boolean conditions over successive cycles. You use ## (cycle delay operator)
to separate one evaluation cycle from the next. The example sequence:
a ## b ## c
is evaluated as condition a true in the first cycle, followed by condition b true on the next, followed by
condition c true on the next cycle. If each cycle of a sequence is true, then the whole sequence is true.
The first condition which is not true causes the sequence to fail on that cycle.
Sequences are used as building blocks of properties and there are a wide range of operators and
constructs to define conditional, repeated, overlapping, parallel, alternate and nested sequences to help
create properties.
Delay of 0 cycles, AKA Sequence Fusion Delay of 1 cycle, AKA Sequence Concatenation
sequence_put_message ##0 sequence_get_message sequence_put_message ##1 sequence_get_message
sequence_put_message sequence_put_message
sequence_get_message sequence_get_message
You can delay sequence evaluation by a constant number of cycles. The delay can be any integral
number between zero and the end of simulation.
A delay of 0 cycles, that is, sequence fusion, places the first cycle of the following sequence coincident
with the last cycle of the preceding sequence.
A delay of 1 cycle, that is, sequence concatenation, places the first cycle of the following sequence after
the last cycle of the preceding sequence.
property SCI;
● Conditional properties are defined with implication @(negedge clk)
operators:
(req && !ack) |-> bsy;
▪ If expra is true, then exprb must occur.
endproperty
▪ If expra is not true, exprb is not checked.
● For the same cycle implication, exprb must be true
in the same cycle as expra. clk
● For implication properties: req
▪ expra is the antecedent or enabling condition.
▪ exprb is the consequent or fulfilling condition. ack
▪ Both can be either sequences or Boolean conditions.
bsy
SCI
true
Conditional properties are those where some initial (or enabling) conditions must be true before a series
of (fulfilling) conditions must be checked. Conditional properties are defined using implication
operators.
SystemVerilog provides two sequence implication operators. This is the “same cycle” implication
operator. If the condition on the left of the operator is true, then the condition on the right-hand side of
the operator must be true in the same cycle.
The left side is called the antecedent or enabling condition.
The right side is called the consequent or fulfilling condition.
In this example, if req is high and ack is low at the failing edge of clk, then bsy must be high in the
same cycle. If req is low or ack is high, then the enabling condition is not true, and bsy is not
checked. As both sides of the operator are single Boolean conditions, the property can be rewritten as an
unconditional property:
@(negedge clk) (req && !ack && bsy) | !req | ack;
Either of the enabling or fulfilling conditions can be sequences. If an enabling sequence runs to
completion, then the first cycle of the fulfilling sequence (or condition) is checked in the same cycle as
the enabling sequence completes.
ack
bsy
NCI NCI
active pass
This is the “next cycle” implication operator. If the condition on the left of the operator is true, then the
condition on the right-hand side of the operator must be true from the next evaluation cycle.
The left-hand side is called the antecedent or enabling condition.
The right-hand side is called the consequent or fulfilling condition.
At each evaluation point (falling edge of clk in these examples), the enabling condition is checked
(req = 1; ack = 0). If the enabling condition is false, the assertion is ignored for this clock cycle. If the
enabling condition is true, then the fulfilling condition is checked at the next evaluation point (bsy = 1
at next falling edge of clk). If the fulfilling condition is true, then the assertion passes. If the fulfilling
condition is false, the assertion fails.
Either of the enabling or fulfilling conditions can be sequences. If an enabling sequence runs to
completion, then the first cycle of the fulfilling sequence (or condition) is checked in the next cycle after
the enabling sequence completes.
If a variable is not included in the enabling condition or fulfilling condition, then its value is “don't
care”. You must be careful to write design properties which check all parts of the required behavior.
For example, should bsy be low in the enabling condition? Should req and ack stay unchanged in the
fulfilling condition?
property STATE;
@(negedge clk) (a ##1 b) |=> c;
endproperty
1 2 3 4 5 6 7 8
clk
Remember that the enabling sequence on the left side of the implication operator must complete before
the fulfilling sequence on the right side is checked.
Here is an analysis of the evaluation property STATE:
Cycle 1: a is true, so attempt 1 of STATE evaluation becomes active.
Cycle 2: b is false, so attempt 1 of STATE evaluation becomes inactive.
Cycle 3: a is true, so attempt 2 of STATE evaluation becomes active.
Cycle 4: b is true, so attempt 2 of STATE evaluation becomes enabled.
Cycle 5: c is true, so attempt 2 of STATE evaluation succeeds and finishes.
Cycle 6: a is true, so attempt 3 of STATE evaluation becomes active.
Cycle 7: b is true, so attempt 3 of STATE evaluation becomes enabled. a is still true, so attempt 4 of
STATE evaluation becomes active.
Cycle 8: c is false, so attempt 3 of STATE evaluation fails. b is false, and attempt 4 of STATE
evaluation becomes inactive.
The key point here is that assertions can overlap. The first condition of the enabling sequence is checked
at every evaluation point. If the first condition is true, another copy of the property becomes active,
regardless of how many other copies of the property are currently active. There can be any number of
copies of a property active at any one time, all at different stages in their evaluation.
Enabling
condition
incomplete
Pass
Active
Fulfilling
Fail sequence
completes
Assertions start in the inactive state. At each evaluation cycle, the enabling condition (or the first
condition of an enabling sequence) is checked. If the condition is true, a copy of the assertion becomes
active. In the active state, the assertion continues checking the enabling condition or sequence. If the
enabling condition(s) do not complete, then the assertion becomes inactive. If the enabling condition(s)
complete, then the assertion is enabled. In the enabled state, the assertion checks the fulfilling
condition(s). If the fulfilling conditions fail to complete, then in the evaluation cycle the failure was
detected, the state of the assertion is fail, after which it becomes inactive. If the fulfilling condition(s)
are true, then the assertion state is pass in the evaluation cycle the final fulfilling condition becomes
true.
A disable can terminate an active or an enabled assertion. For SystemVerilog2005, a disabled assertion
counted as a pass. For formal verification, a disabled assertion is interpreted as a pass. From
SystemVerilog2009, a disabled assertion has a new separate disabled state before returning to inactive.
Remember the enabling condition of every assertion is checked on every evaluation cycle, therefore
multiple versions of a single assertion can exist in different states. This can make management and
debugging of assertions difficult.
Disable is essential for terminating long sequence properties if design conditions change, e.g., in this case a reset
occurs.
If the disable condition (rst) is true, then the assertion is disabled (terminated) immediately, regardless of how
the property is clocked.
You can view the disable as being asynchronous from the property sampling clock.
Up to SystemVerilog2009, disabled properties were defined as a pass, just as if the property had successfully
completed.. This could possibly lead to misleading results when a simulator reported the number of design
properties which have passed or failed. This is why most simulators reported the number of assertions which have
completed or finished, rather than the number which have passed.
From SystemVerilog2009, a new completion state for properties was added, in addition to pass or fail. When the
disable expression of a property is true, the property is counted as “disabled”, and simulators can now report the
number of assertions that are disabled.
In Formal Verification (where assertion languages were first used), disabled assertions are always counted as
passes because the absence of a failure is deemed as a pass.
Default disable iff can be defined for properties which do not have an explicit disable iff in their declaration.
The syntax is
default disable <Boolean>, for example:
disable iff (!rst_n)
The default disable applies only to the scope in which it is declared.
b
!a next +three b
Cycle delay ## just counts evaluation cycles without checking any conditions. The cycle delay
expression can be an integral constant expression or a range between two integral constant expressions.
Here we have a 3-cycle delay. The property succeeds if, after a is low, b is low 4 cycles later. Note that
if the cycle delay comes immediately after the next cycle implication operator, then the cycle count
starts after the cycle when the enabling condition completes. If same cycle implication is used, then the
count starts on the same cycle as the enabling condition completes.
A delay of zero is valid and useful. A zero delay between the end of the left sequence and the start of the
right sequence means that the left sequence ends and the right sequence begins in the same cycle. This is
called sequence fusion.
Example
(a ##1 b) ##0 (c ##1 d) = (a ##1 (b && c) ##1 d)
Cycle delay repetition should be used with care. You are not checking any values during the cycle count,
and you ask yourself whether you should be checking something. In the example above, should a be
high or low during the 3-cycle delay? Can b only be low on the fourth cycle after a, or can it be low on
every cycle?
SystemVerilog provides three sequence repetition operators. The consecutive repetition operator simply
repeats an expression for a set number of times given by N. The consecutive repetition operator can be
used with a Boolean expression, a named sequence, or an inline sequence enclosed within parentheses
(see below).
Consecutive repetition allows an individual condition or a complete sequence to repeat, consecutively, a
set number of times.
Example
(a[*4]) is equivalent to (a ##1 a ##1 a ##1 a)
((a ##1 b)[*2]) is equivalent to (a ##1 b ##1 a ##1 b)
The count value N must be statically computable, that is, known at compilation time and not defined by
a variable or expression.
The consecutive repetition operator can have a range. The range specifies a minimum number of
repetitions which must occur and a maximum number which must not be exceeded.
Both bounds of the range must be statically computable (i.e., a constant expression) and must be defined
in an ascending direction.
In the example, once the enabling condition of a falling edge on a is found, the property will check that
a is false for two cycles (lowest bound of range). We use a same cycle implication operator to begin the
count of a low in the same cycle as a low for the enabling condition completion. Then for the next 3
evaluation points, if a is true, the property completes and passes. Otherwise, the property remains
active. On the final (fourth) cycle, a must be true (upper bound of range exceeded) or the property fails.
This property specifically looks for a falling edge on a, as we interpret the “a goes low” of the
specification to imply this. The property would not catch the case of a starting low at the beginning of
simulation, and then remaining low for more than 4 cycles.
A minimum bound of 0 means that the sequence or condition can be missing. For example:
a ##1 b*[0:2] ##1 c
matches
a ##1 c
a ##1 b ##1 c
a ##1 b ##1 b ##1 c
The SystemVerilog Assertions class covers all the above including the following:
▪ Coding guidelines and recommendations
▪ Practical examples
▪ Formal Friendly SVA coding
▪ Use of Verilog helper code to simplify verification using SVA properties
SystemVerilog has many more advanced property and sequence operators and features. The
SystemVerilog Assertions training covers these plus coding guidelines and recommendations, functional
coverage and working examples.
Module Summary
● SystemVerilog assertions are based on Verilog Boolean conditions
● Constructing a simple Boolean assertion is straightforward:
▪ Write a Boolean condition using Verilog syntax
▪ Define when the condition is checked with a clock expression
▪ Name the condition using the property statement
▪ Assert the property using an assert statement
● SVA has many features for defining multi-cycle and repeated sequences:
▪ For example: consecutive repetition [*N]
Pause here for a moment and review what you have learned about SystemVerilog assertions.
Quiz
Code this property and assert it.
If the req-ack handshake occurs then:
● gnt is true on only the 2nd cycle after the req-ack handshake.
● ends is true on only the 6th cycle after gnt is true.
clk req
req ack
gnt
ack A data B
gnt ends
data
ends clk
Solution
A1 : assert property ( @(posedge clk)
(req ##1 ack) |=> (!gnt ##1 gnt ##1 (!gnt && !ends)[*5] ##1 (!gnt && ends)) );
Labs
Lab 18 Simulating Simple Implication Assertions
● Create simple implication assertions, track assertion failures in simulation and debug the
assertions.
Lab 19 Sequence-Based Properties
● Create sequence-based assertions, track multiple, overlapping assertions in simulation and debug
issues.
Module 22
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Understand the concepts of Direct Programming Interface (DPI)
▪ Data types
▪ Imported tasks and functions
▪ Pure and context definitions
▪ Exported tasks and functions
▪ Compilation
The current interface of VPI is the traditional interface between Verilog and C code.
Advantages Disadvantages
Powerful Difficult, even for simple operations
Safe handling of type conversion Linking is different for each simulator
Full visibility of design hierarchy Multiple applications across multiple simulators
Synchronization to simulation events Create tool management problems
The current interface of VPI is a true application programming interface. Through it you can access and
navigate the full design hierarchy, and can synchronize a C application to any point of simulation time
or to any simulation event. With this power comes difficulty of use, which typically limits its use to a
handful of experts in any company.
DPI Maps between SystemVerilog subroutines and external routines in following two ways:
1. Import to SV: SystemVerilog calls subroutines defined in foreign language layer.
2. Export from SV: SystemVerilog defines subroutines called in foreign language layer.
module top;
...
#include <stdio.h>
import "DPI-C" context c_imp =
Import #include <svdpi.h>
function int imp_func(...);
int c_imp (...)
export "DPI-C" c_exp = function exp_func;
{
Export ...
function int exp_func ...
}
...
endfunction
extern int c_exp (...);
...
356 © Cadence Design Systems, Inc. All rights reserved.
The direct programming interface is a lightweight interface compared to the PLI, designed to interface
between SystemVerilog and any foreign programming language. It allows SystemVerilog subroutine
calls to be mapped to foreign language implementations, and SystemVerilog subroutine declarations to
be mapped to foreign language calls.
The standard clearly separates the specification of the SystemVerilog layer from the foreign language
layer so that, in theory, alternate foreign languages can be swapped in without changing the
SystemVerilog view of the interface.
So, any language which follows the normal C linkage conventions can be used to link with
SystemVerilog using DPI.
OUTPUT
SystemVerilog C: Power of 2 for variable
module calling_the_DPI;
from SV is "49"
import "DPI-C" function calculate_pwr (int s);
initial begin C
int s = 'd7; #include "stdio.h"
calculate_pwr(s); #include "svdpi.h"
end #include "math.h"
SystemVerilog
module parity_calculator()
...
import "DPI-C" context function int parityf (input int a); 3
export "DPI-C" function display_text; 3
44
function void display_text(input string text)
$display("from SV: %s",text); C
#include <stdio.h>
endfunction #include <svdpi.h>
extern display_text(string text)
initial 1
... 1
int parityf(int a); 22
parity = parityf(var); ...
... return parity;
endmodule display_text("Parity done");
358 © Cadence Design Systems, Inc. All rights reserved.
...
An export declaration exports a SystemVerilog subroutine as a C function. The export declaration maps
the SystemVerilog task or function name to a C function name.
In the C layer, you declare the function with extern and call it as if it were a normal C function.
As with imported subroutines, the exported SystemVerilog subroutine and C extern declaration must
match, specifically for the following:
▪ Name (although this can be mapped with a linkage name – see next slide)
▪ Return type
▪ Number of arguments
▪ Type of arguments
▪ Direction of arguments
● The mapping between the basic SystemVerilog data shortint short int
types and the corresponding C types is defined as shown int int
in the table.
longint long
● The DPI also supports the SystemVerilog and C
unsigned integer data types that correspond to the real double
mappings. shortreal float
▪ The table shows their signed equivalents. string const char *
The mapping between SystemVerilog and foreign language data types is specified by the foreign layer.
Here we present the mapping specific to the C foreign language.
The mapping of simple SystemVerilog data types such as byte, int, real and string is
straightforward, as they have obvious C counterparts. The logic type maps to an unsigned char on
the C side to accommodate the four states. For consistency, the bit type also maps to an unsigned char.
Your C code should not use the character value directly, but should instead include svdpi.h and use
the text macros representing the four states. The SystemVerilog LRM describes the name and core
functionality of svdpi.h and permits vendors to differ in their implementations.
--------
typedef signed __int8 int8_t;
typedef uint8_t svScalar;
typedef svScalar svBit;
typedef svScalar svLogic;
#define sv_0 0
#define sv_1 1
#define sv_z 2
#define sv_x 3
void* - 1800_2010-H.7.4
● C include file svdpi.h defines C types for simple SystemVerilog data types.
#include <stdio.h>
module top; #include <svdpi.h>
import "DPI-C" function int c_func (input bit bin1);
... int c_func (svBit b1) {...}
SystemVerilog enumerated types are accessible on the C side as the enumeration base type, but the
enumeration value names are not available.
SystemVerilog packed types are accessible on the C side as arrays of the svBitVecVal type or
svLogicVecVal type as described in svdpi.h.
SystemVerilog unpacked arrays, structures and unions having only the simple types map to their
corresponding C arrays, structures and unions of their corresponding C types.
svBitVecVal and svLogicVecVal are arrays of 32-bit elements.
--------
typedef unsigned __int32 uint32_t;
typedef uint32_t svBitVecVal;
typedef struct vpi_vecval {
uint32_t a;
uint32_t b;
} s_vpi_vecval, *p_vpi_vecval;
typedef s_vpi_vecval svLogicVecVal;
#include "svdpi.h"
print_message(char *t)
{
printf("\nC: Message got from SV is \"%s\"\n",t);
}
module pass_string;
import "DPI-C" function string print_message ();
initial begin
string s = print_message();
$display("%s",s);
end
endmodule
OUTPUT
HI from C World
#include "svdpi.h"
char * print_message()
{
return "HI from C World";
}
initial begin
bit[15:0] arr[5:0] = '{1,2,3,4,5,6};
print_array(arr);
end
endmodule
#include "svdpi.h"
initial begin
print_statement(8'b0011XXZZ);
end
endmodule
#include "svdpi.h"
void print_statement(svLogicVecVal* v)
{
io_printf("v= %x\n",*v);
io_printf("aval = %x\n", v->a);
io_printf("bval = %x\n", v->b);
} OUTPUT
Canonical Representation v= 3c
Actual 0 0 1 1 X X Z Z aval= 3c
Value
a value 0 0 1 1 1 1 0 0 bval= f
b value 0 0 0 0 1 1 1 1
365 © Cadence Design Systems, Inc. All rights reserved.
SystemVerilog
Must match: C
● Return type
● Number of arguments int parityf (int a) {
● Type of arguments ...
● Direction of arguments return parity;
}
As the SystemVerilog and C layers are compiled and elaborated or linked separately, there is no
opportunity for the DPI to check function declarations or verify data type compatibility. It is therefore
critical that you match the import declaration to the C function with respect to the function name, return
type and argument types.
A name mismatch will probably result in only a “symbol not found” error. A type mismatch error can be
more serious and can lead to unpredictable effects as the C layer misinterprets the SystemVerilog
arguments.
module one();
import "DPI-C" parityf = function int calc_parity (input int x);
initial
#include <stdio.h>
parity = calc_parity(x);
#include <svdpi.h>
...
int parityf (int a) {
SystemVerilog1
...
module two(); return parity;
import "DPI-C" parityf = function int calc_parity (input int x); }
C
initial
parity = calc_parity(x); 3. Both calls are 2. Import function with linkage 1. parityf function
... redirected to parityf name calc_parity defined in C
SystemVerilog2
367 © Cadence Design Systems, Inc. All rights reserved.
By default, you call an imported subroutine using the same name as the C function. You can specify a
different SystemVerilog name for the C function if you provide the C linkage name in the import
declaration. This is useful when the C function name is a reserved word or illegal identifier in the
SystemVerilog namespace. It also allows you to import a C function multiple times with different
SystemVerilog names.
The C linkage name must of course be a legal C identifier, and if not a legal SystemVerilog identifier,
must be appropriately escaped.
Verilog supports the use of escaped identifiers, which allow any printable ASCII character to be used in
an identifier. Escaped identifiers must begin with a backslash (\) and end with a whitespace.
Example
\unit@32 \unit-32 \16-bit-bus are all legal escaped identifiers.
You can specify a different ● The SystemVerilog subroutine name and C function
SystemVerilog name for the C name are by default identical.
function, if you provide the C linkage ▪ You can optionally specify a different SystemVerilog
name in the export declaration. name.
SystemVerilog
module one(...);
export "DPI-C" parityf = function calc_parity; C
function int calc_parity(input int x); #include <stdio.h>
... #include <svdpi.h>
endfunction int word, parity;
... extern int parityf(int);
void dostuff(){
1. Definition of calc_parity 2. Exporting calc_parity ...
function in SV as parityf
parity = parityf(word);
dostuff() called from }
SystemVerilog context
3. Linkage name parityf
368 © Cadence Design Systems, Inc. All rights reserved. used on C-side
By default, you call an exported subroutine using the same name as the SystemVerilog subroutine. You
can specify a different SystemVerilog name for the C function if you provide the C linkage name in the
export declaration. This is useful when the SystemVerilog subroutine name is a reserved word or an
illegal identifier in the C namespace.
The C linkage name must of course be a legal C identifier, and if not a legal SystemVerilog identifier,
must be appropriately escaped.
Verilog supports the use of escaped identifiers, which allow any printable ASCII character to be used in
an identifier. Escaped identifiers must begin with a backslash (\) and end with a whitespace.
Example
\unit@32 \unit-32 \16-bit-bus are all legal escaped identifiers.
The SystemVerilog initial block calls the import_c_func, which is linked to the C function named
c_func by the import declaration. This function is imported with the context property so that it
can in turn call exported SystemVerilog subroutines.
The C implementation of the c_func function calls the sv_func function that it has previous
declared to be extern. This function is linked to the SystemVerilog function named
export_sv_func in the export declaration.
The actual usage methodology is vendor dependent, but is likely to be something like this:
▪ You separately compile your C code and link it into a shared object library with a platform-specific
extension, such as .sl or .so for UNIX variants and .dll for Windows.
▪ You compile, elaborate and simulate your SystemVerilog code as usual, and provide the shared-
object library to the simulator.
The standard recommends that command-line options be provided for this purpose, and even suggests
names for them:
▪ The -sv_lib option provides a pathname for the shared object library minus its extension – the
tool provides the correct extension for the platform.
▪ The -sv_root option provides a single directory path that is prefixed to any relative path
specified by the -sv_lib option.
A nonstandard, but widely supported technique is to compile your C code into a shared object library
with the name libdpi (e.g., libdpi.so). A file with this name is automatically linked into the
simulator.
For a simple example, the Cadence simulator can compile and link the C code directly, for example:
xrun taskfunc.c top.sv
Module Summary
In Summary, SystemVerilog offers DPI to communicate easily with foreign language such
as C. The table below summarizes its advantages and potential issues.
Quiz
Write a function import statement to import the exp2f function (from the cmath library) with the base2exp SystemVerilog
identifier. Assume the C function has no side effects. The C function prototype is:
float exp2f(float x);
True or False? The standard supports import and export of class methods.
False: The standard does not support import or export of class methods.
The imported task calls an exported task (implemented in SystemVerilog) that consumes time.
372 © Cadence Design Systems, Inc. All rights reserved.
Lab
Lab 20 Simple DPI Use
● Import simple C functions from standard and maths C libraries.
Interprocess Synchronization
Module 23
Revision 1.0
Version 21.10
Estimated time:
● Lecture
● Lab
Module Objectives
In this module, you
● Apply SystemVerilog interprocess synchronization mechanisms
▪ Nonblocking event trigger
▪ Event sequences
▪ Mailboxes
▪ Semaphores
Inactive
event e;
integer i = 0;
NBA
always @e $display("i:%0d",i);
Observed
initial begin
i <= 1;
-> e; // blocking i:0 Re-active
...
Re-inactive
Postponed
376 © Cadence Design Systems, Inc. All rights reserved.
Traditional Verilog event triggered are blocking. The event is triggered instantaneously in the Active
region of the event scheduler. This can make synchronization difficult. A process to be triggered must be
waiting for the event at the instant in the Active region when the event trigger is executed. If the
triggered process needs to consume time, then you have race conditions between the event trigger and
process event control.
Another issue with traditional events is demonstrated by the example. As the event is triggered in the
Active region, any nonblocking behavior of the design cannot be captured by a process triggered by the
event.
● You can trigger SystemVerilog events with a nonblocking ->> operator: Active
▪ Executes without blocking
▪ Schedules an event for the NBA region Inactive
▪ Helps prevent races between processes
▪ Can include optional embedded event control
NBA
event e;
integer i = 0;
Observed
always @e $display("i:%0d",i);
Re-active
initial begin
i <= 1;
->> e; // nonblocking i:1 Re-inactive
->> #3 e;
->> @(posedge clk) e; Postponed
...
377 © Cadence Design Systems, Inc. All rights reserved.
SystemVerilog adds a nonblocking event trigger (->>) that schedules the event for the NBA region. You
can also use intra-assignment delays just as you do with the nonblocking assignment operator.
The nonblocking event trigger prolongs the visibility of the event and allows nonblocking behavior to
be captured by processes triggered by the event. It also allows us to schedule an event for some time or
event in the future.
The nonblocking event trigger serves the same general purpose as the nonblocking assignment – to
prevent a race between a block that triggers the event and a block that waits for the event in the same
delta cycle.
An event is not persistent. If a process waits for an event after it occurs, the process can go on waiting
forever.
The first example illustrates this. The forked blocks can be scheduled in any order. It is very likely that
one of the event controls is too late – its associated event has already occurred.
The second example utilizes the nonblocking event trigger, which schedules the event for the NBA
region, after the event control has had a chance to wait for that event. The example also illustrates use of
the triggered event property, which persists to the end of the time step. If the e2 event has already
occurred when the wait statement executes, the triggered property is still true.
Defining a Mailbox
IEEE 1800-2012 15.4
Mailbox Methods
Method Description Syntax
new() Mailbox constructor which optionally specifies maximum size function new(int bound = 0);
Note by default no maximum size
num() Returns the number of messages currently in the mailbox function int num();
At 0ns, RX try_get fails At 9ns, RX gets all mailbox data From 10ns, TX put triggers RX get
Parameterized Mailboxes
Common declarations
IEEE 1800-2012 15.4.9 class pkt;
rand bit [4:0] addr;
endclass
typedef enum {pass, fail} state_t;
You can type-parameterize a mailbox:
mailbox #(string) smbox = new;
● Define type upon declaration. mailbox #(pkt) pmbox = new;
mailbox #(state_t) tmbox = new;
● Holds messages of that and equivalent types. ...
● Compiler detects type mismatch.
TX
string pstring;
pkt ppkt = new; RX
string gstring;
state_t pstatus;
pkt gpkt;
... state_t gstatus;
pstring = "test one";
...
assert(ppkt.randomize);
smbox.get(gstring);
smbox.put(pstring); Channel pmbox.get(gpkt);
pmbox.put(ppkt);
tmbox.get(gstatus);
tmbox.put(pstatus); ...
...
383 © Cadence Design Systems, Inc. All rights reserved.
You can simply use a separate mailbox for each message type.
You specify the one type a mailbox may accept by overriding its type parameter when you declare the
mailbox variable.
The compiler can now detect any attempt to store or retrieve messages of an incompatible type.
Defining a Semaphore
IEEE 1800-2012 15.3
semaphore sync;
sync = new(4); // semaphore with 4 keys
384 © Cadence Design Systems, Inc. All rights reserved.
Semaphore Methods
put() Returns a set number of keys to the semaphore function void put(int keyCount=1);
▪ The new() constructor constructs a semaphore with the specified number of keys. The default
value of its argument is 0 – no keys.
▪ The blocking get() requests one or more keys and suspends the calling process if the requested
number of keys are not available. The blocked process resumes when sufficient keys are put into the
semaphore.
▪ The nonblocking try_get() returns 0 if the requested number of keys are not available.
▪ The put() method puts keys into the semaphore. The user is responsible for key management, for
example, to ensure that a process returns only those keys that it previously retrieved.
In this example:
▪ At time 0, process P1 requests a key, gets it, and accesses the resource.
▪ At time 1, process P2 requests a key and blocks, waiting for the key.
▪ At time 2, process P1 completes its resource access and returns the key.
• Process P2 now unblocks, gets the key, and accesses the resource.
▪ At time 3, process P2 completes its resource access and returns the key.
Remember the user is responsible for defining, managing and utilizing the resource. Common user
errors include:
▪ Accessing the resource without first obtaining the requisite number of keys.
▪ Failing to return keys after completing the resource access.
▪ Returning more keys to the semaphore than were taken out of the semaphore.
1. Event Variables
A SystemVerilog event variable is a handle pointer to a synchronization queue.
● You can assign and compare these handles to each other.
▪ Assignment causes both handles to point to the same queue.
...
event e1, e2;
initial
fork
#1 e2 = e1; // e1, e2 both now have e1 synchronization queue
#2 @e1 $display ("e1 triggered");
#2 @e2 $display ("e2 triggered");
#3 -> e2; // trigger e2 (and also e1)
join
...
Output
e1 is triggered
e2 is triggered
A SystemVerilog event variable is a handle that points to a synchronization queue. The queue is a list of
processes waiting for the event.
This example declares two event variables and then assigns one to the other. This makes both event
variables point to the same list of processes. Processes can access this synchronization queue through
either variable.
2. Merging Events
Merging events merges only the event variables.
● Both now “point” to the RHS synchronization queue.
...
event e1, e2;
initial
fork
#1 @e1 $display ("e1 triggered");
#1 @e2 $display ("e2 triggered");
#2 e2 = e1; // e1, e2 both now have e1 synchronization queue
// e2 synchronization queue is lost
#3 -> e2; // trigger e2 (and also e1)
join
...
Output e1 is triggered
Event merging is really just the merging of the event variables. The source synchronization queue is
assigned to the target event variable, so that both variables “point” to the same queue. If no other
variable still “points” to the synchronization queue of the target variable, processes on that queue can
wait forever.
3. Reclaiming Events
You can assign the null value to an event.
● Triggering a null event shall have no effect.
● The effect of waiting on a null event is undefined.
module test;
event e1; Event e1 is set to null
initial synchronization
fork queue is released
#1 @e1 $display ("e1 triggered once");
#2 -> e1; // trigger e1
#3 e1 = null; // e1 synchronization queue lost
#4 @e1 $display ("e1 triggered twice"); At time 4 e1 is null
#5 -> e1; // nothing happens @e1 may not block or
join may block forever
endmodule
Assigning the null value to an event variable assigns a “null” synchronization queue. This disassociates
the variable from its previous synchronization queue. When a synchronization queue is no longer
associated with any event variable, the simulator can reuse the queue. The effect of waiting on a null
event is undefined. An implementation may choose to wait forever or not wait at all, without warning,
and triggering the null event has no effect.
Module Summary
This module explored interprocess synchronization:
● Enhanced events
▪ Help prevent race conditions
▪ Provide more control over order of execution
● Mailboxes
▪ Multiple-type FIFO operations with built-in blocking synchronization
● Semaphores
▪ Multi-purpose synchronization mechanism with built-in blocking and request “weighting” with keys
Quiz
Where would you use the nonblocking event trigger?
Use a nonblocking event trigger anywhere that a process can potentially “wait” for the event at the same
simulation moment that another process triggers the event, thus causing a “race” condition.
What does a blocking get() or peek() do if the message at the head of the mailbox queue
is the wrong type?
This is a runtime error. You should instead use the nonblocking versions.
What happens if a process puts more keys in a semaphore than the process got from the
semaphore?
Labs
Lab 21 Synchronizing Processes with a Mailbox
● You synchronize multiple processes by use of a mailbox.
Next Steps
Module 24
Revision 1.0
Version 1.0
Estimated Time:
● Lecture 5 minutes
● Lab NA
Learning Maps
Cadence® Training Services learning maps provide a comprehensive visual overview of the learning
opportunities for Cadence customers.
Click here to see all our courses in each technology area and the recommended order in which to
take them.
Click the play button in the figure on this slide to view the demo of Cadence Learning and Support.
Wrap Up
● Complete Post Assessment, if provided
● Complete the Course Evaluation
● Get a Certificate of Course Completion
Thank you!
Verilog-2001 Summary
Appendix A
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
Appendix Objectives
In this appendix, you summarize key Verilog-2001 features, including the following:
● Sensitivity list enhancements
● Input/output lists
● localparam
● Subroutine enhancements
● Variable initialization
● Multidimensional arrays and array selects
● Configurations
In Verilog-1995, the signals in the event list of an always block are separated with the keyword or.
This can be confused with the logical or operator (|). Therefore in Verilog-2001, the signals can be
separated with commas instead of or.
In Verilog-2001, you can define a wildcard sensitivity list for combinational logic only. A wildcard
sensitivity list replaces the signal list with a single asterisk. This makes the always block automatically
sensitive to every signal read within the block. There is one exception to wildcard sensitivity, however.
If a combinational always block contains a call to a function, which reads a signal which is not defined
as a formal argument to the function (i.e., it is accessed via side effects), then this signal will not be
included in the sensitivity list. In other words, when the list of signals for the wildcard sensitivity list is
created, only the arguments of any functions calls are considered, not the contents of the function.
Verilog-1995 Verilog-2001
reg clk; reg clk = 1'b1;
● Verilog-1995: Declaration and initialization of a
variable are separate. initial
clk = 1'b1;
● Verilog-2001: You can combine declaration and
initialization of a variable.
● Verilog-2001: You can declare local variables in Verilog-2001
initial or always blocks. initial begin : IBLK // named block
▪ For example, loop variable. // local declaration
integer i;
▪ Restrictions:
for(i = 0; i<=7; i = i+1)
o Variable is not visible outside block. if (avec[i])
o Block must be named. count = count + 1;
end
In Verilog-1995, if you want to initialize a variable at the start of the simulation, then this must be done
in a initial block, separate from the declaration. In Verilog-2001, the initial value can be combined
into the declaration.
In Verilog-2001, you can also have local variable declarations in an initial or always block. These
are declared after the begin of the block, and before any executable statements. All local declarations
must be grouped together at the beginning of the block – once the first executable statement is found,
any further local declarations will cause compiler errors.
Local declarations are visible only within their declaration block, and override any identifiers with the
same name declared in higher scopes, e.g., the enclosing module.
It is a requirement in Verilog-2001 that a block containing local declarations must be named. This is
done by defining a name after the begin keyword of the block, with a colon separator between name
and begin.
A good use for local declarations is for loop variables. In Verilog-1995, for loop variables can only
be declared at the module level, giving a risk of multiple loops in different procedural blocks all sharing
the same loop variable. With local declarations, each block can have their own, independent loop
variable.
`default_nettype none
module mone (ip, ...); Verilog-1995
IEEE 1800-2012 22.8 input ip;
Undeclared identifiers have an implicit declaration of a single bit wire by default. There is a compiler
directive which can change the default net type – `default_nettype. With this directive, the
default net type can be changed from wire to another net type, for example tri.
The compiler directive must be placed outside of a module and applies across file boundaries until
overridden by another default net type compiler directive or a `resetall is executed.
Multiple net type directives are allowed, but subsequent directives overwrite previous ones and so
default net type for the whole design will be defined by the last directive encountered.
In Verilog-2001, the option none can be applied to the directive. In this case, undeclared identifiers will
generate compiler errors. The aim of the directive is to help debug connectivity problems where a
misspelled identifier name results in a new wire declaration, which breaks a connection.
However, implicit net types are used extensively in traditional Verilog code, so setting the default net
type to none can result in a great many error messages. For example, the following would generate
compiler errors with a default net type of none:
module mone (ip1, ip2, ...);
input ip1, ip2;
...
The ports would have to be explicitly defined as wires to avoid errors:
module mone (ip1, ip2, ...);
input wire ip1, ip2;
...
Input/Output Declarations
IEEE 1800-2012 22.8
In Verilog-1995, each port must be first named in the module header, then sized and given direction in a
separate declaration and finally the data type must be defined. In these examples, we are explicitly
declaring the net data types as well as the registers.
In Verilog-2001, you can combine the direction, size and data type declarations into one statement. You
can also combine the name, direction, size and type declarations into the module header. This is called
an ANSI-C-style input/output declaration, and can also be used for subprogram argument lists.
It greatly simplifies the port or argument declarations.
Verilog-2001
module mux_size
#(parameter SIZE=3) Verilog-1995
(input wire [SIZE-1:0] a, mux_size #(5) u1
input wire [SIZE-1:0] b, (.a(a), .b(b),
input wire sel, .sel(sel), .op(op));
output reg [SIZE-1:0] op);
...
endmodule
Verilog-2001
mux_size #(.SIZE(5)) u1
(.a(a), .b(b),
.sel(sel), .op(op));
● Verilog-2001: You can declare module parameters
before the ANSI-C-style port list.
● Verilog-2001: You can override module parameter by
name in the instance parameter map.
To parameterize port sizes for an ANSI-C-style port list, declare the module parameters before the port
list, enclosed in separate parentheses and prefaced with a hash #.
For Verilog-1995, you map parameters by position, using commas as placeholders for parameters you
do not override:
param_mod #(1, 2, 3) U1 (.a(a), .b(b), .c(c));
param_mod #( , , 3) U2 (.a(a), .b(b), .c(c));
You must know the parameter declaration order. This method can lead to errors with badly ordered
parameter passing.
For Verilog-2001, you can map parameters by name similarly to mapping ports by name:
param_mod #(.PA(1), .P2(2), .P3(3)) U2 (.a(a), .b(b), .c(c));
Verilog-2001
defparam u2.width_b = 7;
Signed Vectors
IEEE 1800-2012 A.2.2.1 reg [3:0] usreg; // 4-bit vector in range 0 to 15
reg signed [3:0] sreg; // 4-bit vector in range -8 to 7
● Vector types can also be defined as unsigned. reg signed [3:0] areg;
▪ Default behavior integer aint = -5;
initial begin
areg = aint; // areg = -5
aint = areg; // aint = -5
...
405 © Cadence Design Systems, Inc. All rights reserved.
For Verilog-2001, you can declare signed vectors. Vectors are by default unsigned. An unsigned vector
is treated as signed for accesses through a signed port.
c = 4'sb1001; // c = -7
d = $unsigned(c); // d = +9;
end
Verilog-1995 Verilog-2001
reg [63:0] data;
● Assigning an unsized x or z value (e.g., ’bz) to a data = 'bz; //'hzzzzzzzzzzzzzzzz
bus greater than 32 bits only sets the lower 32 bits
(unsized literals are assumed to be 32 bits).
● Upper bits are set to 0.
● Setting the entire bus to x or z requires explicitly
specifying the number of bits in the value.
Verilog-2001
● An unsized z or x value automatically expands to
fill the full width of the target vector.
Automatic Tasks
Verilog-2001
IEEE 1800-2012 13.3.1 task automatic neg_clocks
(input [31:0] number_of_edges);
begin
repeat(number_of_edges)
● Verilog-1995: All tasks and functions are static. @(negedge clk);
end
● Concurrent task call and recursive function calls must
endtask
be avoided.
▪ Can cause conflict with internal variables and arguments. initial begin
● Verilog-2001: You can declare tasks and functions to neg_clocks(6);
be dynamic. ...
end
▪ Using keyword automatic.
▪ Allows concurrent task calls and recursive function calls. always @(posedge trigger)
● Automatic task declarations cannot be accessed by begin
hierarchical references. neg_clocks(10);
...
● Automatic arguments cannot be updated with a end
nonblocking assignment.
For Verilog-1995, all subroutines are static. Only one copy of a subroutine arguments and variables
exists. Multiple concurrent task calls and recursive function calls all use the same copy of inputs, local
variables, and outputs.
For Verilog-2001, you can declare an automatic subroutine. A separate copy of the subroutine arguments
and variables exists for each invocation. Multiple concurrent task calls and recursive function calls all
use their own copy of inputs, local variables, and outputs. As the automatic arguments and variables
exist only for the duration of the subroutine execution, you cannot reference them hierarchically from
outside the subroutine, cannot assign to them with nonblocking assignments, and cannot use
$monitor or $strobe with them.
Constant Functions
IEEE 1800-2012 13.4.3
Verilog-2001
parameter addw = 5;
parameter datw = 8;
● Verilog-1995 allows simple constant expressions
for defining limits for vectors, replicates, etc. reg [addw-1:0] address;
reg [datw-1:0] mem [1:memsize(addw)];
● Verilog-2001 allows limits to be defined by
constant functions. function integer memsize;
▪ Function value must be calculable at elaboration. input [15:0] width;
o Usually inputs are constant. begin
case (width)
▪ Greater flexibility for scalable reusable models. ((width%2)==0): memsize = 1024;
▪ Multiple limits can be derived from a single constant . default: memsize = 512;
endcase
end
endfunction
Verilog-1995 module parameter values must be constant expressions, which are limited to operations on
literals and previously declared module parameters.
Verilog-2001 permits you to use a constant function call anywhere you are required to use a constant
expression.
The standard lists restrictions upon constant function calls:
▪ Cannot be placed within any generate scope.
▪ Cannot contain hierarchical references.
▪ Cannot contain system function calls.
▪ Ignores system task calls (except will execute $display in simulator but not elaborator).
▪ Cannot themselves make constant function calls in any context requiring constant expressions.
• Can otherwise make constant function calls to functions local to containing module.
▪ Can access only functions and module parameters and nothing else declared outside the function
definition.
▪ Module parameters must be previously assigned use of defparam can produce undefined results.
● You cannot directly select a bit or part of // bit select of word at address 100,7,15
an array element. reg out1 = array2[100][7][15];
Verilog-2001:
● Multidimensional arrays integer, reg,
reg[range], real, realtime, time
and nets.
● You can directly select a bit or part of an
array element.
[base_expr -: width_expr]
Verilog lets you index an array with a variable, but if you want to select a slice of an array, only one of
the bounds can be a variable expression.
To index a variable slice of an array, you need to use the index part select construct in Verilog.
An indexed part select contains:
▪ A base expression, which can be a variable expression.
▪ A width expression, which must be a constant expression.
▪ An offset direction, which defines whether the width is added to or subtracted from the base.
▪ [base_expr +: width_expr] //positive offset
▪ [base_expr -: width_expr] //negative offset
● Offset indicates whether the width is added or reg [39:0] str = "abcde"; Verilog-2001
subtracted from the base. integer i;
initial begin
● For i = 4, the expression gives:
for (i=0;i<5;i=i+1)
$display("%s", str[39:32]); $display("%s", str[i*8 +: 8] );
...
● Lower bound 32 is derived from the base Loop expansion
expression. reg [39:0] str = "abcde";
integer i;
● Upper bound 39 comes from the positive offset initial begin
and width expression. $display("%s", str[7:0]);
$display("%s", str[15:8]);
$display("%s", str[23:16]);
$display("%s", str[31:24]);
$display("%s", str[39:32]);
...
Offset direction
[base_expr +: width_expr] //positive offset
[base_expr -: width_expr] //negative offset
for (i=0;i<5;i=i+1)
$display("%s", str[i*8 +: 8]);
Expands to:
$display("%s", str[7:0]); // i = 0, output "e"
$display("%s", str[15:8]); // i = 1, output "d"
$display("%s", str[23:16]); // i = 2, output "c"
$display("%s", str[31:24]); // i = 3, output "b"
$display("%s", str[39:32]); // i = 4, output "a"
Generated Instantiation
IEEE 1800-2012 A.4.2
Verilog-2001
genvar i; //required index type in or outside generate
Verilog-2001
module multiplier (a, b, product);
parameter a_width = 8, b_width = 8;
input [a_width-1:0] a;
input [b_width-1:0] b;
output [ (a_width+ b_width)-1:0] product;
endmodule
Verilog-2001
generate
case (WIDTH) // expression must be known at elaboration
1: adder_1bit x1(co, sum, a, b, ci);
2: adder_2bit x1(co, sum, a, b, ci);
// others - scalable carry look-ahead adder
default: adder_cla #(WIDTH) x1(co, sum, a, b, ci);
endcase
endgenerate
Verilog-2001
generate
for (j=0; j<18; j=j+1)
begin: nestgen
if (j=0)
count1[j] = count0[j]; // genblk1 (implicit name)
else
count1[j] = count0[j] & count1[j];
case (j) // genblk2
1,2: alu1 U1(a[j], b[j], c[j]);
default: alu2 U2 (a[j], b[j], c[j]);
endcase
end
endgenerate
The generate…for construct requires an explicit block name. The generate…case and generate…if
constructs do not.
top.v
● Verilog-1995: No mechanism for module library management: module top;
...
▪ People typically use different module definition names and
alu U1 (...);
conditional compilation.
alu U2 (...);
● Verilog-2001: Configurations enable design management: endmodule
▪ Virtual library structures for holding and referencing compiled
code. addr.v
▪ Portable mapping of virtual libraries to specific directories. module adder;
// RTL
▪ Binding of an instance to a specific module from a specific library.
endmodule
▪ Identification of top-level design block.
addr.vg
module adder;
// GATE
419 © Cadence Design Systems, Inc. All rights reserved. endmodule
For the Cadence® Xcelium™ simulator, you provide a file typically called cds.lib in which you define
the libraries the tool may access and where in the file system they are physically located. You make
these statements, one to a line, in the form:
▪ Define logicalLibraryName filesystemLocation
Precedence
● Files can be matched explicitly by a wildcard name
or directory. library rtlLib "./rtl/*.v";
library gateLib "./gate/*.v";
● Precedence should resolve multiple matches.
▪ Otherwise, it is an error
Directory
● Unmatched files are compiled to a default library
named work. library rtlLib "./rtl/";
library gateLib "./gate/";
The compiler stores compiled objects according to library declarations in standard library map file.
Library declaration syntax:
library library_identifier file_path_spec [ { , file_path_spec } ]
• [ -incdir file_path_spec [ { , file_path_spec } ] ;
File system path specifications might use wildcards:
▪ ? matches any single character
▪ * matches any number of characters in a directory or file name
▪ ... matches any number of hierarchical directories
▪ .. matches the parent directory
▪ . matches the directory containing the library map file
Paths which end in / shall include all files in the specified directory (identical to /*).
Paths which do not begin with / are relative to the directory in which the current lib.map file is located.
Note: if you use a path with // or /* in it, you may need to enclose the path between double quotes (“ ”).
Example
library gateLib ./mylib/*.vg;
All files with the .vg extension in the ./mylib directory are compiled into the gateLib library.
The elaborator by default searches libraries for instance definitions in the order you declare the libraries.
You can in a cell configuration declaration specify an alternative default library search order.
You can also in that configuration specify for any cell type or for a specific instance:
▪ A library search order
▪ A specific library
▪ A specific configuration
Configuration Example
library lib1 "./rtl/*.v";
library lib2 "./gate/*.v";
cfg.v
lib.map config vectest;
design lib1.vectest;
default liblist lib2 lib1;
instance vectest.dut1 liblist lib1 lib2;
instance vectest.dut2 use lib1.cpu;
cell foo use lib1.foo;
endconfig
Hierarchical Configurations
config bottom;
design lib1.bottom;
default liblist lib1 lib2;
instance bottom.a1 liblist lib3;
endconfig
config top;
design lib1.top;
default liblist lib2 lib1;
instance top.bot use lib1.bottom:config;
endconfig
Configuration Keywords
config <config_name> endconfig
● Defines the start and end of a configuration along with the configuration name.
design [lib.]<design_unit>;
● Specifies top-level design element; one and only one design statement must exist.
default liblist <lib1> <lib2>;
● Selects all instances not matched with more specific selection clauses.
liblist <lib1> <lib2>;
or
instance <hierarchy_path> use [lib].<design_unit>;
Configuration by cell:
● Binds all object instantiations to a specific module or library search order
cell <design_unit> liblist <lib1> <lib2>;
or
cell <design_unit> use [lib.]<design_unit>;
The IEEE Std. 1364-2001 does not mention whether the cell clause or the instance clause should have
precedence. To ensure portability of your configuration between vendors, you cannot assume that you
can override a cell clause for a specific instance of that cell type.
Verilog-1995
$fclose(data_chan);
end
endmodule $fgetc returns -1 (EOF) if an error occurs
code = $fgets(str, fd) – Copies line of data (including newline character) from file fd into
str (or fills str) and returns the number of characters read into code. code = zero on read error.
code = $rewind(fd) – Sets the next file operation to the start of the fd file.
$swrite(b|o|h) as $fwrite but write data as a string to a reg variable.
$sformat is similar to $swrite except the second argument is interpreted as a format string.
$fscanf(fd, format, args) – Reads characters from the fd file and interprets them according to
the control string format. $sscanf works similarly with strings instead of files.
$fread(obj, fd) – Reads binary data from the fd file into obj which is a reg or memory object. If
obj is a memory, additional arguments can define a starting address and location count in the memory.
$ftell(fd) – Returns the number of the next byte to be read from the fd file.
code = $fseek(fd, offset, op) – Sets the file pointer (position of the next file operation) for
the fd file, offset from the beginning. The current position or end of the file according to the value of op
is as shown here:
▪ op = 0 – Sets pointer to beginning of file plus offset
▪ op = 1 – Sets pointer to current position plus offset
▪ op = 2 – Sets pointer to end of file plus offset
offset is a signed value. code = -1 if reposition fails (e.g., op = 2 and offset is positive), 0 if
successful.
Appendix B
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
Appendix Objective
In this appendix, you
● Analyze Event Scheduler
A simulation timeslot is divided into ordered regions to provide a predictable interaction between design
constructs.
The Verilog event schedule has four regions for each simulation time:
▪ The Active region is for executing process statements.
▪ The Inactive region is for executing process statements postponed with a zero (#0) procedural delay.
▪ The NBA region is for updating nonblocking assignments.
▪ The Monitor region is for executing $monitor and $strobe and for calling user routines
registered for execution during this read-only region. You cannot create additional events within this
region.
The first three of these regions are iterative – they can schedule events that require return to the Active
region. When no more events exist for the current simulation time, the simulator executes Monitor
statements and then advances simulation time to the next time for which events are scheduled. The
simulation terminates when no such future events exist.
SystemVerilog adds regions to provide a predictable interaction between assertions, design code and
testbench code.
The SystemVerilog event schedule has 13 regions for each simulation time. Some regions used only by
the VPI are not shown on the slide and not described here:
▪ The Preponed region is for sampling signal values before anything in the time slice changes their
values. It is equivalent to the #1step sampling delay used in clocking blocks.
▪ The Active region is for executing interface and module statements (same as Verilog).
▪ The Inactive region is for executing statements postponed with zero (#0) procedural delay from the
Active region (same as Verilog).
▪ The NBA region is for updating nonblocking assignments from the Active region (same as Verilog).
▪ The Observed region is for assertion evaluation.
▪ The Reactive region is for executing program block statements, blocking delay assignment in
program blocks and assertion action blocks.
▪ The Re-inactive region is for executing statements postponed with a zero (#0) procedural delay from
the Reactive region.
▪ The Re-NBA region is for updating nonblocking assignments from the Reactive region.
▪ The Postponed region is for system tasks that record signal values at the end of the time slice.
The Active through Re-inactive regions are iterative. Most of these regions can schedule events that
require return to the Active region.
Programs
Appendix C
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
Appendix Objective
In this appendix, you
● Identify the features of programs for verification code
Programs
IEEE 1800-2012 24 program memtest (
output wire [7:0] data;
output bit [4:0] addr;
output bit read, write);
● A program is very similar to a module, but intended for ...
initial begin
testbench code.
...
▪ Allows separation of design and test. end
▪ Is executed in a separate time region to avoid design/test endprogram : memtest
races.
o Programs execute in the Reactive region.
module top;
o Modules execute in the Active region.
wire [7:0] data;
● Program blocks have special features and restrictions wire [4:0] address;
for testbench use. wire read, write;
This table lists most of the restrictions on specific construct use in programs. Constructs which are not
allowed will generate compile-time errors.
As a general rule, constructs that clearly represent design rather than verification are not allowed.
Note that concurrent assertions can be declared and executed in programs. However, assertions have a
clearly defined scheduling in that they are sampled in Preponed, evaluated in Observed and their action
blocks executed in Reactive. Therefore, they do not use program scheduling of execution in the
Reactive region.
A SystemVerilog philosophy is that the testbench should know about the design, but the design should
not be aware of the testbench. Therefore, only programs can access variables or nets declared in a
program (collectively called program signals). Likewise, only programs can call subroutines declared in
programs. Programs can access signals or subroutines declared in other programs via hierarchical
references. Programs can access signals or subroutines anywhere in the design via hierarchical
references. When a program calls a subroutine defined in a module or interface, it is executed according
to the program scheduling – i.e., in the Reactive region.
Active
Observed
Reactive
Postponed
439 © Cadence Design Systems, Inc. All rights reserved. To next time slot
The active region is for executing design code defined in modules and interfaces:
▪ The Active region is for executing interface and module statements.
▪ The Inactive region is for executing statements postponed with zero (#0) procedural delay from the
Active region.
▪ The NBA region is for updating nonblocking assignments from the Active region.
The Reactive region is for executing testbench code defined in programs:
▪ The Reactive region is for executing program block statements, blocking delay assignment in
program blocks and assertion action blocks.
▪ The Re-inactive region is for executing statements postponed with a zero (#0) procedural delay from
the Reactive region.
▪ The Re-NBA region is for updating nonblocking assignments from the Reactive region.
The Active and Reactive regions are iterative. Most of these regions can schedule events that require
return to the Active region.
exit
exit
finish
A SystemVerilog program can call $exit to immediately terminate its initial blocks and their
child processes and remove any pending assignments from the update queue. The $exit does not
apply to continuous assignments and does not apply to subroutines declared within the program but
invoked from other programs. Only a program can call $exit.
The $exit system task differs from the $stop and $finish system tasks, in that $exit
immediately applies only to the program that starts it. The simulation terminates when all programs
have terminated, either naturally or through $exit calls.
A program can be nested inside a module or interface declaration. If the nested program has no port
connections, then you do not need to directly instantiate the program. If you do not explicitly instantiate
the program, then it will be implicitly instantiated once with an instantiation name matching the
program declaration name. Likewise, a top-level program is also implicitly instantiated if there is no
explicit instance declared.
You can define anonymous programs in packages and in compilation unit scopes. Anonymous programs
can declare a restricted set of program items without declaring a new scope, i.e., the declarations share
the same name space as the package or compilation unit scope in which the program. Declarations in an
anonymous program cannot be referenced from other programs via a hierarchical pathname.
Miscellaneous Features
Appendix D
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
Appendix Objectives
In this appendix, you
● Analyze miscellaneous SystemVerilog constructs, including
▪ final blocks
▪ Unions
▪ Fine-grain process control
▪ $root
▪ Nested modules
▪ $urandom and $urandom_range
▪ randsequence
final begin
if (timeout_error)
$display ("ERROR: %0t: Test Timed Out", $time);
else
$display ("INFO: %0t: Test Complete", $time);
$display("Error Count: %d", error_count);
$display("Fifo Overflow Count: %d", fifo_overflow);
end
A final block cannot consume simulation time, but is otherwise similar to an initial block. You
can place a final block anywhere you can place an initial block. SystemVerilog executes all final blocks
exactly once at the end of simulation in an unspecified but repeatable order.
SystemVerilog executes the final blocks upon encountering $finish or upon running out of events to
process. Any final block that itself calls $finish or calls a user-defined system task or function that
terminates the simulation aborts execution of the final blocks. After SystemVerilog completes or
terminates execution of the final blocks, it calls any PLI routines you have scheduled for execution at
the end of the simulation.
Unions
IEEE 1800-2012 7.3
The union construct provides a way to view the same memory location as different data types without
the need to cast the data types. The SystemVerilog union is identical to the C union, with two
exceptions:
1. You cannot provide a tag name for a SystemVerilog structure or union. You can alternatively use the
typedef keyword to declare a type name for the structure or union.
2. You can further qualify the SystemVerilog union as “tagged”. A SystemVerilog tagged union is type-
checked. The next slide has an example of a tagged union.
Union Example
Structures can contain unions and unions can contain structures.
typedef struct { union { int i;
shortreal f;
} n; // anonymous union type
bit isfloat;
} tagged_s; // named struct type
The top example declares an array of a named structure type that contains a field that is a union. The
other structure field is a bit that indicates whether the structure union field is currently being used as an
int type or as a float type.
SystemVerilog provides the tagged union qualifier to serve exactly that purpose. A tagged union stores
both its current value and its current tag. Subsequent expressions accessing the tagged value must be of
the current tag type. This type checking is generally done during runtime.
● Every static and dynamic thread creates an instance of the class process.
class process;
enum state { FINISHED, RUNNING, WAITING, SUSPENDED, KILLED };
...
The process class is built in. SystemVerilog creates an instance of process for every process created,
either statically, for initial and always blocks, or dynamically, for fork blocks. You obtain the handle of a
process by calling its self() method in the block and assigning to a handle of the process type. By
declaring that handle somewhere accessible to other processes, they can then manipulate the state of that
process.
You cannot create your own instances of the process class. Nor can the process class be extended with
sub-classes.
$root
● $root is a reference to top-most design element in hierarchical paths.
● In Verilog, local paths take precedence:
A
▪ From REF, relative path A.B is always absolute path A.REF.A.B.
Reused
instance B
name
Verilog does not differentiate between an absolute hierarchical path name and a relative hierarchical
path name. If the path names happen to be identical, Verilog assumes you mean the relative path. To
avoid such confusion, most users name their top-level module something obvious like top and do not
reuse that instance name lower in the hierarchy. For SystemVerilog, you can use “dollar root” ($root)
to refer to a virtual simulation root, thus unambiguously specifying an absolute path.
Module References
For Verilog, you need to declare your modules in the definitions name space. All module
declarations are globally visible and can be instantiated in any other module.
Issues: A
● Different modules with the same name cause naming
conflicts:
▪ E.g., two modules named FSM. FSM B IP C
▪ Verilog-2001 configurations can help.
Solution:
E
SystemVerilog adds nested module declarations (more
on next page). ● Two different modules named FSM
! ● Using IP module E externally
449 © Cadence Design Systems, Inc. All rights reserved.
For Verilog, you need to declare your modules in the definitions name space. All module declarations
are globally visible and can be instantiated in any other module. As a developer of intellectual property,
you might want to prohibit a module of your design to be instantiated anywhere outside the IP hierarchy.
Or you may want to simply use the same module name that someone else on your team has already
used.
Nested Modules
● You can declare (nest) a module in a module module FSM (...);
declaration. ...
endmodule : FSM
● Nested modules can further declare nested
modules. module IP(...);
endmodule : IP
module D (...);
...
endmodule : D
dmod.v
450 © Cadence Design Systems, Inc. All rights reserved.
A nested module definition is visible only in the module where it is declared or in any sub-hierarchy of
the module where it is declared. You can declare modules in the top level of your own design
component, and thus restrict their use to just that component.
Such local declarations override declarations in the definitions name space. This allows the use of
different module definitions of the same name.
Deeply nesting module definitions can make your code less readable. You may want to judiciously
utilize include (`include) directives and place each module definition in a separate file.
The FSM module in the example may be instantiated before it is declared. This is consistent with such
declarations in the definitions name space.
int value;
bit [63:0] addr;
If all you want is a random integer value, you can use the SystemVerilog $urandom() system
function to obtain a random unsigned 32-bit integer, or $urandom_range() system function to
constrain the value to a range of values. These functions differ from the Verilog $random() function
in that:
1. They return unsigned values rather than signed values.
2. The random sequences are independent of each other, i.e., they use the SystemVerilog
randomization implementation. For backward compatibility, the Verilog $random still uses the
Verilog randomization implementation where all randomization calls are part of the same single
sequence.
function int unsigned $urandom [ (int seed) ] ;
function int unsigned $urandom_range(int unsigned maxval, int unsigned
minval=0);
randsequence ( main )
main : arith stk last;
arith : inc | dec;
● You can use randsequence to execute a random Nonterminals
stk : push | pop;
sequence of statements. inc : { do_inc(); };
dec : { do_dec(); };
● Based on rules called a production list.
push : { do_push(); };
Terminals
● Production list items are either: pop : { do_pop(); };
last : { do_check(); };
▪ Terminal: an indivisible item that needs no more definition.
endsequence
▪ Nonterminal: Defined in terms of terminals and other
nonterminals.
Valid Combinations
● All productions must ultimately decompose into
terminal productions. do_inc() do_push() do_check()
do_dec() do_push() do_check()
do_inc() do_pop() do_check()
do_dec() do_pop() do_check()
You can use the randsequence keyword to execute a random sequence of statements based on rules
called a production list. The argument to the randsequence is the identifier of the production to start
with. This defaults to the first listed production.
Each production consists of a function declaration, with optional argument and return type declarations,
followed by a colon, followed by rules for the production, where rules are separated by the bitwise OR
(|) operator.
Each rule is a production list of one or more productions, and may have a weight associated with it, and
may have a code block associated with it.
A code block is data declarations and statements between curly brackets ({}).
Each randsequence construct creates its own automatic scope, which means that production
declarations are not visible outside of the randsequence construct. Furthermore, each code block
creates an anonymous automatic scope, so its declarations are also not visible outside the code block.
The sequence in this example starts with the main production, which defines one production rule
consisting of the arithmetic (arith), stack (stk), and last productions, in sequence. The arithmetic
production defines the increment (inc) and decrement (dec) alternative rules, each equally likely to be
selected. The stack production defines the push and pop alternative rules, each also equally likely to
be selected.
randsequence ( main )
inc is twice as
main : arith stk last;
likely as dec
arith : inc:=2 | dec:=1;
stk : push:=3 | pop;
inc : { do_inc(); }; push is thrice
dec : { do_dec(); }; as likely as pop
push : { do_push(); };
pop : { do_pop(); };
last : { do_check(); };
endsequence
You can weight production alternatives. These are the relative weights of production rules within a
production list, and do not weight the production list itself relative to any other production list.
randsequence ( main )
main : repeat(5) arith stk last;
arith : if (incr)
inc; Very limited constructs in
else productions
dec;
stk : case (which)
0: push;
1: pop; Less limited constructs
endcase; in code blocks
inc : { do_inc(); };
dec : { do_dec(); };
push : { do_push(); };
pop : { do_pop(); };
last : { do_check(); };
endsequence
A production list can contain a restricted set of procedural constructs. It can contain the case and
if...else constructs to make the production conditional, and can contain the repeat construct to
repeat the production a number of times.
i == 1 produces B B C
i == 2 produces A
455 © Cadence Design Systems, Inc. All rights reserved.
Your code blocks can use the break keyword and the return keyword to abort productions. These
keywords have special semantics within a randsequence construct. Within a randsequence
construct, the break keyword terminates the entire randsequence production, and not just any loop it
happens to be in, and the return keyword terminates only the current production. As every production
declaration is a local function declaration, and every production item is a task call, the code block of a
production item can utilize those arguments and can return a value.
randsequence ( main )
main : arith($urandom) {$display(arith);} stk last;
int arith(int i) : inc(i) {return inc;} |
dec(i) {return dec;};
stk : push | pop;
int inc(int i) : { return do_inc(i); };
int dec(int i) : { return do_dec(i); };
push : { do_push(); };
pop : { do_pop(); };
last : { do_check(); };
endsequence
The syntax for declaring a production is similar to the syntax for declaring a function. However, a
production is not a function; the production code block can consume time.
The syntax for passing data to a production is similar to a task call.
A production for which you have declared a type can return data. You return the data in the return
statement. The data is available as the production item name after the production item “call” in the
“calling” production.
This example passes a random number to the arithmetic production and displays the returned value. The
arithmetic production further exchanges this value with either the increment or decrement production.
The increment and decrement productions each call a function to implement their operation.
always
wait_order(e1.triggered, e2, e3)
$display("events in order");
A process can wait for a specific sequence of else
events: $warning("events out of order");
A process can wait for a specific sequence of events. The first listed event can optionally be the
triggered state of an event. The wait_order statement has characteristics very similar to a concurrent
assertion. Occurrence in turn of each event implies that the next event in the list has not yet occurred
and will occur before any of the later events in the list. Re-occurrence of an event has no additional
effect. Events out of order constitute an error by default. You can change this in an associated action
block.
Appendix E
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
Appendix Objectives
In this appendix, you
● Analyze key SV2012 features which are enhancements for addressing verification
▪ Interface classes
▪ Remove restrictions on NBA assignments to class members
▪ Soft constraints
▪ Constraint to generate unique elements
task write();
task read();
task burst_write();
task burst_read();
Error class AHB_BUS extends A,B,C,D
460 © Cadence Design Systems, Inc. All rights reserved.
Interface Classes
interface classes may declare
IEEE 1800-2012 8.26 pure virtual prototypes
● Classes which extend or implement the interface class can only reference the virtual methods:
▪ Type and parameter declarations are visible only through the method declarations.
▪ Cannot be referenced directly.
● An interface class:
▪ Cannot be declared inside of another class (nested class).
▪ Shall not be nested within another class.
1. Parameters and typedefs within an interface class are inherited by extending interface class and not by
implementing them.
2. Parameters and typedefs defined in an interface class are static.
3. They can be accessed by the scope resolution operator but not by a dotted reference.
Error
class base4 implements cbase;
...
endclass
Hard Constraints
hardsoft.sv
module base;
IEEE 1800-2012 18.5
class hard_con; Hard constraint
rand bit [4:0] var1;
constraint c1 {var1<4;}
● All SystemVerilog 2009 constraints are hard: endclass
▪ All constraints must be met in randomization.
hard_con hc1 = new(); Inline constraint conflicting
● Probable ways of resolving conflicts are given with declarative constraint
below:
initial
▪ Disabling default constraints. if(!(hc1.randomize() with {var1 == 5;}))
▪ Using soft constraints. $error("Randomization Error");
● Soft constraints can be sacrificed to reach a
endmodule
solution.
if(!(hc1.randomize() with {var1 == 5;})) Constraint violation on randomization
|
xmsim: *W,SVRNDF (./hardsoft.sv,11|21): The randomize method call failed.
xmsim: *W,RNDOCS: These constraints contribute to the set of conflicting constraints:
Constraints which are used till SV2009 are called as hard constraints except distribution constraints.
One way to overcome this is by disabling default constraints before overriding them with conflicting
constraints.
class soft_con;
rand bit [4:0] var1;
● Constraints can be made soft using the constraint c1 {soft var1<4;}
keyword soft endclass
● Order of declaration of soft constraints is soft_con sc1 = new(); Inline constraint conflicting
with declarative constraint
important.
initial
▪ Prioritized in order of declaration. if(!(sc1.randomize() with {var1 == 5;}))
o See the next slide. $error("Randomization Error");
endmodule
RESULT:
Value of var1 is 5
No constraint violation –
soft constraint is sacrificed
The priorities of soft constraints are defined such that the last constraint specified prevails.
Consequently, constraints specified in subsequent layers in a verification environment are assigned
higher priorities than those in preceding layers.
class base1;
rand bit [4:0] var1; Priority Low
constraint c1 { soft var1 < 4;}
endclass
Solution satisfying C1
NO
Solution satisfying C2
NO
Discard both C1 & C2
NO
Uniqueness Constraints
IEEE 1800-2012 18.5.5
class base1;
rand bit [1:0] a,b,c,d;
rand bit [1:0] list[5];
class base1;
rand bit [1:0] a,b,c,d; List having group of variables
rand bit [1:0] list[5]; with equivalent types
randc bit [1:0] var;
base1 b1 = new();
initial
repeat(1)
if(!b1.randomize()) $display("Randomization error");
base1 b1 = new();
initial begin
if(!b1.randomize()) $error("Randomization error");
...
DPI Reference
Appendix F
Revision 1.0
Version 21.10
Estimated Time:
● Lecture
● Lab
● In addition to the above, imports support the following formal argument types:
▪ Packed arrays/structures/unions of bit or logic.
▪ Enumerated types (as their base type only, without value names).
▪ Further constructs of supported types:
o Including unpacked arrays, packed or unpacked structures, packed unions, typedefs of all the above.
▪ A C function imported as a task must return the void type.
▪ “Small” type input arguments are passed by value, others by reference.
You can place import declarations in the compilation unit scope, packages, interfaces, modules or
programs. Placing it in a package simplifies sharing it among multiple design and test units.
You can import any C function. It is common to import functions from the C math library.
SystemVerilog returns the function results by value, so restricts the function return types to the “small”
types. It passes small arguments by value and large arguments by reference, which on the C side means
by pointer.
C functions imported as a SystemVerilog task must return the void type.
You can give a C function imported as a SystemVerilog task or function the context property. You can
give a C function imported as a SystemVerilog function the pure property. Following slides describe
these properties.
● Exported subroutines:
▪ Accept the same argument data types as imported subroutines.
▪ Are always context (aware of their scope).
▪ Can be called by imported context subroutines.
o Functions cannot call tasks.
▪ Exported tasks can consume time.
o This is how you create a time delay in the C layer.
You can place export declarations in the compilation unit scope, packages, interfaces, modules or
programs. Placing it in a package simplifies sharing it among multiple design and test units.
An export declaration can export only the subroutines declared in its own scope, and can export a
subroutine at most once. An export declaration is illegal within a class type declaration and so cannot
export a class method.
An imported subroutine declared as context can call an exported subroutine. This is how you create a
time layer in C. The time is actually consumed by the exported SystemVerilog task called by an
imported task. A SystemVerilog function cannot of course call a SystemVerilog task regardless of what
language actually implements the task.
● Some imported subroutines may need to know the scope (context) of their call:
▪ For visibility of variables, etc., in the local scope.
● A noncontext imported subroutine cannot access any SystemVerilog data other than its actual arguments.
Pure Functions
IEEE 1800-2012 35.5.2
The SystemVerilog initial block calls the import_c_func, which is linked to the C function named
c_func by the import declaration. This function is imported with the context property so that it
can in turn call exported SystemVerilog subroutines.
The C implementation of the c_func function calls the sv_func function that it has previous
declared to be extern. This function is linked to the SystemVerilog function named
export_sv_func in the export declaration.
© Cadence Design Systems, Inc. All rights reserved worldwide. Cadence, the Cadence logo, and the other Cadence marks found at https://www.cadence.com/go/trademarks are trademarks or registered trademarks of Cadence Design
Systems, Inc. Accellera and SystemC are trademarks of Accellera Systems Initiative Inc. All Arm products are registered trademarks or trademarks of Arm Limited (or its subsidiaries) in the US and/or elsewhere. All MIPI
specifications are registered trademarks or service marks owned by MIPI Alliance. All PCI-SIG specifications are registered trademarks or trademarks of PCI-SIG. All other trademarks are the property of their respective owners.