UVM HowTo
UVM HowTo
UVM HowTo
GOOGLE CONFIDENTIAL
Contents
Overview
Goals of a Flexible Futureproof UVM Verification Environment
Document Outline
Big Picture (and some background)
Testbench Top - TB Top (Synthesizable)
Testbench Top - DV Top
Agent
Environment
Test
Virtual Sequence
How to Code it
The Interface Protocol Agent
Configuration Object
Interface
Item
Driver
Monitor
Sequencer
Agent
Sequence
Nominal Sequence
Helper Sequence
Sequence List
Package
Package (Synthesizable)
Parameterizing Widths
DUT Testbench / Environment
DUT Specific Agents
Testbench Top - TB Top (Synthesizable)
Testbench Top - DV Top (UVM Side)
Configuration Object
Scoreboard
Virtual Sequencer
Environment
Test
Base Test
Derived Tests
gChips DV Google Confidential pg 2 / 93
Google Proprietary & Confidential
Test List
Virtual Sequences
Base Virtual Sequence
Virtual Agent
Test Base Virtual Sequence
Derived Test Virtual Sequence
Test Directed Base Virtual Sequence
Test Sequence List
Environment Package
Useful Agents / Objects
Clock Agent
Reset Agent
Delay Object
Activity Watchdog Component
Time Package
IRQ Reactive Agent
Ready / Valid Active Request Agent
Ready / Valid Reactive Response Agent
'No Protocol' Agent
Directory Structure
Overview
To accomplish these goals our environment implementation needs to obey the principle of "separation of
concerns."
Separate:
● interface protocols
● interface dimensions / widths / payload types
● environment integration
● environment configuration
● test sequencing (both random and directed)
● dut power on sequencing
● dut configuration sequencing
● dv environment configuration
● static concerns (how many / how much / how wide / overridden constraints) from dynamic
concerns (when / timing / relative timing / sequencing)
Document Outline
This document will provide a big picture overview along with a small amount of background information. Next,
we'll go bottom up - describing how to implement each element of a good UVM verification environment. Then,
we'll discuss requirements for emulation. Finally, we'll quickly cover the recommended directory structure. For
those that use the genesis pre-processor, we'll cover what additional code needs to be present - both in-line and
in some extra files at the end.
The reader should already be familiar with the UVM library.
The above diagram shows how all of the pieces of a UVM verification environment come together around the
DUT.
to the DUT ports. This module contains only the synthesizable portion of the design -- the DUT RTL and the
synthesizable interfaces; the UVM portion of the verification environment is done outside the testbench top (aka
the DV top.) This split top has two benefits: 1) ensures a quick path to testbench acceleration / emulation - where
the tb_top runs in the emulator and the dv_top runs in the simulator and 2) faster re-elaboration times when only
the DV code has changed and the RTL has remained the same.
Agent
An agent both drives and monitors a protocol. An agent refers to both a collection of files required in an agent's
implementation and the agent file / class itself. An agent collection includes the following files:
Environment
The environment class encapsulates all of the agents along with the support classes - virtual sequencer and
scoreboard.
● scoreboard class - connects to all of the agents via analysis ports; when an item is received by an
analysis port that item is compared, stored, or dropped -- depending on the details of how the
scoreboard has been configured
● virtual sequencer class - a collection of handles pointing to each of the agent sequencers
instanced in the environment; used by the test virtual sequences to send ordered traffic across
any or all of the available DUT interfaces
Test
A test class is used to configure and instance the environment class and to launch a test virtual sequence.
An environment class commonly has several corresponding test classes - each configuring and / or constraining
the environment differently.
A test class configures the environment by instancing the configuration objects for each of the instanced agents -
and setting the configuration object members. Configuration objects are set into the uvm configuration database
so as to be available to be retrieved by the agents.
Virtual Sequence
A virtual sequence is a sequence that isn't specialized to run on a particular protocol interface. A virtual
sequence constructs, randomizes, and starts one or more protocol agent sequences or virtual sequences. A
virtual sequence can be used to control traffic on a multi-interface protocol -- a protocol supported by multiple
interface agents. More commonly, a virtual interface is used to drive ordered traffic to the DUT to create a test
sequence; these are called test virtual sequences. A test virtual sequence launches reactive sequences and
then active sequences in an ordered fashion to stimulate the DUT.
Note that we distinguish between a test - which configures the environment and environment constraints, and
launches the test sequence, from the test sequence itself. This distinction allows us to reuse our test sequences
with varied constraints from varied tests without having to reimplement our (often complicated and/or verbose)
sequences.
How to Code it
In this section we'll explore how to actually code each of the pieces. We'll start with the agent - since it is
ubiquitous and used everywhere.
Then we'll go bottom up and describe how to build: tb_top, environment, agent specializations, scoreboard,
virtual sequencer, and the set of common virtual sequencers.
Configuration Object
The configuration object is the description of how the agent is configured. This is a testbench static description
as to how the agent should behave. (As opposed to the testbench dynamic description of how a particular
transfer should occur - which is described in the item.)
This object contains the members that define how the environment is built - and in some ways - how the
environment is to behave. The configuration is built and consumed at the build phase. Configuration objects
should not be consumed at any point in the run phase.
The configuration object is the single / only encapsulation of all of the configurable variables across the agent. All
configuration members should reside in the configuration object - rather than independently within any of the
driver, monitor, sequencer, agent, etc.
Examples of what is included in the configuration object are: whether the agent is active or passive; how many
cycles before timing out (when waiting for the DUT to respond); debug mode activation; whether the monitor
should include X checking; etc etc.
Note that the configuration object doesn't include interface widths. Any signal width parameterization comes from
a parent specialization of this agent. The agent and corresponding interface needs to be parameterized if
interface widths can vary. Below are some guidelines:
A configuration object is an extension of the uvm_object class and contains state member variables along with
optional public helper functions - to set / get those members. Like all classes, it is recommended to use an 'm_'
prefix (exclusively) for class member variables.
A configuration object should be named with a '_cfg' suffix.
A configuration object should contain no tasks.
A configuration object should register with the factory using the object_utils macro.
Each member variable of a configuration object should be included in the field macros - or be implemented in
each of the do_ methods (do_copy, compare, print).
The 'do_' methods provide a little more control over the outcome in exchange for some extra typing . The 'do_'
methods really come in handy when you have a type that isn't in the pre-build field macros.
The 'do_' methods can be mixed with the field macros.
//--------------------------------------------------------------------------------------------------
// Reset Agent Config
//--------------------------------------------------------------------------------------------------
class reset_cfg extends uvm_object;
// Constructor
extern function new(string name = "");
// Field Macros
`uvm_object_utils_begin(reset_cfg)
`uvm_field_enum(uvm_active_passive_enum, m_uvm_active_passive_h, UVM_ALL_ON)
`uvm_field_enum(reset_active_t, m_reset_active, UVM_ALL_ON)
`uvm_field_enum(reset_assert_t, m_reset_assert, UVM_ALL_ON)
`uvm_field_enum(reset_deassert_t, m_reset_deassert, UVM_ALL_ON)
`uvm_object_utils_end
endclass: reset_cfg
//------------------------------------------------------------------------------
function reset_cfg::new(string name = "");
super.new(name);
// Defaults
m_uvm_active_passive_h = UVM_ACTIVE;
m_reset_active = RESET_ACTIVE_LOW;
m_reset_assert = RESET_ASSERT_ASYNC;
m_reset_deassert = RESET_DEASSERT_SYNC;
endfunction: new
The configuration object should follow the following structure - with do_ methods:
//--------------------------------------------------------------------------------------------------
// Reset Agent Config
//--------------------------------------------------------------------------------------------------
class reset_cfg extends uvm_object;
// Factory Registration
`uvm_object_utils(reset_cfg)
// Constructor
extern function new(string name = "");
// Do Methods
extern virtual function void do_copy(uvm_object rhs);
extern virtual function bit do_compare(uvm_object rhs, uvm_comparer comparer);
extern virtual function void do_print(uvm_printer printer);
extern virtual function void do_pack(uvm_packer packer);
extern virtual function void do_unpack(uvm_packer packer);
endclass: reset_cfg
//------------------------------------------------------------------------------
function reset_cfg::new(string name = "");
super.new(name);
// Defaults
m_uvm_active_passive_h = UVM_ACTIVE;
m_reset_active = RESET_ACTIVE_LOW;
m_reset_assert = RESET_ASSERT_ASYNC;
m_reset_deassert = RESET_DEASSERT_SYNC;
endfunction: new
//------------------------------------------------------------------------------
function void reset_cfg::do_copy(uvm_object rhs);
this_type_t rhs_;
super.do_copy(rhs);
if (!$cast(rhs_, rhs)) begin
`uvm_fatal(get_name(), "do_copy() type mismatch")
end
m_uvm_active_passive_h = rhs_.m_uvm_active_passive_h;
m_reset_active = rhs_.m_reset_active;
m_reset_assert = rhs_.m_reset_assert;
m_reset_deassert = rhs_.m_reset_deassert;
endfunction: do_copy
//------------------------------------------------------------------------------
function bit reset_cfg::do_compare(uvm_object rhs, uvm_comparer comparer);
this_type_t rhs_;
bit ret_val;
end
ret_val = (super.do_compare(rhs, comparer) &&
(m_uvm_active_passive_h == rhs_.m_uvm_active_passive_h) &&
(m_reset_active == rhs_.m_reset_active) &&
(m_reset_assert == rhs_.m_reset_assert) &&
(m_reset_deassert == rhs_.m_reset_deassert)
);
return(ret_val);
endfunction: do_compare
//------------------------------------------------------------------------------
function void reset_cfg::do_print(uvm_printer printer);
super.do_print(printer);
//printer.print_field_int( "m_payload", m_payload, $bits(m_payload) ); // int
//printer.print_generic ( "m_payload", "enum", $bits(m_payload), m_payload.name()); // enum
printer.print_generic(
"m_uvm_active_passive_h",
"enum",
$bits(m_uvm_active_passive_h),
m_uvm_active_passive_h.name()
);
printer.print_generic(
"m_reset_active",
"enum",
$bits(m_reset_active),
m_reset_active.name()
);
printer.print_generic(
"m_reset_assert",
"enum",
$bits(m_reset_assert),
m_reset_assert.name()
);
printer.print_generic(
"m_reset_deassert",
"enum",
$bits(m_reset_deassert),
m_reset_deassert.name()
);
endfunction : do_print
//------------------------------------------------------------------------------
function void reset_cfg::do_pack(uvm_packer packer);
super.do_pack(packer);
packer.pack_field_int(m_uvm_active_passive_h, $bits(m_uvm_active_passive_h));
packer.pack_field_int(m_reset_active, $bits(m_reset_active));
packer.pack_field_int(m_reset_assert, $bits(m_reset_assert));
packer.pack_field_int(m_reset_deassert, $bits(m_reset_deassert));
endfunction : do_pack
//------------------------------------------------------------------------------
function void reset_cfg::do_unpack(uvm_packer packer);
super.do_unpack(packer);
m_uvm_active_passive_h = packer.unpack_field_int($bits(m_uvm_active_passive_h));
m_reset_active = packer.unpack_field_int($bits(m_reset_active));
m_reset_assert = packer.unpack_field_int($bits(m_reset_assert));
m_reset_deassert = packer.unpack_field_int($bits(m_reset_deassert));
endfunction : do_unpack
Interface
The interface contains a list of the signals when connected to the driver and monitor components.
The interface should be named with a '_if' suffix.
The interface should declare the clock as an input of type bit.
The interface should declare all of the interface nets as type logic.
In order to streamline the path to emulation - clocking blocks and modports should not be used.
endinterface: reset_if
Item
The item is the description of all of the transfers that can occur on the interface. It includes rand member
variables that contain: named transfers as enum type, members to hold data / control messaging to/from and any
interface delays.
Said better, from the perspective of the driver - the enum type says what transfer should occur, members that
hold data / control say what should be transferred, and the delay members describe what delays should occur.
The item class name should have a '_item' suffix.
There should be no methods implemented other than the 'do_' methods or those generated by the uvm field
macros. (Additional methods to aid the driver / monitor implementations are ok - example: crc / parity calculation,
packing / unpacking with specific formatting, etc).
The item is a collection of rand (and optionally state) variables and constraints. The rest of the code of an item is
boiler plate.
The first rand variable of an item is commonly an enum that enumerates all of the kinds of operations that can
occur in the driver.
The constraints should be marked as valid, legal, and typical - either directly named as such - or with those
names as a prefix. Using these names as prefix makes them easy to find to override or disable.
● Valid - need to be true or agent won't work correctly (things like positive values for delays)
● Legal - need to be true to obey protocol; can override to create protocol invalid transfers
● Typical - overridable constraint; use to set some reasonable bounds
We use these names consistently to make it clear which ones should be overridden (or disabled) and which
should be enabled at all times.
An item is an extension of the uvm_sequence_item class and contains rand member variables and constraints
for those member variables.
An item should contain no tasks or functions other than the 'do_' methods or those generated by the uvm field
macros.
An item should register with the factory using the object_utils macro.
Each member variable of an item should be included in the field macros - or be implemented in each of the do_
methods (do_copy, compare, print).
The 'do_' methods provide a little more control over the outcome in exchange for some extra typing . The 'do_'
methods really come in handy when you have a type that isn't in the pre-build field macros.
The 'do_' methods can be mixed with the field macros.
See the configuration object for an example of how to use the 'do_' methods.
//--------------------------------------------------------------------------------------------------
// Reset Agent Item
//--------------------------------------------------------------------------------------------------
class reset_item extends uvm_sequence_item;
// Members
// async assert, positive edge sync deassert reset
rand reset_kind_t m_reset_kind; // Kind of reset to apply
rand int unsigned m_preactive_ns; // time (in ns) to wait before asserting;
// if first reset then time that reset pin is X
rand int unsigned m_preactive_cycles; // time (in cycles) to wait before asserting;
// if first reset then time that reset pin is X
rand int unsigned m_active_ns; // time (in ns) to hold reset active
rand int unsigned m_active_cycles; // time (in cycles) to hold reset active
rand int unsigned m_postactive_ns; // time (in ns) to wait after deasserting reset
// (before unblocking the sequence)
rand int unsigned m_postactive_cycles; // time (in cycles) to wait after deasserting reset
// (before unblocking the sequence)
// Field Macros
`uvm_object_utils_begin(reset_item)
`uvm_field_enum(reset_kind_t, m_reset_kind, UVM_ALL_ON)
`uvm_field_int(m_preactive_ns, UVM_ALL_ON)
`uvm_field_int(m_preactive_cycles, UVM_ALL_ON)
`uvm_field_int(m_active_ns, UVM_ALL_ON)
`uvm_field_int(m_active_cycles, UVM_ALL_ON)
`uvm_field_int(m_postactive_ns, UVM_ALL_ON)
`uvm_field_int(m_postactive_cycles, UVM_ALL_ON)
`uvm_object_utils_end
// Constraints
// Three Types of Constraint - valid, legal, typical
// Valid - need to be true or agent won't work correctly
// (things like positive values for delays)
// Legal - need to be true to obey protocol; can override to create protocol invalid transfers
// Typical - overridable constraint; use to set some reasonable bounds
constraint valid;
constraint legal;
constraint typical;
// Constructor
function new(string name = "");
super.new(name);
endfunction: new
endclass: reset_item
//------------------------------------------------------------------------------
constraint reset_item::valid {}
//------------------------------------------------------------------------------
constraint reset_item::legal {}
//------------------------------------------------------------------------------
constraint reset_item::typical {
soft m_preactive_ns dist {
[75:140] :/ 25,
[141:160] :/ 50,
[161:200] :/ 25
};
soft m_active_ns dist {
[2:8] :/ 25,
[9:11] :/ 50,
[12:20] :/ 25
};
soft m_active_cycles dist {
[2:8] :/ 25,
[9:11] :/ 50,
[12:20] :/ 25
};
soft m_postactive_ns dist {
0 :/ 50,
[1:10] :/ 25,
[11:20] :/ 25
};
soft m_postactive_cycles dist {
0 :/ 50,
[1:10] :/ 25,
[11:20] :/ 25
};
}
Driver
The driver converts item descriptions into 'pin wiggles' on the agent interface.
A driver is an extension of the uvm_driver class.
A driver should be named with a '_driver' suffix.
A driver should register with the factory using the component_utils macro.
A driver should implement the build, connect, and run phase methods.
In the build method, the driver should grab a handle to the configuration object which it can then optionally use to
configure the behavior of the driver. For consistency, the config object handle should be named m_cfg_h.
In the build method, the driver should grab a handle to the virtual interface. For consistency, the virtual interface
handle should be named m_vif and be of the interface type.
A driver can optionally include protected helper functions / variables to aid in implementation of the build and/or
config phases.
When moving data from 4 state to 2 state variables - do an X check and present an error if an X is present.
A driver can optionally included protected helper tasks / variables to aid in the implementation of the run phase
task. I recommend including a simple drv_init() task to initialize all of the interface members at time 0 and a
drv_interface() task to do the item interpretation and driving of the interface.
Note that we protect these to prevent a user from being tempted to interface to the driver through any
mechanism other than the standard UVM sequence driven flow.
The driver should also include analysis ports for each of the req and rsp items. The req item to show the transfer
when requested and the rsp item to show the completed transfer.
The analysis ports should be named - simply - m_req_analysis_port and m_rsp_analysis_port.
// Config
reset_cfg m_cfg_h;
// Interface
reset_if_t m_vif;
// Analysis Port
uvm_analysis_port#(reset_item) m_req_analysis_port;
uvm_analysis_port#(reset_item) m_rsp_analysis_port;
// Constructor
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
// Phase Methods
extern virtual function void build_phase(uvm_phase phase);
extern virtual function void connect_phase(uvm_phase phase);
extern virtual task run_phase(uvm_phase phase);
// Helper Methods
extern protected virtual task drv_init();
extern protected virtual task drv_interface();
endclass: reset_driver
//------------------------------------------------------------------------------
function void reset_driver::build_phase(uvm_phase phase);
typedef uvm_config_db#(reset_cfg) config_db_cfg_t;
super.build_phase(phase);
endfunction: build_phase
//------------------------------------------------------------------------------
function void reset_driver::connect_phase(uvm_phase phase);
typedef uvm_config_db#(reset_if_t) cfg_db_vif_t;
endfunction: connect_phase
//------------------------------------------------------------------------------
task reset_driver::run_phase(uvm_phase phase);
super.run_phase(phase);
drv_init();
drv_interface();
endtask: run_phase
//------------------------------------------------------------------------------
task reset_driver::drv_init();
endtask: drv_init
//------------------------------------------------------------------------------
task reset_driver::drv_interface();
forever begin
// Construct Response
rsp = RSP::type_id::create("rsp", this);
// Assert Reset
m_vif.reset <= m_cfg_h.m_reset_active;;
end
end
endtask: drv_interface
Monitor
The monitor converts 'pin wiggles' into item descriptions output via an analysis port.
A monitor is an extension of the uvm_monitor class.
A monitor should be named with a '_monitor' suffix.
The analysis port should be named - simply - m_analysis_port.
A monitor should register with the factory using the component_utils macro.
A monitor should implement the build, config, and run phase methods.
In the build method, the monitor should grab a handle to the configuration object which it can then optionally use
to configure the behavior of the monitor. For consistency, the config object handle should be named m_cfg_h.
In the build method, the monitor should grab a handle to the virtual interface. For consistency, the virtual
interface handle should be named m_vif and be of the interface type.
When moving data from 4 state to 2 state variables - do an X check and present an error if an X is present.
A monitor can optionally include protected helper functions / variables to aid in implementation of the build and/or
config phases.
A monitor can optionally included protected helper tasks / variables to aid in the implementation of the run phase
task.
// Config
reset_cfg m_cfg_h;
// Interface
reset_if_t m_vif;
// Analysis Port
uvm_analysis_port#(reset_item) m_analysis_port;
// Constructor
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
// Phase Methods
extern virtual function void build_phase(uvm_phase phase);
extern virtual function void connect_phase(uvm_phase phase);
extern virtual task run_phase(uvm_phase phase);
// Helper Methods
extern protected virtual task mon_interface();
endclass: reset_monitor
//------------------------------------------------------------------------------
function void reset_monitor::build_phase(uvm_phase phase);
typedef uvm_config_db#(reset_cfg) config_db_cfg_t;
super.build_phase(phase);
endfunction: build_phase
//------------------------------------------------------------------------------
function void reset_monitor::connect_phase(uvm_phase phase);
typedef uvm_config_db#(reset_if_t) cfg_db_vif_t;
endfunction: connect_phase
//------------------------------------------------------------------------------
task reset_monitor::run_phase(uvm_phase phase);
super.run_phase(phase);
mon_interface();
endtask: run_phase
//------------------------------------------------------------------------------
task reset_monitor::mon_interface();
int unsigned active_count;
reset_item mon_item_h;
forever begin
end
// Write the Item out - if the item values changed (or if its the first pass)
m_analysis_port.write(mon_item_h);
// Wait a cycle
@(m_vif.clk);
end
endtask: mon_interface
Sequencer
The sequencer bridges a sequence item from a sequence to the driver.
The interface specific sequencer is factory registered empty extension of the base sequencer class. We provide
this implementation (as opposed to just using the base sequencer directly) to allow factory overrides. (And for
completeness / consistency / cleanliness)
A sequencer is an item specialized extension of the uvm_sequencer class.
A sequencer should be named with a '_sequencer' suffix.
A sequencer should register with the factory using the component_utils macro.
// Constructor
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
endclass: reset_sequencer
Agent
The agent is a component that encapsulates the driver, monitor, sequencer for the interface.
It also includes handles to the analysis ports of the enclosed driver and monitor.
An agent is an extension of the uvm_agent class.
An agent should be named with a '_agent' suffix.
The driver, monitor, and sequencer variables should be named - simply - m_driver, m_monitor, and
m_sequencer.
In the build method, the agent should grab a handle to the configuration object which it can then optionally use to
configure the behavior of the agent. For consistency, the config object handle should be named m_cfg_h.
The analysis ports should be named - simply - m_driver_req_analysis_port, m_driver_rsp_analysis_port, and
m_monitor_analysis_port.
This naming makes interaction with an instance of this agent straightforward and consistent.
An agent should register with the factory using the component_utils macro.
An agent should implement the build and config methods. Build should construct the child components and
analysis ports. The build method should set the sequencer into the config space; this allows easy automatic
generation of the virtual sequencer.
The connect method should connect the ports / exports.
No other members or methods should be required.
reset_cfg m_cfg_h;
// Analysis Fifos
uvm_analysis_port#(reset_item) m_driver_req_analysis_port;
uvm_analysis_port#(reset_item) m_driver_rsp_analysis_port;
uvm_analysis_port#(reset_item) m_monitor_analysis_port;
// Child Components
reset_sequencer m_sequencer;
reset_driver m_driver;
reset_monitor m_monitor;
// Constructor
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
// Phase Methods
extern virtual function void build_phase(uvm_phase phase);
endclass: reset_agent
//------------------------------------------------------------------------------
function void reset_agent::build_phase(uvm_phase phase);
typedef uvm_config_db#(reset_cfg) config_db_cfg_t;
typedef uvm_config_db#(reset_sequencer) config_db_seqr_t;
super.build_phase(phase);
begin
int active;
if(uvm_config_db#(int)::get(this, "", "is_active", active)) begin
is_active = uvm_active_passive_enum'(active);
end
else begin
is_active = m_cfg_h.m_uvm_active_passive_h;
end
end
endfunction: build_phase
//------------------------------------------------------------------------------
function void reset_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Connect
if (is_active == UVM_ACTIVE) begin
m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
m_driver.m_req_analysis_port.connect(m_driver_req_analysis_port);
m_driver.m_rsp_analysis_port.connect(m_driver_rsp_analysis_port);
end
m_monitor.m_analysis_port.connect(m_monitor_analysis_port);
endfunction: connect_phase
Sequence
Nominal Sequence
The 'nominal' sequence is a wrapper of the item class. It bridges the cumbersome create, start_item, randomize,
finish_item, get_response method sequence of an item to the easy to follow create, start method sequence of
the uvm_sequence.
The constraints of the item are carried to the nominal sequence - resulting in some redundant code, that is
implemented only once. (Since these constraints are the rules for the protocol - not the constraints that are used
to create specific testcases.)
Note that we continue to use the same constraint names that we defined in the item - valid, legal, and typical.
The nominal sequence is an item specialized extension of the uvm_sequence class.
The nominal sequence should be named with a '_nominal_seq' suffix.
The nominal sequence, like all sequences, should register with the factory using uvm_object_utils. It should use
the field macros (or 'do_' methods) if it has members.
// Members
// async assert, positive edge sync deassert reset
rand reset_kind_t m_reset_kind; // Kind of reset to apply
// Reset Timing
// NOTE: _ns times for async and _cycle times from sync
rand int unsigned m_preactive_ns; // time (in ns) to wait before asserting;
// if first reset then time that reset pin is X
rand int unsigned m_preactive_cycles; // time (in cycles) to wait before asserting;
// if first reset then time that reset pin is X
rand int unsigned m_active_ns; // time (in ns) to hold reset active
rand int unsigned m_active_cycles; // time (in cycles) to hold reset active
rand int unsigned m_postactive_ns; // time (in ns) to wait after deasserting reset
// (before unblocking the sequence)
rand int unsigned m_postactive_cycles; // time (in cycles) to wait after deasserting reset
// (before unblocking the sequence)
// Field Macros
`uvm_object_utils_begin(reset_nominal_seq)
`uvm_field_enum(reset_kind_t, m_reset_kind, UVM_ALL_ON)
`uvm_field_int(m_preactive_ns, UVM_ALL_ON)
`uvm_field_int(m_preactive_cycles, UVM_ALL_ON)
`uvm_field_int(m_active_ns, UVM_ALL_ON)
`uvm_field_int(m_active_cycles, UVM_ALL_ON)
`uvm_field_int(m_postactive_ns, UVM_ALL_ON)
`uvm_field_int(m_postactive_cycles, UVM_ALL_ON)
`uvm_object_utils_end
// Constraints
// Three Types of Constraint - valid, legal, typical
// Valid - need to be true or agent won't work correctly (things like positive values for delays)
// Legal - need to be true to obey protocol; can override to create protocol invalid transfers
// Typical - overridable constraint; use to set some reasonable bounds
constraint valid;
constraint legal;
constraint typical;
// Constructor
function new(string name = "reset_nominal_seq");
super.new(name);
endfunction: new
// Sequence Body
extern virtual task body();
endclass: reset_nominal_seq
//------------------------------------------------------------------------------
constraint reset_nominal_seq::valid {}
//------------------------------------------------------------------------------
constraint reset_nominal_seq::legal {}
//------------------------------------------------------------------------------
constraint reset_nominal_seq::typical {
soft m_preactive_ns dist {
[4:16] :/ 25,
[17:22] :/ 50,
[23:40] :/ 25
};
soft m_preactive_cycles dist {
[2:8] :/ 25,
[9:11] :/ 50,
[12:20] :/ 25
};
soft m_active_ns dist {
[4:16] :/ 25,
[17:22] :/ 50,
[23:40] :/ 25
};
soft m_active_cycles dist {
[2:8] :/ 25,
[9:11] :/ 50,
[12:20] :/ 25
};
soft m_postactive_ns dist {
0 :/ 50,
[1:20] :/ 25,
[21:40] :/ 25
};
soft m_postactive_cycles dist {
0 :/ 50,
[1:10] :/ 25,
[11:20] :/ 25
};
}
//------------------------------------------------------------------------------
task reset_nominal_seq::body();
start_item(req);
if (!req.randomize() with {
// Constrain item with local rand variables
// Example: m_data == local::m_data;
req.m_reset_kind == local::m_reset_kind;
req.m_preactive_ns == local::m_preactive_ns;
req.m_active_cycles == local::m_active_cycles;
req.m_postactive_cycles == local::m_postactive_cycles;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
finish_item(req);
get_response(rsp);
endtask: body
Helper Sequence
Often a sequence library will include more than just the nominal sequence with one or more 'helper' sequences.
These helper sequences are created by the protocol agent author to assist with common tasks / common
sequences.
For example - if the reset protocol author thought that folks would be needing an immediate assert reset
sequence that author would create something like the following.
Note that we start a protocol sequence on m_sequencer - which is guaranteed to be the protocol sequencer
type.
A user of this sequence would only have to create and start it - without having to use the nominal sequence. It
also provides a nice example for the user if / when the user needs to use the more flexible nominal sequence.
The helper sequence is an item specialized extension of the uvm_sequence class.
The helper sequence should be named with a '_seq' suffix.
The helper sequence, like all sequences, should register with the factory using uvm_object_utils. It should use
the field macros (or 'do_' methods) if it has members.
//--------------------------------------------------------------------------------------------------
// Reset Assert Sequence
//--------------------------------------------------------------------------------------------------
class reset_assert_seq extends uvm_sequence#(reset_item);
`uvm_object_utils(reset_assert_seq)
// Constructor
function new(string name = "");
super.new(name);
endfunction: new
// Sequence Body
extern virtual task body();
endclass: reset_assert_seq
//------------------------------------------------------------------------------
task reset_assert_seq::body();
reset_nominal_seq reset_nominal_seq_h;
reset_nominal_seq_h = reset_nominal_seq::type_id::create(
"reset_nominal_seq_h",
null,
get_full_name()
);
if (!reset_nominal_seq_h.randomize() with {
m_reset_kind == RESET_KIND_ASSERT;
}) begin
`uvm_fatal(get_name(), "Randomize Failed");
end
reset_nominal_seq_h.start(m_sequencer);
endtask: body
Sequence List
The sequence list is a compilation aid. It "tick includes" all of the sequences that are available with this agent.
`include "reset_nominal_seq.svh"
`include "reset_assert_seq.svh"
(Note that this sequence list doesn't have many sequences listed - some agents have quite a few)
Package
The interface protocol agent package is the namespaced encapsulation of the entire agent.
In the package we import from other packages, to get access to types used in this package. We declare types
that are shared across the agent. And we include the files of the agent - to place the contained classes in this
package.
If our agent uses 'pound' (#) delays or calls to time system calls (like $time) then the package also needs to set a
timeunit and precision.
Packaging our protocol agent allows other users to easily import our package to use our agent.
A package should be named with a '_pkg' suffix.
`include "uvm_macros.svh"
//--------------------------------------------------------------------------------------------------
// Reset Agent Package
//--------------------------------------------------------------------------------------------------
package reset_pkg;
timeunit 1ps;
timeprecision 1fs;
import uvm_pkg::*;
typedef enum {
RESET_KIND_SEQUENCE,
RESET_KIND_ASSERT,
RESET_KIND_DEASSERT
} reset_kind_t;
`include "reset_cfg.svh"
`include "reset_item.svh"
`include "reset_driver.svh"
`include "reset_monitor.svh"
`include "reset_sequencer.svh"
`include "reset_agent.svh"
`include "reset_seq_list.svh"
endpackage: reset_pkg
Package (Synthesizable)
In the event that a protocol agent has an interface that include types and / or parameters declared in the
package - those types should be moved to a separate package called the synthesizable package.
We need to do this to ensure that the interface maintains its synthesizability. A package that includes classes
cannot be synthesized - so we need to split the package into a '_synth_pkg' and a '_pkg.'
This would commonly look like:
//--------------------------------------------------------------------------------------------------
// Foo Agent Synthesizable Package
//--------------------------------------------------------------------------------------------------
package foo_synth_pkg;
typedef struct packed {
logic [31:0] something;
logic [1:0] otherthing;
} some_type_t;
endpackage: foo_synth_pkg
...
endinterface: foo_if
`include "uvm_macros.svh"
//--------------------------------------------------------------------------------------------------
// Foo Agent Package
//--------------------------------------------------------------------------------------------------
package foo_pkg;
timeunit 1ps;
timeprecision 1fs;
import uvm_pkg::*;
import foo_synth_pkg::*;
`include "foo_cfg.svh"
`include "foo_item.svh"
`include "foo_driver.svh"
`include "foo_monitor.svh"
`include "foo_sequencer.svh"
`include "foo_agent.svh"
`include "foo_seq_list.svh"
endpackage: foo_pkg
Parameterizing Widths
Many protocol agents connect to interfaces where the protocol allows a variety of widths. Because of how types
are defined a parameterized interface requires parameterizing the driver, monitor, and agent. And since that
parameterization also likely affects the item members, requires parameterizing the item, sequencer, and
sequences as well.
These parameters (and any typedefs) should be defined in the above mentioned synthesizable package.
This parameterization can result in some cumbersome interactions - so to streamline, use typedefs liberally.
A common typedef - 'this_type':
class myitem #(int WDTH) extends uvm_sequence_item;
typedef myitem#(WDTH) this_type_t;
..
endclass: myitem
With the "this_type" typedef the code inside myitem that uses the class' type will be more streamlined and easier
to read.
Do a similar thing at the top of an agent:
class myagent #(int WDTH) extends uvm_agent;
This typedefing results in code that is both more readable and more compact.
import some_protocol_synth_pkg::*;
endpackage: dut_specific_protocol_synth_pkg
and:
`include "uvm_macros.svh"
//--------------------------------------------------------------------------------------------------
// DUT Specific Protocol Agent Package
//--------------------------------------------------------------------------------------------------
package dut_specific_protocol_pkg;
import uvm_pkg::*;
import some_protocol_pkg::*;
import dut_specific_protocol_synth_pkg::*;
typedef some_protocol_payload_obj #(
DUT_WDTH
) dut_specific_protocol_payload_obj_t;
typedef some_protocol_item #(
DUT_WDTH
) dut_specific_protocol_item_t;
typedef some_protocol_driver#(
DUT_WDTH
) dut_specific_protocol_driver_t;
typedef some_protocol_monitor#(
DUT_WDTH
) dut_specific_protocol_monitor_t;
typedef some_protocol_sequencer#(
DUT_WDTH
) dut_specific_protocol_sequencer_t;
typedef some_protocol_agent#(
DUT_WDTH
) dut_specific_protocol_agent_t;
typedef some_protocol_nominal_seq#(
DUT_WDTH
) dut_specific_protocol_nominal_seq_t;
`include "dut_specific_protocol_seq_list.svh"
endpackage: dut_specific_protocol_pkg
Notice the (optional) inclusion of the 'dut_specific_protocol_seq_list' file. Our DUT specific specialization of the
general protocol often comes with the need to create some DUT specific sequences. These can be created and
then included in this sequence list file. By doing this we keep the protocol implementation separate from our DUT
specific requirements. It also allows us to keep code neatly separated in our DUT environment - keeping these
dut specific protocol sequences encapsulated independent of our DUT virtual sequences.
A typical tb_top looks like the following (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO TestBench
//--------------------------------------------------------------------------------------------------
module foo_tb;
//
// Declare Variables
//
logic clk;
logic clk_csr;
logic rst_n;
logic test_mode;
logic [FOO_STRIPES_WDTH-1:0] dyn_clk_en;
logic [FOO_STRIPES_WDTH-1:0] foo_irq;
...
//
// Instantiate DUT
//
foo dut (
.clk (clk),
.clk_csr (clk_csr),
.rst_n (rst_n),
.test_mode (test_mode),
.dyn_clk_en (dyn_clk_en),
.foo_irq (foo_irq),
...
//
// Connect DUT
//
// Clock
assign clk = u_clock_if.clock_pin;
assign clk_csr = u_clock_if.clock_pin;
// Reset
assign rst_n = u_reset_if.reset;
// IRQ Interface
generate
for (genvar foo_stripe = 0; foo_stripe < FOO_STRIPES; foo_stripe += 1) begin: gen_irq_if
assign u_foo_irq_if[foo_stripe].irq = foo_irq[foo_stripe];
end: gen_irq_if
endgenerate
...
// Instantiate Interfaces
clock_if u_clock_if();
reset_if u_reset_if (
.clk(clk)
);
foo_irq_if u_foo_irq_if[FOO_STRIPES] (
.clk(clk),
.rst_n(rst_n)
);
foo_irq_if u_foo_irq_err_if[FOO_STRIPES] (
.clk(clk),
.rst_n(rst_n)
);
foo_csr_if #(
.CSR_ADDR_WDTH (FOO_CSR_ADDR_WDTH ),
.CSR_WORD_MASK_WDTH(FOO_CSR_WORD_MASK_WDTH),
.CSR_DATA_WDTH (FOO_CSR_DATA_WDTH )
) u_foo_csr_if (
.clk(clk)
);
//
// Assertions
//
name_of_assert: assert property (@(posedge clk) disable iff (!rst_n)
...
) else begin
`uvm_error("foo_tb", $sformatf("some error message"))
end
endmodule: foo_tb
initial begin
// Disable Assertions until Reset Rises (and repeat if reset falls)
forever begin
$assertoff;
@(posedge foo_tb.rst_n);
$asserton;
@(negedge foo_tb.rst_n);
end
end
//
// Pass Interfaces to Cfg DB
//
// Clock Interface
initial begin: clock_vif
typedef virtual clock_if clock_if_t;
// Reset Interface
initial begin: reset_vif
typedef virtual reset_if reset_if_t;
uvm_config_db#(reset_if_t)::set(null, "*", "reset_vif", foo_tb.u_reset_if);
end: reset_vif
// IRQ Interface
generate
for (genvar foo_stripe = 0; foo_stripe < FOO_STRIPES; foo_stripe += 1) begin: gen_irq_vif
initial begin: foo_irq_vif
typedef virtual foo_irq_if foo_irq_if_t;
uvm_config_db#(foo_irq_if_t)::set(
null,
$sformatf("*foo_irq_agent_h[%0d]*", foo_stripe),
"foo_irq_vif",
foo_tb.u_foo_irq_if[foo_stripe]
);
end: foo_irq_vif
end: gen_irq_vif
endgenerate
// CSR Interface
initial begin: foo_csr_vif
typedef virtual foo_csr_if #(
.CSR_ADDR_WDTH (FOO_CSR_ADDR_WDTH ),
.CSR_WORD_MASK_WDTH(FOO_CSR_WORD_MASK_WDTH),
.CSR_DATA_WDTH (FOO_CSR_DATA_WDTH )
) foo_csr_if_t;
uvm_config_db#(foo_csr_if_t)::set(
null, "*", "foo_csr_vif", foo_tb.u_foo_csr_if
);
end: foo_csr_vif
...
// Run Test
// Start UVM
run_test();
end: test_run_entry_point
endmodule: foo_dv
Configuration Object
In the same way that each agent comes with a single encapsulation with all of its configuration members - so
does the DUT environment. The same rules and suggestions that apply to the interface protocol agent's
configuration object also apply to this DUT environment configuration object.
This object contains the members that define how the environment is built - and in some ways - how the
environment is to behave. The configuration is built and consumed at the build phase. Configuration objects
should not be consumed at any point in the run phase. (Members of configuration objects can be used in the run
phase.)
The configuration object extends the uvm_object and registers with the factory using the uvm_object_utils
macro.
The environment configuration object should be named with a dut name prefix and '_cfg' suffix.
The configuration object class will have a handle pointing to the RAL object (if RAL is being used).
It is not unusual for this class to begin as an empty (no member configuration variables) with members being
added as the project progresses. Always create a configuration object - even if the DUT environment begins with
nothing that is configurable.
A typical configuration object looks like the following (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Environment Config
//--------------------------------------------------------------------------------------------------
class foo_cfg extends uvm_object;
// Constructor
extern function new(string name = "");
// Field Macros
`uvm_object_utils_begin(foo_cfg)
`uvm_field_object(m_ral_model_h, UVM_ALL_ON)
`uvm_field_sarray_enum(
stimulus_gen_pkg::data_mode_e,
m_stimulus_mode,
UVM_ALL_ON
)
`uvm_object_utils_end
endclass: foo_cfg
//------------------------------------------------------------------------------
function foo_cfg::new(string name = "");
super.new(name);
// Default to Stimulus Generator to INCREMENTING mode
m_stimulus_mode = stimulus_gen_pkg::INCREMENTING;
endfunction: new
Scoreboard
The scoreboard is the central component that interfaces to some (or all) of the agent analysis ports to check the
DUT against expected results.
The scoreboard will typically check on receipt of items and at end of test.
The scoreboard includes an object that represents the current configuration of the DUT. An object which is kept
up-to-date either within the scoreboard, or in another component - with the handle passed to the scoreboard
through the configuration database. This DUT configuration is commonly held as a RAL object - contained within
the DUT environment configuration object.
The scoreboard may include a handle to a scoreboard debug vif - a virtual interface with informational members
used to aid in the debug of failing tests. With members like events to mark when an error occurs, enum to mark
the type of error, and any additional information that could be useful during waveform debug.
Scoreboard implementations vary - as how a DUT is implemented varies. A common scoreboard will capture
input items as they are sent to the DUT and then, on receipt of a corresponding output item, will transform the
input item and compare to the received output. Often the transform is influenced by how the DUT is configured -
so the scoreboard will query the current configuration from the RAL (or similar DUT CSR object) at the time the
output is received.
debug vif (if present) and connect the analysis exports to their corresponding fifos. The run phase should fork off
all of the fifo handler methods. The check phase should do an end of test cleanup check; for example, to ensure
that the DUT processed all of the information that it was sent.
Usage of typical TLM analysis write callbacks vs. exports/fifos is an implementation decision based on the
complexity and requirements of the particular scoreboard when there are more than one TLM connections. While
the TLM FIFO is preferred, sufficiently simple implementations may benefit from using the
`uvm_analysis_imp_decl() method. It is advised not to mix these two approaches in a single component --
choose one or the other, refactor if necessary.
If the items already have source identifying information (or the source does not matter) it is best to connect all
agents to a single implementation port and route them inside the scoreboard.
uvm_analysis_export + uvm_analysis_fifo:
● Pros:
○ Supported arrayed inputs; from an array of repeated agents or an associative array based
on which agent is connected (string, enum).
○ Safe interprocess communication; since objects go through a mailbox, parent processes of
the write method being killed cannot affect processing the item.
○ Single handler can pull from multiple ports; e.g. an array of all the input type can funnel
objects into common logic.
○ Processing happens in a task, so users can implement time-consuming logic or fork off
new processes unlike a function.
● Cons:
○ More boilerplate code; supporting two objects being made; extra code in the connect and
run phases which may otherwise be omitted.
`uvm_analysis_imp_decl():
● Pros:
○ Direct callback methods for each connection
○ Less boilerplate code, behaves similarly to a regular uvm_analysis_imp
● Cons:
○ Macro magic should be avoided
○ Possible namespace collisions within a package
○ Doesn’t scale to large numbers of ports
○ Cannot support arrays of inputs; e.g. `uvm_analysis_imp_decl(_port_0),
`uvm_analysis_imp_decl(_port_1), etc.
○ Often leads to repeated code in each write callback
○ Need to pass objects to tasks for time-consuming logic or forks
○ Does not work with parameters since classes and callback methods must be defined at
compile time.
//--------------------------------------------------------------------------------------------------
class foo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(foo_scoreboard)
uvm_analysis_export#(foo_input_item_t) m_foo_input_analysis_export;
protected uvm_tlm_analysis_fifo#(foo_input_item_t) m_foo_input_analysis_fifo;
`uvm_analysis_imp_decl(_input)
//--------------------------------------------------------------------------------------------------
class foo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(foo_scoreboard)
endclass
Here’s an example where a number of repeated agents are connected to a scoreboard (whose items do not
have source identifying information):
uvm_analysis_export#(port_item_t) m_port_analysis_export[NUM_PORTS];
uvm_tlm_analysis_fifo#(port_item_t) m_port_analysis_fifo[NUM_PORTS];
A typical scoreboard component looks like the following (for a DUT named "foo" using analysis fifos):
//--------------------------------------------------------------------------------------------------
// FOO Scoreboard
//--------------------------------------------------------------------------------------------------
class foo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(foo_scoreboard)
// Environment CFG
foo_cfg m_cfg_h;
// Helper Model
foo_transform_model_obj_t m_foo_transform_model_obj_h;
// Memory Model
foo_mem_model_obj_t m_foo_mem_model_obj_h;
// Analysis Exports
uvm_analysis_export#(foo_input_item_t) m_foo_input_analysis_export;
uvm_analysis_export#(foo_output_item_t) m_foo_output_analysis_export;
// Analysis FIFOs
protected uvm_tlm_analysis_fifo#(foo_input_item_t) m_foo_input_analysis_fifo;
protected uvm_tlm_analysis_fifo#(foo_output_item_t) m_foo_output_analysis_fifo;
// Debug Interface
virtual foo_scoreboard_debug_if m_vif;
// Constructor
extern function new(string name, uvm_component parent);
endclass : foo_scoreboard
//------------------------------------------------------------------------------
function foo_scoreboard::new(string name, uvm_component parent);
super.new(name, parent);
endfunction: new
//------------------------------------------------------------------------------
function void foo_scoreboard::build_phase(uvm_phase phase);
typedef uvm_config_db#(foo_cfg) config_db_cfg_t;
m_foo_transform_model_obj_h = foo_transform_model_obj_t::type_id::create(
"m_foo_transform_model_obj_h", this
);
"m_foo_mem_model_obj_h",
this
);
m_foo_input_analysis_export = new(
"m_foo_input_analysis_export", this
);
m_foo_input_analysis_fifo = new(
"m_foo_input_analysis_fifo", this
);
m_foo_output_analysis_export = new(
"m_foo_output_analysis_export", this
);
m_foo_output_analysis_fifo = new(
"m_foo_output_analysis_fifo", this
);
foreach (m_foo_input_fifo[ii]) begin
m_foo_input_fifo[ii] = new(
$sformatf("m_foo_input_fifo[%0d]", ii), this, 0
);
end
endfunction: build_phase
//------------------------------------------------------------------------------
function void foo_scoreboard::connect_phase(uvm_phase phase);
typedef uvm_config_db#(virtual foo_scoreboard_debug_if) config_db_vif_t;
m_foo_input_analysis_export.connect(
m_foo_input_analysis_fifo.analysis_export
);
m_foo_output_analysis_export.connect(
m_foo_output_analysis_fifo.analysis_export
);
endfunction: connect_phase
//------------------------------------------------------------------------------
task foo_scoreboard::run_phase(uvm_phase phase);
endtask: run_phase
//------------------------------------------------------------------------------
function void foo_scoreboard::check_phase(uvm_phase phase);
begin
foo_input_item_t foo_input_item_h;
foreach (m_foo_input_fifo[ii]) begin
while (!m_foo_input_fifo[ii].is_empty()) begin
void'(m_foo_input_fifo[ii].try_get(
foo_input_item_h)
);
-> m_vif.error;
`uvm_error(
get_name(),
$sformatf(
"Outstanding Input - Transaction ID 0x%0x:\n%s",
ii,
foo_input_item_h.sprint()
)
)
end
end
end
endfunction: check_phase
//------------------------------------------------------------------------------
task foo_scoreboard::input_fifo_handler(uvm_phase phase);
foo_input_item_t foo_input_item_h;
forever begin
m_foo_input_analysis_fifo.get(foo_input_item_h);
`uvm_info(
get_name(),
$sformatf(
"foo_input_item_h\n%s",
foo_input_item_h.sprint()
),
UVM_HIGH
)
// Make sure that transaction ids aren't being reused before freed
if (!m_foo_input_fifo[
foo_input_item_h.m_transaction_id
].is_empty()) begin
`uvm_fatal(
get_name(),
$sformatf(
"Attempt to write into full fifo - transaction id 0x%0x",
foo_input_item_h.m_transaction_id
)
)
end
end
endtask: input_fifo_handler
//------------------------------------------------------------------------------
task foo_scoreboard::output_fifo_handler(uvm_phase phase);
foo_output_item_t foo_output_item_h;
foo_input_item_t foo_input_item_h;
forever begin
m_foo_output_analysis_fifo.get(foo_output_item_h);
`uvm_info(
get_name(),
$sformatf(
"foo_output_item_h\n%s",
foo_output_item_h.sprint()
),
UVM_HIGH
)
)
end
else begin
bit [FOO_DATA_WDTH-1:0] expected_data;
`uvm_info(
get_name(),
$sformatf("foo_input_item_h\n%s",
foo_input_item_h.sprint()
),
UVM_HIGH
)
if (m_cfg_h.m_ral_model_h.transform_enabled()) begin
expected_data = m_foo_transform_model_obj_h.transform(
foo_input_item_h.m_data
);
end
else begin //bypass mode
expected_data = foo_input_item_h.m_data;
end
// Compare
if (foo_output_item_h.m_data != expected_data) begin
-> m_vif.error;
`uvm_error(
get_name(),
$sformatf(
"Mismatch - Expected 0x%0x, Got 0x%0x; Input\n%s\nOutput\n%s",
expected_data,
foo_output_item_h.m_data,
foo_input_item_h.sprint(),
foo_output_item_h.sprint()
)
)
end
else begin
`uvm_info(
get_name(),
$sformatf("Match - Expected 0x%0x, Got 0x%0x; Input\n%s\nOutput\n%s",
expected_data,
foo_output_item_h.m_data,
foo_input_item_h.sprint(),
foo_output_item_h.sprint()
),
UVM_HIGH
)
end
end
end
endtask: output_fifo_handler
Virtual Sequencer
The virtual sequencer component is a hierarchical element with handles to each of the sequencers contained in
the agents of the DUT environment.
The virtual sequencer provides virtual sequences a handle on which to run; or more accurately - a handle on
which to start the virtual sequence.
The virtual sequencer extends the uvm_sequencer component specialized to a uvm_sequence_item type and
registers with the factory using the uvm_component_utils macro.
The virtual sequencer should be named with a dut name prefix and '_virtual_sequencer' suffix.
A virtual sequencer only implements the connect phase. In the connect phase a virtual sequencer uses the
config database to get handles to the virtual sequencers instanced in the environment.
Instance variable names should match the name of the class being instanced (where possible - when there is
more than one then choose names appropriately.) This makes starting sequences on the virtual sequencer
intuitive.
A typical virtual sequencer component looks like the following (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Virtual Sequencer
//--------------------------------------------------------------------------------------------------
class foo_virtual_sequencer extends uvm_sequencer#(uvm_sequence_item);
`uvm_component_utils(foo_virtual_sequencer)
clock_sequencer m_clock_sequencer_h;
reset_sequencer m_reset_sequencer_h;
...
foo_input_sequencer_t m_foo_input_sequencer_h;
...
endclass: foo_virtual_sequencer
//------------------------------------------------------------------------------
function foo_virtual_sequencer::new(string name, uvm_component parent);
super.new( name, parent );
endfunction: new
//------------------------------------------------------------------------------
function void foo_virtual_sequencer::connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Clock Sequencer
begin
typedef uvm_config_db#(clock_sequencer) config_db_clock_seqr_t;
if (!config_db_clock_seqr_t::get(
this,
"*",
"m_clock_agent_h.m_sequencer",
m_clock_sequencer_h
)) begin
`uvm_fatal(
get_name(),
"Failed to Grab clock_sequencer from Config DB"
)
end
end
// Reset Sequencer
begin
typedef uvm_config_db#(reset_sequencer) config_db_reset_seqr_t;
if (!config_db_reset_seqr_t::get(
this,
"*",
"m_reset_agent_h.m_sequencer",
m_reset_sequencer_h
)) begin
`uvm_fatal(
get_name(),
"Failed to Grab reset_sequencer from Config DB"
)
end
end
...
...
endfunction: connect_phase
Environment
The DUT environment component, like the agent component, is just a hierarchical element. The environment
only builds child components: agents, sub-environments, scoreboard, and virtual sequencer. And connects the
scoreboard analysis exports to agent analysis ports.
The environment implements no other phase methods.
The environment extends the uvm_env component and registers with the factory using the uvm_component_utils
macro.
The environment should be named with a dut name prefix and '_env' suffix.
Instance variable names should match the name of the class being instanced (where possible - when there is
more than one then choose names appropriately.)
Instance name of the environment config should be m_cfg_h. Instance name of the virtual sequencer should be
m_virtual_sequencer_h. Using these names keeps the interface consistent.
A typical virtual sequencer component looks like the following (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Environment
//--------------------------------------------------------------------------------------------------
class foo_env extends uvm_env;
`uvm_component_utils(foo_env)
// Environment CFG
foo_cfg m_cfg_h;
// Virtual Sequencer
foo_virtual_sequencer_t m_virtual_sequencer_h;
// Member Agents
clock_agent m_clock_agent_h;
reset_agent m_reset_agent_h;
foo_csr_agent_t m_foo_csr_agent_h;
...
foo_input_agent_t m_foo_input_agent_h;
...
// Scoreboard
foo_scoreboard_t m_foo_scoreboard_h;
endclass: foo_env
//------------------------------------------------------------------------------
function foo_env::new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
//------------------------------------------------------------------------------
function void foo_env::build_phase(uvm_phase phase);
super.build_phase(phase);
// Virtual Sequencer
m_virtual_sequencer_h = foo_virtual_sequencer_t::type_id::create(
"m_virtual_sequencer_h", this
);
//
// Member Agents
//
// Clock/Reset/CSR Agents
m_clock_agent_h = clock_agent::type_id::create("m_clock_agent_h", this);
m_reset_agent_h = reset_agent::type_id::create("m_reset_agent_h", this);
m_foo_csr_agent_h = foo_csr_agent_t::type_id::create(
"m_foo_csr_agent_h", this
);
...
// Foo Input Agent
m_foo_input_agent_h = foo_input_agent_t::type_id::create(
"m_foo_input_agent_h", this
);
// Scoreboard
m_foo_scoreboard_h = foo_scoreboard_t::type_id::create(
"m_foo_scoreboard_h", this
);
endfunction : build_phase
//------------------------------------------------------------------------------
function void foo_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
//
// Connect RAL
//
m_ral_model_h = m_cfg_h.m_ral_model_h;
if ( m_ral_model_h.get_parent() == null ) begin // if top level
m_ral_model_h.reg_map.set_sequencer(
.sequencer( m_foo_csr_agent_h.m_sequencer ),
.adapter( m_foo_csr_agent_h.m_reg_adapter )
);
end
m_ral_model_h.reg_map.set_auto_predict( .on( 0 ) );
m_foo_csr_reg_predictor_h.map = m_ral_model_h.reg_map;
m_foo_csr_reg_predictor_h.adapter = m_foo_csr_agent_h.m_reg_adapter;
m_foo_csr_agent_h.m_monitor_analysis_port.connect(m_foo_csr_reg_predictor_h.bus_in);
//
// Connect Scoreboard
//
// CSR Agent
m_foo_csr_agent_h.m_monitor_analysis_port.connect(
m_foo_scoreboard_h.m_foo_csr_analysis_export
);
...
// Input Agent
m_input_agent_h.m_monitor_analysis_port.connect(
m_foo_scoreboard_h.m_foo_input_analysis_export
);
...
endfunction : connect_phase
Test
The test component is launched by the tb top module when run_test() is called. The test that is run is determined
by the UVM_TESTNAME pluarg passed to the simulator.
The test configures the environment and then launches a test sequence.
We distinguish between a test - that which configures and constrains the environment; and the test sequence -
the ordered set of things that the test case does.
Base Test
The base test creates and initializes all of the configuration objects available in the environment.
Every base test must include a run_vseq_from_plusarg task. This task takes the TEST_SEQUENCE plusarg
input - and launches the virtual sequence so named on the environment's virtual sequencer. It is this flexibility
that allows us to separate the test -- the environment configuration / factory overrides, from the test sequence --
the ordered set of things that the test does. This separation allows us to build a library of test sequences and run
those sequences against a set of factory override constraints (and / or environment configurations).
The base test extends the uvm_test component and registers with the factory using the uvm_component_utils
macro.
The base test should be named with a dut name prefix and '_base_test' suffix.
The base test includes a handle to the uvm_factory, named m_factory, used by the run from plusarg task and by
derived tests.
The base test includes a handle to the uvm command line processor - for plusarg handling.
The base test may include objects to produce delays (like the below referenced 'delay object') and to measure
time (like the below referenced 'time object.')
The base test will include a default time for the bench timeout - named m_tb_timeout_ms. (Note the use of the
_ms suffix to indicate milliseconds.)
The base test will include a default drain time - time to wait after the test sequence completes before ending the
test.
The base test will include a heartbeat time - a time interval wherein, on each expiration, the test will report status.
This is useful to indicate run progress when the test is run with NONE level verbosity.
The base test may also include a wall time timeout - triggered by the heartbeat interval.
The base test will include a handle to the DUT configuration object, named m_cfg_obj_h, created in the build
method.
The base test will include a handle to the RAL model (if RAL is used) - grabbed from the configuration object.
The base test will include handles to all of the agent configuration objects, for all agents instanced in the
environment. These objects will be created in the build phase function, members set to values that make sense
for the DUT environment, and then set into the config database.
The base test will include a handle to the DUT environment, named m_env_h, which it will create in the build
phase function.
The base test will include a handle to the virtual sequencer, named m_virtual_sequencer_h, which it will grab
from the DUT environment in the connect phase function.
A typical base test component looks like the following (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Base Test
//--------------------------------------------------------------------------------------------------
class foo_base_test extends uvm_test;
`uvm_component_utils(foo_base_test)
// UVM Factory
const uvm_factory m_uvm_factory;
// Delay Interface
delay_obj#("delay_vif") m_delay_obj_h;
// Time Markers
time_wall_obj m_time_wall_obj_h;
...
// Configuration
int unsigned m_tb_timeout_ms;
int unsigned m_tb_timeout_wall_secs;
int unsigned m_tb_drain_time_us;
int unsigned m_tb_heartbeat_time_us;
ral_model m_ral_model_h;
foo_cfg m_cfg_h;
clock_cfg m_clock_cfg_h;
reset_cfg m_reset_cfg_h;
...
foo_csr_cfg_t m_foo_csr_cfg_h;
foo_input_cfg_t m_foo_input_cfg_h;
...
// DUT Environment
foo_env m_env_h;
// Helper Methods
extern protected virtual task run_vseq_from_plusarg();
endclass: foo_base_test
//------------------------------------------------------------------------------
function foo_base_test::new(string name, uvm_component parent);
super.new(name,parent);
m_uvm_factory = uvm_factory::get();
m_uvm_cmdline_processor_h = uvm_cmdline_processor::get_inst();
...
//------------------------------------------------------------------------------
function void foo_base_test::build_phase(uvm_phase phase);
bit hold_payload;
stimulus_gen_pkg::data_mode_e stimulus_gen_mode;
super.build_phase(phase);
...
begin
string stimulus_gen_mode_str;
stimulus_gen_mode = stimulus_gen_pkg::RAND; // Default to RAND
if (
m_uvm_cmdline_processor_h.get_arg_value("+STIMULUS_GEN_MODE=", stimulus_gen_mode_str)
) begin
`uvm_info(
get_name(),
$sformatf("STIMULUS_GEN_MODE = %s", stimulus_gen_mode_str),
UVM_MEDIUM
)
case(stimulus_gen_mode_str)
"INCR": begin
stimulus_gen_mode = stimulus_gen_pkg::INCR;
end
"RAND": begin
stimulus_gen_mode = stimulus_gen_pkg::RAND;
end
default: begin
`uvm_fatal(
get_name(),
$sformatf("Invalid STIMULUS_GEN_MODE: %s", stimulus_gen_mode_str)
)
end
endcase
end
end
...
m_reset_cfg_h.m_reset_assert = RESET_ASSERT_ASYNC;
m_reset_cfg_h.m_reset_deassert = RESET_DEASSERT_SYNC;
uvm_config_db#(reset_cfg)::set(this, "*", "reset_cfg", m_reset_cfg_h);
...
...
...
// Create Environment
m_env_h = foo_env::type_id::create("m_env_h", this);
endfunction : build_phase
//------------------------------------------------------------------------------
function void foo_base_test::connect_phase( uvm_phase phase );
m_virtual_sequencer_h = m_env_h.m_virtual_sequencer_h;
endfunction: connect_phase
//------------------------------------------------------------------------------
task foo_base_test::run_phase(uvm_phase phase);
time drain_time;
drain_time = m_tb_drain_time_us * 1us;
// Set the amount of time to wait in this phase after all objections have been dropped.
phase.phase_done.set_drain_time(this, drain_time);
fork
// Periodically display the time to indicate forward progress.
// List all active objections - helps when test does not make forward progress.
begin
forever begin
m_delay_obj_h.delay_time(m_tb_heartbeat_time_us, "us"); // #(heartbeat_us*1us)
`uvm_info(
get_full_name(),
$sformatf("run_phase() heartbeat: elapsed wall time %s",
m_time_wall_obj_h.seconds_to_string(m_time_wall_obj_h.elapsed())
),
UVM_NONE
)
if (uvm_report_enabled(UVM_LOW)) begin
process proc = process::self();
string proc_rng_state;
if (proc != null) begin
proc_rng_state = proc.get_randstate();
end
phase.phase_done.display_objections();
if (proc != null) begin
proc.set_randstate(proc_rng_state);
end
end
if (m_time_wall_obj_h.elapsed() >= m_tb_timeout_wall_secs) begin
`uvm_fatal(
get_name(),
$sformatf(
"run_phase() - Wall time timeout- sim has exceeded %0d seconds",
m_tb_timeout_wall_secs
)
)
end
end
end
join_none
super.run_phase(phase);
run_vseq_from_plusarg();
endtask: run_phase
//------------------------------------------------------------------------------
task foo_base_test::run_vseq_from_plusarg();
uvm_object tmp;
string vseq_testname;
uvm_sequence#(uvm_sequence_item) test_plusarg_vseq_h; // Typed Sequence
tmp = m_uvm_factory.create_object_by_name(
vseq_testname, m_virtual_sequencer_h.get_full_name(), "test_plusarg_vseq_h"
);
if (!test_plusarg_vseq_h.randomize()) begin
`uvm_fatal(get_type_name(), "Randomize Failed for the initial vseq")
end
endtask: run_vseq_from_plusarg
Derived Tests
These tests extend from the base test. They override the base class configuration members with different values
and / or provide factory overrides that affect how the environment items / sequence items behave.
Always name derived tests based on what is being configured / factory overridden. This enables easy test / test
sequence combination selection when a regression list is being assembled.
Note that test derived tests do not change which test sequence is run. The test sequence is always selected by
the TEST_SEQUENCE plusarg.
A derived test does one or more of the following:
1. factory override of items / sequences (commonly just overrides of constraints)
2. change the configuration of the config objects instanced in the base test
Both of these are accomplished in the build phase function.
Note that we don't need to 'set' the config into the config db after updating a member - since the handle of the
configuration object has already been placed in the database.
The derived test extends the uvm_test component and registers with the factory using the uvm_component_utils
macro.
The derived test should be named with a dut name prefix and '_test' suffix. The name of the test should
correspond with the action of the test to indicate the configuration and/or override.
A typical derived test component looks like the following (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Special Test
//--------------------------------------------------------------------------------------------------
class foo_special_test extends foo_base_test;
`uvm_component_utils( foo_special_test )
endclass: foo_special_test
//------------------------------------------------------------------------------
function void foo_special_test::build_phase(uvm_phase phase);
super.build_phase(phase);
endfunction : build_phase
Test List
The test list is a compilation aid. It "tick includes" all of the tests that are available with this environment.
The test list follows the following structure (for a DUT named "foo"):
`include "foo_base_test.svh"
...
`include "foo_special_test.svh"
...
Virtual Sequences
The virtual sequences are used to generate sequenced / ordered traffic across one or more independent agents.
The base virtual sequence follows the following structure (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Base Virtual Sequence
//--------------------------------------------------------------------------------------------------
class foo_base_vseq extends uvm_sequence#(uvm_sequence_item);
`uvm_object_utils( foo_base_vseq )
`uvm_declare_p_sequencer( foo_virtual_sequencer )
// UVM Factory
const uvm_factory m_uvm_factory;
// Memory Model
protected foo_mem_model_obj_t m_foo_mem_model_obj_h;
// Delay Interface
protected delay_obj#("delay_vif") m_delay_obj_h;
// Constructor
extern function new(string name = "");
// Body
extern virtual task pre_body();
extern virtual task body();
// Helper Methods
extern protected virtual task delay_time(int unsigned timeval, string timeunitstr = "ns");
extern protected virtual task delay_cycles(
int unsigned cycles,
int unsigned timeout = 1,
string timeoutunit = "ms"
);
endclass: foo_base_vseq
//------------------------------------------------------------------------------
function foo_base_vseq::new(string name = "");
super.new( name );
m_uvm_factory = uvm_factory::get();
endfunction: new
//------------------------------------------------------------------------------
task foo_base_vseq::pre_body();
super.pre_body();
//------------------------------------------------------------------------------
task foo_base_vseq::body();
// NOTE: no super.body() here .. since UVM uses this to check that body has been implemented
begin
typedef uvm_config_db#(ral_model) config_db_ral_model_t;
if (!config_db_ral_model_t::get(
null, "*", "m_ral_model_h", m_ral_model_h)
) begin
`uvm_fatal(get_name(), "Failed to Grab ral_model from Config DB")
end
end
m_delay_obj_h = delay_obj#("delay_vif")::create_singleton("m_delay_obj_h");
endtask: body
//------------------------------------------------------------------------------
task foo_base_vseq::delay_time(int unsigned timeval, string timeunitstr = "ns");
m_delay_obj_h.delay_time(timeval, timeunitstr);
endtask: delay_time
//------------------------------------------------------------------------------
task foo_base_vseq::delay_cycles(
int unsigned cycles,
int unsigned timeout = 1,
string timeoutunit = "ms"
);
if (m_delay_obj_h == null) begin
m_delay_obj_h = delay_obj#("delay_vif")::create_singleton("m_delay_obj_h");
end
endtask: delay_cycles
Virtual Agent
A virtual agent is a virtual sequence that combines and / or organizes traffic from multiple independent agents to
produce a single logical protocol interface.
A virtual agent is just a concept - its implemented like any other virtual sequence. But since most virtual
sequences are tests we make this distinction.
These are the sequences that implement a protocol interface.
Example: slave interface that is made up of several instances of 'ready-valid' agents. The DUT initiates transfers
on one agent, the slave responds on another; and these transfers are unordered and non-blocking.
The virtual agent sequence extends the dut named base virtual sequence and registers with the factory using the
uvm_object_utils macro.
The virtual agent should be named with a dut name prefix and '_vseq' suffix. The name should correspond to the
protocol being implemented.
Since a virtual agent is, like all virtual sequences, randomized before being started a virtual agent can (and will
often) have rand members that control how the sequence should behave.
The virtual agent sequence follows the following structure (for a DUT named "foo"; this example shows a request
/ response memory interface):
//--------------------------------------------------------------------------------------------------
// DUT Fetch Request / Response Virtual Sequence
//--------------------------------------------------------------------------------------------------
class foo_rv_fetch_req_rsp_vseq extends foo_base_vseq;
`uvm_object_utils(foo_rv_fetch_req_rsp_vseq)
endclass: foo_rv_fetch_req_rsp_vseq
//------------------------------------------------------------------------------
function foo_rv_fetch_req_rsp_vseq::new(string name="");
super.new( name );
foreach(m_foo_rv_fetch_req_nominal_seq_mbox_h[id]) begin
m_foo_rv_fetch_req_nominal_seq_mbox_h[id] = new(0); // unbounded mbox
end
m_rv_fetch_req_entries_mbox_h = new(0); // unbounded mbox
endfunction: new
//------------------------------------------------------------------------------
task foo_rv_fetch_req_rsp_vseq::body();
super.body();
begin
rsp_thread();
end
join
endtask: body
//------------------------------------------------------------------------------
task foo_rv_fetch_req_rsp_vseq::req_thread();
// Request Thread
// starts request sequence, on request - mailboxes request info, relaunches request sequence
// This thread cannot block! Don't want to drop any requests
forever begin
foo_rv_fetch_req_nominal_seq_t foo_rv_fetch_req_nominal_seq_h;
int unsigned req_transaction_id; // ID of transaction
foo_rv_fetch_req_nominal_seq_h = foo_rv_fetch_req_nominal_seq_t::type_id::create(
"foo_rv_fetch_req_nominal_seq_h", null, get_full_name()
);
if (!foo_rv_fetch_req_nominal_seq_h.randomize() with {
if (m_req_delay_kind == REQ_DELAY_ZERO) {
m_valid2ready_delay_cycles == 0;
}
...
rand rsp_delay_kind_t m_rsp_delay_kind;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
foo_rv_fetch_req_nominal_seq_h.start(p_sequencer.m_foo_rv_fetch_req_sequencer_h);
req_transaction_id = foo_rv_fetch_req_nominal_seq_h.m_transaction_id;
`uvm_info(
get_name(),
$sformatf("Req ID: 'h%0x", req_transaction_id),
UVM_MEDIUM
)
`uvm_info(
get_name(),
$sformatf("Put Side:\n%s", foo_rv_fetch_req_nominal_seq_h.sprint()),
UVM_HIGH
)
if (!m_foo_rv_fetch_req_nominal_seq_mbox_h[req_transaction_id].try_put(
foo_rv_fetch_req_nominal_seq_h
)) begin
`uvm_fatal(
get_name(),
$sformatf(
"Failed to put into m_foo_rv_fetch_req_nominal_seq_mbox_h[%0d]",
req_transaction_id
)
);
end
if (!m_rv_fetch_req_entries_mbox_h.try_put(1'b1)) begin
`uvm_fatal(
get_name(),
$sformatf(
"Failed to put into m_rv_fetch_req_entries_mbox_h"
)
);
end
end
endtask: req_thread
//------------------------------------------------------------------------------
task foo_rv_fetch_req_rsp_vseq::rsp_thread();
// Response Thread - wait for request, choose transaction id channel, respond to that channel
typedef foo_trans_id#(FOO_TRANSACTION_IDS) trans_id_t;
trans_id_t foo_trans_id_h;
forever begin
bit tmp;
int unsigned response_id;
foo_rv_fetch_rsp_nominal_seq_t foo_rv_fetch_rsp_nominal_seq_h;
// If Fetch Data Width isn't 256- let me know; since I hardcoded the packed static arrays
if (FOO_FETCH_DATA_WDTH != 256) begin
`uvm_fatal(get_name(),
$sformatf("FOO_FETCH_DATA_WDTH not set to 256; set to 'd%0d", FOO_FETCH_DATA_WDTH)
)
end
if (!m_foo_rv_fetch_req_nominal_seq_mbox_h[response_id].
try_get(foo_rv_fetch_req_nominal_seq_h)
) begin
`uvm_fatal(get_name(),
"Invalid ID - something bad happened with randomize of the foo_trans_id_h object"
)
end
`uvm_info(
get_name(),
$sformatf("Rsp ID: 'h%0x", req_rsp_transaction_id),
UVM_MEDIUM
)
`uvm_info(get_name(),
$sformatf("Get Side:\n%s", foo_rv_fetch_req_nominal_seq_h.sprint()),
UVM_HIGH
)
if (!foo_rv_fetch_rsp_nominal_seq_h.randomize() with {
m_payload_obj.m_payload.m_transaction_id == local::req_rsp_transaction_id;
if (req_transaction_size == foo_rv_fetch_req_pkg::BYTES_64) {
m_payload_obj.m_payload.m_data_transfer_words == 2;
}
else {
m_payload_obj.m_payload.m_data_transfer_words == 1;
}
foreach(m_payload_obj.m_payload.m_data[ii]) {
m_payload_obj.m_payload.m_data[ii] == local::rsp_data_word[ii];
}
if (m_rsp_delay_kind == REQ_DELAY_ZERO) {
m_ready2valid_delay_cycles == 0;
}
...
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
foo_rv_fetch_rsp_nominal_seq_h.start(p_sequencer.m_foo_rv_fetch_rsp_sequencer_h);
end
end
endtask: rsp_thread
//------------------------------------------------------------------------------
function int unsigned foo_rv_fetch_req_rsp_vseq::rand_id();
typedef rand_choose_from_list_obj#(int unsigned) trans_id_t;
int unsigned valid_list[$];
trans_id_t foo_trans_id_h;
foo_trans_id_h = trans_id_t::type_id::create(
"foo_trans_id_h", null, get_full_name()
);
valid_list.delete();
// Check Each Mailbox - if it has data then mark it as valid
foreach(m_foo_rv_fetch_req_nominal_seq_mbox_h[id]) begin
if (m_foo_rv_fetch_req_nominal_seq_mbox_h[id].num() > 0) begin
valid_list.push_back(id);
end
end
foo_trans_id_h.set_list_ref(valid_list);
if (!foo_trans_id_h.randomize()) begin
`uvm_fatal(get_name(), "write_pull(): Randomize Failed")
end
`uvm_info(
get_name(),
$sformatf(
"write_pull(): Valid IDs: %s",
foo_trans_id_h.sprint_valid()
),
UVM_HIGH
)
return(foo_trans_id_h.m_selected);
endfunction: rand_id
The test base virtual sequence follows the following structure (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Test Base Virtual Sequence
//--------------------------------------------------------------------------------------------------
class foo_test_base_vseq extends foo_base_vseq;
`uvm_object_utils( foo_test_base_vseq )
// Test Config
int unsigned m_test_seq_drain_time_us;
rand int unsigned m_num_init_stripes; // Number of Stripes to start with "initial stripes"
foo_reg_ctrl_obj m_reg_foo_ctrl_obj_h;
foo_reg_stripes_obj m_reg_foo_stripes_obj_h;
// Config Coverage
foo_reg_cov_obj m_reg_cov_obj_h;
// EOT Monitor
foo_eot_obj m_foo_eot_obj_h;
bit m_irq[FOO_STRIPES];
event m_irq_event[FOO_STRIPES];
event m_irq_triggered_event[FOO_STRIPES];
foo_reg_stripe_obj m_eof_foo_reg_stripe_obj_h;
event m_eof_foo_reg_stripe_obj_event;
// Constraints
// Three Types of Constraint - valid, legal, typical
// Valid - need to be true or agent won't work correctly (things like positive values for delays)
// Legal - need to be true to obey protocol; can override to create protocol invalid transfers
// Typical - overridable constraint; use to set some reasonable bounds
constraint valid_base;
constraint legal_base {}
constraint typical_base {}
endclass: foo_test_base_vseq
//------------------------------------------------------------------------------
constraint foo_test_base_vseq::valid_base {
m_num_init_stripes <= FOO_STRIPES; // Max Number of initial stripes to launch
soft m_num_init_stripes > 0; // At least one stripe
}
//------------------------------------------------------------------------------
function foo_test_base_vseq::new(string name = "");
super.new( name );
m_test_seq_drain_time_us = 5; // default to 5us of drain time before EOT checks
end
//------------------------------------------------------------------------------
task foo_test_base_vseq::pre_body();
super.pre_body();
// EOT Object
m_foo_eot_obj_h = foo_eot_obj::type_id::create("m_foo_eot_obj_h");
m_foo_eot_obj_h.init(FOO_STRIPES);
endtask: pre_body
//------------------------------------------------------------------------------
task foo_test_base_vseq::body();
super.body();
// Start Clock
start_clock();
// Send Reset
reset();
endtask: body
//------------------------------------------------------------------------------
task foo_test_base_vseq::post_body();
super.post_body();
delay_time(m_test_seq_drain_time_us, "us");
m_ral_model_h.eot_check();
endtask: post_body
//------------------------------------------------------------------------------
task foo_test_base_vseq::start_rv_fetch_req_rsp();
m_foo_rv_fetch_req_rsp_vseq_h = foo_rv_fetch_req_rsp_vseq::type_id::create(
"m_foo_rv_fetch_req_rsp_vseq_h",
p_sequencer
);
if (!m_foo_rv_fetch_req_rsp_vseq_h.randomize()) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
fork
m_foo_rv_fetch_req_rsp_vseq_h.start(p_sequencer, this);
join_none
endtask: start_rv_fetch_req_rsp
//------------------------------------------------------------------------------
task foo_test_base_vseq::start_clock(int unsigned halfperiod = 'd1_111, bit init = 1'b0);
// NOTE: depends on clock timeunit being set in clock_cfg to picoseconds
clock_nominal_50duty_seq clock_nominal_50duty_seq_h;
clock_nominal_50duty_seq_h = clock_nominal_50duty_seq::type_id::create(
"clock_nominal_50duty_seq_h",
p_sequencer
);
if (!clock_nominal_50duty_seq_h.randomize() with {
m_init == local::init;
m_halfperiod_time == local::halfperiod;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
clock_nominal_50duty_seq_h.start(p_sequencer.m_clock_sequencer_h, this);
endtask: start_clock
//------------------------------------------------------------------------------
task foo_test_base_vseq::stop_clock(bit init = 1'b0);
// NOTE: depends on clock timeunit being set in clock_cfg to picoseconds
clock_nominal_50duty_seq clock_nominal_50duty_seq_h;
clock_nominal_50duty_seq_h = clock_nominal_50duty_seq::type_id::create(
"clock_nominal_50duty_seq_h",
p_sequencer
);
if (!clock_nominal_50duty_seq_h.randomize() with {
m_init == local::init;
m_halfperiod_time == 'd0; // period of zero stops the clock
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
clock_nominal_50duty_seq_h.start(p_sequencer.m_clock_sequencer_h, this);
endtask: stop_clock
//------------------------------------------------------------------------------
task foo_test_base_vseq::reset();
reset_nominal_seq reset_nominal_seq_h;
reset_nominal_seq_h = reset_nominal_seq::type_id::create("reset_nominal_seq_h", p_sequencer);
if (!reset_nominal_seq_h.randomize() with {
m_reset_kind == RESET_KIND_SEQUENCE;
m_postactive_cycles > 'd1;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
// Reset CSRs
m_ral_model_h.reset();
`uvm_info(
get_name(),
$sformatf("Reset\n%s",reset_nominal_seq_h.sprint()),
UVM_HIGH
)
// Send Reset
reset_nominal_seq_h.start(p_sequencer.m_reset_sequencer_h, this);
endtask: reset
//------------------------------------------------------------------------------
task foo_test_base_vseq::monitor_irq();
foo_irq_nominal_forever_seq_t foo_irq_nominal_forever_seq_h[];
foo_irq_nominal_forever_seq_h = new[FOO_STRIPES];
foreach(foo_irq_nominal_forever_seq_h[stripe]) begin
foo_irq_nominal_forever_seq_h[stripe] = foo_irq_nominal_forever_seq_t::type_id::create(
$sformatf("foo_irq_nominal_forever_seq_h[%0d]", stripe),
p_sequencer
);
if (!foo_irq_nominal_forever_seq_h[stripe].randomize()) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
fork
int stripe_auto = stripe;
begin
foo_irq_nominal_forever_seq_h[stripe_auto].start(
p_sequencer.m_foo_irq_sequencer_h[stripe_auto],
this
);
end
join_none
end
foreach(foo_irq_nominal_forever_seq_h[stripe]) begin
fork
int stripe_auto = stripe;
forever begin
@(foo_irq_nominal_forever_seq_h[stripe_auto].m_irq_event);
m_irq[stripe_auto] = foo_irq_nominal_forever_seq_h[stripe_auto].m_irq;
-> m_irq_event[stripe_auto];
end
join_none
end
endtask: monitor_irq
//------------------------------------------------------------------------------
task foo_test_base_vseq::irq_handler();
foreach(m_irq[stripe]) begin
if (m_irq_handler_stripe_en[stripe]) begin
fork
int unsigned stripe_auto = stripe;
forever begin
irq_handling(stripe_auto);
end
join_none
end
end
endtask: irq_handler
//------------------------------------------------------------------------------
m_foo_eot_obj_h.stripe_done(stripe);
end
end
endtask: irq_handling
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_enable(
int unsigned stripe,
bit enable = 1
);
foo_csr_int_enable_seq csr_int_enable_seq_h;
csr_int_enable_seq_h = foo_csr_int_enable_seq::type_id::create(
"csr_int_enable_seq_h"
);
if (!csr_int_enable_seq_h.randomize() with {
m_stripe == stripe;
m_intr == enable;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
csr_int_enable_seq_h.start(p_sequencer.m_foo_csr_sequencer_h, this);
endtask: int_enable
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_enable_all(
bit enable = 1
);
// Enable / UnMask All Interrupts
for (int unsigned stripe = 0; stripe < FOO_STRIPES; stripe += 1) begin
int_enable(stripe, enable);
end
endtask: int_enable_all
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_mask(
int unsigned stripe,
bit mask = 1
);
foo_csr_int_mask_seq csr_int_mask_seq_h;
csr_int_mask_seq_h = foo_csr_int_mask_seq::type_id::create(
"csr_int_mask_seq_h"
);
if (!csr_int_mask_seq_h.randomize() with {
m_stripe == stripe;
m_intr == mask;
}) begin
endtask: int_mask
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_mask_all(
bit mask = 1
);
// Enable / UnMask All Interrupts
for (int unsigned stripe = 0; stripe < FOO_STRIPES; stripe += 1) begin
int_mask(stripe, mask);
end
endtask: int_mask_all
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_test(
int unsigned stripe,
bit test
);
foo_csr_int_test_seq csr_int_test_seq_h;
csr_int_test_seq_h = foo_csr_int_test_seq::type_id::create(
"csr_int_test_seq_h"
);
if (!csr_int_test_seq_h.randomize() with {
m_stripe == stripe;
m_intr == test;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
csr_int_test_seq_h.start(p_sequencer.m_foo_csr_sequencer_h, this);
endtask: int_test
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_test_strobe(int unsigned stripe);
int_test(stripe, 'd0);
int_test(stripe, 'd1);
int_test(stripe, 'd0);
endtask: int_test_strobe
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_isr_read(
int unsigned stripe,
bit isr_ovf,
output bit status
);
foo_csr_int_status_seq csr_int_status_seq_h;
csr_int_status_seq_h = foo_csr_int_status_seq::type_id::create(
"csr_int_status_seq_h"
);
if (!csr_int_status_seq_h.randomize() with {
m_isr_ovf == isr_ovf;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
csr_int_status_seq_h.start(p_sequencer.m_foo_csr_sequencer_h, this);
status = csr_int_status_seq_h.m_intr[stripe];
endtask: int_isr_read
//------------------------------------------------------------------------------
task foo_test_base_vseq::int_isr_clear(
int unsigned stripe,
bit isr_ovf,
bit clear = 1'b1 // W1C
);
foo_csr_int_clear_seq csr_int_clear_seq_h;
csr_int_clear_seq_h = foo_csr_int_clear_seq::type_id::create(
"csr_int_clear_seq_h"
);
if (!csr_int_clear_seq_h.randomize() with {
m_stripe == stripe;
m_isr_ovf == isr_ovf;
m_intr == clear;
}) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
csr_int_clear_seq_h.start(p_sequencer.m_foo_csr_sequencer_h, this);
endtask: int_isr_clear
//------------------------------------------------------------------------------
task foo_test_base_vseq::build_regs();
// Randomize the Config -- if already constructed, use that reg config (don't randomize)
if (m_reg_foo_ctrl_obj_h == null) begin
m_reg_foo_ctrl_obj_h = foo_io_foo_reg_ctrl_obj::type_id::create("m_reg_foo_ctrl_obj_h");
if (!m_reg_foo_ctrl_obj_h.randomize()) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
end
// If not null - some derived class has pre-speced the stripe registers
if (m_reg_foo_stripes_obj_h == null) begin
m_reg_foo_stripes_obj_h = foo_reg_stripes_obj::type_id::create("m_reg_foo_stripes_obj_h");
if (!m_reg_foo_stripes_obj_h.randomize()) begin
`uvm_fatal(get_name(), "Randomize Failed")
end
m_foo_eot_obj_h.add(m_reg_foo_stripes_obj_h.m_new_config_h);
end
end
endtask: build_regs
//------------------------------------------------------------------------------
task foo_test_base_vseq::config_dut();
foo_csr_reg_config_seq csr_reg_config_seq_h;
csr_reg_config_seq_h = foo_csr_reg_config_seq::type_id::create(
"csr_reg_config_seq_h"
);
endtask: config_dut
//------------------------------------------------------------------------------
Like all virtual sequences - they are randomized before starting - so make use of rand members to make the test
different with each run.
The typical derived test virtual sequence follows the following structure (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Test Some Feature
//--------------------------------------------------------------------------------------------------
class foo_test_somefeature_vseq extends
foo_test_stripe_config_base_vseq;
`uvm_object_utils( foo_test_somefeature_vseq )
constraint legal;
constraint typical;
endclass: foo_test_somefeature_vseq
//------------------------------------------------------------------------------
constraint foo_test_somefeature_vseq::legal {
m_feature <= FOO_FEATURE_MAX;
}
//------------------------------------------------------------------------------
constraint foo_test_somefeature_vseq::typical {
soft m_feature == 16;
}
//------------------------------------------------------------------------------
function foo_test_somefeature_vseq::new(string name = "");
super.new( name );
// Defaults
m_feature = 16;
m_mode = FOO_SOME_MODE;
endfunction: new
//------------------------------------------------------------------------------
task foo_test_somefeature_vseq::body();
// Single Stripe
m_num_init_stripes = 'd1;
endtask: body
The typical directed base test virtual sequence follows the following structure (for a DUT named "foo"):
//--------------------------------------------------------------------------------------------------
// FOO Directed Base Virtual Sequence
//--------------------------------------------------------------------------------------------------
class foo_test_directed_base_vseq extends foo_test_base_vseq;
`uvm_object_utils( foo_test_directed_base_vseq )
// Helper Methods
extern virtual task do_the_thing(input int unsigned id, output bit success);
extern virtual task undo_the_thing(int unsigned id);
endclass: foo_test_directed_base_vseq
//------------------------------------------------------------------------------
task foo_test_directed_base_vseq::body();
super.body(); // Reset the DUT, Launch the Clocks
endtask: body
//------------------------------------------------------------------------------
task foo_test_directed_base_vseq::do_the_thing(input int unsigned id, output bit success);
// Create, Randomize, Start the Thing Sequence
...
// Check The Thing
...
endtask: do_the_thing
//------------------------------------------------------------------------------
task foo_test_directed_base_vseq::undo_the_thing(int unsigned id);
// Create, Randomize, Start the Thing Sequence - clear the Thing
...
endtask: undo_the_thing
The test list follows the following structure (for a DUT named "foo"):
// Base
`include "foo_base_vseq.svh"
// Virtual Agents
`include "foo_mem_vseq.svh"
...
// Test Sequences
`include "foo_test_base_vseq.svh"
...
`include "foo_test_csr_access_vseq.svh"
...
`include "foo_test_something_vseq.svh"
Environment Package
The DUT environment package is the namespaced encapsulation of the entire environment.
In the package we import from other packages, to get access to types used in this package. We declare types
that are shared across the environment. And we include the files of the environment - to place the contained
classes in this package.
The package also needs to set a timeunit and precision.
The package should be named with a DUT name prefix and a '_pkg' suffix.
The package follows the following structure (for a DUT named "foo"):
`include "uvm_macros.svh"
//--------------------------------------------------------------------------------------------------
// FOO Package
//--------------------------------------------------------------------------------------------------
package foo_pkg;
import uvm_pkg::*;
import ral_pkg::*;
import time_pkg::*;
import clock_pkg::*;
import reset_pkg::*;
import delay_pkg::*;
import activity_watchdog_pkg::*;
import foo_irq_pkg::*;
...
import foo_csr_pkg::*;
import foo_input_pkg::*;
import foo_output_pkg::*;
...
`include "foo_cfg.svh"
`include "foo_virtual_sequencer.svh"
`include "foo_scoreboard.svh"
`include "foo_env.svh"
`include "foo_vseq_list.svh"
`include "foo_test_list.svh"
endpackage: foo_pkg
Clock Agent
A clock agent - interface level clock generator that can be started / stopped / frequency updated from a
sequence; like any standard UVM agent.
Reset Agent
A reset agent - interface level reset generator agent that can be configured active high / low and any combination
of assert / deassert sync / async.
Allows driving reset from a sequence.
Delay Object
An object / interface pair that allows the generation of delay from a sequence through an object. Delay can be in
units of time or cycles. In an effort to be emulation friendly: cycle delays are implemented in the interface and
time delays are implemented using the uvm_delay macro.
Time Package
A packaged collection of objects that enable measuring time in units of wall time, cpu time, and simulation time.
Useful for seeing how long your sim is running (wall time) and to determine your test's efficiency (sim time / wall
time).
Directory Structure
The directory structure for a test environment is as follows (for a DUT called 'foo'):
➔ / path / to / verif / library
◆
agents
● some_protocol
○ some_protocol_cfg.svh
○ some_protocol_item.svh
○ some_protocol_driver.svh
○ some_protocol_monitor.svh
○ some_protocol_sequencer.svh
○ some_protocol_agent.svh
○ some_protocol_synth_pkg.sv
○ some_protocol_pkg.sv
○ some_protocol_if.sv
○ sequences
◆
some_protocol_nominal_seq.svh
◆
some_protocol_something_seq.svh
◆
some_protocol_something_else_seq.svh
◆
some_protocol_seq_list.svh
● some_other_protocol
○ ...
➔ foo
◆
agents
● foo_custom_protocol
○ foo_custom_protocol_cfg.svh
○ foo_custom_protocol_item.svh
○ foo_custom_protocol_driver.svh
○ foo_custom_protocol_monitor.svh
○ foo_custom_protocol_sequencer.svh
○ foo_custom_protocol_agent.svh
○ foo_custom_protocol_synth_pkg.sv
○ foo_custom_protocol_pkg.sv
○ foo_custom_protocol_if.sv
○ sequences
◆
foo_custom_protocol_nominal_seq.svh
◆
foo_custom_protocol_something_seq.svh
◆
foo_custom_protocol_something_else_seq.svh
◆
foo_custom_protocol_seq_list.svh
● foo_some_protocol
○ sequences
◆
foo_some_protocol_nominal_override_dothing_seq.svh
◆
foo_some_protocol_seq_list.svh
○ foo_some_protocol_synth_pkg.sv
○ foo_some_protocol_pkg.sv
◆ sequences
● foo_base_vseq.svh
● ...
● foo_mem_vseq.svh
● ...
● foo_mem_override_zerodelay_vseq.svh
● ...
● foo_test_base_vseq.svh
● foo_test_csr_access_vseq.svh
● ...
● foo_test_something_vseq.svh
● ...
● foo_test_directed_base_vseq.svh
● foo_test_directed_plusarg_vseq.svh
● ...
● foo_test_seq_list.svh
◆ tests
● foo_test_base.svh
● foo_test_special.svh
● ...
● foo_test_list.svh
◆ foo_cfg.svh
◆ foo_virtual_sequencer.svh
◆ foo_scoreboard.svh
◆ foo_scoreboard_debug_if.sv
◆ foo_env.svh
◆ foo_synth_pkg.sv
◆ foo_pkg.sv
The reset sequence list file will have this at the top - to tell genesis to generate and include the child files in the
final file list:
//; include("foo_header.svph");
/*; begin_multiline_perl
my $reset_nominal_seq = generate_base_include(
'reset_nominal_seq',
'dummy_reset_nominal_seq'
);
my $reset_assert_seq = generate_base_include(
'reset_assert_seq',
'dummy_reset_assert_seq'
);
end_multiline_perl ;*/
Similar to the sequences directory - the root agent directory "reset" will contain a reset.vpf file:
reset_if.svp
reset_cfg.svhp
reset_item.svhp
reset_driver.svhp
reset_monitor.svhp
reset_sequencer.svhp
reset_agent.svhp
-inputlist sequences/reset_sequences.vpf
reset_pkg.svp
And the reset_pkg.svp file will include the following at the top, again, to tell genesis to generate and include the
child files in the final file list:
//; include("foo_header.svph");
/*; begin_multiline_perl
my $reset_cfg = generate_base_include(
'reset_cfg',
'dummy_reset_cfg'
);
my $reset_item = generate_base_include(
'reset_item',
'dummy_reset_item'
);
my $reset_driver = generate_base_include(
'reset_driver',
'dummy_reset_driver'
);
my $reset_monitor = generate_base_include(
'reset_monitor',
'dummy_reset_monitor'
);
my $reset_sequencer = generate_base_include(
'reset_sequencer',
'dummy_reset_sequencer'
);
my $reset_agent = generate_base_include(
'reset_agent',
'dummy_reset_agent'
);
my $reset_seq_list = generate_base_include(
'reset_seq_list',
'dummy_reset_seq_list'
);
end_multiline_perl ;*/
This examples holds throughout the verification environment with a couple of additional details.
When we reference files that aren't header files (interfaces and packages) we use 'generate_base' rather than
'generate_base_include' - since these files are compiled as part of the file list and not `included.
When we need to import a package into "this" we also need to import the corresponding vpf file into "this" vpf file.
Our foo environment would have a vpf that looks like this:
# RTL Package
../path/to/the/rtl/foo_rtl_pkg.svp
# Agents
-inputlist ../path/to/the/agents/clock/clock.vpf
-inputlist ../path/to/the/agents/reset/reset.vpf
-inputlist ../path/to/the/agents/delay/delay.vpf
...
# DMA Environment
foo_scoreboard_debug_if.svp
foo_cfg.svhp
foo_virtual_sequencer.svhp
foo_scoreboard.svhp
foo_env.svhp
foo_pkg.svp
# Virtual Sequences
-inputlist sequences/foo_sequences.vpf
# Tests
-inputlist tests/foo_tests.vpf